pytest parametrize: testes parametrizados com exemplos

Aprenda pytest.mark.parametrize com testes de CEP, IDs legíveis, exceções, fixtures e indirect. Execute um projeto completo e evite erros comuns.

06 Oct 2026 9 min de leitura Equipe Python Dev BR

Use @pytest.mark.parametrize para executar a mesma regra de teste com várias entradas e resultados esperados, criando um caso independente para cada combinação. A lista de cenários fica explícita, as falhas aparecem separadamente e você não precisa copiar funções quase iguais. Use pytest.param(..., id="nome") para identificar cada cenário e reserve indirect para valores que precisam ser transformados por uma fixture.

Neste tutorial, vamos construir testes de normalização de CEP: entrada com ou sem hífen, espaços, zero à esquerda e formatos inválidos. O exemplo é útil em formulários e importações de cadastros brasileiros, mas validar o formato não confirma que o CEP existe. Não faremos consultas ao ViaCEP nem acessaremos dados pessoais.

Se você ainda não conhece descoberta de testes e fixtures, comece pelo guia de testes com pytest. Aqui o foco é escolher e organizar os cenários, entender a coleta e evitar armadilhas da parametrização.

Parametrize, loop ou fixture: qual escolher?

NecessidadeRecurso indicadoMotivo
Mesma regra, várias entradas e saídas esperadas@pytest.mark.parametrizeCada cenário vira um teste independente
Preparar arquivo temporário para o testeFixture, como tmp_pathSepara preparação da asserção
Executar vários testes com duas configuraçõesFixture com paramsCompartilha a matriz de configurações
Transformar um parâmetro em recursoindirect + fixturePreparação ocorre durante a execução do teste
Verificar uma coleção inteira como uma unidadeUm teste com loop pode bastarA coleção é o objeto do comportamento verificado

Um loop não é proibido. A diferença é o diagnóstico: se a segunda asserção falhar dentro de uma função, a terceira geralmente não será executada. Com parametrização, o pytest continua nos demais casos, salvo opções de interrupção como -x ou --maxfail=1.

Projeto completo: normalizar CEP e testar os formatos

Os exemplos usam Python 3.10 ou superior e pytest. Crie uma pasta vazia, um ambiente virtual e instale a dependência:

python -m venv .venv

Ative o ambiente no Linux ou macOS:

source .venv/bin/activate

No PowerShell do Windows, o comando correspondente é:

.\.venv\Scripts\Activate.ps1

Depois da ativação:

python -m pip install pytest

Se precisar de ajuda, consulte como criar um ambiente virtual. A estrutura inicial será:

projeto/
├── cep.py
└── test_cep.py

1. Implemente uma regra pequena e explícita

Salve como cep.py:

import re


def normalizar_cep(valor: str) -> str:
    if not isinstance(valor, str):
        raise TypeError("CEP deve ser texto")

    texto = valor.strip()
    if re.fullmatch(r"[0-9]{5}-?[0-9]{3}", texto) is None:
        raise ValueError("Formato de CEP inválido")

    return texto.replace("-", "")

O contrato é intencionalmente restrito: oito dígitos ASCII, hífen opcional na posição correta e espaços externos permitidos. Espaços internos e pontuação extra são rejeitados. Usamos [0-9] em vez de \d, pois \d também reconhece outros dígitos Unicode.

CEP é identificador, não quantidade. Mantenha-o como texto para preservar o zero inicial de 01001000. Não converta para int em uma planilha ou no carregamento de um CSV. Veja também validação de CPF, CNPJ e CEP para distinguir validação local de consulta a uma API.

2. Defina entradas e resultados esperados

Salve como test_cep.py:

import pytest

from cep import normalizar_cep


@pytest.mark.parametrize(
    "entrada, esperado",
    [
        pytest.param("01001000", "01001000", id="sem-hifen"),
        pytest.param("01001-000", "01001000", id="com-hifen"),
        pytest.param(" 01001-000 ", "01001000", id="espacos-externos"),
        pytest.param("30140-071", "30140071", id="outro-formato-valido"),
    ],
)
def test_normalizar_cep_valido(entrada, esperado):
    assert normalizar_cep(entrada) == esperado


@pytest.mark.parametrize(
    "entrada",
    [
        pytest.param("", id="vazio"),
        pytest.param("0100100", id="sete-digitos"),
        pytest.param("010010000", id="nove-digitos"),
        pytest.param("0100-1000", id="hifen-fora-de-posicao"),
        pytest.param("01001 000", id="espaco-interno"),
        pytest.param("ABCDE-000", id="letras"),
        pytest.param("01001000", id="digitos-nao-ascii"),
    ],
)
def test_rejeitar_formato_invalido(entrada):
    with pytest.raises(ValueError, match="Formato de CEP inválido"):
        normalizar_cep(entrada)


@pytest.mark.parametrize(
    "entrada",
    [
        pytest.param(None, id="nulo"),
        pytest.param(1001000, id="inteiro"),
    ],
)
def test_rejeitar_tipo_invalido(entrada):
    with pytest.raises(TypeError, match="CEP deve ser texto"):
        normalizar_cep(entrada)

