Projetos Node.js precisam de testes rápidos, confiáveis e fáceis de executar em qualquer ambiente. O Node Test Runner, disponível no módulo node:test, oferece uma solução nativa para testes unitários e de integração, com suporte a subtestes, hooks, mocks, filtros, concorrência e cobertura.
Usar o test runner nativo reduz dependências e facilita a atualização do projeto. Isso não significa que bibliotecas como Vitest ou Jest deixaram de ter valor. A escolha depende de integração com navegador, transformação de código, snapshots e ecossistema. Para serviços Node.js, a solução nativa já cobre muitos cenários.
Neste guia, você aprenderá a criar testes, usar assertions, organizar suites, controlar concorrência, mockar funções e timers, gerar cobertura e testar código assíncrono.
Primeiro teste
const test = require('node:test');
const assert = require('node:assert/strict');
test('soma dois números', () => {
assert.equal(2 + 3, 5);
});Salve como math.test.js e execute:
node --testO Node.js procura arquivos compatíveis com os padrões do test runner. A documentação oficial de node:test detalha opções e recursos. A API de assertions está na documentação de node:assert.
Para revisar a plataforma, consulte o que é Node.js e o que é JavaScript.
Testes assíncronos
test('carrega usuário', async () => {
const user = await findUser('123');
assert.equal(user.id, '123');
assert.equal(user.active, true);
});Quando a função retorna Promise, o runner aguarda resolução ou rejeição. Não misture callback e Promise no mesmo teste.
Testando rejeições
test('rejeita identificador vazio', async () => {
await assert.rejects(
() => findUser(''),
error => {
assert.equal(error.code, 'INVALID_ID');
return true;
}
);
});Validar apenas a mensagem torna o teste frágil. Prefira tipo, código e propriedades relevantes.
Suites com describe e it
const { describe, it } = require('node:test');
describe('calculateTotal', () => {
it('soma itens', () => {
assert.equal(calculateTotal([10, 20]), 30);
});
it('retorna zero para lista vazia', () => {
assert.equal(calculateTotal([]), 0);
});
});Suites agrupam comportamento e tornam relatórios mais legíveis. Evite árvores profundas que escondem a finalidade do teste.
Hooks
const { describe, it, before, after, beforeEach } =
require('node:test');
describe('repository', () => {
before(async () => {
await database.start();
});
after(async () => {
await database.stop();
});
beforeEach(async () => {
await database.clear();
});
it('salva registro', async () => {
const item = await repository.create({ name: 'Teste' });
assert.ok(item.id);
});
});Hooks precisam limpar recursos mesmo quando o teste falha. Use after() e blocos try/finally quando necessário.
Subtestes
test('valida campos', async t => {
await t.test('nome obrigatório', () => {
assert.throws(() => validate({}), /nome/);
});
await t.test('email válido', () => {
assert.doesNotThrow(() => validate({
name: 'Ana',
email: 'ana@example.com'
}));
});
});Aguarde subtestes assíncronos para o teste pai não terminar antes deles.
Concorrência
test('operação independente', {
concurrency: true
}, async () => {
// teste isolado
});Paralelismo reduz duração, mas expõe estado compartilhado. Cada teste deve usar banco, diretório, porta e variáveis isoladas. Não ative concorrência para testes que alteram o mesmo recurso.
Filtrando testes
node --test --test-name-pattern="repository"Filtros aceleram desenvolvimento local. No CI, execute a suíte completa para evitar dependência acidental da ordem.
Skip e todo
test.skip('depende de serviço externo', () => {});
test.todo('trata reconexão automática');Itens ignorados devem ter justificativa e acompanhamento. Muitos testes permanentemente pulados criam falsa confiança.
Mocks de função
test('envia notificação', async t => {
const send = t.mock.fn(async () => ({ id: 'msg-1' }));
const service = createService({ send });
await service.notify({ userId: '123' });
assert.equal(send.mock.callCount(), 1);
assert.deepEqual(send.mock.calls[0].arguments[0], {
userId: '123'
});
});Injetar dependências torna mocks mais simples e evita substituir módulos globais.
Mockando métodos
test('registra evento', t => {
const logger = {
info() {}
};
const mock = t.mock.method(logger, 'info');
runOperation(logger);
assert.equal(mock.mock.callCount(), 1);
});O contexto do teste restaura mocks automaticamente em muitos cenários. Ainda assim, não compartilhe o mesmo objeto entre testes concorrentes.
Timers simulados
test('executa após atraso', t => {
t.mock.timers.enable({ apis: ['setTimeout'] });
let completed = false;
setTimeout(() => {
completed = true;
}, 1000);
t.mock.timers.tick(1000);
assert.equal(completed, true);
});APIs de timers simulados podem variar entre versões. Confirme a versão mínima do Node.js. Para timers canceláveis, veja AbortController no Node.js.
Testando EventEmitter
const { once } = require('node:events');
test('emite conclusão', async () => {
const event = once(service, 'completed');
service.run();
const [payload] = await event;
assert.equal(payload.status, 'ok');
});Registre a espera antes da ação para não perder eventos síncronos. Veja EventEmitter no Node.js.
Diretórios temporários
const fs = require('node:fs/promises');
const os = require('node:os');
const path = require('node:path');
test('gera arquivo', async t => {
const directory = await fs.mkdtemp(
path.join(os.tmpdir(), 'test-')
);
t.after(() => fs.rm(directory, {
recursive: true,
force: true
}));
await generateFile(directory);
});Isso evita que testes se interfiram. Para arquivos, consulte File System no Node.js.
Testando APIs HTTP
Inicie o servidor em uma porta aleatória e feche ao final:
test('GET /health', async t => {
const server = app.listen(0);
t.after(() => new Promise(resolve => server.close(resolve)));
const address = server.address();
const response = await fetch(
`http://127.0.0.1:${address.port}/health`
);
assert.equal(response.status, 200);
});Não use uma porta fixa no CI, pois execuções paralelas podem colidir.
Cobertura de código
node --test --experimental-test-coverageO suporte e o nome da opção dependem da versão do Node.js. Cobertura mostra linhas executadas, mas não mede qualidade dos cenários. Uma cobertura alta pode ignorar validações importantes.
Relatórios
O runner oferece reporters para terminal e integração com ferramentas. Escolha formato legível localmente e formato estruturado no CI. Preserve logs apenas quando ajudam a diagnosticar falhas.
Isolamento de ambiente
Não dependa da ordem dos testes. Restaure variáveis de ambiente, relógio, mocks e recursos externos. Para configurações, crie funções que recebam um objeto em vez de ler process.env em todos os módulos.
Testes unitários e integração
Testes unitários exercitam lógica isolada e são rápidos. Testes de integração confirmam banco, rede, arquivos e serialização. Uma suíte saudável possui ambos. Não transforme todo teste em mock, pois você pode deixar de verificar contratos reais.
Flakiness
Testes instáveis geralmente dependem de tempo real, ordem, rede externa ou dados compartilhados. Não resolva aumentando sleeps. Use eventos, polling com limite, relógio simulado e recursos isolados.
Erros comuns
- Não aguardar Promise ou subteste.
- Compartilhar banco e diretório entre testes concorrentes.
- Usar sleeps fixos para sincronização.
- Mockar detalhes internos em vez do contrato.
- Não fechar servidor, socket ou timer.
- Executar apenas testes filtrados no CI.
- Confundir cobertura com qualidade.
- Depender de API externa real em toda execução.
Boas práticas
- Escreva nomes que descrevam comportamento.
- Use
assert/strict. - Isole estado por teste.
- Limpe recursos em hooks ou
t.after(). - Use portas e diretórios temporários.
- Teste sucesso, erro e limites.
- Evite mocks excessivos.
- Execute a suíte completa no CI.
- Monitore testes instáveis.
- Defina uma versão mínima do Node.js.
Conclusão
O Node Test Runner oferece testes, suites, hooks, mocks, concorrência e cobertura dentro da própria plataforma. Para APIs e serviços, ele pode reduzir dependências e manter a suíte próxima das APIs nativas.
O valor dos testes depende de isolamento e cenários reais. Limpe recursos, não dependa do relógio ou da ordem e combine testes unitários com integração. Com essas práticas, o runner nativo sustenta uma suíte rápida e confiável.




