Testes — Python SWE Agent
Pirâmide de Testes — Proporções Esperadas
/\
/ \ E2E / Integration (10-15%)
/----\
/ \ Integration / API (20-30%)
/--------\
/ \ Unit Tests (55-70%)
/____________\
Regra prática: Para cada Use Case, espera-se:
- N testes unitários de domínio (entidades, value objects, domain services)
- 1-2 testes de integração do use case
- 1 teste de API (happy path + principal error path)
TDD — Workflow Obrigatório para Lógica de Negócio
RED → GREEN → REFACTOR
1. Escreva o teste que falha (descreve o comportamento esperado)
2. Escreva o mínimo de código para passar
3. Refatore sem quebrar o teste
Exemplo TDD Completo
# PASSO 1: RED — teste descreve o comportamento
def test_money_addition_same_currency():
price = Money(Decimal("10.00"), "BRL")
tax = Money(Decimal("1.50"), "BRL")
result = price.add(tax)
assert result.amount == Decimal("11.50")
assert result.currency == "BRL"
def test_money_addition_different_currencies_raises():
brl = Money(Decimal("10.00"), "BRL")
usd = Money(Decimal("10.00"), "USD")
with pytest.raises(CurrencyMismatchError):
brl.add(usd)
# PASSO 2: GREEN — implementação mínima
@dataclass(frozen=True)
class Money:
amount: Decimal
currency: str
def add(self, other: "Money") -> "Money":
if self.currency != other.currency:
raise CurrencyMismatchError()
return Money(self.amount + other.amount, self.currency)
Estrutura de Testes — Padrão AAA
# tests/unit/domain/test_order.py
import pytest
from decimal import Decimal
from uuid import uuid4
from domain.entities.order import Order
from domain.value_objects.money import Money
from domain.exceptions import OrderNotEditableError
class TestOrderAddItem:
"""Agrupa testes relacionados ao comportamento add_item."""
def test_add_item_to_pending_order_succeeds(self):
# Arrange
order = Order(id=uuid4(), customer_id=uuid4())
item = build_order_item(price=Money(Decimal("50.00"), "BRL"))
# Act
order.add_item(item)
# Assert
assert len(order.items) == 1
assert order.total == Money(Decimal("50.00"), "BRL")
def test_add_item_to_confirmed_order_raises(self):
# Arrange
order = Order(id=uuid4(), customer_id=uuid4())
order.confirm() # muda status
# Act & Assert
with pytest.raises(OrderNotEditableError) as exc_info:
order.add_item(build_order_item())
assert exc_info.value.order_id == order.id
Fixtures — Boas Práticas
# tests/conftest.py
import pytest
from decimal import Decimal
from uuid import uuid4
@pytest.fixture
def customer_id():
return uuid4()
@pytest.fixture
def pending_order(customer_id):
return Order(id=uuid4(), customer_id=customer_id)
@pytest.fixture
def confirmed_order(pending_order):
pending_order.confirm()
return pending_order
# Factory para evitar repetição
def build_order_item(
product_id=None,
price=None,
quantity=1,
):
return OrderItem(
product_id=product_id or uuid4(),
price=price or Money(Decimal("10.00"), "BRL"),
quantity=quantity,
)
Mocks e Fakes — Quando Usar Cada Um
# FAKE — implementação real simplificada (preferível para repositórios)
class InMemoryOrderRepository(OrderRepository):
def __init__(self):
self._store: dict[UUID, Order] = {}
def find_by_id(self, order_id: UUID) -> Order | None:
return self._store.get(order_id)
def save(self, order: Order) -> Order:
self._store[order.id] = order
return order
# USO em testes de use case
def test_create_order_saves_to_repository():
repo = InMemoryOrderRepository()
use_case = CreateOrderUseCase(repo=repo)
result = use_case.execute(CreateOrderDTO(customer_id=uuid4()))
assert repo.find_by_id(result.id) is not None
# MOCK — para verificar interações (use com moderação)
from unittest.mock import MagicMock, AsyncMock
def test_create_order_sends_welcome_email():
repo = InMemoryOrderRepository()
email_service = MagicMock()
use_case = CreateOrderUseCase(repo=repo, email_service=email_service)
use_case.execute(CreateOrderDTO(customer_id=uuid4()))
email_service.send_confirmation.assert_called_once()
Testes de API FastAPI
# tests/integration/api/test_orders_api.py
import pytest
from fastapi.testclient import TestClient
from httpx import AsyncClient
@pytest.mark.asyncio
async def test_create_order_returns_201(async_client: AsyncClient):
payload = {"customer_id": str(uuid4()), "items": []}
response = await async_client.post("/orders/", json=payload)
assert response.status_code == 201
data = response.json()
assert "id" in data
assert data["status"] == "pending"
@pytest.mark.asyncio
async def test_create_order_with_invalid_data_returns_422(async_client: AsyncClient):
response = await async_client.post("/orders/", json={})
assert response.status_code == 422
# conftest.py para API
@pytest.fixture
async def async_client(app):
async with AsyncClient(app=app, base_url="http://test") as client:
yield client
Testes de Contrato (Pact)
# tests/contract/test_payment_provider_contract.py
# Garante que o contrato com serviços externos não quebra
import pytest
from pact import Consumer, Provider
pact = Consumer("order-service").has_pact_with(Provider("payment-service"))
def test_payment_service_create_payment():
expected_response = {
"id": "pay_123",
"status": "pending",
"amount": 100.00,
}
(pact
.given("payment service is available")
.upon_receiving("a request to create a payment")
.with_request("POST", "/payments", body={"amount": 100.00, "currency": "BRL"})
.will_respond_with(201, body=expected_response))
with pact:
result = PaymentClient(pact.uri).create(amount=100.00, currency="BRL")
assert result["status"] == "pending"
Cobertura — Configuração e Requisitos
# pyproject.toml
[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
[tool.coverage.run]
source = ["src"]
omit = ["*/migrations/*", "*/settings/*", "*/conftest.py"]
[tool.coverage.report]
fail_under = 80 # mínimo aceitável
show_missing = true
skip_covered = false
exclude_lines = [
"pragma: no cover",
"if TYPE_CHECKING:",
"raise NotImplementedError",
"@abstractmethod",
]
# Rodar testes com cobertura
pytest --cov=src --cov-report=term-missing --cov-report=html -v
# Rodar apenas testes rápidos (unit)
pytest tests/unit/ -v
# Rodar com paralelismo
pytest -n auto tests/
Nomenclatura de Testes — Convenção
test_<unidade>_<cenário>_<resultado_esperado>
✅ test_order_add_item_to_confirmed_order_raises_not_editable_error
✅ test_money_add_different_currencies_raises_currency_mismatch
✅ test_create_order_use_case_with_out_of_stock_item_raises_insufficient_stock
❌ test_order_1
❌ test_fail
❌ test_create
Anti-Patterns de Teste
| Anti-Pattern | Problema |
|---|---|
| Testar implementação, não comportamento | Frágil a refatoração |
| Teste depende de ordem de execução | Compartilhamento de estado |
| Mock de tudo | Teste não valida nada real |
| Assert único por teste | Não sabe qual falhou |
| Setup enorme antes do assert | Teste difícil de entender |
| Testar métodos privados diretamente | Viola encapsulamento |