Python

Testes Parametrizados e Property-Based Testing com Hypothesis: Do Básico ao Avançado

15 min de leitura

Testes Parametrizados e Property-Based Testing com Hypothesis: Do Básico ao Avançado

O Problema Tradicional dos Testes Unitários Quando começamos a escrever testes, geralmente fazemos algo assim: criamos um caso de teste para entrada específica, verificamos a saída esperada e marcamos como "resolvido". Porém, essa abordagem tem um problema fundamental. Um teste de unidade tradicional valida apenas os casos que você pensou em testar. Se um bug existe em uma combinação de entrada que você nunca considerou, ele passará despercebido em produção. Imagine um algoritmo de ordenação que funciona perfeitamente para listas com 10 elementos, mas falha misteriosamente com 1.000. Ou uma função de validação de email que passa em todos os seus testes, mas quebra com domínios internacionalizados. O problema é que testes manuais não escalam cognitivamente. Você não pode prever todos os casos de uso possíveis, e é exatamente aqui que entram os testes parametrizados e o property-based testing. Testes Parametrizados: Reutilizando Lógica de Teste O Conceito Fundamental Testes parametrizados permitem que você execute a mesma lógica de teste com

<h2>O Problema Tradicional dos Testes Unitários</h2>

<p>Quando começamos a escrever testes, geralmente fazemos algo assim: criamos um caso de teste para entrada específica, verificamos a saída esperada e marcamos como &quot;resolvido&quot;. Porém, essa abordagem tem um problema fundamental. Um teste de unidade tradicional valida apenas os casos que você <em>pensou</em> em testar. Se um bug existe em uma combinação de entrada que você nunca considerou, ele passará despercebido em produção.</p>

<p>Imagine um algoritmo de ordenação que funciona perfeitamente para listas com 10 elementos, mas falha misteriosamente com 1.000. Ou uma função de validação de email que passa em todos os seus testes, mas quebra com domínios internacionalizados. O problema é que <strong>testes manuais não escalam cognitivamente</strong>. Você não pode prever todos os casos de uso possíveis, e é exatamente aqui que entram os <strong>testes parametrizados</strong> e o <strong>property-based testing</strong>.</p>

<h2>Testes Parametrizados: Reutilizando Lógica de Teste</h2>

<h3>O Conceito Fundamental</h3>

<p>Testes parametrizados permitem que você execute a mesma lógica de teste com múltiplos conjuntos de dados. Em vez de escrever dez funções de teste idênticas com valores diferentes, você escreve uma única função e passa diferentes parâmetros para ela. Isso reduz duplicação, melhora manutenibilidade e deixa claro quais casos você está validando.</p>

<p>Em Python, a biblioteca <code>pytest</code> oferece suporte nativo a testes parametrizados através do decorator <code>@pytest.mark.parametrize</code>. Vamos ver um exemplo prático:</p>

<pre><code class="language-python">import pytest

def calcular_desconto(preco, categoria):

&quot;&quot;&quot;Calcula desconto baseado na categoria do produto.&quot;&quot;&quot;

descontos = {

&#039;premium&#039;: 0.20,

&#039;padrão&#039;: 0.10,

&#039;basico&#039;: 0.05

}

desconto_percentual = descontos.get(categoria, 0)

return preco * (1 - desconto_percentual)

