404 Not Found

404 Not Found


nginx

Testes — Garantia de Qualidade Ponta-a-Ponta com pytest

Testar é como uma rede de segurança — você não a vê enquanto caminha na corda bamba (escrevendo código), mas se escorregar (encontrar um bug), ela é sua única salvação. Uma apresentação sem rede de segurança está fadada a terminar em acidente mais cedo ou mais tarde.

1. O Que Você Vai Aprender


2. A História Real da Alice

(1) Problema: Descobrindo Bugs Apenas Após o Lançamento

Depois que Alice implantou o PriceTracker, descobriu que o tempo de expiração do token JWT estava configurado para 1 segundo, importações em lote estavam pulando todas as validações, e a limitação de taxa para usuários Pro era a mesma que para usuários Free. Toda vez que corrigia um bug, introduzia um novo, e Bob reclamava: "O recurso que funcionava na semana passada não funciona mais esta semana." Alice não tinha testes automatizados; dependia inteiramente de clicar manualmente pelo Swagger UI, e cada teste de regressão levava duas horas.

(2) Soluções para Testes Automatizados com pytest

pytest + httpx TestClient garante que cada endpoint da API tenha testes automatizados; dependency_overrides substitui o banco de dados de produção pelo banco de dados de teste; e toda vez que git push é executado, todos os testes são executados automaticamente, detectando todos os problemas de regressão em 2 minutos.

PYTHON
def test_create_product(client):
    response = client.post("/api/v1/products", json={"name": "Widget", "price": 9.99})
    assert response.status_code == 201
    assert response.json()["name"] == "Widget"

(3) Resultado

O teste de regressão passou de um processo manual de 2 horas para um automatizado de 2 minutos; um bug de expiração JWT foi capturado durante os testes na fase de desenvolvimento; e após a implantação, o número de bugs caiu de 5 por semana para 1 por mês.


3. Fundamentos do TestClient

(1) A Pirâmide de Testes

100%
graph TD
    E2E[Testes E2E - Poucos] --> INT[Testes de Integração - Médio]
    INT --> UNIT[Testes Unitários - Muitos]
    
    UNIT --- U1[Validação de Modelos Pydantic]
    UNIT --- U2[Funções do Repository]
    UNIT --- U3[Lógica de Serviço]
    
    INT --- I1[Endpoint da API + DB]
    INT --- I2[Fluxo de Autenticação]
    INT --- I3[Operações CRUD]
    
    E2E --- E1[Jornada Completa do Usuário]
    E2E --- E2[WebSocket + API]
Nível Quantidade Velocidade Dependências
Testes Unitários Muitos (100+) Rápido (< 1 ms) Sem Dependências Externas
Testes de Integração Médio (30-50) Médio (10-100 ms) Banco de Dados de Teste
Testes E2E Poucos (5-10) Lento (1-5 s) Ambiente Completo

(1) ▶Exemplo: Fixture Básica do TestClient

PYTHON
# tests/conftest.py
import pytest
from fastapi.testclient import TestClient
from app.main import app
from app.core.deps import get_db

# Sobrescrever dependência do banco de dados
def get_test_db():
    # Usar SQLite em memória para testes
    engine = create_async_engine("sqlite+aiosqlite:///test.db")
    # ... configuração da sessão
    yield session
    # ... limpeza

@pytest.fixture
def client():
    app.dependency_overrides[get_db] = get_test_db
    with TestClient(app) as c:
        yield c
    app.dependency_overrides.clear()

Saída:

TEXT
# Função definida com sucesso

(2) ▶Exemplo: Teste Básico de Endpoint

PYTHON
# tests/test_health.py
def test_health_check(client):
    response = client.get("/health")
    assert response.status_code == 200
    data = response.json()
    assert data["status"] == "healthy"
    assert "service" in data

def test_openapi_docs_available(client):
    response = client.get("/docs")
    assert response.status_code == 200

Saída:

TEXT
# Função definida com sucesso

4. Isolamento do Banco de Dados de Teste

(1) Um banco de dados separado para cada teste

(1) ▶Exemplo: Fixture do banco de dados de teste

PYTHON
# tests/conftest.py
import pytest
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from app.models import Base

TEST_DATABASE_URL = "sqlite+aiosqlite:///test_pricetracker.db"

