A suite de testes está verde. Fixtures bem organizadas, parametrize cobrindo os casos óbvios, mocks isolando as dependências externas. Cobertura em 94%. O PR passa no CI e vai para produção.
Três dias depois, um usuário reporta um comportamento estranho. Você reproduz o bug localmente com um input que nunca ocorreu a ninguém testar: uma string vazia em que se esperava pelo menos um caractere, um número negativo em que a função assumia valores positivos, uma lista com um único elemento no qual a lógica de comparação silenciosamente quebra. O teste que teria pego isso seria trivial de escrever — se alguém tivesse pensado em escrever.
Esse é o problema que o Hypothesis resolve. Não substituindo o pytest, mas mudando quem é responsável por inventar os inputs.
Testar o que você sabe vs. testar o que você não sabe
No artigo sobre pytest além do básico, o foco foi em
ferramentas que tornam os testes mais expressivos e menos frágeis: fixtures com escopo
controlado, parametrize para eliminar duplicação, pytest-mock para isolar
dependências. Essas ferramentas partem do mesmo pressuposto: você sabe quais casos
precisam ser testados, e a questão é como organizá-los bem.
O Hypothesis parte de um pressuposto diferente. Você descreve as propriedades que uma função deve satisfazer — invariantes que valem para qualquer input válido — e a biblioteca gera os inputs automaticamente, tentando encontrar um contra-exemplo. Isso é chamado de property-based testing.
A diferença prática é significativa. Com parametrize, você testa:
| |
Com Hypothesis, você testa:
| |
No segundo caso, o Hypothesis vai gerar dezenas de inteiros positivos — incluindo casos
de borda como 1, 2, o maior inteiro possível — e rodar o teste para cada um. Se
encontrar um contra-exemplo, vai reportar o menor valor em que a função quebra. Esse
processo de minimização automática do contra-exemplo é chamado de shrinking.
Instalação e setup
| |
Nenhuma configuração adicional é necessária para começar. O Hypothesis funciona como
plugin do pytest: qualquer teste decorado com @given é reconhecido e executado
automaticamente.
Um bug real que o pytest não pegaria
Considere uma função que calcula o índice de um elemento numa lista ordenada usando busca binária:
| |
Uma suite pytest razoável testaria alguns casos:
| |
Todos passam. Agora com Hypothesis:
| |
Esse teste vai passar também — a implementação acima está correta. Mas agora suponha uma variante com um bug sutil, comum em implementações antigas de busca binária em linguagens sem inteiros de precisão arbitrária:
| |
O Hypothesis encontra o contra-exemplo em poucos segundos:
Falsifying example: test_busca_binaria_propriedade(
lista=[0, 1], alvo=1
)
E então minimiza: o menor input em que a função entra em loop infinito é uma lista de dois elementos em que o alvo é o segundo. Nenhum dos testes manuais acima teria coberto esse caso específico.
Estratégias: descrevendo o espaço de inputs
A interface central do Hypothesis é o módulo strategies (importado convencionalmente
como st). Uma estratégia descreve um conjunto de valores possíveis — o Hypothesis
amostra desse conjunto durante a execução do teste.
As estratégias mais usadas no dia a dia:
| |
Estratégias podem ser compostas e transformadas:
| |
Um exemplo com domínio real
Testes de propriedade brilham especialmente em funções de transformação de dados em que a propriedade natural é a reversibilidade. Uma função que serializa e desserializa dados deve satisfazer:
| |
Esse único teste cobre qualquer combinação de id, nome de cliente e valor decimal —
incluindo clientes com nomes unicode, valores com zeros à direita, ids no limite do
inteiro. Nenhum parametrize cobre esse espaço de forma equivalente.
Banco de exemplos e reprodução determinística
O Hypothesis mantém um banco de exemplos local (por padrão em .hypothesis/) que
persiste entre execuções. Quando um contra-exemplo é encontrado, ele é salvo e
reexecutado automaticamente nas próximas rodadas — garantindo que um bug descoberto
não passe despercebido se a correção for incompleta.
Para reproduzir um caso específico sem depender do banco, o decorador @example força
um input fixo:
| |
O .hypothesis/ deve ir para o .gitignore em projetos pessoais ou entrar no
controle de versão em projetos de equipe — a escolha depende de querer ou não
compartilhar o banco de contra-exemplos encontrados.
Configurando o comportamento
O Hypothesis tem configurações sensatas por padrão, mas dois parâmetros valem conhecer cedo:
| |
O max_examples controla quantos inputs são gerados por execução. Aumentar para 500
ou 1000 em funções críticas custa tempo de CI mas aumenta a cobertura do espaço de
inputs de forma significativa.
O que property-based testing não substitui
Testes de propriedade não substituem testes de unidade tradicionais — eles cobrem espaços diferentes.
Um teste com @example explícito ou parametrize documenta intenção: “esse caso
específico deve se comportar dessa forma”. É útil para casos de borda conhecidos,
comportamentos contratados por uma API pública, ou regressões de bugs encontrados
em produção. Um teste com @given explora o espaço desconhecido: “essa propriedade
vale para qualquer input válido”. É útil para invariantes matemáticas, propriedades
de reversibilidade, e funções com contratos amplos.
Na prática, os dois coexistem no mesmo arquivo de teste, frequentemente no mesmo teste:
| |
O Hypothesis está na lista de dependências de projetos como o Django, o attrs e o cryptography — projetos em que um bug de borda custa caro. Para código Python de produção em que as entradas vêm do mundo externo e o espaço de inputs é grande, ele passa a ser menos uma opção e mais uma camada necessária da suite de testes.
Se tiver dúvidas ou quiser compartilhar um contra-exemplo interessante que o Hypothesis encontrou no seu código, encontra-me no Fediverse: @riverfount@bolha.us.
