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.

Para que servem os testes

Todo desenvolvedor testa o próprio código: roda a aplicação, clica, confere o resultado. O problema é que esse teste manual acontece uma vez, cobre o que a pessoa lembrou de conferir e não se repete quando alguém muda o código seis meses depois.

Um teste automatizado é um programa que executa uma parte do sistema e confere o resultado. Ele roda em segundos, sempre do mesmo jeito, a cada mudança. Os ganhos:

  • Confiança para mudar: uma refatoração que quebra algo é detectada antes do deploy.
  • Documentação executável: o teste mostra, com exemplos, o que o código deve fazer.
  • Retorno rápido: o erro aparece minutos depois de escrito, e não em produção.
  • Revisão de código gerado: quando uma IA escreve a função, os testes dizem se ela faz o que foi pedido.

O último ponto pesa cada vez mais. Ler código gerado e achar que está certo é diferente de comprovar que está certo. Um teste que exercita os casos importantes, inclusive os de erro, é a forma mais barata de comprovar.

A anatomia de um teste

Quase todo teste tem três partes, conhecidas como preparar, agir, conferir:

it("aplica 10% de desconto limitado a R$ 50", () => {
  // Preparar: os dados de entrada
  const totalEmCentavos = 80_000; // R$ 800,00

  // Agir: executa o que está sendo testado
  const desconto = calcularDesconto(totalEmCentavos, "DEZ");

  // Conferir: o resultado é o esperado?
  expect(desconto).toBe(5_000); // R$ 50,00, e não R$ 80,00
});

O nome do teste descreve o comportamento, e não a implementação. Quando ele falha, o nome já diz o que quebrou.

Os três níveis

NívelO que testaDependênciasVelocidadeQuando falha, aponta para
UnitárioUma função ou classe, isoladaNenhuma real: banco e serviços são simulados ou não existemMilissegundosUma função específica
IntegraçãoVárias partes juntas: código e banco, código e filaReais, ou muito próximas das reaisSegundosUma combinação de partes
Ponta a pontaO sistema inteiro, como o usuário usaTodas, com navegador ou chamadas HTTP reaisSegundos a minutosAlgum lugar do caminho inteiro

Unitário

Testa uma regra de negócio, um cálculo, uma validação, sem banco, rede ou arquivo. Por isso roda em milissegundos, e é possível ter milhares deles. É o nível certo para regras com muitos casos: impostos, descontos, prazos, permissões.

Integração

Testa se as partes conversam corretamente: se a consulta SQL funciona no banco de verdade, se a mensagem publicada chega à fila, se a rota HTTP grava o que deveria. Pega os erros que moram entre as partes, que um teste unitário não alcança por definição.

Ponta a ponta

Testa um fluxo completo pela interface que o usuário usa: abre o navegador, faz login, adiciona ao carrinho, finaliza a compra. É o que mais se parece com o uso real, e também o mais lento, o mais caro de manter e o mais sujeito a falhar por motivos alheios ao código.

O que um teste simulado não vê

Para isolar o código, os testes unitários costumam substituir o banco por um dublê: um objeto falso que devolve respostas prontas. Isso deixa o teste rápido, mas cria um ponto cego: o teste confere o código contra a ideia que você tem do banco, e não contra o banco.

Reproduzimos esse ponto cego com uma função que cadastra um cliente e devolve null se o e-mail já existir, usando INSERT ... ON CONFLICT (email) DO NOTHING.

TesteResultado
Unitário, com o banco simulado, para cliente novoPassou
Unitário, com o banco simulado, para e-mail repetidoPassou
Integração, num PostgreSQL real, com a migração do projetoFalhou

O erro do PostgreSQL: não existe uma restrição única que corresponda ao ON CONFLICT. A migração tinha esquecido o UNIQUE na coluna email. Os testes com o banco simulado nunca perceberiam, porque o dublê devolve o que mandaram ele devolver. Com o UNIQUE na migração, o teste de integração passou.

A pirâmide

A recomendação clássica é ter muitos testes unitários, alguns de integração e poucos de ponta a ponta, formando uma pirâmide:

        /\         poucos de ponta a ponta: fluxos críticos
       /  \
      /----\       alguns de integração: banco, filas, APIs
     /      \
    /--------\     muitos unitários: regras de negócio

