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
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.