✅ Verificação, Validação e Teste de Software

pytest  |  TDD  |  Cobertura  |  Integração  |  Performance

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écnicaO que testaConhece o código?
Caixa Branca (White Box)Estrutura interna, caminhos de execução, cobertura de códigoSim
Caixa Preta (Black Box)Comportamento externo — entrada e saída, sem ver o códigoNão
Caixa Cinza (Gray Box)Combina os dois — conhece parte da estruturaParcialmente
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égiaQuando escrever o testeBenefício
TDDAntes do código (Red→Green→Refactor)Design emergente, cobertura garantida
BDDBaseado em comportamento (Given/When/Then)Linguagem compartilhada com negócio
Test-afterApós implementarMais 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étricaO que medeMeta típica para API
Latência (p50)Tempo de resposta para 50% das requisiçõesp50 < 100ms
Latência (p99)Tempo de resposta para 99% das requisiçõesp99 < 1000ms
Throughput (RPS)Requisições por segundo suportadasDepende do sistema
Taxa de erro% de requisições com erro< 0.1% em produção
Apdex ScoreSatisfaçã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.
📝 Cola — VVT
Verificação vs Validação
VerificaçãoValidação
PerguntaConstruímos CERTO?Construímos O CERTO?
FocoEspecificação técnicaNecessidade do usuário
ExemploCode review, análise estáticaTeste de aceitação, UAT
Pirâmide de Testes
TipoQtdFerramentaVelocidade
UnitáriosMuitospytestms
Integração / APIMédiosTestClientsegundos
E2E / PerformancePoucosLocustminutos
⭐ Regras de Ouro
Regra
Rollback em fixtures de banco — cada teste começa limpo e isolado
Regra
Mock serviços externos — teste não deve depender de internet ou APIs reais
RegraSempre testar: criação, autenticação, 403 no BOLA e 401 sem token
Regra
Cobertura mínima: 80% de statements. 100% de branches nas regras de negócio críticas
Regraruff + mypy + bandit no pre-commit — qualidade automática antes de commitar
RegraPerformance: p99 < 1s. Taxa de erro < 0.1%. Monitore sempre em produção