pytest.mark.parametrize executa a mesma função de teste com diferentes argumentos. Cada combinação vira um caso independente, o que melhora a localização de falhas e evita copiar a preparação e a asserção.

Nomear casos importantes

import pytest


@pytest.mark.parametrize(
    ("texto", "esperado"),
    [
        pytest.param("  Python ", "python", id="espacos-e-caixa"),
        pytest.param("", "", id="vazio"),
        pytest.param("API", "api", id="sigla"),
    ],
)
def test_normalizar(texto: str, esperado: str) -> None:
    assert texto.strip().lower() == esperado

IDs curtos tornam o relatório legível. Evite inserir dados sensíveis ou objetos enormes no ID. Para exceções, parametrize entradas inválidas e use pytest.raises dentro do teste; não misture muitos fluxos diferentes apenas para reduzir linhas.

Dois decorators empilhados criam um produto cartesiano. Isso é útil para compatibilidade entre dimensões independentes, mas pode multiplicar casos sem ganho. Se apenas algumas combinações importam, liste-as explicitamente.

Parametrização fornece dados; fixtures controlam preparação, dependências e limpeza. O artigo sobre fixtures, mocks e monkeypatch mostra como dividir essas responsabilidades. Uma falha em um caso não deve contaminar o seguinte, portanto evite estado global mutável.

A documentação oficial de parametrização do pytest, consultada em 22 de julho de 2026, explica parâmetros em funções, fixtures, módulos e geração dinâmica.

Começar pelo comportamento, não por uma massa de dados

Um bom conjunto de parâmetros descreve uma regra com exemplos representativos. Comece pelo sucesso comum, limites e uma falha que já causou problema. Centenas de valores arbitrários alongam o relatório sem necessariamente validar uma propriedade relevante.

def custo_frete(total: int) -> int:
    if total < 0:
        raise ValueError("total deve ser nao negativo")
    return 0 if total >= 100 else 12


@pytest.mark.parametrize(
    ("total", "esperado"),
    [
        pytest.param(0, 12, id="pedido-vazio"),
        pytest.param(99, 12, id="abaixo-do-limite"),
        pytest.param(100, 0, id="no-limite"),
        pytest.param(101, 0, id="acima-do-limite"),
    ],
)
def test_custo_frete(total: int, esperado: int) -> None:
    assert custo_frete(total) == esperado

O ID deve explicar por que o caso existe. Nomes estáveis do domínio, como no-limite, ajudam mais que caso-3. IDs explícitos são especialmente úteis quando os valores são opacos, longos ou propensos a mudança.

Testar exceções sem esconder a asserção

Separe fluxos válidos e inválidos quando as asserções forem diferentes. Assim o contrato fica visível e o teste não precisa de condicionais.

@pytest.mark.parametrize("total", [-1, -50], ids=["menos-um", "menos-cinquenta"])
def test_custo_frete_rejeita_total_negativo(total: int) -> None:
    with pytest.raises(ValueError, match="nao negativo"):
        custo_frete(total)

Verificar o tipo da exceção é o mínimo. Compare uma parte estável da mensagem somente quando ela fizer parte da interface. Não prenda o teste a traceback, pontuação ou detalhes internos.

Combinar fixtures e parâmetros com intenção

Parâmetros descrevem variações de entrada ou expectativa. Fixtures fornecem dependências, preparação e limpeza. Ambos podem aparecer na assinatura:

@pytest.mark.parametrize("papel", ["leitor", "editor"])
def test_perfil_visivel(client, usuario_factory, papel: str) -> None:
    usuario = usuario_factory(papel=papel)
    resposta = client.get(f"/usuarios/{usuario.id}")
    assert resposta.status_code == 200

Quando uma fixture precisa interpretar o parâmetro, use parametrização indireta:

@pytest.fixture
def banco(request):
    return conectar_para_teste(engine=request.param)


@pytest.mark.parametrize("banco", ["sqlite", "postgres"], indirect=True)
def test_repositorio_salva_e_le(banco) -> None:
    repositorio = Repositorio(banco)
    repositorio.salvar({"id": 1, "nome": "Ada"})
    assert repositorio.buscar(1)["nome"] == "Ada"

Use o modo indireto com moderação, pois o valor ganha significado em dois lugares. Uma fixture comum ou uma fixture fábrica costuma ser mais clara em cenários isolados.

Entender decorators empilhados

Decorators empilhados geram todas as combinações. Dois navegadores e três idiomas resultam em seis testes:

@pytest.mark.parametrize("idioma", ["en", "es", "pt"])
@pytest.mark.parametrize("navegador", ["chromium", "firefox"])
def test_pagina_inicial(navegador: str, idioma: str) -> None:
    ...

Isso faz sentido quando as dimensões são independentes e todas as combinações são suportadas. Se apenas algumas configurações existem em produção, enumere tuplas explicitamente. Uma matriz menor e intencional é mais rápida e fácil de interpretar.

Marcar casos individuais

pytest.param permite anexar marcas a um caso. Use xfail para uma limitação conhecida e documentada, e skip quando o teste realmente não puder rodar no ambiente.

