Testes Unitários em Python com pytest
Aprenda pytest do zero: escrevendo asserções, usando fixtures, parametrizando testes e organizando uma suíte com conftest.py.
pytest é o framework de testes mais popular do Python. Ele permite escrever funções de teste pequenas e legíveis usando instruções assert simples — sem classes de boilerplate — e ainda assim escala para suítes de testes complexas com fixtures compartilhadas, parametrização e plugins.
Este capítulo cobre tudo o que você precisa para testar código Python com pytest: instalação, escrevendo seu primeiro teste, asserções e exceções esperadas, fixtures, parametrize, organização de testes com conftest.py, opções úteis de linha de comando e os erros mais comuns.
Por que usar pytest?
O Python já vem com o módulo unittest, então por que usar pytest?
| Recurso | unittest | pytest |
|---|---|---|
| Sintaxe de teste | Classe + método | Função simples |
| Asserções | self.assertEqual(a, b) | assert a == b |
| Fixtures | setUp / tearDown | @pytest.fixture (composável) |
| Parametrize | Loop manual | @pytest.mark.parametrize |
| Ecossistema de plugins | Mínimo | Mais de 1.000 plugins (coverage, mock, etc.) |
O pytest também executa testes no estilo unittest sem alterações, então você pode adotá-lo gradualmente.
Instalação
O pytest não faz parte da biblioteca padrão. Instale-o com pip dentro de um ambiente virtual:
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install pytestVerifique a instalação:
pytest --version
# pytest 8.x.xConsulte Python pip se precisar de uma revisão sobre gerenciamento de pacotes.
Seu Primeiro Teste
O pytest descobre arquivos de teste automaticamente. Por padrão, ele procura por:
- Arquivos nomeados
test_*.pyou*_test.py - Funções cujos nomes começam com
test_
Crie math_utils.py com uma função simples:
# math_utils.py
def add(a, b):
return a + bAgora crie test_math_utils.py no mesmo diretório:
# test_math_utils.py
from math_utils import add
def test_add_positive_numbers():
assert add(2, 3) == 5
def test_add_negative_numbers():
assert add(-1, 1) == 0
def test_add_zeros():
assert add(0, 0) == 0Execute os testes:
pytest test_math_utils.pySaída:
collected 3 items
test_math_utils.py ... [100%]
3 passed in 0.01sCada ponto representa um teste aprovado. Um teste com falha exibe F e mostra o diff completo da asserção.
Asserções
O pytest reescreve instruções assert simples no momento da coleta para que as falhas mostrem um diff detalhado — sem necessidade de métodos de asserção especiais.
def test_assertion_diff():
result = [1, 2, 4]
expected = [1, 2, 3]
assert result == expected # pytest shows exactly where lists differUma saída de falha se parece com:
AssertionError: assert [1, 2, 4] == [1, 2, 3]
At index 2: 4 != 3Comparações de Ponto Flutuante
Nunca compare floats com == — erros de arredondamento tornam isso não confiável. Use pytest.approx:
import pytest
import math
def circle_area(r):
return math.pi * r * r
def test_circle_area():
assert circle_area(5) == pytest.approx(78.53981633974483)pytest.approx aceita uma tolerância opcional abs ou rel:
assert 0.1 + 0.2 == pytest.approx(0.3, abs=1e-9)Testando Exceções Esperadas
Use pytest.raises como gerenciador de contexto para verificar que uma exceção específica é lançada:
import pytest
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
def test_divide_by_zero():
with pytest.raises(ValueError, match="Cannot divide by zero"):
divide(10, 0)O argumento match é uma expressão regular verificada contra a mensagem de exceção. Se a exceção não for lançada, o pytest reprovará o teste — garantindo que você detecte regressões em que o tratamento de erros foi removido acidentalmente.
Consulte Python Try...Except para uma visão mais aprofundada do tratamento de exceções, e Lançando Exceções para saber como lançá-las intencionalmente.
Parametrize: Executando um Teste com Múltiplas Entradas
@pytest.mark.parametrize permite executar a mesma lógica de teste contra múltiplos conjuntos de dados sem escrever um loop:
import pytest
from math_utils import add
@pytest.mark.parametrize("a, b, expected", [
(2, 3, 5),
(-1, 1, 0),
(0, 0, 0),
(10, -5, 5),
])
def test_add(a, b, expected):
assert add(a, b) == expectedO pytest gera um caso de teste separado para cada tupla e os reporta individualmente:
test_math_utils.py::test_add[2-3-5] PASSED
test_math_utils.py::test_add[-1-1-0] PASSED
test_math_utils.py::test_add[0-0-0] PASSED
test_math_utils.py::test_add[10--5-5] PASSEDIsso é muito mais limpo do que um loop manual — falhas individuais são isoladas e fáceis de identificar.
Fixtures
Uma fixture é uma função decorada com @pytest.fixture que fornece configuração compartilhada (e desmontagem opcional) para os testes. Em vez de repetir o código de configuração em cada teste, você declara uma fixture uma vez e a injeta pelo nome como parâmetro do teste.
Fixture Básica
import pytest
class UserStore:
def __init__(self):
self.users = []
def add_user(self, name):
self.users.append(name)
def count(self):
return len(self.users)
@pytest.fixture
def store():
return UserStore()
def test_empty_store(store):
assert store.count() == 0
def test_add_user(store):
store.add_user("Alice")
assert store.count() == 1O pytest percebe que test_add_user tem um parâmetro chamado store, procura uma fixture com esse nome, a chama e passa o resultado. Cada teste recebe uma instância nova da fixture — alterações em um teste nunca vazam para outro.
Fixtures com Desmontagem (yield)
Use yield dentro de uma fixture para dividi-la em configuração (antes do yield) e desmontagem (após o yield). Isso garante que a limpeza sempre seja executada, mesmo que o teste falhe:
import pytest
import tempfile
import os
@pytest.fixture
def temp_file():
fd, path = tempfile.mkstemp(suffix=".txt")
os.close(fd)
yield path # test receives the path here
if os.path.exists(path):
os.unlink(path) # always runs after the test
def test_write_to_temp_file(temp_file):
with open(temp_file, "w") as f:
f.write("hello")
with open(temp_file) as f:
assert f.read() == "hello"Escopo de Fixture
Por padrão, as fixtures são criadas e desmontadas uma vez por função de teste. Você pode ampliar o escopo para reduzir configurações custosas:
| Escopo | Criada uma vez por |
|---|---|
"function" (padrão) | Cada função de teste |
"class" | Cada classe de teste |
"module" | Cada arquivo de teste |
"session" | Execução completa dos testes |
@pytest.fixture(scope="session")
def database_connection():
conn = create_db_connection()
yield conn
conn.close()Use o escopo "session" para recursos custosos como conexões de banco de dados ou processos de servidor. Use o escopo "function" (o padrão) para qualquer coisa que altere estado.
Fixtures Integradas
O pytest inclui várias fixtures integradas que você pode usar sem importar nada:
tmp_path— umpathlib.Pathapontando para um diretório temporário exclusivo para o teste.monkeypatch— substitui atributos, variáveis de ambiente ou entradas de dicionário durante a execução de um teste e reverte automaticamente.capsys— captura a saída destdout/stderrpara que você possa verificar o texto impresso.
def greet(name):
print(f"Hello, {name}!")
def test_greet_output(capsys):
greet("World")
captured = capsys.readouterr()
assert captured.out == "Hello, World!\n"Usando monkeypatch
monkeypatch é a forma idiomática de substituir dependências externas nos testes sem uma biblioteca de mock de terceiros:
import time
def get_timestamp():
return time.time()
def test_get_timestamp(monkeypatch):
monkeypatch.setattr(time, "time", lambda: 1_000_000.0)
assert get_timestamp() == 1_000_000.0Após o teste, time.time é restaurado à sua implementação original. Consulte Python Decorators se quiser entender como @pytest.fixture funciona por baixo dos panos.
Organizando Testes com conftest.py
Quando uma fixture é necessária por testes em múltiplos arquivos, coloque-a em conftest.py. O pytest descobre arquivos conftest.py automaticamente e disponibiliza suas fixtures para todos os testes no mesmo diretório e abaixo — sem necessidade de importação.
project/
├── conftest.py # shared fixtures live here
├── test_users.py
├── test_orders.py
└── utils/
├── conftest.py # fixtures scoped to this subdirectory
└── test_helpers.py# conftest.py
import pytest
@pytest.fixture
def admin_user():
return {"name": "Admin", "role": "admin", "active": True}# test_users.py — no import needed; pytest injects admin_user automatically
def test_admin_is_active(admin_user):
assert admin_user["active"] is TrueTestes Baseados em Classes
Você pode agrupar testes relacionados em uma classe. Ao contrário de unittest.TestCase, as classes pytest não requerem herança:
class TestCalculator:
def test_add(self):
assert 2 + 2 == 4
def test_multiply(self):
assert 3 * 4 == 12
def test_subtract(self):
assert 10 - 3 == 7As classes são úteis para agrupar testes que compartilham uma preocupação lógica. Evite classes quando o agrupamento não traz benefício real — funções simples são mais diretas.
Marcadores: Ignorando e Rótulos Personalizados
O sistema de marcadores do pytest permite anotar testes com metadados para execução seletiva.
Ignorar um Teste
import pytest
import sys
@pytest.mark.skip(reason="Not implemented yet")
def test_future_feature():
assert False
@pytest.mark.skipif(sys.platform == "win32", reason="Linux only")
def test_linux_feature():
assert TrueMarcadores Personalizados
Registre marcadores personalizados em pytest.ini (ou pyproject.toml) para categorizar testes:
# pytest.ini
[pytest]
markers =
slow: marks tests as slow (deselect with -m "not slow")
integration: marks integration tests@pytest.mark.slow
def test_large_dataset():
...Execute apenas os testes lentos:
pytest -m slowExecute tudo exceto os testes lentos:
pytest -m "not slow"Opções Úteis de Linha de Comando
pytest # run all discovered tests
pytest test_math_utils.py # run a specific file
pytest test_math_utils.py::test_add # run one test by name
pytest -v # verbose: show each test name
pytest -x # stop on first failure
pytest --tb=short # shorter traceback (default is long)
pytest -k "add" # run tests whose name contains "add"
pytest --lf # re-run only last-failing tests
pytest -q # quiet: minimal outputCobertura de Testes
Instale o plugin de cobertura para medir quais linhas seus testes exercitam:
pip install pytest-cov
pytest --cov=math_utils --cov-report=term-missingA saída adiciona uma coluna de cobertura mostrando quais linhas não foram atingidas:
Name Stmts Miss Cover Missing
---------------------------------------------
math_utils.py 2 0 100%Busque alta cobertura na lógica de negócios crítica, mas não persiga 100% — testar getters triviais frequentemente adiciona ruído sem valor.
Erros Comuns
1. Fixture não encontrada. Se o pytest reportar fixture 'foo' not found, verifique se a fixture está em conftest.py ou no mesmo arquivo e se a função está decorada com @pytest.fixture.
2. Erros de importação no momento da coleta. Se o pytest não conseguir importar seu módulo, ele apresentará erro antes de executar qualquer teste. Execute python -c "import your_module" para diagnosticar.
3. Argumentos padrão mutáveis em fixtures. Assim como em funções Python regulares, as fixtures devem evitar argumentos padrão mutáveis. Use o escopo "function" (o padrão) para qualquer fixture que construa um objeto mutável.
4. assert em funções auxiliares. Se você chamar uma função auxiliar em um teste e essa função contiver assert, certifique-se de que o nome dela começa com assert_ (convenção do pytest) para que o pytest reescreva a asserção com uma mensagem de erro melhor.
5. Misturando unittest.TestCase e fixtures do pytest. O pytest executa testes unittest.TestCase, mas você não pode injetar fixtures do pytest em métodos TestCase. Use classes no estilo pytest ou os métodos de configuração do unittest — não ambos ao mesmo tempo.