Jest, Mocha, Chai: você ainda precisa deles? O Node.js agora tem Test Runner nativo

Fala Dev, tudo em riba?

Testes automatizados são fundamentais. Isso não é novidade para ninguém que trabalha com desenvolvimento de software há algum tempo.

O que talvez seja novidade é que você não precisa mais instalar Jest, Mocha, Chai ou qualquer outra biblioteca para escrever e executar testes no Node.js.

A partir da versão 20, o Node.js trouxe um test runner completo de forma nativa. E quando eu digo completo, estou falando de describe, test, hooks, mocks e assertions. Tudo que você usa no dia a dia.

E o melhor? Além de eliminar dependências, os testes rodam significativamente mais rápido. Em alguns casos, os testes são executados 4 vezes mais rápido!

Vamos entender como isso funciona e como você pode começar a usar hoje.

O problema das dependências de teste

Antes de entrar no código, vale refletir sobre algo que muita gente ignora.

Quando você instala o Jest no seu projeto, não está instalando apenas o Jest. Está trazendo dezenas de sub-dependências junto. Cada uma dessas dependências é um ponto potencial de vulnerabilidade.

Isso acontece porque você está confiando no mantenedor do Jest e nos mantenedores de todas as bibliotecas que o Jest usa internamente. Se qualquer um deles deixar passar uma falha de segurança, seu projeto está exposto.

Utilizei o Jest como exemplo, porém as demais dependências utilizadas para testes como o Mocha, Chai, Sinon, etc. também se enquadram nos mesmos casos citados acima (sub-denpendências e possíveis vulnerabilidades).

Com o test runner nativo, essa preocupação diminui drasticamente. O código faz parte do core do Node.js, mantido pela mesma equipe que cuida do runtime que você já confia para rodar sua aplicação em produção.

Como era antes: escrevendo testes com Jest

Vamos a um exemplo prático. Imagine uma função simples que soma dois números:

// sum.js
export function sum(a, b) {
  return a + b;
}

Para testar essa função com Jest, você faria algo assim:

// sum.test.js
import { describe, test, expect } from '@jest/globals';
import { sum } from './sum.js';

describe('Função sum', () => {
  test('1 + 2 deve ser igual a 3', () => {
    expect(sum(1, 2)).toBe(3);
  });

  test('-1 + 1 deve ser igual a 0', () => {
    expect(sum(-1, 1)).toBe(0);
  });

  test('números decimais devem funcionar', () => {
    expect(sum(0.1, 0.2)).toBeCloseTo(0.3);
  });
});

Para executar, você rodaria npx jest ou configuraria um script no package.json.

Funciona perfeitamente. Mas exige que o Jest esteja instalado, com todas as suas dependências.

Como fica agora: Test Runner nativo

O mesmo teste usando apenas recursos nativos do Node:

// sum.native.test.js
import { describe, test } from 'node:test';
import assert from 'node:assert';
import { sum } from './sum.js';

describe('Função sum', () => {
  test('1 + 2 deve ser igual a 3', () => {
    assert.strictEqual(sum(1, 2), 3);
  });

  test('-1 + 1 deve ser igual a 0', () => {
    assert.strictEqual(sum(-1, 1), 0);
  });

  test('números decimais devem funcionar', () => {
    const resultado = sum(0.1, 0.2);
    assert.ok(Math.abs(resultado - 0.3) < 0.0001);
  });
});

Para executar, basta rodar node --test sum.native.test.js. Sem instalar nada.

Perceba que a estrutura é praticamente idêntica. O describe e o test funcionam da mesma forma. A diferença principal está nas assertions: em vez de expect().toBe(), usamos assert.strictEqual().

Entendendo o módulo assert

O módulo node:assert é a ferramenta nativa para fazer verificações nos seus testes. Ele oferece vários métodos úteis:

assert.strictEqual(atual, esperado) verifica se dois valores são estritamente iguais, usando comparação ===.

assert.strictEqual(soma(2, 2), 4); // passa
assert.strictEqual('4', 4); // falha, tipos diferentes

assert.deepStrictEqual(atual, esperado) compara objetos e arrays recursivamente.

assert.deepStrictEqual(
  { nome: 'Pedro', idade: 30 },
  { nome: 'Pedro', idade: 30 }
); // passa

assert.deepStrictEqual([1, 2, 3], [1, 2, 3]); // passa

assert.ok(valor) verifica se o valor é truthy.

assert.ok(true); // passa
assert.ok(1); // passa
assert.ok('texto'); // passa
assert.ok(0); // falha

assert.throws(funcao, erro) verifica se uma função lança uma exceção.

assert.throws(
  () => { throw new Error('Deu ruim'); },
  { message: 'Deu ruim' }
); // passa

assert.rejects(promise ou AsyncFunction) é a versão assíncrona do throws, para promises.

await assert.rejects(
  async () => { throw new Error('Deu ruim'); },
  { message: 'Deu ruim' }
); // passa

Com esses métodos, você cobre a grande maioria dos cenários de teste.

Hooks: before, after, beforeEach, afterEach

Assim como no Jest e Mocha, o test runner nativo suporta hooks para setup e teardown.

import { describe, test, before, after, beforeEach, afterEach } from 'node:test';
import assert from 'node:assert';

describe('Testes com hooks', () => {
  let conexaoBanco;
  let contador = 0;

  before(() => {
    // Executa uma vez antes de todos os testes
    conexaoBanco = { conectado: true };
    console.log('Conexão estabelecida');
  });

  after(() => {
    // Executa uma vez depois de todos os testes
    conexaoBanco = null;
    console.log('Conexão fechada');
  });

  beforeEach(() => {
    // Executa antes de cada teste
    contador = 0;
  });

  afterEach(() => {
    // Executa depois de cada teste
    console.log(`Teste finalizado. Contador: ${contador}`);
  });

  test('primeiro teste', () => {
    contador++;
    assert.ok(conexaoBanco.conectado);
  });

  test('segundo teste', () => {
    contador++;
    assert.strictEqual(contador, 1);
  });
});

