Testes baseados em propriedades em Rust: a arte de capturar bugs que você não procurava
\n\nVocê já escreveu testes unitários, executou-os, tudo verde, e depois o aplicativo falhou na primeira entrada não padrão do usuário? Familiar. Testes clássicos são como verificar se uma fechadura funciona inserindo apenas sua própria chave. E se um invasor (ou apenas o acaso) trouxer uma chave de fenda, um clipe ou um pedaço de arame?
\n\nEm Rust, uma linguagem conhecida por sua segurança, existe uma ferramenta poderosa que permite não apenas verificar cenários específicos, mas provar que o código se comporta corretamente para todos os dados de entrada possíveis. Isso é o teste baseado em propriedades. E hoje vamos analisá-lo na prática, com exemplos reais, para que você possa aplicar esse conhecimento imediatamente em seus projetos.
\n\nO que é teste baseado em propriedades e como ele difere do teste comum?
\n\nImagine que você está testando uma função de ordenação. Um teste unitário comum se parece com isso:
\n\nfn test_sort() {\n let mut arr = vec![3, 1, 2];\n arr.sort();\n assert_eq!(arr, vec![1, 2, 3]);\n}\n\nFunciona? Funciona. Mas e se o array já estiver ordenado? E se estiver vazio? E se tiver 10.000 elementos em ordem inversa? Escrever testes para cada caso manualmente é um caminho sem fim.
\n\nTeste baseado em propriedades inverte a abordagem. Em vez de definir dados de entrada específicos, você descreve uma propriedade (property) que deve ser verdadeira para qualquer entrada válida. E uma biblioteca especial (por exemplo, proptest ou quickcheck) gera centenas e milhares de casos de teste aleatórios, tentando encontrar aquele único que viola sua propriedade.
Para ordenação, a propriedade pode ser: "O resultado da ordenação sempre tem o mesmo comprimento que o array original, e cada elemento do array original aparece no resultado exatamente uma vez". Ou, mais simplesmente: "O resultado está ordenado em ordem crescente".
\n\nPor que Rust é o ambiente ideal para testes baseados em propriedades
\n\nRust já captura muitos erros em tempo de compilação (gerenciamento de memória, condições de corrida). Mas há uma classe de bugs que o compilador não vê: erros lógicos, tratamento incorreto de casos limite, comportamento inesperado com combinações específicas de dados. É aqui que entram os testes baseados em propriedades em Rust.
\n\nEles complementam perfeitamente o sistema de tipos: os tipos garantem que os dados tenham a estrutura correta, e os testes baseados em propriedades garantem que a lógica de processamento desses dados esteja correta para todo o espaço de valores possíveis.
\n\nFerramentas: proptest vs quickcheck
\n\nNo ecossistema Rust, existem dois frameworks principais: proptest e quickcheck. Ambos fazem a mesma coisa, mas com filosofias diferentes.
\n\n- \n
- quickcheck — mais antigo, minimalista. Funciona através da trait
Arbitrary. Você escreve uma função que recebe dados aleatórios e o teste é executado várias vezes. \n - proptest — moderno, poderoso, com várias estratégias de geração de dados. Permite ajustar finamente quais dados gerar e consegue "reduzir" (shrink) um exemplo com falha ao menor caso reproduzível. É o padrão ouro atualmente. \n
Usaremos o proptest, pois oferece mais controle e é melhor documentado. Vamos instalá-lo:
\n\n# Cargo.toml\n[dev-dependencies]\nproptest = "1.4"\n\nEscrevendo nosso primeiro teste baseado em propriedades em Rust
\n\nVamos voltar à nossa ordenação. Vamos escrever um teste que verifica se o resultado está realmente ordenado:
\n\nuse proptest::prelude::*;\n\nfn is_sorted<T: Ord>(slice: &[T]) -> bool {\n slice.windows(2).all(|w| w[0] <= w[1])\n}\n\nproptest!