Python Admin

Como Usar pytest em Python: Fundamentos, Fixtures e Organização de Testes em Produção Já leu

Introdução ao pytest: Por Que Abandonar print() nos Testes Quando comecei a programar, assim como muitos, testava meu código inserindo em diversos pontos e verificando manualmente a saída. Isso funciona para scripts pequenos, mas quando seu projeto cresce, essa abordagem se torna caótica e improdutiva. O pytest é um framework que transforma testes em uma prática sistemática, legível e automatizável — permitindo que você execute centenas de testes em segundos e saiba exatamente o que quebrou. O pytest se diferencia de outras ferramentas porque é minimalista: você escreve funções Python normais com nomes começados em , e ele as encontra e executa automaticamente. Não há necessidade de herdar classes, usar decoradores complicados ou aprender uma sintaxe própria. Vamos entender como começar e, progressivamente, construir uma estratégia sólida de testes. Fundamentos: Escrevendo Seu Primeiro Teste Instalação e Primeiro Teste A instalação é simples — use pip para adicionar pytest ao seu projeto: Agora, crie um arquivo chamado : Execute com ou

Introdução ao pytest: Por Que Abandonar print() nos Testes

Quando comecei a programar, assim como muitos, testava meu código inserindo print() em diversos pontos e verificando manualmente a saída. Isso funciona para scripts pequenos, mas quando seu projeto cresce, essa abordagem se torna caótica e improdutiva. O pytest é um framework que transforma testes em uma prática sistemática, legível e automatizável — permitindo que você execute centenas de testes em segundos e saiba exatamente o que quebrou.

O pytest se diferencia de outras ferramentas porque é minimalista: você escreve funções Python normais com nomes começados em test_, e ele as encontra e executa automaticamente. Não há necessidade de herdar classes, usar decoradores complicados ou aprender uma sintaxe própria. Vamos entender como começar e, progressivamente, construir uma estratégia sólida de testes.

Fundamentos: Escrevendo Seu Primeiro Teste

Instalação e Primeiro Teste

A instalação é simples — use pip para adicionar pytest ao seu projeto:

pip install pytest

Agora, crie um arquivo chamado test_calculadora.py:

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

def subtrai(a, b):
    return a - b

def test_soma_positivos():
    assert soma(2, 3) == 5

def test_soma_negativos():
    assert soma(-1, -2) == -3

def test_subtrai():
    assert subtrai(10, 5) == 5

Execute com pytest test_calculadora.py ou simplesmente pytest (pytest procura automaticamente por arquivos e funções que começam com test_). A saída será clara: quantos testes passaram, quantos falharam e por quê.

Entendendo assert e Mensagens de Erro

O assert é a coluna vertebral dos testes em pytest. Você escreve uma condição e, se for falsa, o teste falha. Mas pytest é inteligente: ele mostra exatamente o que diferiu. Veja um exemplo real:

def calcula_desconto(preco, percentual):
    return preco * (1 - percentual / 100)

def test_desconto_inválido():
    resultado = calcula_desconto(100, 10)
    assert resultado == 91.0, f"Esperado 90.0, mas obtive {resultado}"

Se colocar uma lógica errada na função, a mensagem será clara. Pytest mostra o valor real vs. esperado automaticamente, tornando debug muito mais rápido que print() tradicional.

Organizando Testes em Diretórios

Para projetos maiores, organize seus testes em uma estrutura clara:

meu_projeto/
├── src/
│   ├── calculadora.py
│   ├── validador.py
│   └── __init__.py
├── tests/
│   ├── test_calculadora.py
│   ├── test_validador.py
│   └── conftest.py
├── pytest.ini
└── requirements.txt

Crie um arquivo pytest.ini na raiz do projeto para configurar o comportamento padrão:

