Como usar pytest parametrize para cobrir múltiplos casos de teste
1. Introdução ao pytest parametrize: por que usar?
Escrever testes unitários repetitivos é um dos maiores pesadelos da manutenção de software. Imagine precisar testar uma função soma(a, b) com 20 combinações diferentes de entrada. Sem parametrização, você criaria 20 funções de teste quase idênticas, violando o princípio DRY (Don't Repeat Yourself) e tornando o código difícil de manter.
O @pytest.mark.parametrize resolve esse problema ao permitir que você execute o mesmo teste com múltiplos conjuntos de argumentos. Com um único decorator, você define os dados de entrada e o pytest gera automaticamente um caso de teste para cada combinação.
Benefícios principais:
- Cobertura ampla: teste centenas de combinações com poucas linhas
- Legibilidade: os dados de teste ficam claramente separados da lógica
- Facilidade de manutenção: adicionar ou remover casos é trivial
2. Sintaxe básica do decorator parametrize
A estrutura fundamental do parametrize é:
@pytest.mark.parametrize("arg1, arg2", [
(valor1_a, valor2_a),
(valor1_b, valor2_b)
])
Vamos testar uma função simples de soma:
# calculadora.py
def soma(a, b):
return a + b
# test_calculadora.py
import pytest
from calculadora import soma
@pytest.mark.parametrize("a, b, esperado", [
(1, 2, 3),
(0, 0, 0),
(-1, 1, 0),
(100, -50, 50),
(2.5, 3.5, 6.0)
])
def test_soma(a, b, esperado):
assert soma(a, b) == esperado
Quando executamos pytest -v, vemos cinco testes separados, cada um nomeado com os valores dos parâmetros. Se um falhar, sabemos exatamente qual combinação causou o problema.
3. Combinando múltiplos decorators parametrize
Você pode empilhar vários decorators parametrize no mesmo teste. O pytest cria o produto cartesiano entre todos os argumentos:
@pytest.mark.parametrize("operacao", ["soma", "subtracao"])
@pytest.mark.parametrize("a, b", [
(10, 5),
(0, 1),
(-3, -7)
])
def test_operacoes(operacao, a, b):
if operacao == "soma":
resultado = a + b
else:
resultado = a - b
# lógica de teste...
Isso gera 6 testes (2 operações × 3 pares de valores). Cuidado com a explosão combinatória: se você adicionar muitos parâmetros, o número de testes cresce exponencialmente, podendo impactar o tempo de execução.
4. Trabalhando com fixtures e parametrize juntos
Fixtures podem ser combinadas com parametrize para criar testes ainda mais flexíveis. Exemplo com configurações de banco de dados:
import pytest
import sqlite3
@pytest.fixture(params=[
{"db": ":memory:", "timeout": 5},
{"db": "test.db", "timeout": 10}
])
def config_conexao(request):
return request.param
@pytest.mark.parametrize("query, esperado", [
("SELECT 1", [(1,)]),
("SELECT 2+2", [(4,)])
])
def test_conexao_banco(config_conexao, query, esperado):
conn = sqlite3.connect(**config_conexao)
cursor = conn.cursor()
cursor.execute(query)
resultado = cursor.fetchall()
assert resultado == esperado
conn.close()
Aqui, cada configuração de banco é combinada com cada consulta SQL, gerando 4 testes no total.
5. Cobertura de casos de borda e exceções
Testar exceções é crucial. Use pytest.raises dentro de testes parametrizados:
import pytest
def dividir(a, b):
if b == 0:
raise ValueError("Divisão por zero")
return a / b
@pytest.mark.parametrize("a, b, esperado_ou_excecao", [
(10, 2, 5.0),
(0, 5, 0.0),
(-6, 3, -2.0),
(1, 0, ValueError),
(None, 2, TypeError)
])
def test_dividir(a, b, esperado_ou_excecao):
if esperado_ou_excecao in (ValueError, TypeError):
with pytest.raises(esperado_ou_excecao):
dividir(a, b)
else:
assert dividir(a, b) == esperado_ou_excecao
Inclua sempre casos de borda: valores nulos, vazios, negativos, extremos (máximo inteiro, mínimo float). Organize os dados em tuplas nomeadas ou dicionários para melhor legibilidade quando houver muitos parâmetros.
6. Boas práticas e dicas avançadas
Nomeando casos com ids:
@pytest.mark.parametrize("a, b, esperado", [
(1, 2, 3),
(0, 0, 0),
(-1, 1, 0)
], ids=[
"positivos_simples",
"zeros",
"opostos"
])
def test_soma(a, b, esperado):
assert soma(a, b) == esperado
Os ids aparecem nos relatórios de teste, facilitando a identificação de falhas.
Usando dicionários como parâmetros:
CASOS_TESTE = [
{"a": 10, "b": 5, "esperado": 15, "descricao": "soma_de_dois_positivos"},
{"a": -3, "b": 7, "esperado": 4, "descricao": "soma_com_negativo"}
]
@pytest.mark.parametrize("caso", CASOS_TESTE, ids=[c["descricao"] for c in CASOS_TESTE])
def test_soma_dict(caso):
assert soma(caso["a"], caso["b"]) == caso["esperado"]
Evite mutabilidade: nunca use listas ou dicionários mutáveis como valores padrão em parametrize, pois eles podem ser alterados entre execuções. Prefira tuplas imutáveis.
7. Exemplo completo: testando uma calculadora com parametrize
Vamos construir uma bateria de testes para operações matemáticas:
# calculadora.py
def soma(a, b): return a + b
def subtracao(a, b): return a - b
def multiplicacao(a, b): return a * b
def divisao(a, b):
if b == 0:
raise ValueError("Divisão por zero")
return a / b
def potencia(a, b): return a ** b
# test_calculadora.py
import pytest
from calculadora import soma, subtracao, multiplicacao, divisao, potencia
OPERACOES = {
"soma": soma,
"subtracao": subtracao,
"multiplicacao": multiplicacao,
"potencia": potencia
}
CASOS_PADRAO = [
(2, 3, 6, "positivos"),
(0, 5, 0, "zero_e_positivo"),
(-2, 4, -8, "negativo_e_positivo"),
(-3, -2, 6, "dois_negativos")
]
@pytest.mark.parametrize("operacao_nome, func", OPERACOES.items())
@pytest.mark.parametrize("a, b, esperado, nome_caso", CASOS_PADRAO)
def test_operacoes_padrao(operacao_nome, func, a, b, esperado, nome_caso):
resultado = func(a, b)
assert resultado == esperado, f"Falhou em {operacao_nome} - {nome_caso}"
@pytest.mark.parametrize("a, b", [
(10, 0),
(0, 0),
(-5, 0)
], ids=["positivo_div_zero", "zero_div_zero", "negativo_div_zero"])
def test_divisao_por_zero(a, b):
with pytest.raises(ValueError):
divisao(a, b)
Este exemplo cobre 16 combinações (4 operações × 4 casos) para operações padrão, mais 3 casos de divisão por zero. Integre com linters como pytest-flake8 e pytest-black para garantir consistência de código.
8. Conclusão: como parametrize se encaixa no pipeline de qualidade
O pytest parametrize é uma ferramenta essencial no pipeline de qualidade de software. Ele se conecta diretamente com:
- Cobertura de código: ferramentas como
pytest-covmostram quais linhas são executadas em cada caso parametrizado - Análise estática: linters detectam testes parametrizados mal escritos (parâmetros não utilizados, nomes inconsistentes)
- TDD (Test-Driven Development): parametrize acelera o ciclo vermelho-verde-refatoração, permitindo adicionar rapidamente novos casos conforme descobre bordas
Para ir além, explore plugins como:
- pytest-xdist para executar testes parametrizados em paralelo
- pytest-randomly para embaralhar a ordem de execução
- pytest-benchmark para medir desempenho de diferentes combinações
Com parametrize, você transforma testes repetitivos em dados declarativos, ganhando tempo e confiança na qualidade do código.
Referências
- Documentação oficial do pytest parametrize — Guia completo com exemplos de sintaxe, combinação de decorators e boas práticas
- Real Python: How to Use pytest for Unit Testing — Tutorial prático cobrindo fixtures, parametrize e testes de exceções
- Python Testing with pytest (Brian Okken) — Livro referência sobre pytest, com capítulo dedicado a parametrização avançada
- TestDriven.io: Pytest Parametrize Guide — Artigo detalhado com exemplos de casos de borda, ids e dicionários como parâmetros
- PyPI: pytest-cov — Plugin para medir cobertura de código, essencial para validar a abrangência dos testes parametrizados
- PyPI: pytest-xdist — Plugin para execução paralela de testes, útil quando muitos casos parametrizados tornam a suite lenta