Testes e Validação¶
🧭 Defeito, erro e falha¶
Antes de falar em testes, vale distinguir três termos que costumam ser usados de forma intercambiável, mas que têm significados técnicos distintos:
- Defeito (fault/bug): uma característica estática e incorreta do código-fonte — uma linha escrita errada, uma condição invertida.
- Erro (error): um estado interno incorreto do sistema em tempo de execução, causado por um defeito ao ser exercitado.
- Falha (failure): o comportamento observável e incorreto do sistema, visível para o usuário ou para outro sistema — a consequência externa de um erro.
Um defeito pode existir no código por anos sem nunca causar uma falha, se o caminho de código que o contém nunca for exercitado com as condições certas. Testes automatizados existem justamente para aumentar a chance de descobrir defeitos antes que eles se tornem falhas em produção.
🔬 Inspeções e revisões¶
Nem todo defeito é encontrado executando o código. Inspeções e revisões (Capítulo 5) são técnicas de verificação estática — analisar o código sem executá-lo — e são complementares aos testes: uma inspeção pode encontrar um problema de lógica que um teste mal escrito não cobriria, e vice-versa. Times maduros combinam as duas abordagens: revisão de código para design e lógica, análise estática (Capítulo 5) para padrões mecânicos, e testes automatizados para comportamento em tempo de execução.
🔺 A pirâmide de testes¶
A pirâmide de testes, popularizada por Mike Cohn, organiza os testes automatizados em camadas conforme seu escopo, velocidade e custo de manutenção:
/\
/ \ Testes de E2E / Aceitação
/----\ (poucos, lentos, alto valor de confiança)
/ \
/--------\ Testes de Integração
/ \ (moderados, verificam componentes juntos)
/------------\
/ \ Testes Unitários
/________________\ (muitos, rápidos, isolados)
- Testes unitários verificam uma unidade de código isolada (uma função, um método), sem dependências externas reais (banco de dados, rede). São rápidos, numerosos e baratos de manter — a base da pirâmide.
- Testes de componente/integração verificam a colaboração entre algumas unidades reais, ou entre a aplicação e uma dependência externa real (um banco de dados de teste, por exemplo).
- Testes de sistema/aceitação verificam o sistema completo, muitas vezes através de suas interfaces externas (API, UI), simulando um uso real de ponta a ponta. São os mais caros de escrever e manter, e os mais lentos de executar — por isso devem ser relativamente poucos, cobrindo os fluxos mais críticos.
O antipadrão do 'sorvete invertido'
Um erro comum é ter muitos testes de ponta a ponta (lentos, frágeis, caros de manter) e poucos testes unitários — o oposto da pirâmide. Isso é às vezes chamado de "cone de sorvete invertido": a suíte de testes fica lenta, instável e cara de manter, exatamente o oposto do que se espera de uma boa suíte de testes.
🧪 Tipos de teste no ciclo de desenvolvimento¶
Além da divisão por escopo, testes também se distinguem por quem os executa e quando, ao longo do ciclo de vida:
- Testes em desenvolvimento: escritos e executados pelos próprios desenvolvedores, continuamente, enquanto o código é escrito — testes unitários, de componente e de sistema.
- Testes de release: executados por um time (ou processo automatizado) separado, validando uma versão candidata a ser lançada antes de ir para produção.
- Testes de usuário: envolvem usuários reais ou representantes do cliente, tipicamente perto do fim do ciclo:
- Teste alfa: usuários selecionados testam o sistema no ambiente de desenvolvimento, com a equipe observando.
- Teste beta: uma versão é liberada a um grupo maior de usuários reais, em seu próprio ambiente, coletando feedback antes do lançamento geral.
- Teste de aceitação: o cliente (ou seu representante) valida formalmente que o sistema atende aos requisitos acordados — muitas vezes usando os próprios critérios de aceitação escritos junto com os requisitos (Capítulo 3).
🔴🟢🔵 Test-Driven Development (TDD)¶
TDD é uma prática (não apenas uma técnica de teste) em que o teste é escrito antes do código de produção, seguindo um ciclo curto e repetitivo:
- 🔴 Red: escreva um teste para uma funcionalidade que ainda não existe. Ele deve falhar (o código ainda não existe para satisfazê-lo).
- 🟢 Green: escreva o código mínimo necessário para o teste passar — sem se preocupar ainda com elegância.
- 🔵 Refactor: com o teste passando como rede de segurança, melhore o código (Capítulo 5) sem mudar seu comportamento externo.
A promessa do TDD não é "ter mais testes" — é que escrever o teste primeiro força a pensar na interface e no comportamento esperado antes dos detalhes de implementação, e garante que todo código de produção tenha, desde o nascimento, um teste que o cubra.
🧑💻 Hands-on: testando uma API com pytest e FastAPI¶
Vamos aplicar os conceitos acima construindo e testando uma pequena API de tarefas com FastAPI e pytest, a dupla mais usada hoje no ecossistema Python para esse tipo de projeto.
Preparando o ambiente¶
mkdir api-tarefas && cd api-tarefas
python -m venv .venv
source .venv/bin/activate # no Windows: .venv\Scripts\activate
pip install fastapi "uvicorn[standard]" pytest httpx2 pytest-cov
Por que httpx2?
O TestClient do FastAPI (usado nos testes abaixo) é construído sobre o httpx2 — a continuação, sob nova manutenção, do projeto httpx, que hoje está com atividade limitada. É essa biblioteca que permite simular requisições à API sem precisar de um servidor rodando de verdade durante os testes. Instalar apenas o httpx (sem o httpx2) ainda funciona, mas gera um aviso de depreciação a cada execução da suíte de testes.
A aplicação: app/main.py¶
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI(title="API de Tarefas")
class TarefaEntrada(BaseModel):
titulo: str
concluida: bool = False
class Tarefa(TarefaEntrada):
id: int
_tarefas: dict[int, Tarefa] = {}
_proximo_id = 1
@app.post("/tarefas", response_model=Tarefa, status_code=201)
def criar_tarefa(dados: TarefaEntrada) -> Tarefa:
global _proximo_id
if not dados.titulo.strip():
raise HTTPException(status_code=422, detail="Título não pode ser vazio")
tarefa = Tarefa(id=_proximo_id, **dados.model_dump())
_tarefas[tarefa.id] = tarefa
_proximo_id += 1
return tarefa
@app.get("/tarefas", response_model=list[Tarefa])
def listar_tarefas() -> list[Tarefa]:
return list(_tarefas.values())
@app.get("/tarefas/{tarefa_id}", response_model=Tarefa)
def obter_tarefa(tarefa_id: int) -> Tarefa:
tarefa = _tarefas.get(tarefa_id)
if tarefa is None:
raise HTTPException(status_code=404, detail="Tarefa não encontrada")
return tarefa
@app.patch("/tarefas/{tarefa_id}/concluir", response_model=Tarefa)
def concluir_tarefa(tarefa_id: int) -> Tarefa:
tarefa = _tarefas.get(tarefa_id)
if tarefa is None:
raise HTTPException(status_code=404, detail="Tarefa não encontrada")
tarefa.concluida = True
return tarefa
Não deixe de criar um app/__init__.py vazio para que app seja reconhecido como um pacote Python.
Os testes: test_main.py¶
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_criar_tarefa_retorna_201_e_id():
resposta = client.post("/tarefas", json={"titulo": "Estudar pytest"})
assert resposta.status_code == 201
corpo = resposta.json()
assert corpo["titulo"] == "Estudar pytest"
assert corpo["concluida"] is False
assert "id" in corpo
def test_criar_tarefa_com_titulo_vazio_retorna_422():
resposta = client.post("/tarefas", json={"titulo": " "})
assert resposta.status_code == 422
def test_listar_tarefas_inclui_tarefa_criada():
client.post("/tarefas", json={"titulo": "Ler capítulo 6"})
resposta = client.get("/tarefas")
assert resposta.status_code == 200
titulos = [t["titulo"] for t in resposta.json()]
assert "Ler capítulo 6" in titulos
def test_obter_tarefa_inexistente_retorna_404():
resposta = client.get("/tarefas/9999")
assert resposta.status_code == 404
def test_concluir_tarefa_muda_status():
criada = client.post("/tarefas", json={"titulo": "Testar conclusão"}).json()
resposta = client.patch(f"/tarefas/{criada['id']}/concluir")
assert resposta.status_code == 200
assert resposta.json()["concluida"] is True
Repare que cada teste segue implicitamente o padrão Arrange-Act-Assert: prepara os dados de entrada, executa a ação sob teste, e verifica o resultado — uma estrutura que vale a pena manter explícita mesmo quando o código não a nomeia.
Executando e medindo cobertura¶
Saída esperada (validada neste material):
test_main.py::test_criar_tarefa_retorna_201_e_id PASSED
test_main.py::test_criar_tarefa_com_titulo_vazio_retorna_422 PASSED
test_main.py::test_listar_tarefas_inclui_tarefa_criada PASSED
test_main.py::test_obter_tarefa_inexistente_retorna_404 PASSED
test_main.py::test_concluir_tarefa_muda_status PASSED
Name Stmts Miss Cover Missing
-----------------------------------------------
app/main.py 34 2 94% 41, 48
-----------------------------------------------
TOTAL 34 2 94%
5 passed in 1.12s
94% de cobertura é um bom resultado, mas repare que o relatório aponta exatamente quais linhas não foram exercitadas (Missing: 41, 48) — nesse caso, os caminhos de sucesso de obter_tarefa e concluir_tarefa quando a tarefa existe, que não estão diretamente cobertos por um teste isolado (embora sejam exercitados indiretamente por outros testes). Esse é o valor real de medir cobertura: não é uma meta a perseguir por si só, mas um mapa de quais partes do comportamento do sistema ainda não têm uma rede de segurança.
Cobertura alta não é sinônimo de bons testes
100% de cobertura significa apenas que toda linha foi executada por algum teste — não que o comportamento foi verificado corretamente. É possível ter cobertura total com testes que não fazem nenhuma asserção significativa. Use cobertura para encontrar código não testado, nunca como meta isolada de qualidade.
🤖 Testes gerados por IA¶
Assistentes de codificação são cada vez mais usados para gerar testes automaticamente a partir do código de produção — uma forma rápida de aumentar cobertura numérica. O alerta acima vale com ainda mais força nesse caso: um teste gerado a partir do código existente tende a confirmar o que o código já faz, inclusive quando o código tem um defeito, em vez de verificar o que o código deveria fazer segundo o requisito. É o oposto do espírito do TDD (próxima seção), em que o teste nasce do requisito, antes do código existir.
Isso não invalida o uso de IA para gerar testes — apenas desloca a responsabilidade do desenvolvedor: ler e validar cada teste gerado contra o comportamento esperado (não contra o comportamento atual do código) continua sendo indispensável antes de aceitar a mudança na suíte.
Praticando TDD nesta API¶
Como exercício de TDD, tente adicionar um endpoint DELETE /tarefas/{tarefa_id} seguindo o ciclo Red-Green-Refactor:
- Red: escreva
test_deletar_tarefa_remove_da_lista, chamando um endpoint que ainda não existe. - Green: implemente o
DELETE /tarefas/{tarefa_id}da forma mais simples possível para o teste passar. - Refactor: revise o código à luz das práticas de clean code do Capítulo 5.
🎯 Atividades¶
- Rode a suíte de testes deste capítulo localmente e confirme os mesmos resultados de cobertura.
- Complete o exercício de TDD acima (endpoint
DELETE) e verifique se a cobertura deapp/main.pysobe. - Escreva um teste que verifique explicitamente o caso de sucesso de
obter_tarefa(uma tarefa que existe) — a linha que faltava na cobertura do exemplo. - Classifique um bug real que você já enfrentou (em qualquer projeto) em termos de defeito, erro e falha, conforme a distinção do início deste capítulo.
📚 Para saber mais¶
- SOMMERVILLE, Ian. Engenharia de Software. 10. ed. — Capítulo 8.
- COHN, Mike. Succeeding with Agile. Addison-Wesley, 2009 (origem da pirâmide de testes).
- BECK, Kent. Test-Driven Development: By Example. Addison-Wesley, 2002.
- Documentação oficial: pytest, FastAPI Testing