@pytest.fixture(scope="function")
async def test_db():
    # Criar banco de dados de teste novo para cada teste
    engine = create_async_engine(TEST_DATABASE_URL, echo=False)
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)
    
    TestSession = async_sessionmaker(engine, expire_on_commit=False)
    async with TestSession() as session:
        yield session
    
    # Remover todas as tabelas após o teste
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.drop_all)
    await engine.dispose()

Saída:

TEXT
# Função definida com sucesso

(2) ▶Exemplo: Cliente de Teste Assíncrono

PYTHON
# tests/conftest.py
import pytest
from httpx import AsyncClient, ASGITransport
from app.main import app

@pytest.fixture
async def async_client():
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as ac:
        yield ac

Saída:

TEXT
# Função definida com sucesso

(2) Comparação de Estratégias de Isolamento

Estratégia Velocidade Isolamento Aplicabilidade
Criar nova tabela para cada teste Lento Totalmente isolado Teste de integridade de dados
Rollback de Transação Rápido Bom Maioria dos testes de integração
SQLite em Memória Rápido Bom Testes que não dependem de recursos do PostgreSQL

5. Técnicas para Sobrescrever Dependências

(1) Substituir a autenticação e o banco de dados

(1) ▶Exemplo: Sobrescrevendo Dependências de Autenticação

PYTHON
# tests/conftest.py
from app.core.deps import get_current_user, get_db

def get_test_user():
    """Usuário autenticado simulado para testes"""
    return {"id": 1, "email": "alice@test.com", "role": "admin", "subscription": "pro"}

@pytest.fixture
def auth_client(client):
    # Sobrescrever autenticação - sem JWT real necessário
    app.dependency_overrides[get_current_user] = get_test_user
    yield client
    app.dependency_overrides.clear()

Saída:

TEXT
# Função definida com sucesso

(2) ▶Exemplo: Testando Endpoints Protegidos

PYTHON
# tests/test_products.py
def test_list_products_unauthorized(client):
    """Sem token de autenticação, deve retornar 401"""
    response = client.get("/api/v1/products")
    assert response.status_code == 401

def test_list_products_authorized(auth_client):
    """Com autenticação simulada, deve retornar 200"""
    response = auth_client.get("/api/v1/products")
    assert response.status_code == 200

def test_create_product(auth_client):
    response = auth_client.post(
        "/api/v1/products",
        json={"name": "Widget", "category": "electronics", "base_price": 29.99},
    )
    assert response.status_code == 201
    assert response.json()["name"] == "Widget"

Saída:

TEXT
# Função definida com sucesso

(3) ▶Exemplo: Sobrescrevendo Permissões de Teste por Nível de Assinatura

PYTHON
def get_free_user():
    return {"id": 2, "email": "free@test.com", "role": "user", "subscription": "free"}

def get_pro_user():
    return {"id": 3, "email": "pro@test.com", "role": "user", "subscription": "pro"}

def test_bulk_import_free_user_limited(client):
    """Usuários Free podem importar no máximo 1000 preços"""
    app.dependency_overrides[get_current_user] = get_free_user
    prices = [{"product_id": i, "price": 9.99} for i in range(1500)]
    response = client.post("/api/v1/prices/bulk", json=prices)
    assert response.status_code == 403
    assert "limit" in response.json()["detail"].lower()
    app.dependency_overrides.clear()

def test_bulk_import_pro_user(client):
    """Usuários Pro podem importar até 100000 preços"""
    app.dependency_overrides[get_current_user] = get_pro_user
    prices = [{"product_id": i, "price": 9.99} for i in range(5000)]
    response = client.post("/api/v1/prices/bulk", json=prices)
    assert response.status_code == 201
    app.dependency_overrides.clear()

Saída:

TEXT
# Função definida com sucesso

6. Exemplo de uma Suíte de Testes Completa

(1) ▶Exemplo: Teste Unitário de Modelos Pydantic

PYTHON
# tests/test_models.py
import pytest
from pydantic import ValidationError
from app.schemas import ProductCreate, PriceCreate

def test_product_create_valid():
    p = ProductCreate(name="Widget", category="electronics", base_price=29.99)
    assert p.name == "Widget"
    assert p.base_price == 29.99