Isso é especialmente útil quando você precisa preparar um ambiente antes dos testes, como conectar a um banco de dados de teste ou limpar dados entre execuções.

Testes assíncronos

Trabalhar com código assíncrono é simples. Basta usar async/await normalmente:

import { describe, test } from 'node:test';
import assert from 'node:assert';

async function buscaUsuario(id) {
  // Simula uma chamada de API
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ id, nome: 'Pedro' });
    }, 100);
  });
}

describe('Testes assíncronos', () => {
  test('busca usuário por id', async () => {
    const usuario = await buscaUsuario(1);
    
    assert.strictEqual(usuario.id, 1);
    assert.strictEqual(usuario.nome, 'Pedro');
  });

  test('múltiplas chamadas assíncronas', async () => {
    const [usuario1, usuario2] = await Promise.all([
      buscaUsuario(1),
      buscaUsuario(2)
    ]);

    assert.strictEqual(usuario1.id, 1);
    assert.strictEqual(usuario2.id, 2);
  });
});

Mocks nativos

A partir do Node.js 20, o test runner também inclui suporte a mocks. Isso significa que você pode simular funções e módulos sem precisar de bibliotecas como Sinon.

import { describe, test, mock } from 'node:test';
import assert from 'node:assert';

describe('Testes com mock', () => {
  test('mock de função', () => {
    const funcaoOriginal = (x) => x * 2;
    const funcaoMock = mock.fn(funcaoOriginal);

    // Chama a função mockada
    const resultado = funcaoMock(5);

    // Verifica o resultado
    assert.strictEqual(resultado, 10);

    // Verifica quantas vezes foi chamada
    assert.strictEqual(funcaoMock.mock.calls.length, 1);

    // Verifica os argumentos da chamada
    assert.deepStrictEqual(funcaoMock.mock.calls[0].arguments, [5]);
  });

  test('mock que retorna valor fixo', () => {
    const funcaoMock = mock.fn(() => 'valor fixo');

    assert.strictEqual(funcaoMock(), 'valor fixo');
    assert.strictEqual(funcaoMock(1, 2, 3), 'valor fixo');
  });
});

Para cenários mais complexos, você pode mockar módulos inteiros usando mock.module().

Comparação de performance

Aqui está o que realmente impressiona. Rodei os mesmos testes usando Jest e usando o Test Runner nativo. Os resultados falam por si.

Jest: 450-500ms para executar 2 testes simples.

Node nativo: 100-125ms para executar os mesmos 2 testes.

Estamos falando de uma diferença de aproximadamente 4x. Em projetos com centenas ou milhares de testes, essa diferença se traduz em minutos economizados a cada execução.

Isso acontece porque o Jest precisa carregar todo o seu ecossistema antes de rodar qualquer coisa. O test runner nativo já está ali, pronto para usar.

Executando os testes

Existem várias formas de rodar seus testes com o runner nativo.

Arquivo específico:

node --test arquivo.test.js

Todos os arquivos de teste:

node --test

Por padrão, o Node.js procura arquivos que seguem os padrões *.test.js, *.spec.js ou que estejam em pastas chamadas test.

Com watch mode:

node --test --watch

Isso faz os testes rodarem automaticamente sempre que você salvar um arquivo.

Gerando relatório:

node --test --test-reporter spec

Quando ainda faz sentido usar Jest?

O test runner nativo cobre a maioria dos casos de uso, mas existem cenários onde o Jest ainda pode ser preferível.

  • Snapshot testing: Se você usa snapshots para testar componentes React ou outputs complexos, o Jest tem essa funcionalidade bem madura.
  • Cobertura de código integrada: O Jest oferece relatórios de coverage de forma simples. Com o runner nativo, você precisa usar ferramentas adicionais como c8.
  • Ecossistema de plugins: O Jest tem anos de maturidade e uma vasta coleção de plugins para cenários específicos.
  • Projetos legados: Se seu projeto já tem milhares de testes escritos com Jest, migrar pode não valer o esforço imediato.

Para projetos novos ou projetos menores, porém, o test runner nativo é uma excelente escolha.

Como começar a migrar

Se você quer experimentar o Test Runner nativo, sugiro um caminho gradual.

Primeiro, verifique sua versão do Node.js. O test runner está disponível a partir da versão 20. Rode node -v para confirmar.

Segundo, escolha um arquivo de teste simples para converter. Algo com poucos testes e sem muitas dependências de funcionalidades específicas do Jest.

Terceiro, ajuste os imports. Troque @jest/globals por node:test e node:assert.

Quarto, adapte as assertions. A maior mudança é trocar expect().toBe() por assert.strictEqual() e variações.

Quinto, teste e compare. Rode ambas as versões e veja os resultados.

Com o tempo, você pode ir expandindo a migração para outros arquivos de teste.

Conclusão

O Node.js continua evoluindo e entregando ferramentas que antes dependiam de bibliotecas externas.

O test runner nativo é um exemplo claro dessa evolução. Ele oferece tudo que você precisa para escrever testes de qualidade: describe, test, hooks, mocks e assertions. E faz isso de forma mais rápida e segura.

Menos dependências no seu projeto significa menos vulnerabilidades, builds mais rápidos e menos surpresas na hora de atualizar versões.

Da próxima vez que criar um projeto novo, antes de rodar npm install jest, considere dar uma chance ao que o Node.js já oferece de fábrica.

Seus testes vão agradecer. E seu pipeline de CI também.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *