Um teste unitário pode estar verde enquanto a aplicação envia o campo errado para o banco. Um teste de integração pode falhar porque um detalhe interno mudou, embora o comportamento público continue correto. O problema não é escolher um lado para sempre. É escolher a menor fronteira que prova a afirmação certa.

Use teste unitário quando a pergunta é sobre uma unidade de comportamento e seus colaboradores não fazem parte da prova. Use teste de integração quando a confiança depende de módulos cooperando, de serialização ou de uma entrada e saída real. A visão geral dos tipos de testes no desenvolvimento de software ajuda a situar essa escolha entre outras camadas.

Os exemplos abaixo usam JavaScript e o runner nativo node:test. Executei a fixture localmente com Node.js. Ela demonstra a fronteira da decisão, não o comportamento de um banco ou de um provedor específico.

Diagrama compara um teste unitário isolado com um teste de integração que atravessa uma fronteira real.

Resposta curta

  • Teste uma função isolada quando a afirmação é sobre sua lógica e seus dados de entrada.
  • Atravesse uma fronteira quando a afirmação depende da colaboração entre módulos, da serialização ou do I/O.
  • Um fake pode provar a colaboração dentro da aplicação, mas não prova que um banco ou provedor real aceita o contrato.
  • Não escolha a camada pelo nome do arquivo. Escolha pelo que precisa permanecer real para a prova ser confiável.

Qual pergunta cada camada consegue responder?

A pergunta do teste deve aparecer no nome e na preparação do caso. Se você consegue responder usando valores em memória e uma chamada direta, a unidade é um bom começo. Se a resposta depende de uma rota, de uma consulta, de bytes serializados ou de um serviço, a prova precisa manter essa fronteira viva.

Pergunta Camada inicial O que fica real
Esta regra transforma a entrada no resultado correto? Unitária A unidade sob teste
Este serviço e seu repositório concordam sobre o formato? Integração estreita Os módulos e o adapter escolhidos
A aplicação conversa com um banco, arquivo ou API como espera? Integração externa A dependência de teste ou um sandbox controlado
O usuário consegue completar o fluxo publicado? End-to-end A aplicação atravessada pela interface

Essa tabela não é uma taxonomia rígida. Os nomes variam entre equipes. O ponto útil é declarar a fronteira. Martin Fowler descreve a mesma dificuldade no artigo "The Practical Test Pyramid": o vocabulário dos níveis não é totalmente estável, mas testes com granularidades diferentes respondem a riscos diferentes.

Quando o teste unitário é a melhor escolha?

Escolha o teste unitário quando o comportamento importante cabe em uma função ou módulo e a prova não precisa confirmar o comportamento dos colaboradores. Uma regra de preço, um normalizador ou uma decisão de permissão pode receber dados fixos e devolver um resultado verificável sem iniciar rede, banco ou filesystem.

Este é um módulo pequeno, sem dependências externas:

// totals.mjs
export function totalOf(items) {
  return items.reduce(
    (total, item) => total + item.price * item.quantity,
    0,
  );
}

O teste pode chamar a função diretamente. O colaborador real é a própria regra que queremos avaliar:

// totals.unit.test.mjs
import test from "node:test";
import assert from "node:assert/strict";
import { totalOf } from "./totals.mjs";

test("calculates the total for each item", () => {
  assert.equal(
    totalOf([
      { price: 10, quantity: 2 },
      { price: 5, quantity: 1 },
    ]),
    25,
  );
});

O módulo node:test aceita testes síncronos e funções que devolvem uma Promise. A documentação do Node.js, "Test runner" também mostra que uma exceção ou uma Promise rejeitada faz o teste falhar. Isso é suficiente para este caso porque a afirmação não depende de uma porta HTTP nem de uma tabela.

Um teste unitário perde valor quando começa a repetir a implementação. Se ele precisa conhecer a query exata, a ordem de chamadas privadas e vários detalhes de um adapter, talvez esteja tentando provar uma colaboração. Teste o comportamento público da unidade e deixe a integração provar a conversa entre as partes.

Quando o teste de integração é necessário?

