Testes unitários, de integração e ponta a ponta
Os níveis de teste, o que cada um garante e como equilibrar custo e confiança.
As decisões de teste são cinco: o que testar em cada nível, como substituir dependências, como testar com o banco real, o que medir e como evitar testes frágeis.
O que testar em cada nível
| Nível | Teste aqui | Não teste aqui |
|---|---|---|
| Unitário | Regras de negócio, cálculos, validações, transformações de dados, casos-limite | Consultas SQL, integração com frameworks, configuração |
| Integração | Consultas e transações no banco real, restrições, rotas HTTP com banco, publicação e consumo de mensagens | Todas as combinações de uma regra de negócio |
| Ponta a ponta | Os fluxos que, se quebrarem, param o negócio: cadastro, login, compra | Cada variação de cada tela |
Uma regra de desconto com doze casos vira doze testes unitários, que rodam em milissegundos. A consulta que busca os pedidos com desconto vira um teste de integração. E o fluxo de compra com cupom vira um único teste de ponta a ponta.
Formatos de distribuição
| Formato | Distribuição | Quando faz sentido |
|---|---|---|
| Pirâmide | Muitos unitários, alguns de integração, poucos de ponta a ponta | Sistemas com muita regra de negócio |
| Troféu | Ênfase em integração, com unitários para a lógica e poucos de ponta a ponta | APIs e aplicações em que a maior parte do código é conversa com banco e serviços |
| Cone de sorvete | Muitos de ponta a ponta e manuais, poucos unitários | Um sinal de problema: suíte lenta e frágil |
Para um backend típico de CRUD com algumas regras, o troféu costuma render mais: os erros moram nas consultas, nas transações e nas integrações, que só aparecem com o banco de verdade.
Dublês de teste
Quando uma dependência é lenta, externa ou imprevisível, ela é substituída por um dublê. Há tipos diferentes:
| Tipo | O que faz | Exemplo |
|---|---|---|
| Stub | Devolve respostas prontas | Um serviço de frete que sempre responde R$ 20 |
| Mock | Registra as chamadas para o teste conferir | Confere que o e-mail de confirmação foi enviado uma vez |
| Fake | Uma implementação simplificada que funciona | Um repositório em memória no lugar do banco |
| Spy | O objeto real, com o registro das chamadas | O cliente HTTP real, observado |
| Substitua | Não substitua |
|---|---|
| Serviços externos: pagamento, e-mail, APIs de terceiros | O seu próprio banco, quando o que está em teste é o acesso a dados |
| O relógio, para testar prazos e datas | A lógica que está sendo testada |
| Geradores aleatórios, para resultados previsíveis | Bibliotecas que você usa, a ponto de reimplementá-las no teste |
Um sinal de excesso de mocks: o teste quebra a cada refatoração, mesmo quando o comportamento não mudou, porque está preso à forma como o código chama as dependências.
Testar com o banco real
Usar o banco real nos testes de integração era lento e trabalhoso. Hoje, a forma mais comum é subir o banco num container durante os testes.
| Abordagem | Como funciona | Vantagens | Cuidados |
|---|---|---|---|
| Testcontainers | A biblioteca sobe um container do banco no início dos testes e o remove no fim | O mesmo banco e versão de produção; isolado por execução | Exige Docker na máquina e na integração contínua |
| Banco no Docker Compose | Um serviço de banco que fica de pé durante o desenvolvimento | Simples, rápido entre execuções | Estado acumulado entre execuções |
| Banco em memória diferente (SQLite no lugar de PostgreSQL) | Outro banco, mais leve | Muito rápido | Comportamento diferente: tipos, restrições e SQL não batem com produção |
A terceira opção parece prática e engana: um teste que passa no SQLite não garante nada sobre o PostgreSQL. O exemplo do nível Conceito, com o ON CONFLICT, depende de um comportamento específico do banco.
Para manter os testes independentes entre si:
- Transação desfeita no fim de cada teste: rápido, mas não serve para testar código que controla as próprias transações.
- Limpar as tabelas antes de cada teste: mais lento, funciona sempre.
- Dados únicos por teste, como e-mails com um sufixo aleatório: os testes podem rodar em paralelo sem se atrapalhar.
Cobertura: um número útil, mas não um objetivo
A cobertura mede quanto do código foi executado pelos testes. Ela mostra o que não está testado, mas não diz se o que foi executado foi conferido.
it("calcula o desconto", () => {
calcularDesconto(80_000, "DEZ"); // executa todas as linhas
// e não confere nada
});Esse teste dá 100% de cobertura na função e não pegaria nenhum defeito. Use a cobertura para encontrar áreas importantes sem teste, e não como meta. Uma meta de cobertura obriga a escrever testes para código trivial e incentiva testes sem conferência.
Uma técnica que mede a qualidade dos testes é o teste de mutação: uma ferramenta altera o código de propósito (troca > por >=, remove uma linha) e confere se algum teste falha. Se nenhum falhar, os testes não estavam conferindo aquele comportamento. Ferramentas: Stryker para JavaScript e C#, mutmut para Python, PIT para Java.
Testes frágeis
Um teste frágil (flaky) às vezes passa e às vezes falha, sem mudança no código. Ele destrói a confiança na suíte: depois de algumas falhas falsas, a equipe passa a ignorar as verdadeiras.
| Causa | Exemplo | Correção |
|---|---|---|
| Dependência do horário | Um teste que falha à meia-noite ou no último dia do mês | Injetar o relógio e fixar a data no teste |
| Ordem dos testes | Um teste depende de dados criados por outro | Cada teste prepara os próprios dados |
| Espera fixa | sleep(1000) esperando algo assíncrono terminar | Esperar a condição, com tempo limite |
| Dados compartilhados | Dois testes em paralelo usam o mesmo registro | Dados únicos por teste |
| Dependência de dados que mudam | Um teste que assume quais registros existem | Derivar o esperado dos próprios dados |
A última linha aconteceu neste próprio site. Um teste de ponta a ponta da busca procurava o tópico "Índices" e esperava que ele aparecesse como "em breve". Quando o tópico foi publicado, o teste falhou, sem nenhum defeito no código. A correção foi fazer o teste escolher um tópico "em breve" a partir do índice de busca, em vez de fixar um nome.
Testes na integração contínua
| Prática | Por quê |
|---|---|
| Todos os testes rodam a cada mudança proposta | O defeito é pego antes de entrar na branch principal |
| A mudança só entra se os testes passarem | Uma suíte que pode falhar sem consequência deixa de ser olhada |
| Testes rápidos primeiro | Retorno em minutos para a maioria dos erros |
| Testes de ponta a ponta contra a build de produção | Confere o que vai de fato para o ar |
| Testes frágeis tratados como defeito | Corrigidos ou removidos, e não ignorados |
Resumo da decisão
- Unitários para regras de negócio e casos-limite, sem banco.
- Integração com o banco real em container, para consultas, transações e restrições.
- Ponta a ponta só para os fluxos críticos.
- Dublês para serviços externos, relógio e aleatoriedade, e não para o que você precisa confirmar.
- Cobertura para achar lacunas, e não como meta; mutação para medir a qualidade.
- Falhas investigadas, nunca silenciadas.
O próximo nível mostra o mesmo teste unitário nas cinco linguagens, um teste de integração com Testcontainers e a revisão de testes gerados por IA.
Prompts para aprofundar
Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.
Montar a estratégia de testes do meu sistema
Quero definir a estratégia de testes do meu sistema. Faça estas perguntas, uma por vez, e espere as respostas: - Que tipo de sistema é: muita regra de negócio, ou principalmente consultas e integrações? - Quais testes existem hoje e quanto tempo a suíte leva? - Quais fluxos, se quebrarem, param o negócio? - Quais dependências externas o sistema tem? - Os testes rodam na integração contínua? Com as respostas, proponha o que testar em cada nível, a distribuição entre os níveis, como testar o acesso ao banco, quais dependências substituir por dublês e quais fluxos cobrir de ponta a ponta. Termine com um plano em ordem de prioridade, começando pelo que dá mais confiança com menos esforço.
Testar com o banco de dados real
Quero passar a testar o acesso a dados contra o banco real, em vez de simular o banco. Pergunte qual linguagem, framework de testes e banco eu uso. Explique: 1. Por que um banco diferente nos testes, como SQLite no lugar de PostgreSQL, dá uma falsa sensação de segurança. 2. Como usar Testcontainers na minha linguagem para subir o banco durante os testes. 3. Como aplicar as migrações do projeto antes dos testes. 4. As formas de isolar um teste do outro: transação desfeita, limpeza de tabelas e dados únicos, com as vantagens de cada uma. 5. Como manter a suíte rápida: um container por suíte, testes em paralelo. 6. Como rodar isso na integração contínua. Termine com a estrutura de arquivos e a configuração recomendadas.
Acabar com os testes frágeis
Minha suíte tem testes que às vezes passam e às vezes falham. Quero entender as causas e corrigir. Explique as causas mais comuns de testes frágeis, com um exemplo de código para cada uma e a correção: 1. Dependência do horário ou da data. 2. Dependência da ordem dos testes. 3. Esperas fixas por algo assíncrono. 4. Dados compartilhados entre testes em paralelo. 5. Dependência de dados que mudam com o tempo. 6. Dependência de serviços externos. Explique também como identificar quais testes são frágeis, e por que acrescentar novas tentativas automáticas esconde o problema em vez de resolvê-lo. Peça que eu cole um teste frágil do meu projeto, se tiver um, e ajude a encontrar a causa.