Guia

Testes com pytest: Guia Completo

Aprenda a testar seu código Python com pytest. Fixtures, parametrize, mocking, cobertura de testes e boas práticas para projetos reais

5 min de leitura

Introdução

Testar código é uma das habilidades mais importantes para qualquer desenvolvedor Python. O pytest é o framework de testes mais popular do ecossistema Python, usado por projetos como Django, Flask e Requests. Ele é simples para começar e poderoso o suficiente para projetos complexos.

Neste guia, vamos aprender pytest do básico ao avançado, cobrindo fixtures, parametrize, mocking e cobertura de testes.

Instalação

Crie um ambiente virtual e instale o pytest:

python3 -m venv venv
source venv/bin/activate
pip install pytest

Verifique a instalação:

pytest --version

Seu primeiro teste

Crie um arquivo calculadora.py:

def somar(a, b):
    return a + b


def dividir(a, b):
    if b == 0:
        raise ValueError("Divisao por zero nao permitida")
    return a / b


def eh_par(numero):
    return numero % 2 == 0

Agora crie test_calculadora.py:

from calculadora import somar, dividir, eh_par
import pytest


def test_somar_positivos():
    assert somar(2, 3) == 5


def test_somar_negativos():
    assert somar(-1, -2) == -3


def test_dividir():
    assert dividir(10, 2) == 5.0


def test_dividir_por_zero():
    with pytest.raises(ValueError):
        dividir(10, 0)


def test_eh_par():
    assert eh_par(4) is True
    assert eh_par(7) is False

Execute os testes:

pytest

O pytest descobre automaticamente arquivos que começam com test_ e funções que começam com test_. A saída mostra pontos verdes para testes que passaram e F vermelho para falhas.

Opções úteis da linha de comando

# Saida detalhada (verbose)
pytest -v

# Parar no primeiro erro
pytest -x

# Executar apenas um arquivo
pytest test_calculadora.py

# Executar apenas um teste especifico
pytest test_calculadora.py::test_somar_positivos

# Filtrar por nome (executa testes que contem "dividir")
pytest -k "dividir"

# Mostrar prints durante os testes
pytest -s

Organizando testes

A estrutura recomendada para projetos maiores:

meu-projeto/
    src/
        meu_pacote/
            __init__.py
            servicos.py
            modelos.py
    tests/
        __init__.py
        test_servicos.py
        test_modelos.py
    pyproject.toml

Configure no pyproject.toml:

[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]

Fixtures

Fixtures são funções que preparam dados ou recursos para os testes. Elas substituem o setup/teardown de outros frameworks:

import pytest


@pytest.fixture
def lista_de_usuarios():
    return [
        {"nome": "Ana", "idade": 28},
        {"nome": "Carlos", "idade": 35},
        {"nome": "Maria", "idade": 22},
    ]


def test_quantidade_usuarios(lista_de_usuarios):
    assert len(lista_de_usuarios) == 3


def test_usuario_mais_novo(lista_de_usuarios):
    mais_novo = min(lista_de_usuarios, key=lambda u: u["idade"])
    assert mais_novo["nome"] == "Maria"

O pytest injeta a fixture automaticamente quando o nome do parâmetro corresponde ao nome da fixture.

Fixtures com escopo

Por padrão, fixtures são criadas para cada teste. Você pode alterar o escopo:

@pytest.fixture(scope="module")
def conexao_banco():
    """Criada uma vez por modulo de teste."""
    conexao = criar_conexao()
    yield conexao
    conexao.fechar()


@pytest.fixture(scope="session")
def configuracao():
    """Criada uma vez por sessao inteira de testes."""
    return carregar_configuracao()

O yield permite executar código de limpeza após o teste terminar.

conftest.py

Fixtures compartilhadas entre vários arquivos de teste ficam no conftest.py:

# tests/conftest.py
import pytest


@pytest.fixture
def cliente_api():
    from meu_pacote.cliente import ClienteAPI
    return ClienteAPI(base_url="http://localhost:8000")

Todos os testes na mesma pasta (e subpastas) podem usar fixtures definidas no conftest.py sem importação.

Parametrize

O decorator @pytest.mark.parametrize permite rodar o mesmo teste com diferentes entradas:

import pytest
from calculadora import somar, eh_par


@pytest.mark.parametrize("a, b, esperado", [
    (1, 2, 3),
    (0, 0, 0),
    (-1, 1, 0),
    (100, 200, 300),
    (0.1, 0.2, pytest.approx(0.3)),
])
def test_somar(a, b, esperado):
    assert somar(a, b) == esperado


@pytest.mark.parametrize("numero, esperado", [
    (2, True),
    (3, False),
    (0, True),
    (-4, True),
    (101, False),
])
def test_eh_par(numero, esperado):
    assert eh_par(numero) == esperado

Note o uso de pytest.approx() para comparar floats, evitando problemas de precisão de ponto flutuante.

Mocking

Mocking substitui partes do código por objetos simulados durante os testes. Isso é essencial para testar código que faz chamadas de rede, acessa banco de dados ou depende de serviços externos:

from unittest.mock import patch, MagicMock


def buscar_dados_api(url):
    import requests
    resposta = requests.get(url)
    return resposta.json()


def test_buscar_dados_api():
    dados_falsos = {"nome": "Python", "versao": "3.12"}

    with patch("requests.get") as mock_get:
        mock_get.return_value = MagicMock(
            json=MagicMock(return_value=dados_falsos)
        )

        resultado = buscar_dados_api("https://api.exemplo.com/dados")
        assert resultado["nome"] == "Python"
        mock_get.assert_called_once_with("https://api.exemplo.com/dados")

Testando exceções

import pytest


def test_divisao_por_zero():
    with pytest.raises(ValueError) as exc_info:
        dividir(10, 0)

    assert "zero" in str(exc_info.value)


def test_tipo_invalido():
    with pytest.raises(TypeError):
        somar("texto", 5)

Cobertura de testes

Instale o plugin de cobertura:

pip install pytest-cov

Execute os testes com relatório de cobertura:

pytest --cov=src --cov-report=term-missing

A saída mostra quais linhas do código não estão cobertas por testes. Para gerar um relatório HTML detalhado:

pytest --cov=src --cov-report=html

Abra htmlcov/index.html no navegador para ver um relatório visual.

Markers personalizados

Crie markers para categorizar testes:

import pytest


@pytest.mark.lento
def test_processamento_grande():
    # Teste que demora muito
    pass


@pytest.mark.integracao
def test_conexao_banco():
    # Teste de integração
    pass

Execute apenas testes de uma categoria:

pytest -m "not lento"
pytest -m integracao

Registre os markers no pyproject.toml para evitar avisos:

[tool.pytest.ini_options]
markers = [
    "lento: testes que demoram para executar",
    "integracao: testes de integração com serviços externos",
]

Boas práticas

  • Nomeie testes de forma descritiva: test_somar_numeros_negativos_retorna_valor_correto
  • Cada teste deve verificar apenas uma coisa
  • Testes devem ser independentes entre si
  • Use fixtures para evitar repetição de setup
  • Mantenha testes rápidos: moque dependências externas
  • Execute testes antes de cada commit
  • Busque cobertura acima de 80%, mas foque na qualidade, não apenas no número

Conclusão

O pytest é uma ferramenta poderosa que torna os testes em Python simples e agradáveis. Com fixtures, parametrize e mocking, você pode testar qualquer tipo de código de forma eficiente. Investir tempo em testes automatizados economiza horas de debugging e aumenta a confiança no código que você entrega. Comece com testes simples e evolua conforme a complexidade do projeto aumenta.