Use uma integração quando o risco está na conversa entre componentes. O código pode calcular o total corretamente e ainda persistir um objeto incompatível com o repositório. Um teste unitário do cálculo não consegue descobrir esse acordo quebrado porque o adapter foi retirado da prova.

Uma integração estreita pode manter os módulos da aplicação reais e substituir apenas a infraestrutura que não precisa ser exercitada naquele caso. Neste exemplo, o serviço usa um repositório em memória. Isso prova a colaboração entre serviço e repositório, mas não afirma que PostgreSQL, índices ou transações estão corretos.

// orders.mjs
import { totalOf } from "./totals.mjs";

export function createOrder({ items, orders }) {
  const order = { items, total: totalOf(items) };
  return orders.insert(order);
}
// orders.integration.test.mjs
import test from "node:test";
import assert from "node:assert/strict";
import { createOrder } from "./orders.mjs";

test("stores the total produced by the service", () => {
  const stored = [];
  const orders = {
    insert(order) {
      const saved = { id: "order-1", ...order };
      stored.push(saved);
      return saved;
    },
  };

  const result = createOrder({
    items: [{ price: 10, quantity: 2 }],
    orders,
  });

  assert.deepEqual(result, stored[0]);
  assert.equal(stored[0].total, 20);
});

O adapter em memória é deliberado. Ele deixa o exemplo rápido e reproduzível, mas não deve ser chamado de integração com o banco. Para provar o contrato do banco, crie uma camada separada que inicialize um banco de teste, grave uma linha e leia o valor de volta. Para uma API externa, use um sandbox ou um servidor de teste controlado. Não use produção como fixture.

O que deve permanecer real no teste?

O colaborador real é uma decisão sobre a afirmação, não um prêmio por usar menos mocks. Um teste unitário pode usar um stub para uma porta externa. Um teste de integração pode usar um fake para um provedor distante e ainda manter reais o serviço, o parser e o adapter que você quer revisar.

Se o risco está em... Mantenha real... Substitua...
Regra de negócio A função e seus valores Banco, rede e relógio externo
Conversa entre módulos Serviço e módulos envolvidos Dependências fora da fronteira
JSON ou protocolo HTTP Serialização, headers e parser Rede externa instável
Esquema e transação Banco de teste e adapter Dados de produção

O guia de mocks de funções do Vitest separa spy e mock por finalidade: observar uma chamada não é o mesmo que fornecer uma implementação falsa. Essa distinção ajuda a revisar o teste, mas não transforma o Vitest em requisito. O mesmo raciocínio vale para fakes escritos pela equipe e para as APIs de mocking do próprio Node.js.

Se cada dependência é substituída, o teste pode confirmar apenas que o código chamou os dublês da maneira esperada. Se nada é substituído, o caso pode ficar lento, frágil e difícil de diagnosticar. A escolha melhor é manter real exatamente o que sustenta a afirmação e controlar o restante.

Como evitar duplicar a mesma prova?

Uma suíte saudável não repete todos os casos de borda em cada camada. Coloque a matriz de regras na camada mais barata que consiga prová-la. Depois acrescente uma integração para confirmar a fronteira que a unidade não atravessa. Um fluxo end-to-end deve acrescentar confiança sobre o caminho completo, não copiar todas as combinações internas.

Considere um endpoint que calcula um pedido e grava o total:

  1. Um teste unitário cobre desconto, quantidade inválida e arredondamento.
  2. Um teste de integração confirma que o serviço serializa e persiste o formato esperado.
  3. Um teste end-to-end confirma que uma requisição publicada atravessa autenticação, rota e resposta.

Se o terceiro teste falha porque o total está errado, o primeiro deve falhar também. Se apenas o terceiro falha porque a rota não foi registrada, não copie a regra de desconto para o teste end-to-end. Corrija a camada que não tinha uma prova.

Para um fluxo de entrega de eventos, teste a fronteira de um webhook separando assinatura, duplicatas e retry. Quando a prova depende do navegador e do caminho publicado, use testes end-to-end no navegador para cobrir o que as camadas menores não atravessam.

