Voltar ao Glossario
Glossario Python

pytest: O que É e Como Funciona | Python Brasil

pytest é o framework de testes Python mais popular. Aprenda fixtures, conftest.py, markers, cobertura de código, mocks e integração contínua.

O que é pytest?

O pytest é o framework de testes mais popular do Python. Ele torna a escrita de testes simples e intuitiva, sem a necessidade de classes ou boilerplate excessivo. Com pytest, você escreve testes usando funções simples e a palavra-chave assert, enquanto o framework se encarrega de encontrar, executar e reportar os resultados.

Por que pytest em vez de unittest?

O unittest, que vem embutido no Python, exige herança de classes e métodos específicos. O pytest é muito mais conciso e expressivo, com mensagens de falha detalhadas, descoberta automática de testes e um ecossistema rico de plugins.

# unittest (verboso)
import unittest

class TestCalculadora(unittest.TestCase):
    def test_somar(self):
        self.assertEqual(somar(2, 3), 5)
        self.assertRaises(ValueError, dividir, 10, 0)

# pytest (simples e direto)
def test_somar():
    assert somar(2, 3) == 5

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

Exemplo prático completo

# calculadora.py
def somar(a, b):
    return a + b

def dividir(a, b):
    if b == 0:
        raise ValueError("Divisão por zero!")
    return a / b

def calcular_desconto(preco: float, percentual: float) -> float:
    if not 0 <= percentual <= 100:
        raise ValueError(f"Percentual inválido: {percentual}")
    return preco * (1 - percentual / 100)


# test_calculadora.py
import pytest
from calculadora import somar, dividir, calcular_desconto

def test_somar():
    assert somar(2, 3) == 5
    assert somar(-1, 1) == 0
    assert somar(0, 0) == 0

def test_dividir():
    assert dividir(10, 2) == 5.0
    assert dividir(7, 2) == pytest.approx(3.5)  # Para floats, use approx

def test_dividir_por_zero():
    with pytest.raises(ValueError, match="Divisão por zero"):
        dividir(10, 0)

def test_desconto_invalido():
    with pytest.raises(ValueError, match="Percentual inválido"):
        calcular_desconto(100, 150)

# Parametrize: um teste, múltiplos cenários
@pytest.mark.parametrize("a, b, esperado", [
    (1, 1, 2),
    (2, 3, 5),
    (-1, -1, -2),
    (100, 200, 300),
    (0, 0, 0),
])
def test_somar_parametrizado(a, b, esperado):
    assert somar(a, b) == esperado

Fixtures: configuração reutilizável de testes

Fixtures são funções que fornecem dados ou objetos para os testes. São injetadas automaticamente pelo nome do parâmetro:

import pytest

@pytest.fixture
def usuario_padrao():
    """Fixture simples: retorna um dicionário com dados de usuário."""
    return {
        'id': 1,
        'nome': 'Ana Silva',
        'email': 'ana@exemplo.com',
        'ativo': True
    }

@pytest.fixture
def banco_de_dados_teste():
    """Fixture com setup e teardown usando yield."""
    # Setup: executado antes do teste
    db = ConexaoDB(url='sqlite:///:memory:')
    db.criar_tabelas()

    yield db  # O teste recebe 'db' aqui

    # Teardown: executado após o teste (mesmo se falhar)
    db.fechar()

def test_criar_usuario(banco_de_dados_teste, usuario_padrao):
    # Ambas as fixtures são injetadas automaticamente
    banco_de_dados_teste.inserir(usuario_padrao)
    resultado = banco_de_dados_teste.buscar(id=1)
    assert resultado['nome'] == 'Ana Silva'

Escopos de fixtures

O escopo controla com que frequência a fixture é criada e destruída:

import pytest

# function (padrão): criada/destruída para cada função de teste
@pytest.fixture(scope='function')
def dado_por_funcao():
    print("\nSetup por função")
    yield {'contador': 0}
    print("\nTeardown por função")

# class: uma instância por classe de testes
@pytest.fixture(scope='class')
def dado_por_classe():
    print("\nSetup por classe")
    yield {'usuarios': []}
    print("\nTeardown por classe")

# module: uma instância por arquivo de teste
@pytest.fixture(scope='module')
def conexao_db():
    print("\nAbrindo conexão com banco")
    db = criar_conexao_teste()
    yield db
    print("\nFechando conexão com banco")
    db.fechar()

# session: uma instância para toda a sessão de testes
@pytest.fixture(scope='session')
def servidor_externo():
    print("\nIniciando servidor de testes")
    servidor = ServidorTeste.iniciar(porta=9999)
    yield servidor
    print("\nDesligando servidor")
    servidor.parar()

conftest.py: fixtures compartilhadas

O arquivo conftest.py é carregado automaticamente pelo pytest e permite compartilhar fixtures entre múltiplos arquivos de teste sem importações explícitas:

meu_projeto/
├── conftest.py                 # Fixtures disponíveis para todos os testes
├── tests/
│   ├── conftest.py             # Fixtures disponíveis apenas em tests/
│   ├── test_usuario.py
│   ├── test_pedido.py
│   └── api/
│       ├── conftest.py         # Fixtures apenas para testes de API
│       └── test_endpoints.py
# conftest.py (raiz do projeto)
import pytest
from minha_app import criar_app
from minha_app.banco import db as _db

@pytest.fixture(scope='session')
def app():
    """Cria a aplicação Flask/FastAPI para toda a sessão."""
    app = criar_app(config='testing')
    return app

@pytest.fixture(scope='function')
def client(app):
    """Cliente HTTP para testar endpoints."""
    with app.test_client() as client:
        yield client

