Testes com pytest: fixtures, markers e plugins

1. Introdução ao pytest e sua filosofia

O pytest se destaca como um dos frameworks de teste mais populares do ecossistema Python. Diferentemente do unittest, que exige classes herdadas de TestCase e métodos específicos, o pytest adota uma abordagem mais funcional e concisa. Sua filosofia central é permitir que você escreva testes simples com código Python puro, sem cerimônias.

Para instalar o pytest, basta um comando:

pip install pytest

A estrutura básica de um teste é surpreendentemente simples:

# test_exemplo.py
def test_soma():
    assert 1 + 1 == 2

def test_string():
    assert "python".upper() == "PYTHON"

O pytest descobre automaticamente arquivos com prefixo test_ ou sufixo _test.py, e funções com prefixo test_. Para executar, use:

pytest

2. Fixtures: o coração do pytest

Fixtures são funções que fornecem dados ou estado para os testes, gerenciando setup e teardown de forma elegante. Elas substituem os métodos setUp e tearDown do unittest.

import pytest

@pytest.fixture
def usuario_padrao():
    return {"nome": "João", "email": "joao@email.com"}

def test_criar_usuario(usuario_padrao):
    assert usuario_padrao["nome"] == "João"

O escopo das fixtures define sua vida útil:

  • function (padrão): recriada para cada teste
  • class: uma vez por classe de teste
  • module: uma vez por módulo
  • session: uma vez por sessão de teste
@pytest.fixture(scope="module")
def conexao_banco():
    print("\nConectando ao banco...")
    conexao = {"status": "conectado"}
    yield conexao
    print("\nDesconectando do banco...")

O uso de yield permite executar código de limpeza após o teste, funcionando como um teardown automático.

3. Compartilhamento e modularização de fixtures

Para compartilhar fixtures entre vários arquivos de teste, use o arquivo conftest.py. Ele é automaticamente descoberto pelo pytest e suas fixtures ficam disponíveis para todos os testes no diretório e subdiretórios.

# conftest.py
import pytest

@pytest.fixture
def dados_api():
    return {"endpoint": "https://api.exemplo.com", "token": "abc123"}

Fixtures podem depender de outras fixtures:

@pytest.fixture
def cliente_api(dados_api):
    return f"Cliente conectado a {dados_api['endpoint']}"

def test_conexao(cliente_api):
    assert "Cliente conectado" in cliente_api

Para parametrizar fixtures, use o parâmetro params:

@pytest.fixture(params=[1, 2, 3])
def numero(request):
    return request.param

def test_dobro(numero):
    assert numero * 2 == numero * 2

4. Markers: organizando e filtrando testes

Markers permitem adicionar metadados aos testes, facilitando a execução seletiva. O pytest já inclui marcadores úteis:

import pytest

@pytest.mark.skip(reason="Funcionalidade ainda não implementada")
def test_futuro():
    pass

@pytest.mark.xfail(reason="Bug conhecido, será corrigido na versão 2.0")
def test_bug_conhecido():
    assert 1 / 0 == 0

Você também pode criar marcadores customizados. Primeiro, registre-os no pyproject.toml ou pytest.ini:

# pytest.ini
[pytest]
markers =
    lento: Testes que demoram muito
    integracao: Testes que dependem de serviços externos

Depois, use-os nos testes:

@pytest.mark.lento
@pytest.mark.integracao
def test_api_externa():
    import time
    time.sleep(5)
    assert True

Para executar apenas testes com determinados marcadores:

pytest -m "lento"          # Apenas testes lentos
pytest -m "not lento"      # Exclui testes lentos
pytest -m "lento and integracao"  # Combinação

5. Parametrização de testes com @pytest.mark.parametrize

A parametrização permite testar múltiplos cenários com o mesmo código de teste:

@pytest.mark.parametrize("entrada, esperado", [
    (2, 4),
    (3, 9),
    (4, 16),
    (0, 0),
    (-2, 4),
])
def test_quadrado(entrada, esperado):
    assert entrada ** 2 == esperado

Para tornar os relatórios mais legíveis, use IDs descritivos:

@pytest.mark.parametrize(
    "senha, valida",
    [
        ("abc123", True),
        ("12345", False),
        ("", False),
        ("senha_segura_2024", True),
    ],
    ids=["válida", "curta", "vazia", "válida longa"]
)
def test_validar_senha(senha, valida):
    assert (len(senha) >= 6) == valida

É possível combinar parametrização com fixtures:

@pytest.fixture
def banco_dados():
    return {"itens": []}

@pytest.mark.parametrize("item, quantidade", [
    ("maçã", 3),
    ("banana", 5),
])
def test_adicionar_itens(banco_dados, item, quantidade):
    for _ in range(quantidade):
        banco_dados["itens"].append(item)
    assert banco_dados["itens"].count(item) == quantidade

6. Plugins essenciais para produtividade

pytest-cov

Mede a cobertura de código dos seus testes:

pip install pytest-cov
pytest --cov=meu_modulo --cov-report=html

pytest-mock

Simplifica o mocking com uma fixture mocker:

pip install pytest-mock

def test_buscar_usuario(mocker):
    mock_resposta = {"id": 1, "nome": "Maria"}
    mocker.patch("requests.get", return_value=mock_resposta)

    resultado = buscar_usuario(1)
    assert resultado["nome"] == "Maria"

pytest-xdist

Executa testes em paralelo para acelerar o feedback:

pip install pytest-xdist
pytest -n auto  # Usa todos os núcleos da CPU

7. Boas práticas e padrões avançados

O pytest oferece fixtures embutidas poderosas:

def test_arquivo_temporario(tmp_path):
    arquivo = tmp_path / "dados.txt"
    arquivo.write_text("conteúdo")
    assert arquivo.read_text() == "conteúdo"

def test_variavel_ambiente(monkeypatch):
    monkeypatch.setenv("DB_URL", "sqlite:///test.db")
    assert obter_url_banco() == "sqlite:///test.db"

Para testes com banco de dados:

@pytest.fixture(scope="function")
def db_session():
    from meu_app import criar_conexao, criar_tabelas
    conexao = criar_conexao("sqlite:///:memory:")
    criar_tabelas(conexao)
    yield conexao
    conexao.close()

def test_inserir_usuario(db_session):
    db_session.executar("INSERT INTO usuarios VALUES (1, 'Ana')")
    resultado = db_session.consultar("SELECT nome FROM usuarios WHERE id=1")
    assert resultado[0][0] == "Ana"

Estrutura de pastas recomendada:

projeto/
├── src/
│   └── meu_modulo/
│       ├── __init__.py
│       ├── calculadora.py
│       └── banco.py
├── tests/
│   ├── conftest.py
│   ├── test_calculadora.py
│   └── test_banco.py
├── pyproject.toml
└── pytest.ini

Com essa estrutura, execute pytest tests/ para rodar todos os testes.

O pytest transforma a escrita de testes em uma experiência produtiva e agradável. Sua combinação de fixtures flexíveis, marcadores poderosos e um ecossistema rico de plugins o torna indispensável para projetos Python de qualquer tamanho.

Referências