O artigo sobre como testar a lógica de retry sem esperar aplica uma separação parecida: a prova do scheduler não é a mesma coisa que a prova de um efeito externo. Separar as afirmações torna a falha mais fácil de localizar.

Como organizar essa escolha no CI?

Separe os comandos pelo custo e pela infraestrutura necessária, mas não use o nome do comando para esconder a fronteira. O runner nativo permite executar todos os arquivos de teste com node --test; diretórios ou padrões diferentes podem organizar os casos unitários e de integração conforme o projeto.

Uma convenção simples pode ser:

{
  "scripts": {
    "test:unit": "node --test test/unit/*.test.mjs",
    "test:integration": "node --test test/integration/*.test.mjs",
    "test": "npm run test:unit && npm run test:integration"
  }
}

O exemplo é ilustrativo. Confirme os padrões aceitos pela versão de Node.js usada no CI e garanta que cada comando encontre os arquivos esperados. Se a integração precisa de um banco, o pipeline deve criar esse serviço, esperar que esteja pronto e limpar os dados sem compartilhar estado entre casos.

O Vitest documenta isolamento por arquivo e permite organizar projetos diferentes para testes unitários e de integração. Use essa opção quando já houver Vitest no projeto. Não introduza um runner novo só para dar nome às pastas.

Quais sinais mostram que a camada está errada?

O teste costuma estar na camada errada quando seu custo e sua afirmação não combinam. Os sinais aparecem na preparação, não apenas no tempo total do CI:

  • Um teste chamado unitário inicia banco, rede ou filesystem sem que isso faça parte da afirmação.
  • Um teste de integração substitui todos os módulos relevantes e nunca atravessa a fronteira que deveria proteger.
  • O teste quebra a cada refatoração interna, embora o contrato público não tenha mudado.
  • A suíte precisa de sleep, ordem fixa ou dados compartilhados para passar.
  • O mesmo caso de negócio aparece em unitário, integração e end-to-end sem acrescentar uma nova confiança.

Quando isso acontece, escreva a afirmação em uma frase. Depois marque os colaboradores que precisam permanecer reais para que essa frase seja verdadeira. Essa revisão normalmente mostra se falta uma unidade rápida, uma integração estreita ou uma prova externa.

Perguntas frequentes

Um teste com mock é sempre um teste unitário?

Não. O nome depende da fronteira que a prova mantém real. Um teste pode combinar módulos reais e usar um fake para uma API distante. Descreva a afirmação e os colaboradores mantidos, em vez de inferir a camada somente pelo uso de mock.

Testes de integração devem usar o banco real?

Devem usar a dependência real quando a afirmação é sobre o contrato dessa dependência. Um banco de teste isolado prova mais que um repositório em memória. Ainda assim, o banco deve ser descartável e separado da produção. Para regras internas, a integração com o banco pode ser desnecessária.

Preciso seguir a pirâmide de testes?

Use a pirâmide como uma heurística de custo e granularidade, não como uma proporção obrigatória. A composição depende do risco do sistema e do tipo de fronteira. A regra mais estável é ter provas rápidas para lógica local e provas explícitas onde dados, módulos ou serviços podem discordar.

Posso chamar qualquer teste de integração?

Você pode usar uma definição local, desde que a equipe a mantenha consistente. Registre o que o nome inclui: dois módulos em memória, um banco de teste, uma API sandbox ou o sistema completo. A clareza do contrato importa mais que vencer uma discussão de vocabulário.

Conclusão

Escolha teste unitário quando a afirmação cabe em uma unidade isolada. Escolha teste de integração quando a confiança depende da colaboração, da serialização ou do I/O que a unidade não atravessa. Acrescente um teste end-to-end somente quando o caminho completo acrescentar uma prova nova.

O resultado não é uma suíte dividida por dogma. É uma suíte em que cada caso explica o risco que cobre, mantém real a fronteira necessária e falha perto da causa. Quando a próxima mudança alterar a implementação, essa separação preservará os testes que ainda descrevem o comportamento.

Fontes consultadas