@pytest.mark.parametrize(
    ("valor", "esperado"),
    [
        ("10", 10),
        pytest.param(
            "١٠",
            10,
            marks=pytest.mark.xfail(reason="algarismos arabes ainda sem suporte"),
            id="algarismos-arabes",
        ),
    ],
)
def test_converter_numero(valor: str, esperado: int) -> None:
    assert converter_numero(valor) == esperado

Não transforme xfail em destino permanente para uma falha inexplicada. Relacione a limitação a um trabalho rastreável, use motivo específico e considere modo estrito para detectar uma aprovação inesperada.

Gerar casos dinamicamente só quando necessário

pytestmark pode parametrizar módulo ou classe. O hook pytest_generate_tests cria casos durante a coleta, útil quando implementações suportadas vêm de um registro ou opção de linha de comando. Para uma lista fixa e curta, o decorator é mais legível.

Não consulte serviços remotos durante a coleta. A suíte fica lenta e instável antes mesmo da execução. Guarde casos de contrato estáveis no repositório e deixe integrações externas em uma suíte explícita.

Evitar valores mutáveis compartilhados

pytest passa os objetos de parâmetro como estão. Se um teste altera uma lista ou dicionário, outro caso pode observar a alteração. Prefira valores imutáveis ou faça uma cópia:

@pytest.mark.parametrize("dados", [{"itens": []}, {"itens": ["livro"]}])
def test_adicionar_item(dados: dict[str, list[str]]) -> None:
    dados_locais = {"itens": dados["itens"].copy()}
    dados_locais["itens"].append("caneta")
    assert "caneta" in dados_locais["itens"]

A mesma regra vale para banco, ambiente, relógio e cache. Parametrização cria itens de teste separados, mas não desfaz estado externo.

Executar e diagnosticar casos

Use pytest -vv para ver IDs completos, pytest -k limite para selecionar casos pelo nome e pytest --collect-only para inspecionar a matriz sem executá-la. O último comando é valioso depois de empilhar decorators.

Quando um caso falha, o ID deve apontar para a fronteira de negócio. A asserção mostra valores real e esperado. Imprimir dados em todos os casos raramente é necessário, pois pytest já oferece boa introspecção.

Checklist de revisão

Confirme se cada linha representa regra ou risco distinto, se os IDs são estáveis e seguros e se uma falha não contamina as demais. Verifique se a matriz não cresceu por acidente, se entradas inválidas exigem a exceção correta e se fixtures continuam responsáveis pela limpeza.

Parametrização reduz repetição quando comportamento e asserção permanecem iguais. Se cada linha exige branches, mocks e verificações diferentes, divida o teste. Uma duplicação clara pode custar menos que uma tabela compacta que esconde vários comportamentos.

Escolher casos de fronteira com método

Para intervalos numéricos, inclua valores logo abaixo, no ponto e logo acima do limite. Em texto, considere entrada vazia, espaços, Unicode e tamanho máximo documentado. Em coleções, diferencie vazio, um item e vários itens. São lembretes, não uma obrigação de combinar todas as possibilidades em cada função.

Parta do contrato público e de falhas conhecidas. Se determinada entrada nunca alcança a função porque uma validação anterior a rejeita, teste na fronteira de validação. Repetir valores impossíveis em testes internos gera ruído e acoplamento à implementação.

Ferramentas pairwise reduzem matrizes grandes, enquanto testes baseados em propriedades exploram espaços amplos e minimizam falhas. parametrize continua ideal para exemplos nomeados que revisores devem compreender isoladamente. As abordagens se complementam.

Manter os dados legíveis

Quando uma linha vira uma tupla longa de booleanos e resultados, crie um objeto de caso imutável:

from dataclasses import dataclass


@dataclass(frozen=True)
class CasoPreco:
    nome: str
    subtotal: int
    membro: bool
    esperado: int


CASOS = [
    CasoPreco("comum", 100, False, 100),
    CasoPreco("desconto-membro", 100, True, 90),
]


@pytest.mark.parametrize("caso", CASOS, ids=lambda caso: caso.nome)
def test_preco_final(caso: CasoPreco) -> None:
    assert preco_final(caso.subtotal, caso.membro) == caso.esperado

Campos nomeados evitam erros de ordem. Mantenha essa estrutura perto dos testes, salvo quando vários módulos realmente compartilham os mesmos contratos. Um catálogo global pode virar outro sistema difícil de navegar.

Não coloque segredos, registros reais de clientes ou conjuntos licenciados nos parâmetros. Use exemplos sintéticos mínimos com o formato relevante. Relatórios e artefatos de integração contínua frequentemente exibem a representação dos valores.

Preservar isolamento entre falhas

pytest pode continuar após a falha de um caso. Essa vantagem some se os casos gravam no mesmo arquivo, consomem um iterador compartilhado ou dependem da ordem. Forneça diretório temporário com tmp_path, objetos novos por fixtures e estado independente no banco.

Não trate a ordem dos decorators como contrato de execução. Os testes devem funcionar com ordem aleatória e execução paralela quando o projeto as suporta. Se o cenário exige uma sequência real de passos, ele é um teste de fluxo, não vários casos parametrizados.

Mantenha o nome da função centrado no comportamento. O ID acrescenta o cenário. Um relatório como test_custo_frete[no-limite] é curto, pesquisável e informa tanto a regra quanto a condição da falha.