A lógica é de custo. Quanto mais alto o nível, mais lento, mais caro de escrever, mais difícil de diagnosticar quando falha. Mas quanto mais alto, mais confiança ele dá de que o sistema funciona de verdade.

A pirâmide é um ponto de partida, e não uma lei. Num backend que é principalmente consultas ao banco e pouca regra de negócio, os testes de integração rendem mais que centenas de testes unitários com o banco simulado. O nível Soluções discute como equilibrar.

Por que isso importa quando a IA escreve o código

Assistentes escrevem testes com facilidade, inclusive quando a única instrução é "crie testes para esta função". Os testes gerados costumam ter estes problemas:

  • Conferem que a função foi chamada, e não que o resultado está certo.
  • Simulam tudo, inclusive o banco, e passam com uma consulta que falharia de verdade.
  • Cobrem só o caminho feliz, sem entradas inválidas, limites e erros.
  • Repetem a lógica da implementação no próprio teste, e passam mesmo com a lógica errada.
  • São ajustados até passar quando falham, em vez de revelar o defeito no código.

O último é o mais perigoso: um teste que falha está dando uma informação. Afrouxar o teste para ele passar esconde o defeito que ele encontrou.

O próximo nível compara as estratégias de teste e mostra como escolher o que testar em cada nível.

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.

  • Testes em cinco minutos

    Explique em no máximo cinco minutos de leitura o que são testes automatizados e os três níveis de teste, para alguém que programa mas sempre testou clicando na aplicação.
    
    Cubra só o essencial: para que servem os testes, a diferença entre unitário, integração e ponta a ponta, e por que um teste com o banco simulado pode passar enquanto o código real falha.
    
    Use o exemplo de uma regra de desconto numa loja virtual. No final, me faça três perguntas curtas para eu verificar se entendi, e corrija as minhas respostas.
  • Os três níveis com o mesmo exemplo

    Quero entender os níveis de teste vendo a mesma funcionalidade testada em cada um.
    
    Aja como um mentor de backend. Pergunte primeiro qual é a minha experiência com testes e em qual linguagem eu trabalho.
    
    Use como exemplo a aplicação de um cupom de desconto numa compra. Conduza assim, uma etapa por vez:
    1. Mostre um teste unitário da regra de desconto e pergunte o que ele garante e o que não garante.
    2. Mostre um teste de integração que grava o pedido com desconto no banco e pergunte que tipo de defeito ele pega que o unitário não pega.
    3. Mostre um teste de ponta a ponta do fluxo de compra com cupom e pergunte por que não vale a pena testar todas as variações nesse nível.
    4. Explique a pirâmide de testes e quando o formato de troféu faz mais sentido.
    
    Espere a minha resposta em cada etapa antes de continuar.
  • O que torna um teste bom

    Quero aprender a distinguir um teste bom de um teste que só parece bom.
    
    Explique as características de um bom teste: confere o comportamento e não a implementação, falha quando o comportamento quebra e só nesse caso, é independente dos outros, é rápido e tem um nome que descreve o comportamento.
    
    Depois me mostre oito testes, um por vez, e pergunte o que há de errado em cada um, se houver. Inclua:
    - um teste sem nenhuma conferência;
    - um que repete a fórmula da implementação no valor esperado;
    - um que só confere que uma função foi chamada;
    - um que depende da data de hoje;
    - um que depende de outro teste ter rodado antes;
    - um com uma espera fixa;
    - dois testes corretos.
    
    Corrija cada resposta e mostre a versão melhorada.
  • Quando o dublê engana

    Quero entender os dublês de teste e o ponto cego que eles criam.
    
    Explique a diferença entre stub, mock, fake e spy, com um exemplo de cada.
    
    Depois mostre um caso concreto: uma função que usa INSERT com ON CONFLICT numa coluna sem restrição única. Mostre um teste unitário com o banco simulado que passa e um teste de integração com o banco real que falha, e explique por quê.
    
    Em seguida, me apresente seis dependências (banco de dados próprio, gateway de pagamento, envio de e-mail, relógio do sistema, uma biblioteca de cálculo de impostos, um serviço interno de outra equipe) e pergunte, para cada uma, se eu substituiria por um dublê nos testes e em qual nível. Corrija as minhas respostas.