<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 "resolvido". 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):
"""Calcula desconto baseado na categoria do produto."""
descontos = {
'premium': 0.20,
'padrão': 0.10,
'basico': 0.05
}
desconto_percentual = descontos.get(categoria, 0)
return preco * (1 - desconto_percentual)
@pytest.mark.parametrize("preco,categoria,esperado", [
(100, 'premium', 80),
(100, 'padrão', 90),
(100, 'basico', 95),
(50, 'premium', 40),
(200, 'padrão', 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):
"""Valida se uma senha atende aos critérios de segurança."""
if len(senha) < 8:
return False, "Senha muito curta"
if not any(c.isupper() for c in senha):
return False, "Falta letra maiúscula"
if not any(c.isdigit() for c in senha):
return False, "Falta dígito"
return True, "Válida"
@pytest.mark.parametrize("senha,valida,motivo", [
("Senha123", True, "Válida"),
("senha123", False, "Falta letra maiúscula"),
("SENHA123", False, "Falta letra minúscula"),
("Senha", False, "Senha muito curta"),
("Abc12345", True, "Válida"),
("ABCD1234", False, "Falta letra minúscula"),
])
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):
"""Adiciona dois números."""
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 <= x <= 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) >= 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) > 0
Estratégia customizada
@given(st.emails())
def test_formato_email(email):
Um email deve conter um @
assert '@' 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):
"""Encontra o índice de um valor em uma lista. Bugado!"""
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) > 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 <= indice < 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):
"""Processa um pedido com validações."""
if valor < 0:
raise ValueError("Valor não pode ser negativo")
if datetime.fromisoformat(data_pedido) > datetime.now():
raise ValueError("Data não pode ser no futuro")
return {
'cliente_id': id_cliente,
'valor': valor,
'data': data_pedido,
'processado': 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['processado'] is True
assert resultado['cliente_id'] == id_cliente
assert resultado['valor'] == 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):
"""Conta números pares em uma lista. Bugado para listas grandes!"""
pares = [x for x in numeros if x % 2 == 0]
if len(pares) > 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 >= 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):
"""Aplica taxa de imposto baseada no estado."""
taxas = {
'SP': 0.18,
'RJ': 0.20,
'MG': 0.15,
'RS': 0.17,
}
if codigo_estado not in taxas:
raise ValueError(f"Estado desconhecido: {codigo_estado}")
taxa = taxas[codigo_estado]
return valor * (1 + taxa)
Testes parametrizados para casos de negócio conhecidos
@pytest.mark.parametrize("valor,estado,esperado", [
(100, 'SP', 118),
(100, 'RJ', 120),
(100, 'MG', 115),
(1000, 'SP', 1180),
(250.50, 'RJ', 300.60), # Teste com decimal
])
def test_aplicar_taxa_casos_conhecidos(valor, estado, esperado):
resultado = aplicar_imposto(valor, estado)
assert abs(resultado - esperado) < 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(['SP', 'RJ', 'MG', 'RS'])
)
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 > valor
E deve estar em um intervalo razoável
assert resultado < valor * 1.25 # Taxa máxima é 20%
def test_aplicar_taxa_estado_invalido():
with pytest.raises(ValueError):
aplicar_imposto(100, 'XX')</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):
"""Valida se uma string é uma URL bem-formada."""
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("url,valida", [
("https://www.google.com", True),
("http://localhost:8000", True),
("ftp://files.example.com/arquivo.zip", True),
("não é url", False),
("", False),
("http://", 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(['http', 'https', 'ftp']),
dominio=st.domains(),
caminho=st.text(alphabet='abcdefghijklmnopqrstuvwxyz0123456789/')
)
def test_url_valida_construida_tem_esquema_e_dominio(esquema, dominio, caminho):
url = f"{esquema}://{dominio}/{caminho}"
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><!-- FIM --></p>