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