def test_product_create_negative_price():
    with pytest.raises(ValidationError) as exc:
        ProductCreate(name="Widget", category="electronics", base_price=-1)
    assert "greater than 0" in str(exc.value)

def test_product_create_empty_name():
    with pytest.raises(ValidationError):
        ProductCreate(name="", category="electronics", base_price=9.99)

def test_price_create_rounds_precision():
    p = PriceCreate(product_id=1, price=9.999, currency="USD", source="test")
    assert p.price == 10.0  # Arredondado para 2 casas decimais

def test_price_create_invalid_currency():
    with pytest.raises(ValidationError):
        PriceCreate(product_id=1, price=9.99, currency="XYZ", source="test")

Saída:

TEXT
# Função definida com sucesso

(2) ▶Exemplo: Teste de Integração para Operações CRUD

PYTHON
# tests/test_crud.py
import pytest
from fastapi.testclient import TestClient

def test_product_crud_lifecycle(auth_client):
    # Criar
    create_resp = auth_client.post(
        "/api/v1/products",
        json={"name": "Test Widget", "category": "electronics", "base_price": 19.99},
    )
    assert create_resp.status_code == 201
    product_id = create_resp.json()["id"]

    # Ler
    get_resp = auth_client.get(f"/api/v1/products/{product_id}")
    assert get_resp.status_code == 200
    assert get_resp.json()["name"] == "Test Widget"

    # Atualizar
    update_resp = auth_client.put(
        f"/api/v1/products/{product_id}",
        json={"name": "Updated Widget", "base_price": 24.99},
    )
    assert update_resp.status_code == 200
    assert update_resp.json()["base_price"] == 24.99

    # Deletar
    delete_resp = auth_client.delete(f"/api/v1/products/{product_id}")
    assert delete_resp.status_code == 200

    # Verificar se foi deletado
    get_resp2 = auth_client.get(f"/api/v1/products/{product_id}")
    assert get_resp2.status_code == 404

Saída:

TEXT
# Função definida com sucesso

❓Perguntas Frequentes

P Qual é melhor usar, TestClient ou AsyncClient?
R Use TestClient (síncrono) para testes simples, e use AsyncClient quando precisar testar lógica assíncrona (como WebSockets). TestClient é suficiente para a maioria dos cenários.
P Devemos usar SQLite ou PostgreSQL para o banco de dados de teste?
R Use SQLite para desenvolvimento (é mais rápido) e PostgreSQL para CI (para corresponder ao ambiente de produção). Note que SQLite não suporta certos recursos do PostgreSQL (como RETURNING e JSONB).
P Como devo escolher o scope para fixtures?
R function (recriado para cada teste) é o mais seguro; session (compartilhado em toda a sessão de teste) é o mais rápido mas oferece isolamento inferior. Use function para o banco de dados e function para o TestClient.
P Como testar WebSocket?
R Use AsyncClient.websocket_connect() para estabelecer uma conexão, e envie e receba mensagens para verificar seu comportamento.
P O que devo fazer se o teste rodar muito lentamente?
R Use pytest-xdist para execução paralela (pytest -n auto), use rollback de transação em vez de criar tabelas, e use testes unitários em vez de testes de integração.
P dependency_overrides afetará outros testes?
R Sim, porque modifica o objeto global do app. Certifique-se de colocar app.dependency_overrides.clear() após o yield na fixture.

📖Resumo


📝Exercícios

  1. Exercício Básico (Dificuldade ⭐): Use TestClient para escrever um teste para o endpoint /health para verificar se ele retorna um código de status 200 e a estrutura JSON correta. Dica: TestClient(app) + client.get("/health")
  2. Problema Avançado (Dificuldade ⭐⭐): Escreva testes de ciclo de vida CRUD — criar, consultar, atualizar e deletar produtos — e verifique os códigos de status e dados retornados em cada etapa. Use dependency_overrides para pular a autenticação. Dica: app.dependency_overrides[get_current_user] = mock_fn
  3. Desafio (Dificuldade ⭐⭐⭐): Implemente uma suíte de testes completa: testes de validação Pydantic (dados inválidos geram ValidationError), testes de autenticação (401 sem token, 403 para usuários Free, 200 para usuários Pro), e testes de paginação (retorno correto dos valores skip e limit). Dica: pytest.raises(ValidationError) + múltiplas fixtures dependency_overrides

---|

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%