Why Swap Jest for Node.js’s Built‑In Test Runner
Node.js’s native test runner lets you write and run unit tests with zero dependencies. This blog walks through a concrete example, shows how to enable coverage and mocking, and explains when the trade‑offs make it the right choice for backend services.
12 Sept 2026, 17:55 UTC

Problem: Too Many Test Dependencies?
Modern JavaScript projects often ship with Jest or Mocha, bringing a large dependency tree, installation time, and a constantly moving API surface. For pure Node services or libraries that don’t need fancy snapshots or an ecosystem of plugins, that extra weight can feel unnecessary. The question becomes: can we get the same test experience with a single, built‑in module?
The Node.js Test Runner (node:test)
Starting with Node 18 (experimental) and becoming stable in Node 20, the node:test module offers a zero‑dependency test runner. It runs tests with node --test or node --test , supports describe / it style, subtests, hooks, and uses the built‑in node:assert/strict for assertions. The API is intentionally minimal, so you avoid the overhead of a full framework.
Basic Test Syntax
Below is a typical file you might place in test/add.js. It tests an async add function and uses the mock utilities that ship with node:test.
import { test, mock } from 'node:test';
import assert from 'node:assert/strict';
// The function under test
import { add } from '../src/add.js';
// A simple mock to track calls
const logger = mock.fn((msg) => console.log(msg));
// Replace a real logger with the mock for this test
const addWithLog = (a, b) => {
const result = add(a, b);
logger(`added ${a} + ${b} = ${result}`);
return result;
};
test('add returns the sum of two numbers', async (t) => {
const result = await addWithLog(3, 4);
assert.strictEqual(result, 7);
assert.strictEqual(logger.mock.calls.length, 1);
assert.match(logger.mock.calls[0].arguments[0], /added 3 \+ 4 = 7/);
});
Running the Test
From the project root, simply execute:
node --test test/add.js
No package.json test script, no npm install of a test framework, and no configuration files. The runner automatically spawns a child process for each test file, providing isolation similar to Jest’s default behavior.
Enabling Code Coverage
Node 20 adds a coverage flag. Run:
node --test --experimental-test-coverage test/add.js
The output will include a coverage report in the console. You can redirect it to a file with --coverage-output if you need a JSON or HTML report. Note that flag names and availability may differ on Node 18 or 22, so double‑check the docs for your runtime.
Mocking and Timers
The mock API is lightweight compared to libraries like Sinon. It supports mock.fn for function stubs and mock.method for object methods. Timer mocking is available via mock.timers, letting you advance time without real delays.
test('timer mock example', async () => {
const timer = mock.timers();
let called = false;
setTimeout(() => called = true, 1000);
timer.setTimeout(() => {}, 1000); // advance the fake timer
assert.strictEqual(called, true);
});
Trade‑offs and Limitations
- Feature Set: While
node:testcovers most unit‑testing needs, it lacks mature snapshot testing, extensive plugin ecosystems, and advanced mocking libraries that Jest and Mocha have built over years. - IDE Integration: Popular editors have built‑in support for Jest reporters, watch mode, and debugging. The native runner offers only basic console output, which may feel sparse for large codebases.
- Watch Mode: The flag
--watchis not part of the stable API; developers may need to roll their own file‑watcher or usenpm run test -- --watchwith external tools. - Child Process Isolation: Each test file runs in a separate process. Shared state (e.g., global mocks) must be re‑initialized per file, which can be a bit different from in‑process frameworks.
- Version Variability: Coverage and mock APIs have seen naming changes between Node 18, 20, and 22. Pinning your tests to a specific LTS line and verifying flag names is essential.
When to Choose node:test
Consider the native runner if:
- Your project is a backend service or library that only needs basic unit tests.
- You want to minimise the dependency surface to reduce supply‑chain risk and installation time.
- Your team prefers a stable, official API that will not change drastically with each Node release.
- You are comfortable with minimal reporting and can supplement it with your own scripts if needed.
If you need advanced snapshot testing, extensive mocking, or a richer IDE experience, a mature framework like Jest may still be the better fit.
Actionable Checklist
- Confirm you’re on Node 20 or later:
node --version. - Create a
test/directory and write tests usingnode:testsyntax. - Run tests with
node --testand validate output. - Enable coverage with
--experimental-test-coverageand verify the report. - If you need mocks or timers, explore
mock.fnandmock.timersin your tests. - Consider adding a simple watch script:
npm run test -- --watch(requiresnodemonor similar). - Document any differences in test behavior (e.g., child process isolation) for new contributors.
By following this path, you can replace external frameworks with a zero‑dependency, officially supported test runner that keeps your service lean and future‑proof.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.