W3docs

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?

Recursounittestpytest
Sintaxe de testeClasse + métodoFunção simples
Asserçõesself.assertEqual(a, b)assert a == b
FixturessetUp / tearDown@pytest.fixture (composável)
ParametrizeLoop manual@pytest.mark.parametrize
Ecossistema de pluginsMínimoMais 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 pytest

Verifique a instalação:

pytest --version
# pytest 8.x.x

Consulte 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_*.py ou *_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 + b

Agora 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) == 0

Execute os testes:

pytest test_math_utils.py

Saída:

collected 3 items

test_math_utils.py ...                                                 [100%]

3 passed in 0.01s

Cada 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 differ

Uma saída de falha se parece com:

AssertionError: assert [1, 2, 4] == [1, 2, 3]
  At index 2: 4 != 3

Comparaçõ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) == expected

O 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] PASSED

Isso é 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() == 1

O 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:

EscopoCriada 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 — um pathlib.Path apontando 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 de stdout / stderr para 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.0

Apó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 True

Testes 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 == 7

As 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 True

Marcadores 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 slow

Execute 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 output

Cobertura 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-missing

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

Prática

Prática
Which decorator marks a pytest function as a fixture?
Which decorator marks a pytest function as a fixture?
Prática
What does pytest.approx() help you do in tests?
What does pytest.approx() help you do in tests?
Prática
Where should you put fixtures that need to be shared across multiple test files?
Where should you put fixtures that need to be shared across multiple test files?
Was this page helpful?