A crate proptest traz para Rust o conceito de testes baseados em propriedades (property-based testing), popularizado por QuickCheck em Haskell. Em vez de testar com exemplos fixos e manualmente escolhidos, você descreve propriedades que devem ser verdadeiras para qualquer entrada, e o framework gera automaticamente centenas de casos de teste aleatórios para tentar quebrá-las.
Essa abordagem é extremamente eficaz para encontrar bugs que exemplos manuais jamais revelariam: edge cases com strings Unicode, números nos limites do overflow, coleções vazias, e combinações inesperadas de inputs. Quando o proptest encontra uma falha, ele automaticamente simplifica (shrinks) o input para o menor caso que reproduz o problema.
Instalação
Adicione ao seu Cargo.toml:
[dev-dependencies]
proptest = "1"
Para usar com derive (geração automática para structs/enums):
[dev-dependencies]
proptest = "1"
proptest-derive = "0.4"
Uso Básico
Seu Primeiro Teste com Proptest
use proptest::prelude::*;
fn reverter(s: &str) -> String {
s.chars().rev().collect()
}
proptest! {
#[test]
fn reverter_reverter_e_identidade(s in "\\PC*") {
// Para qualquer string, reverter duas vezes retorna o original
assert_eq!(reverter(&reverter(&s)), s);
}
#[test]
fn reverter_preserva_tamanho(s in "\\PC*") {
assert_eq!(reverter(&s).len(), s.len());
}
}
O "\\PC*" e uma regex que gera strings arbitrarias (qualquer caractere Unicode). O proptest gera centenas de strings aleatórias e verifica que a propriedade vale para todas.
Gerando Números
use proptest::prelude::*;
fn abs_diferenca(a: i32, b: i32) -> u32 {
if a > b {
(a - b) as u32
} else {
(b - a) as u32
}
}
proptest! {
#[test]
fn abs_diferenca_comutativa(a in -1000i32..1000, b in -1000i32..1000) {
assert_eq!(abs_diferenca(a, b), abs_diferenca(b, a));
}
#[test]
fn abs_diferenca_zero_consigo(a in any::<i32>()) {
assert_eq!(abs_diferenca(a, a), 0);
}
#[test]
fn soma_nao_transborda(a in 0i32..1_000_000, b in 0i32..1_000_000) {
let resultado = a as i64 + b as i64;
assert!(resultado < i32::MAX as i64);
}
}
Gerando Coleções
use proptest::prelude::*;
use proptest::collection::vec;
fn soma_vec(v: &[i64]) -> i64 {
v.iter().sum()
}
proptest! {
#[test]
fn soma_vec_vazio_e_zero(v in vec(0i64..=0, 0..100)) {
assert_eq!(soma_vec(&v), 0);
}
#[test]
fn soma_vec_positivos_nao_negativa(v in vec(0i64..1000, 0..50)) {
assert!(soma_vec(&v) >= 0);
}
#[test]
fn ordenacao_preserva_tamanho(mut v in vec(any::<i32>(), 0..100)) {
let tamanho_original = v.len();
v.sort();
assert_eq!(v.len(), tamanho_original);
}
#[test]
fn ordenacao_produz_vetor_ordenado(mut v in vec(any::<i32>(), 0..100)) {
v.sort();
for janela in v.windows(2) {
assert!(janela[0] <= janela[1]);
}
}
#[test]
fn ordenacao_preserva_elementos(mut v in vec(any::<i32>(), 0..100)) {
let mut copia = v.clone();
v.sort();
copia.sort();
assert_eq!(v, copia);
}
}
Recursos Avançados
Strategies Personalizadas
Strategies definem como os dados de teste são gerados:
use proptest::prelude::*;
// Strategy para emails válidos
fn estrategia_email() -> impl Strategy<Value = String> {
(
"[a-z]{3,10}", // usuário
"[a-z]{3,8}", // domínio
prop_oneof!["com", "br", "org", "net"], // TLD
)
.prop_map(|(usuario, dominio, tld)| {
format!("{}@{}.{}", usuario, dominio, tld)
})
}
// Strategy para CPFs formatados
fn estrategia_cpf() -> impl Strategy<Value = String> {
(
0u32..999,
0u32..999,
0u32..999,
0u32..99,
)
.prop_map(|(a, b, c, d)| {
format!("{:03}.{:03}.{:03}-{:02}", a, b, c, d)
})
}
// Strategy para datas válidas
fn estrategia_data() -> impl Strategy<Value = (u32, u32, u32)> {
(1900u32..2100, 1u32..13, 1u32..29) // dia limitado a 28 para simplificar
}
// Strategy composta para um usuário
#[derive(Debug, Clone)]
struct Usuario {
nome: String,
email: String,
idade: u8,
}
fn estrategia_usuario() -> impl Strategy<Value = Usuario> {
(
"[A-Z][a-z]{2,15} [A-Z][a-z]{2,15}", // nome
estrategia_email(), // email
18u8..100, // idade
)
.prop_map(|(nome, email, idade)| {
Usuario { nome, email, idade }
})
}
proptest! {
#[test]
fn email_tem_arroba(email in estrategia_email()) {
assert!(email.contains('@'));
assert!(email.contains('.'));
}
#[test]
fn cpf_tem_formato_correto(cpf in estrategia_cpf()) {
assert_eq!(cpf.len(), 14);
assert_eq!(&cpf[3..4], ".");
assert_eq!(&cpf[7..8], ".");
assert_eq!(&cpf[11..12], "-");
}
#[test]
fn usuario_valido(usuario in estrategia_usuario()) {
assert!(usuario.idade >= 18);
assert!(usuario.email.contains('@'));
assert!(!usuario.nome.is_empty());
}
}
prop_oneof! e prop_flat_map
use proptest::prelude::*;
#[derive(Debug, Clone, PartialEq)]
enum Forma {
Circulo { raio: f64 },
Retangulo { largura: f64, altura: f64 },
Triangulo { base: f64, altura: f64 },
}
impl Forma {
fn area(&self) -> f64 {
match self {
Forma::Circulo { raio } => std::f64::consts::PI * raio * raio,
Forma::Retangulo { largura, altura } => largura * altura,
Forma::Triangulo { base, altura } => base * altura / 2.0,
}
}
}
fn estrategia_forma() -> impl Strategy<Value = Forma> {
prop_oneof![
(0.1f64..100.0).prop_map(|raio| Forma::Circulo { raio }),
(0.1f64..100.0, 0.1f64..100.0)
.prop_map(|(largura, altura)| Forma::Retangulo { largura, altura }),
(0.1f64..100.0, 0.1f64..100.0)
.prop_map(|(base, altura)| Forma::Triangulo { base, altura }),
]
}
proptest! {
#[test]
fn area_sempre_positiva(forma in estrategia_forma()) {
assert!(forma.area() > 0.0, "Área negativa para {:?}", forma);
}
#[test]
fn area_circulo_maior_que_zero(raio in 0.01f64..1000.0) {
let circulo = Forma::Circulo { raio };
assert!(circulo.area() > 0.0);
}
}
// prop_flat_map para gerar dados dependentes
fn estrategia_vec_e_indice() -> impl Strategy<Value = (Vec<i32>, usize)> {
proptest::collection::vec(any::<i32>(), 1..100)
.prop_flat_map(|vec| {
let len = vec.len();
(Just(vec), 0..len)
})
}
proptest! {
#[test]
fn acesso_indice_valido((vec, idx) in estrategia_vec_e_indice()) {
// O índice é sempre válido para o vetor gerado
let _ = vec[idx]; // Nunca causa panic
}
}
Proptest com Derive
use proptest::prelude::*;
use proptest_derive::Arbitrary;
#[derive(Debug, Clone, Arbitrary)]
struct Pedido {
#[proptest(strategy = "1u64..1_000_000")]
id: u64,
#[proptest(strategy = "proptest::collection::vec(any::<Item>(), 1..10)")]
itens: Vec<Item>,
#[proptest(strategy = "0.0f64..100_000.0")]
desconto: f64,
}
#[derive(Debug, Clone, Arbitrary)]
struct Item {
#[proptest(regex = "[A-Z]{3}-[0-9]{4}")]
codigo: String,
#[proptest(strategy = "1u32..100")]
quantidade: u32,
#[proptest(strategy = "0.01f64..10_000.0")]
preco: f64,
}
impl Pedido {
fn valor_total(&self) -> f64 {
let subtotal: f64 = self.itens.iter()
.map(|i| i.preco * i.quantidade as f64)
.sum();
(subtotal - self.desconto).max(0.0)
}
}
proptest! {
#[test]
fn valor_total_nao_negativo(pedido in any::<Pedido>()) {
assert!(pedido.valor_total() >= 0.0);
}
}
Shrinking (Simplificação de Falhas)
O proptest automaticamente simplifica inputs que causam falha:
use proptest::prelude::*;
fn funcao_com_bug(dados: &[i32]) -> i32 {
// Bug: não trata soma que transborda
dados.iter().sum()
}
proptest! {
// Quando isso falha, proptest simplifica para o menor vetor que causa overflow
// Em vez de mostrar um vetor com 50 elementos, pode mostrar algo como [2147483647, 1]
#[test]
fn soma_nao_causa_panic(dados in proptest::collection::vec(-1000i32..1000, 0..50)) {
let _ = funcao_com_bug(&dados);
}
}
Quando um teste falha, o proptest salva o seed em proptest-regressions/:
# proptest-regressions/meu_teste.txt
# Shrunk case
cc 44b9e33f8b2bf8a39a7b3e6b0eac73d1e8a4c2b...
Isso garante que o caso que falhou sera testado novamente em execuções futuras, mesmo depois que você corrigir o bug.
Configuração e Limites
use proptest::prelude::*;
use proptest::test_runner::Config;
proptest! {
// Rodar mais casos (padrão: 256)
#![proptest_config(Config::with_cases(1000))]
#[test]
fn teste_exaustivo(x in 0i32..100, y in 0i32..100) {
assert!(x + y >= 0);
}
}
// Configuração mais granular
proptest! {
#![proptest_config(Config {
cases: 500,
max_shrink_iters: 10_000,
.. Config::default()
})]
#[test]
fn teste_configurado(s in "\\w{1,100}") {
assert!(!s.is_empty());
}
}
Strategies para Tipos Comuns
use proptest::prelude::*;
use proptest::collection::{btree_map, hash_map, hash_set, vec};
proptest! {
// Vec com tamanho controlado
#[test]
fn teste_vec(v in vec(1i32..100, 5..20)) {
assert!(v.len() >= 5 && v.len() < 20);
assert!(v.iter().all(|&x| x >= 1 && x < 100));
}
// HashMap
#[test]
fn teste_hashmap(m in hash_map("[a-z]{1,5}", 0i32..100, 1..10)) {
assert!(!m.is_empty());
}
// HashSet
#[test]
fn teste_hashset(s in hash_set(0i32..1000, 0..50)) {
// HashSet nunca tem duplicatas (propriedade trivial, mas verifica a geração)
let como_vec: Vec<_> = s.iter().collect();
assert_eq!(como_vec.len(), s.len());
}
// Strings com regex
#[test]
fn teste_regex_str(s in "[A-Z][a-z]{2,10}") {
assert!(s.chars().next().unwrap().is_uppercase());
assert!(s.len() >= 3);
}
// Option
#[test]
fn teste_option(opt in proptest::option::of(1i32..100)) {
if let Some(val) = opt {
assert!(val >= 1 && val < 100);
}
}
// Result
#[test]
fn teste_result(res in proptest::result::maybe_ok(1i32..100, "erro \\d+")) {
match res {
Ok(val) => assert!(val >= 1),
Err(msg) => assert!(msg.starts_with("erro ")),
}
}
// Tuplas
#[test]
fn teste_tupla((a, b, c) in (1i32..10, 10i32..20, 20i32..30)) {
assert!(a < b);
assert!(b < c);
}
}
prop_assert! para Mensagens Melhores
use proptest::prelude::*;
fn dividir(a: i32, b: i32) -> Option<i32> {
if b == 0 { None } else { Some(a / b) }
}
proptest! {
#[test]
fn divisao_propriedades(a in -1000i32..1000, b in 1i32..1000) {
let resultado = dividir(a, b);
// prop_assert! dá mensagens de erro melhores que assert!
prop_assert!(resultado.is_some(), "Divisão por {} deveria funcionar", b);
let valor = resultado.unwrap();
// Verificar que a / b * b está próximo de a
prop_assert!(
(valor * b - a).abs() < b.abs(),
"Inconsistência: {} / {} = {}, mas {} * {} = {}",
a, b, valor, valor, b, valor * b
);
}
}
Boas Práticas
1. Teste Propriedades, Não Exemplos
use proptest::prelude::*;
fn minha_ordenacao(mut v: Vec<i32>) -> Vec<i32> {
v.sort();
v
}
proptest! {
// BOM: testa propriedades genéricas
#[test]
fn ordenacao_produz_resultado_ordenado(v in proptest::collection::vec(any::<i32>(), 0..100)) {
let ordenado = minha_ordenacao(v.clone());
// Propriedade 1: resultado está ordenado
for w in ordenado.windows(2) {
prop_assert!(w[0] <= w[1]);
}
// Propriedade 2: mesmo tamanho
prop_assert_eq!(ordenado.len(), v.len());
// Propriedade 3: mesmos elementos (como multiset)
let mut esperado = v.clone();
esperado.sort();
prop_assert_eq!(ordenado, esperado);
}
}
// RUIM: teste com exemplos fixos tem cobertura limitada
#[test]
fn teste_fixo() {
assert_eq!(minha_ordenacao(vec![3, 1, 2]), vec![1, 2, 3]);
assert_eq!(minha_ordenacao(vec![]), vec![]);
// E os milhões de outros casos?
}
2. Combine com Testes Unitários
use proptest::prelude::*;
struct Pilha<T> {
dados: Vec<T>,
capacidade: usize,
}
impl<T> Pilha<T> {
fn nova(capacidade: usize) -> Self {
Pilha { dados: Vec::new(), capacidade }
}
fn empilhar(&mut self, item: T) -> Result<(), &str> {
if self.dados.len() >= self.capacidade {
return Err("Pilha cheia");
}
self.dados.push(item);
Ok(())
}
fn desempilhar(&mut self) -> Option<T> {
self.dados.pop()
}
fn tamanho(&self) -> usize {
self.dados.len()
}
fn vazia(&self) -> bool {
self.dados.is_empty()
}
}
// Testes unitários para casos específicos
#[test]
fn pilha_vazia_retorna_none() {
let mut pilha: Pilha<i32> = Pilha::nova(10);
assert_eq!(pilha.desempilhar(), None);
}
#[test]
fn pilha_cheia_retorna_erro() {
let mut pilha = Pilha::nova(1);
assert!(pilha.empilhar(42).is_ok());
assert!(pilha.empilhar(43).is_err());
}
// Proptest para propriedades gerais
proptest! {
#[test]
fn empilhar_desempilhar_lifo(itens in proptest::collection::vec(any::<i32>(), 1..50)) {
let mut pilha = Pilha::nova(100);
for &item in &itens {
pilha.empilhar(item).unwrap();
}
// LIFO: desempilhar na ordem inversa
for &item in itens.iter().rev() {
prop_assert_eq!(pilha.desempilhar(), Some(item));
}
prop_assert!(pilha.vazia());
}
#[test]
fn tamanho_correto_apos_operacoes(
ops in proptest::collection::vec(prop_oneof![Just(true), Just(false)], 0..100)
) {
let mut pilha = Pilha::nova(200);
let mut esperado = 0usize;
for &empilhar in &ops {
if empilhar {
pilha.empilhar(42).unwrap();
esperado += 1;
} else if esperado > 0 {
pilha.desempilhar();
esperado -= 1;
}
}
prop_assert_eq!(pilha.tamanho(), esperado);
}
}
3. Restringir Inputs para Evitar Falsos Positivos
use proptest::prelude::*;
proptest! {
// RUIM: pode gerar NaN e infinitos
// #[test]
// fn teste_float(a in any::<f64>(), b in any::<f64>()) {
// assert!((a + b) - a == b); // Falha com NaN, infinitos, etc.
// }
// BOM: restringir a valores finitos e razoáveis
#[test]
fn teste_float_restrito(
a in -1_000_000.0f64..1_000_000.0,
b in -1_000_000.0f64..1_000_000.0
) {
let soma = a + b;
prop_assert!(soma.is_finite());
}
}
4. Use prop_filter para Pré-Condições
use proptest::prelude::*;
proptest! {
#[test]
fn divisao_inteira(
a in any::<i32>(),
b in any::<i32>().prop_filter("divisor não zero", |b| *b != 0)
) {
let resultado = a / b;
// Se a e b têm o mesmo sinal, resultado >= 0
if (a >= 0 && b > 0) || (a <= 0 && b < 0) {
prop_assert!(resultado >= 0);
}
}
}
5. Organize Strategies Complexas em Funções
use proptest::prelude::*;
use std::collections::HashMap;
#[derive(Debug, Clone)]
struct BaseDados {
usuarios: HashMap<u64, String>,
posts: Vec<(u64, String)>, // (autor_id, conteudo)
}
fn estrategia_base_dados() -> impl Strategy<Value = BaseDados> {
proptest::collection::hash_map(1u64..100, "[a-z]{3,10}", 1..20)
.prop_flat_map(|usuarios| {
let ids: Vec<u64> = usuarios.keys().cloned().collect();
let num_ids = ids.len();
let posts_strategy = proptest::collection::vec(
(
proptest::sample::select(ids),
"[a-zA-Z ]{10,50}",
),
0..num_ids * 3,
);
(Just(usuarios), posts_strategy)
})
.prop_map(|(usuarios, posts)| BaseDados { usuarios, posts })
}
proptest! {
#[test]
fn todos_posts_tem_autor_valido(db in estrategia_base_dados()) {
for (autor_id, _) in &db.posts {
prop_assert!(
db.usuarios.contains_key(autor_id),
"Post com autor inexistente: {}",
autor_id
);
}
}
}
Exemplos Práticos
Exemplo Completo: Testando uma Estrutura de Dados
use proptest::prelude::*;
use std::collections::BTreeMap;
// === Implementação: Árvore AVL simplificada como BTreeMap wrapper ===
#[derive(Debug, Clone)]
struct OrdenedMap<K: Ord, V> {
inner: BTreeMap<K, V>,
}
impl<K: Ord + Clone, V: Clone> OrdenedMap<K, V> {
fn nova() -> Self {
OrdenedMap {
inner: BTreeMap::new(),
}
}
fn inserir(&mut self, chave: K, valor: V) -> Option<V> {
self.inner.insert(chave, valor)
}
fn buscar(&self, chave: &K) -> Option<&V> {
self.inner.get(chave)
}
fn remover(&mut self, chave: &K) -> Option<V> {
self.inner.remove(chave)
}
fn tamanho(&self) -> usize {
self.inner.len()
}
fn chaves_ordenadas(&self) -> Vec<&K> {
self.inner.keys().collect()
}
fn contem(&self, chave: &K) -> bool {
self.inner.contains_key(chave)
}
fn merge(&mut self, outro: &OrdenedMap<K, V>) {
for (k, v) in &outro.inner {
self.inner.insert(k.clone(), v.clone());
}
}
}
// === Operações para testes ===
#[derive(Debug, Clone)]
enum Operacao {
Inserir(i32, String),
Remover(i32),
Buscar(i32),
}
fn estrategia_operacao() -> impl Strategy<Value = Operacao> {
prop_oneof![
(any::<i32>(), "[a-z]{1,5}").prop_map(|(k, v)| Operacao::Inserir(k, v)),
any::<i32>().prop_map(Operacao::Remover),
any::<i32>().prop_map(Operacao::Buscar),
]
}
// === Testes Baseados em Propriedades ===
proptest! {
#![proptest_config(proptest::prelude::ProptestConfig::with_cases(500))]
// Propriedade 1: inserir e buscar retorna o valor correto
#[test]
fn inserir_buscar_consistente(
chave in any::<i32>(),
valor in "[a-z]{1,10}"
) {
let mut mapa = OrdenedMap::nova();
mapa.inserir(chave, valor.clone());
prop_assert_eq!(mapa.buscar(&chave), Some(&valor));
}
// Propriedade 2: remover torna o elemento inacessível
#[test]
fn remover_remove(chave in any::<i32>(), valor in "[a-z]{1,10}") {
let mut mapa = OrdenedMap::nova();
mapa.inserir(chave, valor);
mapa.remover(&chave);
prop_assert_eq!(mapa.buscar(&chave), None);
prop_assert!(!mapa.contem(&chave));
}
// Propriedade 3: tamanho é consistente com operações
#[test]
fn tamanho_consistente(
entradas in proptest::collection::vec((0i32..100, "[a-z]{1,5}"), 0..50)
) {
let mut mapa = OrdenedMap::nova();
let mut chaves_unicas = std::collections::HashSet::new();
for (chave, valor) in &entradas {
mapa.inserir(*chave, valor.clone());
chaves_unicas.insert(*chave);
}
prop_assert_eq!(mapa.tamanho(), chaves_unicas.len());
}
// Propriedade 4: chaves sempre ordenadas
#[test]
fn chaves_sempre_ordenadas(
entradas in proptest::collection::vec((any::<i32>(), "[a-z]{1,5}"), 0..100)
) {
let mut mapa = OrdenedMap::nova();
for (chave, valor) in entradas {
mapa.inserir(chave, valor);
}
let chaves = mapa.chaves_ordenadas();
for janela in chaves.windows(2) {
prop_assert!(janela[0] <= janela[1],
"Chaves fora de ordem: {:?} > {:?}", janela[0], janela[1]);
}
}
// Propriedade 5: inserir sobrescreve valor anterior
#[test]
fn inserir_sobrescreve(
chave in any::<i32>(),
valor1 in "[a-z]{1,10}",
valor2 in "[a-z]{1,10}"
) {
let mut mapa = OrdenedMap::nova();
mapa.inserir(chave, valor1);
mapa.inserir(chave, valor2.clone());
prop_assert_eq!(mapa.buscar(&chave), Some(&valor2));
prop_assert_eq!(mapa.tamanho(), 1);
}
// Propriedade 6: merge contém todas as chaves de ambos
#[test]
fn merge_contem_todos(
entradas1 in proptest::collection::vec((0i32..50, "[a-z]{1,5}"), 0..20),
entradas2 in proptest::collection::vec((50i32..100, "[a-z]{1,5}"), 0..20),
) {
let mut mapa1 = OrdenedMap::nova();
let mut mapa2 = OrdenedMap::nova();
for (k, v) in &entradas1 { mapa1.inserir(*k, v.clone()); }
for (k, v) in &entradas2 { mapa2.inserir(*k, v.clone()); }
let tamanho1 = mapa1.tamanho();
let tamanho2 = mapa2.tamanho();
mapa1.merge(&mapa2);
// Sem sobreposição (ranges diferentes), merge deve somar tamanhos
prop_assert_eq!(mapa1.tamanho(), tamanho1 + tamanho2);
// Todas as chaves do mapa2 devem estar no resultado
for (k, _) in &entradas2 {
prop_assert!(mapa1.contem(k), "Chave {} do mapa2 ausente após merge", k);
}
}
// Propriedade 7: modelo de referência (comparar com HashMap)
#[test]
fn modelo_referencia(
ops in proptest::collection::vec(estrategia_operacao(), 0..100)
) {
let mut minha = OrdenedMap::nova();
let mut referencia = std::collections::HashMap::new();
for op in ops {
match op {
Operacao::Inserir(k, v) => {
minha.inserir(k, v.clone());
referencia.insert(k, v);
}
Operacao::Remover(k) => {
minha.remover(&k);
referencia.remove(&k);
}
Operacao::Buscar(k) => {
let a = minha.buscar(&k).cloned();
let b = referencia.get(&k).cloned();
prop_assert_eq!(a, b, "Divergência na busca por chave {}", k);
}
}
}
prop_assert_eq!(minha.tamanho(), referencia.len());
}
}
Comparação com Alternativas
| Crate | Abordagem | Shrinking | Destaques |
|---|---|---|---|
proptest | Strategies compostas | Integrado | Mais popular, muito flexível |
quickcheck | Trait Arbitrary | Integrado | Mais simples, inspirado em Haskell |
bolero | Combina proptest/fuzzing | Integrado | Integra com libFuzzer |
arbitrary | Trait para fuzzing | Manual | Usado com cargo-fuzz |
fake | Dados falsos realistas | N/A | Nomes, emails, endereços |
proptest e a escolha mais popular e flexível. quickcheck e mais simples, mas menos poderoso. Para fuzzing real (encontrar bugs de seguranca), combine com cargo-fuzz.
Conclusão
O proptest transforma a forma como você pensa sobre testes. Em vez de imaginar exemplos que podem falhar, você descreve propriedades que devem sempre valer e deixa o framework explorar o espaço de inputs automaticamente. O shrinking garante que quando um bug é encontrado, você recebe o menor caso possível para debugar.
Combine testes baseados em propriedades com testes unitários tradicionais: use proptest para explorar amplo espaço de inputs e testes unitários para documentar comportamentos específicos e edge cases conhecidos.
Próximos passos:
- Explore criterion para benchmarkar o código testado pelo proptest
- Veja anyhow e thiserror para testar tratamento de erros com proptest
- Combine proptest com
cargo-fuzzpara encontrar bugs de segurança