Execute na raiz do projeto:

python -m pytest -q

São 13 casos: quatro válidos, sete formatos inválidos e dois tipos inválidos. Cada item da lista gera uma execução da função correspondente.

A ordem em "entrada, esperado" corresponde à ordem dos valores de cada pytest.param. A função precisa receber argumentos com esses mesmos nomes. Para um só argumento, passe diretamente os valores; não é necessário embrulhar cada string em uma tupla.

Os resultados esperados são literais definidos a partir do contrato. Evite calculá-los chamando a própria função testada: comparar uma função com ela mesma pode aprovar o mesmo erro duas vezes.

IDs: encontre o caso que falhou sem ler toda a matriz

Sem IDs explícitos, o pytest tenta gerar nomes a partir dos parâmetros. Isso funciona para valores simples, mas pode produzir identificadores pouco úteis para objetos complexos.

pytest.param permite colocar o nome ao lado dos dados, o que facilita manter uma matriz grande. Outra opção é ids=["caso-a", "caso-b"] no decorator; a lista deve acompanhar a quantidade e a ordem dos cenários.

Confira os testes coletados sem executá-los:

python -m pytest --collect-only -q

A lista inclui um identificador como:

test_cep.py::test_normalizar_cep_valido[com-hifen]

Você pode executar apenas esse caso, usando aspas para proteger os colchetes no shell:

python -m pytest 'test_cep.py::test_normalizar_cep_valido[com-hifen]' -v

Também pode selecionar por expressão:

python -m pytest -k 'com-hifen' -v

-k seleciona por nomes e palavras-chave dos testes; não avalia o conteúdo dos parâmetros. Prefira o identificador completo quando precisar de uma seleção exata. Não coloque tokens, e-mails reais ou dados de clientes nos IDs: eles podem aparecer em logs e relatórios do CI.

Exceções: separe sucesso de entrada inválida

No projeto acima, os cenários válidos e inválidos ficam em funções diferentes. Isso mantém a leitura simples: uma função compara o resultado, outra exige uma exceção.

pytest.raises(ValueError) confirma que o bloco lançou o tipo esperado. O argumento match verifica a mensagem com uma expressão regular, não com uma comparação literal. Se a mensagem tiver caracteres especiais como parênteses ou pontos e você quiser uma busca literal, escape o padrão com re.escape().

Não use apenas pytest.raises(Exception) por comodidade. Um erro inesperado de implementação poderia ser aceito como se fosse a rejeição correta da entrada. Para revisar os fundamentos, consulte tratamento de erros em Python.

Combinar fixtures com argumentos parametrizados

Uma fixture comum pode coexistir com parametrize. Por exemplo, tmp_path prepara um diretório temporário separado para cada teste. Acrescente ao mesmo test_cep.py:

@pytest.mark.parametrize(
    "conteudo, esperado",
    [
        pytest.param("01001-000\n", "01001000", id="arquivo-com-hifen"),
        pytest.param("30140071\n", "30140071", id="arquivo-sem-hifen"),
    ],
)
def test_ler_cep_de_arquivo(tmp_path, conteudo, esperado):
    arquivo = tmp_path / "cep.txt"
    arquivo.write_text(conteudo, encoding="utf-8")

    entrada = arquivo.read_text(encoding="utf-8")
    assert normalizar_cep(entrada) == esperado

conteudo e esperado vêm da parametrização; tmp_path vem do pytest. Não inclua tmp_path na lista de nomes do decorator se deseja receber a fixture normalmente.

O teste usa somente dados de demonstração e arquivos temporários. Para pipelines maiores, isso evita depender de uma pasta pessoal ou sobrescrever um arquivo de trabalho. O tutorial de pathlib aprofunda as operações com caminhos.

Quando usar indirect

Use indirect quando o parâmetro descreve como preparar um recurso, e não quando ele já é o valor final que o teste precisa. Acrescente este exemplo ao arquivo de testes:

@pytest.fixture
def arquivo_cep(request, tmp_path):
    arquivo = tmp_path / "entrada.txt"
    arquivo.write_text(request.param, encoding="utf-8")
    return arquivo


@pytest.mark.parametrize(
    "arquivo_cep, esperado",
    [
        pytest.param("01001-000\n", "01001000", id="arquivo-sp"),
        pytest.param("30140-071\n", "30140071", id="arquivo-bh"),
    ],
    indirect=["arquivo_cep"],
)
def test_cep_com_preparacao_indireta(arquivo_cep, esperado):
    entrada = arquivo_cep.read_text(encoding="utf-8")
    assert normalizar_cep(entrada) == esperado

O fluxo é:

  1. O pytest coleta o cenário com uma string de conteúdo.
  2. Na preparação do teste, envia essa string à fixture como request.param.
  3. A fixture cria o arquivo e devolve um objeto Path.
  4. O teste recebe o Path em arquivo_cep e a string final em esperado.

