Proptest: Testes Baseados em Propriedades em Rust

Guia completo da crate proptest para Rust. Aprenda testes baseados em propriedades, strategies, geração de dados arbitrários, proptest! macro, shrinking, reprodução de falhas e exemplos práticos de testes robustos.

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

CrateAbordagemShrinkingDestaques
proptestStrategies compostasIntegradoMais popular, muito flexível
quickcheckTrait ArbitraryIntegradoMais simples, inspirado em Haskell
boleroCombina proptest/fuzzingIntegradoIntegra com libFuzzer
arbitraryTrait para fuzzingManualUsado com cargo-fuzz
fakeDados falsos realistasN/ANomes, 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-fuzz para encontrar bugs de segurança