DevGuideTK
← Trilha Backend

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ívelTeste aquiNão teste aqui
UnitárioRegras de negócio, cálculos, validações, transformações de dados, casos-limiteConsultas SQL, integração com frameworks, configuração
IntegraçãoConsultas e transações no banco real, restrições, rotas HTTP com banco, publicação e consumo de mensagensTodas as combinações de uma regra de negócio
Ponta a pontaOs fluxos que, se quebrarem, param o negócio: cadastro, login, compraCada 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

FormatoDistribuiçãoQuando faz sentido
PirâmideMuitos unitários, alguns de integração, poucos de ponta a pontaSistemas com muita regra de negócio
TroféuÊnfase em integração, com unitários para a lógica e poucos de ponta a pontaAPIs e aplicações em que a maior parte do código é conversa com banco e serviços
Cone de sorveteMuitos de ponta a ponta e manuais, poucos unitáriosUm 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:

TipoO que fazExemplo
StubDevolve respostas prontasUm serviço de frete que sempre responde R$ 20
MockRegistra as chamadas para o teste conferirConfere que o e-mail de confirmação foi enviado uma vez
FakeUma implementação simplificada que funcionaUm repositório em memória no lugar do banco
SpyO objeto real, com o registro das chamadasO cliente HTTP real, observado
SubstituaNão substitua
Serviços externos: pagamento, e-mail, APIs de terceirosO seu próprio banco, quando o que está em teste é o acesso a dados
O relógio, para testar prazos e datasA lógica que está sendo testada
Geradores aleatórios, para resultados previsíveisBibliotecas 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.

AbordagemComo funcionaVantagensCuidados
TestcontainersA biblioteca sobe um container do banco no início dos testes e o remove no fimO mesmo banco e versão de produção; isolado por execuçãoExige Docker na máquina e na integração contínua
Banco no Docker ComposeUm serviço de banco que fica de pé durante o desenvolvimentoSimples, rápido entre execuçõesEstado acumulado entre execuções
Banco em memória diferente (SQLite no lugar de PostgreSQL)Outro banco, mais leveMuito rápidoComportamento 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.

CausaExemploCorreção
Dependência do horárioUm teste que falha à meia-noite ou no último dia do mêsInjetar o relógio e fixar a data no teste
Ordem dos testesUm teste depende de dados criados por outroCada teste prepara os próprios dados
Espera fixasleep(1000) esperando algo assíncrono terminarEsperar a condição, com tempo limite
Dados compartilhadosDois testes em paralelo usam o mesmo registroDados únicos por teste
Dependência de dados que mudamUm teste que assume quais registros existemDerivar 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áticaPor quê
Todos os testes rodam a cada mudança propostaO defeito é pego antes de entrar na branch principal
A mudança só entra se os testes passaremUma suíte que pode falhar sem consequência deixa de ser olhada
Testes rápidos primeiroRetorno em minutos para a maioria dos erros
Testes de ponta a ponta contra a build de produçãoConfere o que vai de fato para o ar
Testes frágeis tratados como defeitoCorrigidos ou removidos, e não ignorados

Resumo da decisão

  1. Unitários para regras de negócio e casos-limite, sem banco.
  2. Integração com o banco real em container, para consultas, transações e restrições.
  3. Ponta a ponta só para os fluxos críticos.
  4. Dublês para serviços externos, relógio e aleatoriedade, e não para o que você precisa confirmar.
  5. Cobertura para achar lacunas, e não como meta; mutação para medir a qualidade.
  6. 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.