@pytest.fixture(scope='function', autouse=True)
def limpar_banco():
    """Limpa o banco antes de cada teste (autouse=True: aplica automaticamente)."""
    yield
    _db.rollback()

Markers: categorizando e controlando testes

import pytest

# Markers integrados
@pytest.mark.skip(reason="Funcionalidade ainda não implementada")
def test_nova_feature():
    assert False  # Nunca executado

@pytest.mark.skipif(sys.platform == 'win32', reason="Não funciona no Windows")
def test_somente_linux():
    pass

@pytest.mark.xfail(reason="Bug conhecido, aguardando correção")
def test_comportamento_com_bug():
    assert calcular_algo() == resultado_esperado  # Falha esperada

# Markers personalizados (registre em pytest.ini ou pyproject.toml)
@pytest.mark.integracao
def test_chamada_api_externa():
    resposta = requests.get("https://api.exemplo.com")
    assert resposta.status_code == 200

@pytest.mark.lento
def test_processo_demorado():
    resultado = processar_milhoes_de_registros()
    assert resultado['total'] > 0

# Executando apenas markers específicos:
# pytest -m "integracao"          - apenas integração
# pytest -m "not lento"           - excluindo testes lentos
# pytest -m "integracao and not lento"

Fixtures especiais: monkeypatch, tmp_path, capfd

import pytest
import os

# monkeypatch: substitui comportamentos temporariamente durante o teste
def test_variaveis_de_ambiente(monkeypatch):
    monkeypatch.setenv('DATABASE_URL', 'sqlite:///:memory:')
    monkeypatch.setenv('DEBUG', '1')

    # A variável de ambiente é restaurada após o teste
    assert os.environ['DATABASE_URL'] == 'sqlite:///:memory:'

def test_substituindo_funcao(monkeypatch):
    def mock_get(url, **kwargs):
        class FakeResponse:
            status_code = 200
            def json(self): return {'resultado': 42}
        return FakeResponse()

    monkeypatch.setattr('requests.get', mock_get)
    resultado = minha_funcao_que_chama_api()
    assert resultado == 42

# tmp_path: diretório temporário criado e limpo automaticamente
def test_escrita_de_arquivo(tmp_path):
    arquivo = tmp_path / "saida.txt"
    arquivo.write_text("conteúdo de teste")

    assert arquivo.exists()
    assert arquivo.read_text() == "conteúdo de teste"

# capfd: captura stdout e stderr
def test_saida_no_terminal(capfd):
    print("Mensagem importante")
    print("Erro ocorreu", file=sys.stderr)

    out, err = capfd.readouterr()
    assert "Mensagem importante" in out
    assert "Erro ocorreu" in err

pytest-mock: mocking elegante

# pip install pytest-mock
import pytest

def test_servico_de_email(mocker):
    # Substitui o método de envio real por um mock
    mock_enviar = mocker.patch('meu_app.email.enviar_email')
    mock_enviar.return_value = True

    servico = ServicoNotificacao()
    servico.notificar_usuario(id=1, mensagem="Bem-vindo!")

    # Verifica se o email foi chamado corretamente
    mock_enviar.assert_called_once()
    args = mock_enviar.call_args
    assert 'Bem-vindo!' in args[1]['mensagem']

def test_api_externa_falha(mocker):
    mocker.patch(
        'requests.get',
        side_effect=ConnectionError("Sem conexão")
    )

    with pytest.raises(ServicoIndisponivel):
        buscar_dados_externos()

Configuração: pytest.ini e pyproject.toml

# pytest.ini
[pytest]
testpaths = tests
python_files = test_*.py *_test.py
python_classes = Test*
python_functions = test_*
markers =
    integracao: Testes que requerem serviços externos
    lento: Testes que demoram mais de 5 segundos
    smoke: Testes de verificação rápida
addopts = -v --tb=short --strict-markers
# pyproject.toml (alternativa moderna)
[tool.pytest.ini_options]
testpaths = ["tests"]
markers = [
    "integracao: Testes de integração com serviços externos",
    "lento: Testes com execução demorada",
]
addopts = "-v --tb=short"

pytest-cov: cobertura de código

# Instalando
pip install pytest-cov

# Executando com relatório de cobertura
pytest --cov=meu_pacote --cov-report=term-missing
pytest --cov=meu_pacote --cov-report=html  # Gera relatório HTML em htmlcov/
pytest --cov=meu_pacote --cov-fail-under=80  # Falha se cobertura < 80%
# .coveragerc
[run]
source = meu_pacote
omit =
    */migrations/*
    */settings.py
    */tests/*

[report]
exclude_lines =
    pragma: no cover
    if TYPE_CHECKING:

Integração contínua (CI)

# .github/workflows/tests.yml
name: Testes

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.11", "3.12", "3.13"]

    steps:
    - uses: actions/checkout@v4

    - name: Configurar Python ${{ matrix.python-version }}
      uses: actions/setup-python@v5
      with:
        python-version: ${{ matrix.python-version }}

    - name: Instalar dependências
      run: |
        pip install -e ".[dev]"
        pip install pytest pytest-cov pytest-mock

    - name: Executar testes
      run: |
        pytest --cov=meu_pacote --cov-report=xml -m "not integracao"

    - name: Enviar cobertura ao Codecov
      uses: codecov/codecov-action@v3

Organizacao de testes em projetos grandes

Em projetos grandes, organize os testes espelhando a estrutura do codigo: tests/unit/ para testes unitários isolados, tests/integration/ para testes que envolvem banco de dados ou APIs reais, e tests/e2e/ para testes de ponta a ponta. Use markers para controlar quais testes rodam em quais ambientes.

Termos Relacionados

  • Python - A linguagem de programação
  • Decorators - Usados em fixtures e marks do pytest
  • Classe - Testes podem ser organizados em classes