Um teste é uma afirmação pequena e repetível sobre o que seu código deve fazer, verificada automaticamente em vez de confiada à memória. Isso parece quase simples demais para precisar de explicação, mas quase toda decisão confusa sobre testes em um projeto Node.js - quantos testes de integração versus testes unitários, se deve usar mock em uma dependência ou iniciar um banco de dados real, se Vitest ou node:test é o runner correto - remonta a trade-offs escondidos dentro dessa definição de uma frase.
Noções Básicas de Testes apresenta uma suíte em execução com exemplos de node:test e Supertest funcionando; as páginas específicas de ferramentas que seguem (node:test, Vitest, Testcontainers, Supertest, Contract/Pact) aprofundam cada camada. Esta página permanece um nível acima: o que um teste realmente verifica, por que a pirâmide tem a forma que tem e como as peças - runner, asserções, duplos - se relacionam entre si sob qualquer ferramenta específica.
Uma suíte de testes é um portfólio de afirmações comportamentais automatizadas, deliberadamente em camadas para que verificações rápidas e estreitas capturem a maioria dos bugs e verificações mais lentas e amplas capturem o restante.
Por que Importa: Toda decisão de teste é, na verdade, um trade-off entre confiança (o quanto da realidade um teste reflete) e velocidade de feedback (o quão rápido ele informa que algo está errado) - entender esse trade-off explica por que a pirâmide, e não um cubo ou uma única camada, é a forma que funciona.
Conceitos Chave:runner de teste, biblioteca de asserção, duplo de teste, determinismo, flakiness (instabilidade), cobertura.
Quando Usar: Decidir quantos testes de integração uma funcionalidade realmente precisa, escolher entre um mock e uma dependência real, diagnosticar uma suíte instável ou explicar a uma equipe por que 100% de cobertura não é o objetivo.
Limitações / Trade-offs: Nenhuma suíte de testes prova a correção - ela prova que os comportamentos específicos que alguém pensou em verificar ainda se mantêm. Caminhos não testados permanecem exatamente tão arriscados quanto se nenhum teste existisse.
Tópicos Relacionados: testes unitários vs. de integração vs. de contrato vs. ponta a ponta, mocking e injeção de dependência, isolamento de testes, portões de integração contínua.
Remova as ferramentas e um teste são três coisas acontecendo em sequência: algum código é executado (o sujeito), o resultado é comparado a uma expectativa (a asserção), e o resultado é reportado como sucesso ou falha sem um humano observando. Toda ferramenta de teste no ecossistema Node, desde o módulo embutido node:test até Vitest e Jest, é, em última análise, apenas infraestrutura em torno dessa mesma forma de três etapas.
Essa infraestrutura, na verdade, se divide em duas funções separáveis que são fáceis de confundir porque a maioria das ferramentas agrupa ambas. Um runner de teste descobre arquivos de teste, os executa, isola falhas e reporta resultados - ele responde "quais testes foram executados e eles passaram?". Uma biblioteca de asserção fornece as funções de comparação dentro de um teste (assert.equal, expect(x).toBe(y)) - ela responde "esta afirmação específica foi verdadeira?". O próprio node:test do Node se emparelha com node:assert; Vitest e Jest agrupam um runner com sua própria API de asserção no estilo expect. Saber que essas são preocupações diferentes explica por que você pode misturar e combinar - usar node:assert dentro de um arquivo executado pelo Vitest funciona porque o runner não se importa com qual estilo de asserção você usa.
Uma analogia útil: pense em uma suíte de testes como um portfólio de pequenas apostas independentes sobre o comportamento do seu código, cada uma barata de fazer e barata de verificar. Nenhuma aposta única prova que todo o sistema funciona - juntas, um conjunto bem escolhido delas torna muito improvável que algo importante esteja silenciosamente quebrado.
// A forma de três etapas, independentemente de qual ferramenta a envolveconst result = calculateTax(100, 0.2); // sujeito é executadoassert.equal(result, 20); // asserção compara// runner reporta sucesso/falha - nenhum humano observou acontecer
A pirâmide de testes - muitos testes unitários, menos testes de integração, ainda menos testes de contrato, um punhado de testes ponta a ponta - não é uma preferência estilística; ela decorre diretamente de um trade-off entre duas coisas que todo teste possui: confiança e velocidade de feedback.
Um teste unitário que chama uma função pura diretamente é rápido (milissegundos), mas estreito - ele prova que essa função se comporta corretamente em isolamento, não dizendo nada sobre se ela está corretamente conectada ao banco de dados, à camada HTTP ou a outro serviço. Um teste ponta a ponta que atinge uma API real implantada prova que todo o sistema funciona em conjunto, mas é lento, caro de executar e toca em muitas partes móveis, de modo que uma falha pode ser causada por quase qualquer coisa. A forma de pirâmide - muitos testes rápidos e estreitos e poucos testes lentos e amplos - é a maneira matematicamente sensata de maximizar a confiança por segundo de tempo de feedback: capture a maior parte dos bugs de forma barata na base e reserve as verificações caras e sistêmicas para as coisas que apenas elas podem provar.
Duplos de teste são o mecanismo que permite manter um teste estreito de propósito, substituindo uma dependência real por um substituto para que o teste isole apenas a lógica que está verificando:
Um stub retorna uma resposta pré-definida, sem lógica - útil para controlar o que uma dependência "diz" sem se importar como foi chamada.
Um mock adicionalmente grava e verifica como foi chamado - útil quando a própria interação (esta função foi chamada exatamente uma vez, com estes argumentos) é o que está sendo testado.
Um fake é uma implementação funcional, mas simplificada - um repositório em memória que substitui um banco de dados real - útil quando você deseja comportamento real sem infraestrutura real.
Recorrer a um duplo em vez de uma dependência real é, em si, um trade-off de confiança/velocidade em miniatura: uma chamada de banco de dados stubada executa em microssegundos, mas não pode capturar um erro de sintaxe SQL real; uma instância real do Postgres via Testcontainers captura esse erro, mas custa segundos de inicialização do container por execução de suíte. Nenhuma escolha é universalmente correta - depende do que aquele teste específico está tentando provar.
// Um stub: resposta pré-definida, sem verificação de como foi chamadoconst repo = { findById: async () => ({ id: "1", status: "paid" }) };// Um mock: mesma forma, mas o teste pode afirmar sobre a chamada em siconst repo = { findById: vi.fn().mockResolvedValue({ id: "1", status: "paid" }) };expect(repo.findById).toHaveBeenCalledWith("1"); // verificando a interação
Duas propriedades determinam se algo disso é confiável: determinismo e isolamento. Um teste determinístico produz o mesmo resultado a cada execução, dada a mesma base de código - sem depender do tempo de relógio, valores aleatórios ou tempo de rede sem ser fixado ou ter mock. Isolamento significa que a configuração ou o estado restante de um teste não pode afetar o resultado de outro, e é por isso que beforeEach resetando estado compartilhado (um mapa, um mock, uma transação de banco de dados) é um requisito estrutural, não uma preferência de estilo. Uma suíte que viola qualquer propriedade produz flakiness (instabilidade) - testes que falham intermitentemente por razões não relacionadas ao erro no código - o que é corrosivo precisamente porque ensina os engenheiros a reexecutar falhas em vez de confiar nelas.
Cobertura - a porcentagem de linhas ou branches que uma suíte de testes executa - é um dos números mais consistentemente mal interpretados na engenharia de software. Cobertura mede qual código executou durante a suíte, não o que foi realmente verificado. Um teste que chama uma função e não afirma nada significativo sobre seu resultado ainda conta como cobertura para todas as linhas que essa função executou, sem provar nada. Cobertura é um sinal útil para encontrar código que ninguém testa; é um alvo ruim para otimizar diretamente, pois perseguir uma porcentagem recompensa testes que tocam código, não testes que capturam bugs.
Em escala, a pirâmide ganha uma camada que a versão básica omite: testes de contrato, que ficam entre integração e ponta a ponta. Quando múltiplos serviços evoluem independentemente, um teste de integração contra uma dependência real prova que o comportamento de hoje funciona, mas não diz nada sobre se o deploy de amanhã dessa dependência vai te quebrar - e um ambiente ponta a ponta completo para cada combinação de versões de serviço não escala. Um teste de contrato, em vez disso, captura o acordo entre um consumidor e um provedor (este endpoint retorna esta forma) e verifica ambos os lados contra esse contrato compartilhado independentemente, capturando mudanças que quebram sem precisar de todos os serviços rodando juntos.
Testes de carga são uma disciplina relacionada, mas distinta, que vale a pena nomear precisamente porque é fácil agrupá-los com "testes" e, em seguida, aplicar o modelo mental errado a eles. Um teste funcional pergunta "o comportamento está correto?". Um teste de carga pergunta "o comportamento correto se sustenta sob concorrência e volume" - ele valida um objetivo de nível de serviço (latência, taxa de erro sob N requisições/segundo), não uma saída específica, e é por isso que ele pertence fora da pirâmide, em vez de ser seu topo.
Camada
Força
Fraqueza
Melhor Ajuste
Testes unitários
Feedback mais rápido; aponta a função exata quebrada
Não prova nada sobre a conexão entre componentes
Lógica pura, cálculos, regras de validação
Testes de integração
Prova que os componentes realmente funcionam juntos (DB real, HTTP real)
Mais lentos; falha pode implicar vários componentes de uma vez
Consultas de repositório/DB, conexões de rota HTTP
Testes de contrato
Captura mudanças que quebram entre serviços sem um ambiente completo
Tão bom quanto a cobertura do contrato do uso real
Serviços implantados independentemente com APIs compartilhadas
Testes ponta a ponta
Maior confiança de que o sistema implantado realmente funciona
Mais lentos, mais frágeis, mais difíceis de depurar em caso de falha
Um punhado de jornadas críticas do usuário, não cobertura geral
As escolhas de ferramentas do ecossistema Node se mapeiam para esse mesmo trade-off. O módulo node:test embutido minimiza dependências e custo de inicialização, favorecendo a extremidade rápida da pirâmide; Vitest adiciona uma API de mock mais rica e ergonomia de modo de observação para equipes que realizam trabalho unitário e de integração mais pesado; Testcontainers existe especificamente para tornar o custo da camada de integração de "dependência real" tolerável, automatizando instâncias Docker descartáveis em vez de um banco de dados de teste compartilhado e com estado.
"Mais testes sempre significam mais confiança." Cem testes unitários sem cobertura de integração podem ainda perder um bug de conexão que um único teste de integração bem colocado pegaria - a confiança vem do que é testado, não da contagem.
"100% de cobertura significa que o código está bem testado." Cobertura prova linhas executadas, não que algo significativo foi afirmado sobre o resultado - uma suíte pode atingir 100% e ainda perder bugs reais.
"Mocks e stubs são a mesma coisa." Um stub retorna um valor; um mock adicionalmente verifica se a interação em si aconteceu como esperado. Usar um mock onde um stub seria suficiente adiciona fragilidade sem benefício.
"Um teste instável é um aborrecimento menor." A instabilidade prejudica ativamente o valor da suíte - uma vez que uma equipe aprende a reexecutar falhas em vez de confiar nelas, falhas reais começam a ser reexecutadas também.
"Testes de integração são estritamente melhores que testes unitários porque são mais realistas." Eles são realistas e lentos e mais difíceis de depurar em caso de falha - a forma de pirâmide existe porque a velocidade e a precisão da camada base importam tanto quanto o realismo da camada superior.
O que realmente faz algo ser um "teste", independentemente da ferramenta usada?
Três coisas: o código é executado, um resultado é comparado a uma expectativa e o resultado é reportado automaticamente sem um humano observando. Todo runner e biblioteca de asserção é infraestrutura em torno dessa mesma forma.
Por que a pirâmide de testes tem essa forma específica em vez de ser uniforme entre as camadas?
Porque a confiança e a velocidade de feedback se compensam - testes estreitos são baratos e rápidos, mas provam menos; testes amplos provam mais, mas custam mais. Ponderar em direção à camada rápida e estreita maximiza quanta confiança você obtém por segundo de tempo de execução do teste.
Como um runner de teste difere de uma biblioteca de asserção?
O runner descobre, executa e reporta sobre arquivos de teste - é infraestrutura para executar coisas. A biblioteca de asserção fornece as funções de comparação usadas dentro de um teste para declarar uma expectativa. Algumas ferramentas (Vitest, Jest) agrupam ambas; node:test e node:assert são módulos separados que se emparelham.
Qual é a diferença prática entre um stub, um mock e um fake?
Um stub retorna um valor pré-definido sem comportamento além disso.
Um mock faz o mesmo, mas também grava e pode verificar como foi chamado.
Um fake é uma implementação simplificada, mas genuinamente funcional, como um armazenamento em memória que substitui um banco de dados real.
Quando devo usar uma dependência real em vez de um duplo de teste?
Quando a coisa que você está realmente tentando verificar depende do comportamento real dessa dependência - a correção de uma consulta SQL, por exemplo, não pode ser provada por um stub que retorna o que você mandou ele retornar. Recorra a uma dependência real (frequentemente via Testcontainers) quando o duplo permitir que um bug real passe despercebido.
Por que testes instáveis importam tanto se eventualmente passam?
Porque um teste que às vezes falha por razões não relacionadas ao código ensina a equipe a desconfiar e reexecutar falhas - o que significa que uma falha genuína introduzida mais tarde tem maior probabilidade de ser reexecutada em vez de investigada.
O que causa instabilidade em primeiro lugar?
Geralmente uma quebra no determinismo ou isolamento - dependência do tempo de relógio real, aleatoriedade sem semente, tempo de rede, ou estado restante de um teste anterior que não foi resetado entre as execuções.
A alta cobertura de testes é um bom objetivo para definir para uma equipe?
Não diretamente - a cobertura mede qual código executou durante a suíte, não o que foi significativamente verificado. É útil para encontrar código completamente não testado, mas otimizar o número em si recompensa testes que tocam código em vez de testes que capturam bugs.
Onde os testes de contrato se encaixam que os testes de integração não cobrem?
Testes de integração provam que a conexão de hoje com uma dependência real funciona no momento. Testes de contrato capturam explicitamente o acordo entre um consumidor e um provedor, para que qualquer um dos lados possa verificar independentemente que uma mudança futura não quebrou o outro - sem precisar de ambos os serviços rodando juntos.
Testes de carga fazem parte da pirâmide de testes?
Não realmente - eles respondem a uma pergunta diferente. Testes funcionais (a pirâmide) perguntam "este comportamento está correto?". Testes de carga perguntam "o comportamento correto se sustenta sob concorrência e volume", validando um objetivo de nível de serviço em vez de uma saída específica.
Devo testar funções privadas e internas diretamente?
Geralmente não - testar através da API pública (uma função exportada, uma rota HTTP) verifica o comportamento que realmente importa para os chamadores e sobrevive a refatorações internas que não alteram esse comportamento. Testar internos acopla a suíte a detalhes de implementação que são livres para mudar.
Por que alguns projetos Node executam tanto `node:test` quanto Vitest?
Geralmente não por design - misturar runners em um repositório tende a confundir os contribuidores sobre qual suíte cobre o quê. A maioria das equipes padroniza em um por repositório, ou no máximo um por pacote em um monorepo, precisamente para evitar essa ambiguidade.