@pytest.mark.parametrize(&quot;preco,categoria,esperado&quot;, [

(100, &#039;premium&#039;, 80),

(100, &#039;padrão&#039;, 90),

(100, &#039;basico&#039;, 95),

(50, &#039;premium&#039;, 40),

(200, &#039;padrão&#039;, 180),

])

def test_calcular_desconto(preco, categoria, esperado):

assert calcular_desconto(preco, categoria) == esperado</code></pre>

<p>Quando você executa <code>pytest</code>, ele roda a função <code>test_calcular_desconto</code> cinco vezes, uma para cada conjunto de parâmetros. Se um teste falhar, você vê exatamente qual combinação causou o problema. Isso é muito mais poderoso que ter cinco funções separadas porque você consegue identificar padrões nas falhas.</p>

<h3>Parametrização Avançada</h3>

<p>Você pode parametrizar múltiplas variáveis simultaneamente e até combinar parametrizações. Aqui está um exemplo com uma estrutura um pouco mais complexa:</p>

<pre><code class="language-python">import pytest

def validar_senha(senha):

&quot;&quot;&quot;Valida se uma senha atende aos critérios de segurança.&quot;&quot;&quot;

if len(senha) &lt; 8:

return False, &quot;Senha muito curta&quot;

if not any(c.isupper() for c in senha):

return False, &quot;Falta letra maiúscula&quot;

if not any(c.isdigit() for c in senha):

return False, &quot;Falta dígito&quot;

return True, &quot;Válida&quot;

@pytest.mark.parametrize(&quot;senha,valida,motivo&quot;, [

(&quot;Senha123&quot;, True, &quot;Válida&quot;),

(&quot;senha123&quot;, False, &quot;Falta letra maiúscula&quot;),

(&quot;SENHA123&quot;, False, &quot;Falta letra minúscula&quot;),

(&quot;Senha&quot;, False, &quot;Senha muito curta&quot;),

(&quot;Abc12345&quot;, True, &quot;Válida&quot;),

(&quot;ABCD1234&quot;, False, &quot;Falta letra minúscula&quot;),

])

def test_validar_senha(senha, valida, motivo):

resultado, mensagem = validar_senha(senha)

assert resultado == valida

if not valida:

assert motivo in mensagem</code></pre>

<p>Este exemplo mostra casos de sucesso e falha, permitindo que você documente explicitamente o que espera de cada cenário. A parametrização deixa evidente quais comportamentos você validou e qual é o motivo de cada teste.</p>

<h2>Property-Based Testing com Hypothesis</h2>

<h3>Por Que Property-Based Testing É Diferente</h3>

<p>Property-based testing inverte a lógica tradicional. Em vez de você decidir quais dados testar, você define <strong>propriedades</strong> que devem ser verdadeiras para <em>qualquer</em> entrada válida, e a ferramenta gera centenas ou milhares de entradas automaticamente para encontrar contraexemplos. É como ter um QA muito persistente testando sua função com dados aleatórios 24/7.</p>

<p>A biblioteca <code>Hypothesis</code> em Python é a implementação mais madura de property-based testing. Ela não gera dados completamente aleatórios — ela é inteligente. Quando encontra um caso que falha, ela <em>reduz</em> esse caso para encontrar o mínimo exemplo que ainda causa o problema. Isso facilita enormemente o debug.</p>

<h3>Conceito de Propriedade</h3>

<p>Uma propriedade é uma afirmação que deve ser verdadeira para toda entrada válida, independentemente dos dados específicos. Vamos começar com um exemplo simples:</p>

<pre><code class="language-python">from hypothesis import given, strategies as st

def adicionar(a, b):

&quot;&quot;&quot;Adiciona dois números.&quot;&quot;&quot;

return a + b

Propriedade: adição é comutativa

@given(st.integers(), st.integers())

def test_adicao_comutativa(a, b):

assert adicionar(a, b) == adicionar(b, a)

Propriedade: adicionar zero não muda o valor

@given(st.integers())

def test_adicao_identidade(a):

assert adicionar(a, 0) == a

Propriedade: adição é associativa

@given(st.integers(), st.integers(), st.integers())

def test_adicao_associativa(a, b, c):

assert adicionar(adicionar(a, b), c) == adicionar(a, adicionar(b, c))</code></pre>

<p>Este é um exemplo com operações matemáticas básicas, mas as propriedades são claras: não importa quais inteiros você passe, essas propriedades sempre devem ser verdadeiras. O Hypothesis tentará quebrar essas propriedades com valores extremos, negativos, zeros e combinações estranhas.</p>

<h3>Estratégias (Strategies) do Hypothesis</h3>

<p>As estratégias definem qual tipo de dado será gerado. O Hypothesis já vem com muitas estratégias prontas:</p>

<pre><code class="language-python">from hypothesis import given, strategies as st

Estratégia básica para inteiros

@given(st.integers(min_value=0, max_value=100))

def test_com_inteiros_limitados(x):

assert 0 &lt;= x &lt;= 100

Estratégia para texto

@given(st.text())

def test_com_texto(texto):

Qualquer string deve ser convertida para string novamente sem erros

assert isinstance(str(texto), str)

Estratégia para listas

@given(st.lists(st.integers()))

def test_com_listas(numeros):

Uma lista sempre tem um tamanho definido

assert len(numeros) &gt;= 0

Estratégia combinada

@given(

st.lists(st.integers(min_value=1, max_value=100), min_size=1),

)

def test_soma_lista_positiva(numeros):

A soma de números positivos é sempre positiva

assert sum(numeros) &gt; 0

Estratégia customizada

@given(st.emails())

def test_formato_email(email):

Um email deve conter um @

assert &#039;@&#039; in email</code></pre>

<p>O Hypothesis oferece estratégias para praticamente qualquer tipo: datas, horas, uuids, decimais, floats, dicionários, e muito mais. Você também pode <strong>combinar</strong> estratégias para criar estruturas de dados complexas.</p>

<h3>Encontrando Bugs Reais com Hypothesis</h3>

<p>Vamos ver um exemplo onde Hypothesis encontra um bug que testes tradicionais perderiam:</p>

<pre><code class="language-python">from hypothesis import given, strategies as st

def buscar_indice(lista, valor):

&quot;&quot;&quot;Encontra o índice de um valor em uma lista. Bugado!&quot;&quot;&quot;

for i in range(len(lista)):

if lista[i] == valor:

return i

return -1

Teste tradicional (passa)

def test_buscar_indice_manual():

assert buscar_indice([1, 2, 3], 2) == 1

assert buscar_indice([1, 2, 3], 5) == -1

Propriedade com Hypothesis

@given(

st.lists(st.integers()).filter(lambda x: len(x) &gt; 0),

st.integers()

)

def test_buscar_indice_property(lista, valor):

indice = buscar_indice(lista, valor)

Se encontrou, o índice deve ser válido e apontar para o valor

if indice != -1:

assert 0 &lt;= indice &lt; len(lista)

assert lista[indice] == valor

else:

Se não encontrou, o valor não deve estar em lugar nenhum

assert valor not in lista</code></pre>

<p>Se adicionássemos um bug à função (como <code>if lista[i] == valor or True: return i</code>), o teste tradicional ainda passaria, mas Hypothesis encontraria rapidamente o problema ao gerar casos onde nenhum valor deveria ser encontrado.</p>

<h3>Usando <code>@given</code> com Múltiplas Estratégias</h3>

<p>Um exemplo mais realista com validação de dados:</p>

<pre><code class="language-python">from hypothesis import given, strategies as st, assume

from datetime import datetime

def processar_pedido(id_cliente, valor, data_pedido):

&quot;&quot;&quot;Processa um pedido com validações.&quot;&quot;&quot;

if valor &lt; 0:

raise ValueError(&quot;Valor não pode ser negativo&quot;)

if datetime.fromisoformat(data_pedido) &gt; datetime.now():

raise ValueError(&quot;Data não pode ser no futuro&quot;)

return {

&#039;cliente_id&#039;: id_cliente,

&#039;valor&#039;: valor,

&#039;data&#039;: data_pedido,

&#039;processado&#039;: True

}

@given(

id_cliente=st.integers(min_value=1, max_value=999999),

valor=st.floats(min_value=0.01, max_value=10000, allow_nan=False),

data_pedido=st.datetimes(max_value=datetime.now())

)

def test_processar_pedido_valido(id_cliente, valor, data_pedido):

resultado = processar_pedido(id_cliente, valor, data_pedido.isoformat())

assert resultado[&#039;processado&#039;] is True

assert resultado[&#039;cliente_id&#039;] == id_cliente

assert resultado[&#039;valor&#039;] == valor</code></pre>

<p>Este teste valida que a função processa corretamente qualquer combinação razoável de dados. Note o uso de <code>allow_nan=False</code> para evitar valores não-numéricos que causariam erro.</p>

<h3>Redução de Exemplos com Hypothesis</h3>

<p>Quando Hypothesis encontra uma falha, ele <strong>reduz</strong> automaticamente o exemplo para o caso mínimo:</p>

<pre><code class="language-python">from hypothesis import given, strategies as st

def contar_pares(numeros):

&quot;&quot;&quot;Conta números pares em uma lista. Bugado para listas grandes!&quot;&quot;&quot;

pares = [x for x in numeros if x % 2 == 0]

if len(pares) &gt; 10: # BUG: essa condição está errada

return -1

return len(pares)

@given(st.lists(st.integers(), min_size=5, max_size=100))

def test_contar_pares(numeros):

resultado = contar_pares(numeros)

O resultado nunca deve ser negativo

assert resultado &gt;= 0</code></pre>

<p>Quando este teste falhar, Hypothesis não vai reportar uma lista aleatória gigante. Ele vai reduzir para algo como <code>[0, 2, 4, 6, 8, 10, 12]</code> — a menor lista que reproduz o problema.</p>

<h2>Combinando Testes Parametrizados e Property-Based Testing</h2>

<h3>Quando Usar Cada Um</h3>

<p>Testes parametrizados são ideais para validar <strong>casos específicos e conhecidos</strong> — casos de negócio, edge cases documentados, ou comportamentos que você quer garantir que funcionam. Property-based testing é melhor para validar <strong>propriedades matemáticas ou invariantes</strong> que devem ser verdadeiras para qualquer entrada válida.</p>

<p>A melhor estratégia é <strong>usar ambos</strong>. Veja um exemplo prático:</p>

<pre><code class="language-python">from hypothesis import given, strategies as st

import pytest

def aplicar_taxa_imposto(valor, codigo_estado):

&quot;&quot;&quot;Aplica taxa de imposto baseada no estado.&quot;&quot;&quot;

taxas = {

&#039;SP&#039;: 0.18,

&#039;RJ&#039;: 0.20,

&#039;MG&#039;: 0.15,

&#039;RS&#039;: 0.17,

}

if codigo_estado not in taxas:

raise ValueError(f&quot;Estado desconhecido: {codigo_estado}&quot;)

taxa = taxas[codigo_estado]

return valor * (1 + taxa)

Testes parametrizados para casos de negócio conhecidos

@pytest.mark.parametrize(&quot;valor,estado,esperado&quot;, [

(100, &#039;SP&#039;, 118),

(100, &#039;RJ&#039;, 120),

(100, &#039;MG&#039;, 115),

(1000, &#039;SP&#039;, 1180),

(250.50, &#039;RJ&#039;, 300.60), # Teste com decimal

])

def test_aplicar_taxa_casos_conhecidos(valor, estado, esperado):

resultado = aplicar_imposto(valor, estado)

assert abs(resultado - esperado) &lt; 0.01 # Tolerância para floats

Property-based testing para garantir invariantes

@given(

valor=st.floats(min_value=0.01, max_value=100000, allow_nan=False, allow_infinity=False),

estado=st.sampled_from([&#039;SP&#039;, &#039;RJ&#039;, &#039;MG&#039;, &#039;RS&#039;])

)

def test_aplicar_taxa_sempre_aumenta_valor(valor, estado):

resultado = aplicar_imposto(valor, estado)

O resultado sempre deve ser maior que o valor original (taxa sempre positiva)

assert resultado &gt; valor

E deve estar em um intervalo razoável

assert resultado &lt; valor * 1.25 # Taxa máxima é 20%

def test_aplicar_taxa_estado_invalido():

with pytest.raises(ValueError):

aplicar_imposto(100, &#039;XX&#039;)</code></pre>

<p>Este exemplo mostra o melhor dos dois mundos: você valida casos de negócio específicos com parametrização, e garante propriedades gerais com property-based testing.</p>

<h3>Exemplo Completo: Validador de URL</h3>

<p>Vamos construir um validador de URL e testá-lo com ambas as técnicas:</p>

<pre><code class="language-python">from hypothesis import given, strategies as st

import pytest

from urllib.parse import urlparse

def validar_url(url):

&quot;&quot;&quot;Valida se uma string é uma URL bem-formada.&quot;&quot;&quot;

try:

resultado = urlparse(url)

Deve ter esquema e netloc (domínio)

return all([resultado.scheme, resultado.netloc])

except Exception:

return False

Casos parametrizados conhecidos

@pytest.mark.parametrize(&quot;url,valida&quot;, [

(&quot;https://www.google.com&quot;, True),

(&quot;http://localhost:8000&quot;, True),

(&quot;ftp://files.example.com/arquivo.zip&quot;, True),

(&quot;não é url&quot;, False),

(&quot;&quot;, False),

(&quot;http://&quot;, False), # Falta host

])

def test_validar_url_casos_conhecidos(url, valida):

assert validar_url(url) == valida

Properties: URLs válidas sempre têm certos componentes

@given(

esquema=st.sampled_from([&#039;http&#039;, &#039;https&#039;, &#039;ftp&#039;]),

dominio=st.domains(),

caminho=st.text(alphabet=&#039;abcdefghijklmnopqrstuvwxyz0123456789/&#039;)

)

def test_url_valida_construida_tem_esquema_e_dominio(esquema, dominio, caminho):

url = f&quot;{esquema}://{dominio}/{caminho}&quot;

URLs que construímos assim sempre devem ser válidas

assert validar_url(url) is True</code></pre>

<h2>Conclusão</h2>

<p>Os três pontos principais que você deve levar desta aula são: <strong>primeiro</strong>, testes parametrizados eliminam duplicação de código de teste e deixam claro quais cenários você validou — use <code>@pytest.mark.parametrize</code> para casos conhecidos e específicos do seu negócio. <strong>Segundo</strong>, property-based testing com Hypothesis inverte o paradigma, deixando a ferramenta gerar entradas para você e focando em propriedades invariantes — isso encontra bugs que testes manuais perdem. <strong>Terceiro</strong>, a combinação de ambas as abordagens é mais poderosa que cada uma isoladamente: parametrização para casos determinísticos, Hypothesis para garantias matemáticas.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://docs.pytest.org/en/latest/how-to/parametrize.html" target="_blank" rel="noopener noreferrer">Documentação oficial do pytest parametrize</a></li>

<li><a href="https://hypothesis.readthedocs.io/" target="_blank" rel="noopener noreferrer">Hypothesis - Property-Based Testing para Python</a></li>

<li><a href="https://hypothesis.readthedocs.io/en/latest/reference.html#strategies" target="_blank" rel="noopener noreferrer">Hypothesis - Strategies Reference</a></li>

<li><a href="https://www.hillelwayne.com/post/property-testing-intro/" target="_blank" rel="noopener noreferrer">Articulo: Property-Based Testing: Theory and Practice</a></li>

<li><a href="https://www.youtube.com/watch?v=jIiehWizieY" target="_blank" rel="noopener noreferrer">PyCon Talk: The Hypothesis Framework</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Python

Dominando Testes de APIs Python: pytest com httpx e TestClient do FastAPI em Projetos Reais
Dominando Testes de APIs Python: pytest com httpx e TestClient do FastAPI em Projetos Reais

Por que Testar APIs com Python? Testar uma API é tão importante quanto desenv...

O que Todo Dev Deve Saber sobre Alembic em Python: Migrations Versionadas com SQLAlchemy
O que Todo Dev Deve Saber sobre Alembic em Python: Migrations Versionadas com SQLAlchemy

O que é Alembic e Por Que Você Precisa Dele Alembic é uma ferramenta de versi...

Dominando asyncio Avançado em Python: Semáforos, Locks e Padrões de Concorrência em Projetos Reais
Dominando asyncio Avançado em Python: Semáforos, Locks e Padrões de Concorrência em Projetos Reais

Introdução: O Problema da Concorrência Controlada Quando trabalhamos com em P...