Home > Blog > Desenvolvimento Web
Desenvolvimento Web
JavaScript
Programação

Node Test Runner: Guia Prático

Atualizado em: 3 de agosto de 2026

Estação de trabalho usada no desenvolvimento de aplicações Node.js

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 --test

O 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-coverage

O 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.

Os 10 Melhores Cursos de Programação de 2026

Descubra os melhores cursos de programação. Aprenda a escolher o curso ideal para iniciar ou avançar na carreira de desenvolvedor

POSTS RELACIONADOS

Ver todos

Seta para a direita