Usamos indirect=["arquivo_cep"], não indirect=True, para transformar apenas o argumento que precisa da fixture. Com True, todos os argumentos parametrizados seriam tratados como nomes de fixtures — inclusive esperado.

Se o teste só precisa de uma string, não use indireção. Ela é útil para clientes, arquivos e recursos com preparação ou limpeza; para uma soma ou validação pura, deixa o exemplo mais complicado sem benefício.

Fixture com params: reutilize configurações entre testes

Quando vários testes devem rodar com as mesmas configurações, a parametrização pode morar na fixture. Este exemplo independente pode ser salvo como test_separadores.py:

import csv
import io

import pytest


@pytest.fixture(params=[",", ";"], ids=["virgula", "ponto-e-virgula"])
def separador(request):
    return request.param


def test_csv_preserva_zero_inicial(separador):
    conteudo = f"cep{separador}cidade\n01001000{separador}São Paulo\n"
    linhas = list(csv.DictReader(io.StringIO(conteudo), delimiter=separador))
    assert linhas[0]["cep"] == "01001000"


def test_csv_preserva_nome_da_cidade(separador):
    conteudo = f"cep{separador}cidade\n01001000{separador}São Paulo\n"
    linhas = list(csv.DictReader(io.StringIO(conteudo), delimiter=separador))
    assert linhas[0]["cidade"] == "São Paulo"

São quatro casos: duas funções vezes dois separadores. A lógica de CSV trata campos como texto nesse exemplo, sem conversão numérica. Para importar dados de planilhas e sistemas distintos, veja leitura e escrita de CSV em Python.

Decorators empilhados geram produto cartesiano

Empilhar parametrizações é útil quando todas as combinações importam. Por exemplo, duas opções de um argumento e três de outro geram seis casos. Não funciona como um zip que combina somente itens de mesma posição.

Se cada entrada tem um resultado específico, coloque ambos na mesma tupla, como fizemos com entrada, esperado. Separar entradas e resultados em decorators diferentes criaria combinações incorretas.

Também cuide do tamanho da suíte: cinco dimensões de dez opções geram 100 mil casos. Teste todas as combinações quando houver razão técnica, não apenas porque o decorator permite. Matrizes pequenas de limites e regressões costumam ser mais fáceis de manter.

Erros comuns em testes parametrizados

  • Nome divergente: o decorator declara entrada, mas a função recebe valor. Corrija os nomes; o problema acontece na coleta, antes da asserção.
  • Quantidade de valores incorreta: três nomes exigem três valores em cada cenário.
  • Argumento chamado request: esse nome é reservado no contexto da parametrização do pytest. Escolha um nome como requisicao para seus dados.
  • Dados mutáveis compartilhados: listas e dicionários são passados como estão, sem cópia automática. Se vários casos referenciam o mesmo objeto e o código o modifica, um caso pode contaminar o seguinte. Crie objetos novos ou prepare cópias adequadas em fixtures.
  • Lista vazia de cenários: com a configuração padrão, uma parametrização vazia resulta em um teste pulado. Se os dados deveriam existir, valide a geração da matriz e considere configurar empty_parameter_set_mark para falhar na coleta.
  • Misturar rede com validação local: um teste de formato não precisa chamar ViaCEP. Teste o cliente HTTP separadamente com transporte controlado e mantenha testes de integração identificados.
  • Usar xfail para esconder um defeito: essa marca comunica uma falha esperada conhecida. Se usada, documente o motivo e considere strict=True para sinalizar quando o teste passar inesperadamente.

Checklist rápido

Antes de enviar a mudança ao repositório:

  • A regra testada tem um contrato explícito?
  • Há cenários comuns, limites e entradas inválidas relevantes?
  • Os resultados esperados são independentes da implementação?
  • Os IDs explicam o cenário sem expor dados pessoais?
  • Fixtures preparam recursos e as funções verificam comportamento?
  • A matriz evita combinações redundantes ou impossíveis?
  • --collect-only mostra a quantidade esperada?
  • Todos os testes passam com python -m pytest -q?

Com todos os arquivos e acréscimos deste tutorial, a suíte tem 21 casos: 13 iniciais, dois com tmp_path, dois com indirect e quatro de CSV. Isso oferece uma checagem simples de que você montou os exemplos por completo.

Conclusão

pytest.mark.parametrize torna uma regra verificável em muitos cenários sem duplicar funções. Comece por entradas e resultados explícitos, nomeie os casos e teste as falhas com exceções específicas. Adicione fixtures quando houver recursos a preparar; use indirect apenas quando a transformação fizer parte dessa preparação.

Para um projeto de portfólio, explique no README quais falhas sua matriz detecta e como executar a suíte. Essa demonstração é mais útil do que uma grande quantidade de testes sem propósito — especialmente ao discutir qualidade de código em uma entrevista para vagas Python.

Referências

E
Equipe Python Dev BR

Contribuidor do Python Dev BR

Artigos relacionados