Diego Betto's Blog
Foto di Jakub Żerdzicki su Unsplash

Diego Betto · September 4, 2026 · 4 min di lettura

Node.js's built-in test runner: is it worth ditching Jest?

A practical guide to node:test, Node.js's built-in test runner: zero-dependency setup, basic syntax, mocking, coverage, and what's still missing compared to Jest.

Condividi:XLinkedInFacebookWhatsApp

Node.js has had a built-in test runner since version 18 (stable since 20), yet the vast majority of projects still install Jest or Vitest by default, often without asking whether they actually need to. The node:test module has matured enough to be a real alternative for a good chunk of projects — not for all of them.

Setup: zero dependencies

Nothing to install. node:test ships with the runtime:

// sum.test.js
import { test } from "node:test";
import assert from "node:assert/strict";
import { sum } from "./sum.js";

test("adds two positive numbers", () => {
  assert.strictEqual(sum(2, 3), 5);
});

test("adds with a negative number", () => {
  assert.strictEqual(sum(5, -2), 3);
});
node --test

No config file, no transform to set up for ESM — node --test automatically looks for files matching common patterns (*.test.js, *.spec.js, test/ folders) and runs everything in parallel by default.

describe/it, hooks, and async tests

The syntax is deliberately close to Jest/Mocha’s, to make migration less painful:

import { describe, it, before, after } from "node:test";
import assert from "node:assert/strict";

describe("UserRepository", () => {
  let repo;

  before(() => {
    repo = new UserRepository();
  });

  after(async () => {
    await repo.close();
  });

  it("finds a user by id", async () => {
    const user = await repo.findById(1);
    assert.equal(user.name, "Diego");
  });

  it("throws when the id does not exist", async () => {
    await assert.rejects(() => repo.findById(999), /not found/);
  });
});

assert.rejects/assert.doesNotReject cover the async cases that in Jest require expect(...).rejects.toThrow() — the API is more verbose in a few places, but covers the same scenarios.

Built-in mocking, no external libraries

From version 20 onward, node:test ships a native mocking system (t.mock), covering a good part of what jest.fn() or sinon used to be needed for:

import { test } from "node:test";
import assert from "node:assert/strict";

test("calls the email service with the right arguments", (t) => {
  const send = t.mock.fn(() => true);

  registerUser({ email: "[email protected]" }, { sendEmail: send });

  assert.strictEqual(send.mock.callCount(), 1);
  assert.deepEqual(send.mock.calls[0].arguments, ["[email protected]"]);
});

t.mock.method() also lets you temporarily replace a method on an existing object (the equivalent of jest.spyOn), and it’s restored automatically at the end of the test.

💡 Consiglio

node --test --watch reruns only the tests for changed files, in the same spirit as jest --watch. Useful during development, though the feedback on which tests got re-triggered is more bare-bones than Jest’s interface.

Coverage without extra packages

node --test --experimental-test-coverage

Coverage is generated internally (it uses V8 instrumentation, not istanbul/nyc), printed to the console by default. For a browsable HTML report you still need to wire up an external reporter — here Jest, with --coverage ready out of the box and mature editor integrations, remains more convenient.

What’s still missing compared to Jest

  • Snapshot testing: node:test has no native equivalent of toMatchSnapshot(). For projects that lean heavily on snapshots (typically UI components), that’s an absence that stings.
  • Built-in DOM/jsdom environment: Jest sets up jsdom with a flag; with node:test you have to wire it up manually. For testing React/Vue components, that’s extra work Jest (or Vitest) saves you.
  • Expressive matchers: assert.strictEqual/assert.deepEqual cover the common cases, but expect’s matcher ecosystem (toBeCloseTo, toContainEqual, custom matchers) is richer and more readable for complex assertions.
  • Plugin ecosystem and editor integration: Jest (and Vitest) have years of plugins, VS Code extensions, and mature CI integrations. node:test is newer and the tooling surface around it is still smaller.

When migrating actually makes sense

For libraries and Node.js packages with no DOM dependencies — utilities, HTTP clients, pure business logic, CLI scripts — node:test is often enough, and it removes an entire dependency from the project, with faster startup (no transform to load) and zero configuration to maintain. For frontend projects with components to render and snapshots to compare, or large, stable test suites already on Jest, migrating rarely pays for the time it costs: node:test’s main benefit is starting without dependencies, not converting something that already works.

For the rest of the Node ecosystem on the security and maintenance side, it’s also worth keeping an eye on the updates covered in the article on Node.js security releases.

Condividi:XLinkedInFacebookWhatsApp
Diego Betto

Written by

Diego Betto

Co-Founder & CTO at PAPION. Senior full-stack engineer specializing in React, TypeScript, Node.js, and application security.