[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = -v --tb=short

Isso garante que pytest sempre procure em tests/ e exiba saída verbosa com rastreamento de erro reduzido.

Fixtures: Reutilizando Recursos e Dados Entre Testes

O Conceito de Fixture e Por Que Usar

Uma fixture é uma função que prepara dados ou recursos que seus testes precisam. Imagine que você testa uma classe UsuarioService que precisa conectar a um banco de dados. Sem fixtures, você escreveria o setup em cada teste — código duplicado. Com fixtures, você define uma única vez e pytest a executa automaticamente para cada teste que a solicita.

Fixtures resolvem três problemas: eliminar duplicação, garantir limpeza de recursos (como fechar conexões) e facilitar testes que dependem do mesmo estado inicial.

Criando e Usando Fixtures Básicas

Crie um arquivo tests/conftest.py — esse é o arquivo especial onde pytest procura por fixtures:

import pytest

@pytest.fixture
def usuario_padrao():
    return {
        "id": 1,
        "nome": "João Silva",
        "email": "joao@example.com",
        "ativo": True
    }

@pytest.fixture
def lista_usuarios():
    return [
        {"id": 1, "nome": "João", "email": "joao@example.com"},
        {"id": 2, "nome": "Maria", "email": "maria@example.com"},
        {"id": 3, "nome": "Pedro", "email": "pedro@example.com"},
    ]

Agora, em qualquer teste, você solicita a fixture como argumento:

def test_usuario_tem_email(usuario_padrao):
    assert usuario_padrao["email"] == "joao@example.com"

def test_usuario_ativo(usuario_padrao):
    assert usuario_padrao["ativo"] is True

def test_contar_usuarios(lista_usuarios):
    assert len(lista_usuarios) == 3

Pytest injeta as fixtures automaticamente. Se você precisar usar usuario_padrao em 20 testes, basta adicioná-la como argumento em cada um — zero duplicação.

Fixtures com Setup e Teardown

Frequentemente você precisa preparar algo antes do teste e limpar depois. Use yield para isso:

import sqlite3
import pytest

@pytest.fixture
def db_conexao():
    # Setup: criar conexão
    conexao = sqlite3.connect(":memory:")
    cursor = conexao.cursor()
    cursor.execute("""
        CREATE TABLE usuarios (
            id INTEGER PRIMARY KEY,
            nome TEXT NOT NULL,
            email TEXT UNIQUE
        )
    """)
    conexao.commit()

    # O teste roda aqui
    yield conexao

    # Teardown: fechar conexão
    conexao.close()

def test_inserir_usuario(db_conexao):
    cursor = db_conexao.cursor()
    cursor.execute("INSERT INTO usuarios (nome, email) VALUES (?, ?)", 
                   ("João", "joao@example.com"))
    db_conexao.commit()

    cursor.execute("SELECT * FROM usuarios WHERE email = ?", 
                   ("joao@example.com",))
    resultado = cursor.fetchone()
    assert resultado is not None
    assert resultado[1] == "João"

O yield marca o ponto onde o teste roda. Código antes do yield é setup, código depois é teardown. Isso garante que a conexão sempre seja fechada, mesmo se o teste falhar.

Escopos de Fixture: Quando Reutilizar

Fixtures têm escopos que determinam quantas vezes são criadas. O padrão é function (uma por teste), mas existem outras opções:

@pytest.fixture(scope="function")  # Padrão: nova instância por teste
def recurso_por_funcao():
    return {"valor": "novo"}

@pytest.fixture(scope="module")  # Uma instância para todos os testes do módulo
def recurso_modulo():
    return sqlite3.connect(":memory:")

@pytest.fixture(scope="session")  # Uma instância para toda a sessão de testes
def recurso_sessao():
    return {"config": "global"}

def test_um(recurso_por_funcao):
    recurso_por_funcao["valor"] = "modificado"

def test_dois(recurso_por_funcao):
    # Aqui recurso_por_funcao é uma nova instância, não afetada pelo test_um
    assert recurso_por_funcao["valor"] == "novo"

Use module ou session com cuidado — testes podem se afetar mutuamente se compartilharem estado. Geralmente, function é a escolha segura.

Fixtures Parametrizadas

Às vezes você quer executar o mesmo teste com dados diferentes. Use params:

@pytest.fixture(params=[
    {"entrada": 2, "esperado": 4},
    {"entrada": 3, "esperado": 9},
    {"entrada": -1, "esperado": 1},
])
def casos_potencia(request):
    return request.param

def quadrado(n):
    return n ** 2

def test_potencia(casos_potencia):
    assert quadrado(casos_potencia["entrada"]) == casos_potencia["esperado"]

Pytest rodará test_potencia três vezes — uma para cada item em params. Você obtém 3 testes por 1 função de teste, reduzindo duplicação massivamente.

Organização Profissional de Testes: Estrutura e Boas Práticas

Estrutura de Diretórios Escalável

Conforme seu projeto cresce, organize testes por módulos de funcionalidade:

projeto/
├── src/
│   ├── usuarios/
│   │   ├── models.py
│   │   ├── services.py
│   │   └── __init__.py
│   ├── pedidos/
│   │   ├── models.py
│   │   ├── services.py
│   │   └── __init__.py
│   └── __init__.py
├── tests/
│   ├── conftest.py  # Fixtures globais
│   ├── test_usuarios/
│   │   ├── conftest.py  # Fixtures específicas de usuarios
│   │   ├── test_models.py
│   │   └── test_services.py
│   └── test_pedidos/
│       ├── conftest.py
│       ├── test_models.py
│       └── test_services.py
└── pytest.ini

Pytest procura por conftest.py em cada nível de diretório, permitindo fixtures globais e específicas por módulo. Isso mantém tudo organizado e evita que você carregue fixtures desnecessárias.

Estrutura de Testes para Classes

Para código orientado a objetos, organize testes em classes:

class Usuario:
    def __init__(self, nome, email):
        self.nome = nome
        self.email = email
        self.ativo = True

    def desativar(self):
        self.ativo = False

    def validar_email(self):
        return "@" in self.email

class TestUsuario:
    @pytest.fixture
    def usuario(self):
        return Usuario("João", "joao@example.com")

    def test_criacao(self, usuario):
        assert usuario.nome == "João"

    def test_desativar(self, usuario):
        usuario.desativar()
        assert usuario.ativo is False

    def test_validar_email_valido(self, usuario):
        assert usuario.validar_email() is True

    def test_validar_email_invalido(self):
        usuario_invalido = Usuario("Maria", "maria_sem_email")
        assert usuario_invalido.validar_email() is False

Classes agrupam testes relacionados, facilitando leitura e manutenção. Pytest as trata normalmente — não é necessário herdar de nada.

Marcadores (Markers) para Categorizar Testes

Use marcadores para executar subconjuntos de testes:

import pytest

@pytest.mark.rapido
def test_soma():
    assert 1 + 1 == 2

@pytest.mark.lento
def test_leitura_arquivo_grande():
    # Simula teste que demora
    import time
    time.sleep(2)
    assert True

@pytest.mark.integracao
def test_conectar_banco_dados():
    # Simula conexão real
    assert True

No terminal, execute apenas testes rápidos com pytest -m rapido ou evite os lentos com pytest -m "not lento". Registre marcadores no pytest.ini:

[pytest]
markers =
    rapido: testes que executam em menos de 1 segundo
    lento: testes que demoram mais de 1 segundo
    integracao: testes que acessam banco de dados ou APIs externas

Testando Exceções e Comportamentos Esperados

Às vezes você quer verificar se uma exceção é lançada:

import pytest

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

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

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

pytest.raises() garante que a exceção é lançada. O parâmetro match verifica se a mensagem contém o padrão esperado. Isso testa comportamento defensivo — código que reage bem a entradas inválidas.

Mocking: Testando sem Dependências Externas

Frequentemente você testa código que depende de APIs, bancos de dados ou serviços externos. Use unittest.mock para simular essas dependências:

from unittest.mock import Mock, patch
import pytest

class PedidoService:
    def __init__(self, db):
        self.db = db

    def criar_pedido(self, usuario_id, itens):
        # Simula criação de pedido
        pedido = {"usuario_id": usuario_id, "itens": itens}
        self.db.salvar(pedido)
        return pedido

def test_criar_pedido_com_mock():
    # Cria um mock (objeto falso) do banco de dados
    db_mock = Mock()
    service = PedidoService(db_mock)

    resultado = service.criar_pedido(1, ["item1", "item2"])

    # Verifica se o método foi chamado com os argumentos corretos
    db_mock.salvar.assert_called_once()
    args = db_mock.salvar.call_args[0][0]
    assert args["usuario_id"] == 1
    assert len(args["itens"]) == 2

Mocks permitem testar lógica sem depender de infraestrutura real, tornando testes mais rápidos e confiáveis.

Configuração Avançada e Boas Práticas

Arquivo pytest.ini Completo

Configure pytest para comportar-se como você espera:

[pytest]
testpaths = tests
python_files = test_*.py *_test.py
python_classes = Test*
python_functions = test_*
addopts = 
    -v
    --tb=short
    --strict-markers
    --disable-warnings
    -ra
markers =
    rapido: testes de execução rápida
    lento: testes que demoram mais de 1 segundo
    integracao: testes que acessam recursos externos
    unitario: testes unitários isolados
filterwarnings =
    ignore::DeprecationWarning

-v mostra cada teste, --tb=short reduz mensagens de erro verbosas, -ra resumo de tudo (skipped, xfailed, etc).

Cobertura de Testes com pytest-cov

Saiba quanto do seu código está sendo testado:

pip install pytest-cov
pytest --cov=src --cov-report=html

Isso gera um relatório HTML mostrando linhas cobertas e não cobertas. Não é sobre alcançar 100% — é sobre testar o código crítico e conhecer seus gaps.

Testes Parametrizados com pytest.mark.parametrize

Para múltiplas combinações de entrada, use parametrize:

import pytest

@pytest.mark.parametrize("entrada,esperado", [
    (2, 4),
    (3, 9),
    (-1, 1),
    (0, 0),
])
def test_quadrado(entrada, esperado):
    def quadrado(n):
        return n ** 2
    assert quadrado(entrada) == esperado

Pytest rodará o teste 4 vezes, uma para cada tupla. A sintaxe é simples: nomes das variáveis e lista de valores.

Conclusão

Depois de caminhar por esses conceitos, retenha três pontos essenciais: Primeiro, fixtures são o coração do pytest — use-as para eliminar duplicação e gerenciar recursos com setup/teardown automático. Elas transformam testes de algo repetitivo em algo elegante e mantenível. Segundo, organize seus testes em uma estrutura clara com diretórios, conftest.py e marcadores — isso permite executar subconjuntos de testes rapidamente e mantém projetos escaláveis. Um teste deveria responder: "o que estou testando e por quê?" em sua estrutura de arquivo. Terceiro, use marcadores, mocking e parametrização para cobrir casos reais sem depender de infraestrutura externa — testes rápidos e confiáveis são testes que serão executados frequentemente.

Referências


Artigos relacionados