Verificação, Validação e Tipos de Teste
Verificação
"Estamos construindo certo o produto?"
Checa se o software segue as especificações. Ex: code review, inspeção, análise estática.
Validação
"Estamos construindo o produto certo?"
Checa se o software atende às necessidades do usuário. Ex: testes de aceitação, UAT.
Técnicas de Teste
| Técnica | O que testa | Conhece o código? |
| Caixa Branca (White Box) | Estrutura interna, caminhos de execução, cobertura de código | Sim |
| Caixa Preta (Black Box) | Comportamento externo — entrada e saída, sem ver o código | Não |
| Caixa Cinza (Gray Box) | Combina os dois — conhece parte da estrutura | Parcialmente |
Critérios de Cobertura
# Cobertura de código com pytest-cov
pip install pytest-cov
pytest --cov=app --cov-report=html tests/
# Gera relatório HTML mostrando quais linhas foram executadas
# Tipos de cobertura:
# Statement coverage: % de linhas executadas
# Branch coverage: % de desvios (if/else) testados
# Path coverage: % de caminhos possíveis (muito difícil 100%)
# Cobertura mínima recomendada: 80% para projetos sérios
Estratégias de Teste
| Estratégia | Quando escrever o teste | Benefício |
| TDD | Antes do código (Red→Green→Refactor) | Design emergente, cobertura garantida |
| BDD | Baseado em comportamento (Given/When/Then) | Linguagem compartilhada com negócio |
| Test-after | Após implementar | Mais comum — valida o que foi feito |
BDD — Given/When/Then
Given: um usuário autenticado com saldo R$100
When: ele tenta sacar R$150
Then: deve receber erro "Saldo insuficiente" e saldo permanece R$100
Exercício 1: Qual a diferença entre verificação e validação?
→ Verificação: construímos certo (spec)? Validação: construímos o certo (necessidade)?
Você pode verificar que o software faz exatamente o que foi especificado (verificação OK) mas a especificação estar errada — o usuário queria outra coisa (validação falhou). Ambas são necessárias.
pytest Avançado
Fixtures — Reutilizar setup de testes
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from main import app, get_db, Base
# Banco de teste em memória
SQLALCHEMY_TEST_URL = "sqlite:///./test.db"
engine_test = create_engine(SQLALCHEMY_TEST_URL)
TestSession = sessionmaker(bind=engine_test)
@pytest.fixture(scope="session")
def db():
"""Cria banco de teste uma vez por sessão"""
Base.metadata.create_all(bind=engine_test)
yield
Base.metadata.drop_all(bind=engine_test)
@pytest.fixture
def session(db):
"""Nova sessão por teste — rollback ao final"""
connection = engine_test.connect()
transaction = connection.begin()
session = TestSession(bind=connection)
yield session
session.close()
transaction.rollback() # desfaz alterações do teste
@pytest.fixture
def client(session):
"""Cliente HTTP com banco de teste injetado"""
def override_db():
yield session
app.dependency_overrides[get_db] = override_db
yield TestClient(app)
app.dependency_overrides.clear()
@pytest.fixture
def usuario_token(client):
"""Cria usuário e retorna token de autenticação"""
client.post("/usuarios", json={"nome": "Ana", "email": "ana@test.com", "senha": "123456"})
r = client.post("/auth/login", json={"email": "ana@test.com", "senha": "123456"})
return r.json()["access_token"]
Mocks e Patches
from unittest.mock import patch, MagicMock
import pytest
# Mock de serviço externo (email, pagamento, etc.)
def test_criar_usuario_envia_email():
with patch("services.email.enviar_boas_vindas") as mock_email:
# Chama a função que deveria enviar email
criar_usuario("Ana", "ana@test.com")
# Verifica que o email foi chamado com os dados certos
mock_email.assert_called_once_with("ana@test.com", "Ana")
# Mock de resposta de API externa
def test_buscar_cep():
mock_response = MagicMock()
mock_response.json.return_value = {"cidade": "Campo Grande", "uf": "MS"}
mock_response.status_code = 200
with patch("httpx.get", return_value=mock_response):
resultado = buscar_cep("79000-000")
assert resultado["cidade"] == "Campo Grande"
# Parametrize — testar múltiplos casos
@pytest.mark.parametrize("email,valido", [
("ana@exemplo.com", True),
("nao-e-um-email", False),
("faltou@dominio", False),
("", False),
])
def test_validar_email(email, valido):
assert validar_email(email) == valido
Organização dos testes
projeto/
├── app/
│ ├── main.py
│ ├── services/
│ └── repositories/
└── tests/
├── conftest.py # fixtures compartilhadas
├── unit/
│ ├── test_services.py
│ └── test_utils.py
├── integration/
│ ├── test_api_usuarios.py
│ └── test_api_pedidos.py
└── e2e/
└── test_fluxo_compra.py
# Rodar só unitários (rápido)
pytest tests/unit/ -v
# Rodar tudo com cobertura
pytest --cov=app --cov-report=term-missing
Exercício 2: Por que usar rollback nas fixtures de teste de banco?
→ Garante que cada teste começa com o banco limpo, sem dados de outros testes.
Sem rollback, um teste que cria um usuário "ana@test.com" pode fazer outro teste falhar porque o email já existe. Com rollback, cada teste é isolado e independente — pode rodar em qualquer ordem.
Testes de API e Integração
Testes de API com TestClient (FastAPI)
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
# Teste de criação de usuário
def test_criar_usuario(client):
response = client.post("/usuarios", json={
"nome": "Ana Silva",
"email": "ana@teste.com",
"senha": "senha123"
})
assert response.status_code == 201
data = response.json()
assert data["email"] == "ana@teste.com"
assert "id" in data
assert "senha" not in data # senha nunca na resposta!
# Teste de autenticação
def test_login_correto(client):
# Criar usuário primeiro
client.post("/usuarios", json={"nome": "Ana", "email": "ana@t.com", "senha": "123"})
# Fazer login
r = client.post("/auth/login", json={"email": "ana@t.com", "senha": "123"})
assert r.status_code == 200
assert "access_token" in r.json()
def test_login_senha_errada(client):
r = client.post("/auth/login", json={"email": "ana@t.com", "senha": "errada"})
assert r.status_code == 401
# Teste de rota protegida
def test_rota_sem_token(client):
r = client.get("/perfil")
assert r.status_code == 401
def test_rota_com_token(client, usuario_token):
r = client.get("/perfil",
headers={"Authorization": f"Bearer {usuario_token}"})
assert r.status_code == 200
# Teste de BOLA
def test_nao_acessa_pedido_alheio(client, usuario_token):
# Cria pedido de outro usuário
outro_pedido_id = criar_pedido_outro_usuario()
r = client.get(f"/pedidos/{outro_pedido_id}",
headers={"Authorization": f"Bearer {usuario_token}"})
assert r.status_code == 403 # Acesso negado!
Contract Testing — APIs entre serviços
pip install pact-python
# Consumer (quem chama a API) define o contrato esperado
from pact import Consumer, Provider
pact = Consumer("UsuarioService").has_pact_with(Provider("PedidoService"))
def test_buscar_pedidos_usuario():
pact.given("usuário 42 tem pedidos").upon_receiving(
"requisição de pedidos"
).with_request(
method="GET", path="/pedidos", query={"usuario_id": "42"}
).will_respond_with(200, body=[
{"id": 1, "total": 150.0, "status": "entregue"}
])
with pact:
resultado = buscar_pedidos_usuario(42)
assert len(resultado) > 0
Exercício 3: Por que testar que a senha não aparece na resposta da API?
→ Confirma que o serializer está correto e nunca expõe dados sensíveis, mesmo que o modelo tenha o campo.
Se o Pydantic response_model não excluir corretamente o campo senha, o hash vai aparecer na resposta. O teste assert "senha" not in data garante isso em toda atualização do código.
Testes de Performance e Qualidade de Código
Testes de Performance com Locust
pip install locust
# locustfile.py
from locust import HttpUser, task, between
class UsuarioSimulado(HttpUser):
wait_time = between(1, 3) # espera 1-3s entre requests
token = None
def on_start(self):
"""Executado uma vez por usuário virtual — faz login"""
r = self.client.post("/auth/login",
json={"email": "teste@teste.com", "senha": "123456"})
self.token = r.json()["access_token"]
@task(3) # peso 3 — executado 3x mais que peso 1
def listar_produtos(self):
self.client.get("/produtos",
headers={"Authorization": f"Bearer {self.token}"})
@task(1)
def criar_pedido(self):
self.client.post("/pedidos",
json={"produto_id": 1, "quantidade": 2},
headers={"Authorization": f"Bearer {self.token}"})
# Rodar: locust -f locustfile.py
# Interface web: http://localhost:8089
# Define: usuários simultâneos (ex: 100) e ramp-up (ex: 10/s)
Métricas de Performance
| Métrica | O que mede | Meta típica para API |
| Latência (p50) | Tempo de resposta para 50% das requisições | p50 < 100ms |
| Latência (p99) | Tempo de resposta para 99% das requisições | p99 < 1000ms |
| Throughput (RPS) | Requisições por segundo suportadas | Depende do sistema |
| Taxa de erro | % de requisições com erro | < 0.1% em produção |
| Apdex Score | Satisfação do usuário com a performance | > 0.9 (excelente) |
Qualidade de Código — Análise Estática
pip install ruff mypy bandit
# ruff — linter e formatter ultrarrápido (substitui flake8+black+isort)
ruff check . # verificar problemas
ruff check . --fix # corrigir automaticamente
ruff format . # formatar código
# mypy — verificação de tipos estáticos
mypy app/
# Exemplos de erros que o mypy detecta:
def soma(a: int, b: int) -> int:
return a + b
soma("texto", 2) # mypy detecta: error: Argument 1 has incompatible type "str"; expected "int"
# bandit — análise de segurança estática
bandit -r app/
# Detecta: senhas hardcoded, uso de MD5, SQL injection potencial
# pre-commit — roda automaticamente antes de cada commit
pip install pre-commit
# .pre-commit-config.yaml
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
hooks:
- id: ruff
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
hooks:
- id: mypy
⚠ Automatize qualidade com CI
No GitHub Actions, rode automaticamente a cada push:
ruff + mypy + bandit + pytest + coverage. Se qualquer um falhar, o PR não pode ser mergeado. Qualidade de código vira obrigatória, não opcional.