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

- Autore / Author: Diego Betto (https://diegobetto.com/about/)
- Pubblicato / Published: 2026-09-04
- URL: https://diegobetto.com/en/node-test-runner-vs-jest/
- Tag: nodejs, testing, javascript

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:

```js
// 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);
});
```

```bash
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:

```js
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:

```js
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: "diego@example.com" }, { sendEmail: send });

  assert.strictEqual(send.mock.callCount(), 1);
  assert.deepEqual(send.mock.calls[0].arguments, ["diego@example.com"]);
});
```

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

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

```bash
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](https://diegobetto.com/en/nodejs-security-releases-january-2026/).
