Durante anos, escrever testes em Node.js significava instalar um framework externo — Jest, Mocha, Vitest — antes da primeira linha de teste. Desde o Node.js 20, o próprio runtime traz um test runner nativo e estável no módulo node:test: suítes, hooks, mocks, relatórios e modo watch, tudo sem nenhuma dependência no package.json.
O primeiro teste
Basta importar test de node:test e usar o node:assert/strict para as asserções. O arquivo abaixo testa uma função simples de soma:
// soma.js
export function soma(a, b) {
return a + b;
}
// soma.test.js
import test from 'node:test';
import assert from 'node:assert/strict';
import { soma } from './soma.js';
test('soma dois números', () => {
assert.equal(soma(2, 3), 5);
});
test('funciona com negativos', () => {
assert.equal(soma(-4, 1), -3);
});
Para rodar, use a flag --test. Sem argumentos, o Node procura automaticamente arquivos com nomes como *.test.js, *-test.js, *_test.js, test-*.js ou qualquer arquivo dentro de uma pasta test/:
node --test
Um script no package.json deixa o comando com a cara de sempre:
{
"type": "module",
"scripts": {
"test": "node --test"
}
}
describe, it e hooks
Quem vem do Jest ou do Mocha encontra a mesma API: describe() e it() são aliases de suite() e test(), e os hooks before(), after(), beforeEach() e afterEach() preparam e limpam o estado entre os testes:
import { describe, it, beforeEach } from 'node:test';
import assert from 'node:assert/strict';
class Carrinho {
itens = [];
adicionar(produto, preco) {
this.itens.push({ produto, preco });
}
total() {
return this.itens.reduce((soma, item) => soma + item.preco, 0);
}
}
describe('Carrinho', () => {
let carrinho;
beforeEach(() => {
carrinho = new Carrinho();
});
it('começa vazio', () => {
assert.equal(carrinho.total(), 0);
});
it('soma o preço dos itens', () => {
carrinho.adicionar('Teclado', 150);
carrinho.adicionar('Mouse', 50);
assert.equal(carrinho.total(), 200);
});
});
Testes assíncronos funcionam com async/await sem nenhuma configuração extra — se a Promise rejeitar, o teste falha.
Mocks sem biblioteca externa
O objeto mock cria funções espiãs e substitui métodos de objetos. Cada mock registra quantas vezes foi chamado, com quais argumentos e o que retornou:
import { test, mock } from 'node:test';
import assert from 'node:assert/strict';
async function notificar(usuario, mailer) {
if (!usuario.ativo) return false;
await mailer.enviar(usuario.email, 'Bem-vindo!');
return true;
}
test('envia e-mail só para usuários ativos', async () => {
const mailer = { enviar: mock.fn(async () => 'ok') };
await notificar({ ativo: true, email: 'ana@site.com' }, mailer);
await notificar({ ativo: false, email: 'bruno@site.com' }, mailer);
assert.equal(mailer.enviar.mock.callCount(), 1);
assert.deepEqual(mailer.enviar.mock.calls[0].arguments, ['ana@site.com', 'Bem-vindo!']);
});
Dentro de um teste, t.mock.method(objeto, 'metodo') espiona um método existente e é restaurado automaticamente quando o teste termina. Versões recentes também oferecem mock.timers, que controla setTimeout e companhia para testar código baseado em tempo sem esperar de verdade.
Pular, marcar como pendente e focar
As opções skip, todo e only controlam quais testes rodam:
import test from 'node:test';
test('integração com o gateway de pagamento', { skip: 'sem credenciais no CI' }, () => {
// nunca executado
});
test('exportar relatório em PDF', { todo: true }, () => {
// executado, mas uma falha aqui não quebra a suíte
});
test('cálculo de frete', { only: true }, () => {
// com --test-only, só os testes marcados com only rodam
});
Reporters, cobertura e watch
O formato de saída padrão é o spec, legível para humanos. Para CI, há reporters prontos como tap, dot, junit e lcov:
# saída compacta
node --test --test-reporter=dot
# XML no formato jUnit, para o CI ler os resultados
node --test --test-reporter=junit --test-reporter-destination=resultado.xml
# relatório de cobertura de código (ainda experimental)
node --test --experimental-test-coverage
# roda os testes de novo a cada alteração nos arquivos
node --test --watch
Quando usar
- Bibliotecas e utilitários que não querem carregar um framework de testes inteiro como dependência.
- APIs e scripts em Node.js puro, onde
describe/ite mocks básicos resolvem tudo. - Projetos novos que querem menos configuração: nada de arquivo de config, transpilador ou plugin para começar.
- Pipelines de CI mais rápidos, sem o tempo de instalar e inicializar um runner externo.
Cuidados
O test runner cobre o essencial, mas não replica tudo do Jest: não há snapshot de componentes de UI nem ambiente jsdom embutido, então projetos de front-end costumam continuar com Vitest ou Jest. A cobertura de código e o modo watch do test runner ainda são marcados como experimentais na documentação, e os padrões de descoberta de arquivos mudaram entre versões — prefira uma versão LTS recente.
Compatibilidade
O módulo node:test chegou no Node.js 18 como experimental e se tornou estável no Node.js 20. Qualquer versão LTS a partir da 20 já traz test(), describe()/it(), hooks e mocks sem flag nenhuma.
Conclusão
O test runner nativo tira do caminho a decisão "qual framework instalar" para a maioria dos projetos Node.js. Com node --test, describe/it, hooks e mocks já no runtime, dá para começar a testar com zero dependências e migrar para algo mais pesado só quando realmente precisar.
0 Comentário
Deixe seu Comentário!
Dúvida, sugestão ou só um "valeu!" — tudo é bem-vindo por aqui.