Node.js Built-in Test Runner (node:test deep features)

Node.js Built-in Test Runner — Deep Feature Surface

Overview

This reference is about using node:test as a real test framework — the depth that lets it stand in for Jest or Mocha: nested subtests, lifecycle hooks, a full mocking system (functions, methods, properties, timers, modules), built-in coverage with failing thresholds, pluggable reporters, a CLI filtering/sharding/isolation model, and snapshot testing. It assumes you already know how to write one test(name, fn).

It is the “now build a suite” companion to three siblings that own neighbouring layers:

The mental model: node:test is TAP-emitting and isolation-by-default. The test() function and the TestContext/MockTracker objects are the API; the --test* CLI flags are the runner that discovers files, isolates them in child processes, filters by name, and aggregates reporters — both halves matter. Two import surfaces: functions come from node:test (test, describe, it, before, mock, …); reporter classes from node:test/reporters (spec, tap, lcov, …). The runner is stable since Node.js 20 (coverage thresholds and snapshots stabilized later — noted per feature).

Core concepts

1. Test structure: test / describe / it, subtests, and the TestContext

test(name, options?, fn) registers a test; fn receives a TestContext named t (and may be async, or use the second done callback for callback-style). describe() (alias suite()) groups tests and it() (alias of test) reads naturally inside it — describe/it is the BDD style, bare test() the flat style. Both compose.

2. Lifecycle hooks: before / after / beforeEach / afterEach

Four hooks, importable as top-level functions or callable as t.before*:

3. Mocking functions, methods, getters/setters, and properties

The MockTracker (import { mock } from 'node:test', or per-test t.mock) creates spies/stubs and tracks calls. Top-level mock persists across tests — call mock.reset()/mock.restoreAll() (or prefer t.mock, which auto-restores).

4. Mocking time: mock.timers

mock.timers fakes timers and Date so time-dependent code is deterministic and fast.

5. Module mocking: mock.module() (experimental)

mock.module(specifier, options?) replaces a module’s exports for code imported after the mock is installed — covering ESM, CommonJS, JSON, and builtin modules. Requires the --experimental-test-module-mocks flag (and the API is experimental).

6. Coverage, reporters, and the filtering / execution model

Coverage. Start with --experimental-test-coverage to collect and print line/ branch/function coverage after the run. Thresholds are stable (Node 22.8+): --test-coverage-lines=<pct>, --test-coverage-branches=<pct>, --test-coverage-functions=<pct> make the process exit non-zero when coverage is below target. --test-coverage-exclude=<glob> / --test-coverage-include=<glob> tune scope; node_modules/, core modules, and the matched test files themselves are excluded by default. Emit a real report with the lcov reporter (below) for CI.

Reporters (node:test/reporters): spec (human-readable, the CLI default on a TTY), tap (TAP, the default when piped), dot (compact ./X), junit (JUnit XML for CI), and lcov (an lcov.info file, only meaningful with --experimental-test-coverage). Select with --test-reporter and route output with --test-reporter-destination (stdout or a file). The two flags pair positionally, so you can run several at once — e.g. spec to the terminal and junit to a file. A custom reporter is any module default-exporting a function/stream that consumes the TestsStream events (test:pass, test:fail, test:diagnostic, test:coverage, …).

Filtering & execution. node --test discovers files by default globs: *.test.*, *-test.*, *_test.*, files named test.*, files starting with test-, and any .js/.cjs/.mjs under a test/ directory (recursively). You can instead pass quoted glob args. Filter by name with --test-name-pattern (regex; tests whose name matches run) and --test-skip-pattern (regex; matches are skipped) — supply both and a test must satisfy both. only-mode: mark test('x', { only: true }, …) (or t.runOnly(true)) and run with --test-only to execute just those. --test-concurrency caps parallel files (default availableParallelism() - 1). --test-isolation chooses process (default — each file in its own child process, crash-isolated) or none (all files in one process; faster, shares state, forces concurrency 1). --test-shard=<i>/<n> runs only the i-th of n shards for splitting across machines (incompatible with watch). --watch with --test re-runs affected tests on file change (generic --watch semantics live in nodejs-builtin-modules-modern).

7. Snapshots and assertions

Key APIs / flags

API / flag Purpose Notes
test / it / describe(suite) Register tests / suites it=alias of test; describe groups
t.test(name, fn) Subtest must be awaited by the parent
before/after/beforeEach/afterEach Lifecycle hooks options { signal, timeout }; afterEach/after run on failure
t.plan / t.diagnostic / t.signal Assertion count / log / cancel t.signal aborts on timeout
skip / todo / only (option or t.*) Per-test control only needs --test-only
mock.fn / mock.method / mock.getter/setter / mock.property Spies & stubs mock.property Node 24+
<m>.mock.calls / callCount() / mockImplementation(Once) / resetCalls() / restore() Mock introspection MockFunctionContext
mock.timers.enable({apis,now}) / tick / runAll / setTime / reset Fake timers + Date apis: setTimeout/Interval/Immediate, Date, scheduler.wait
mock.module(spec, {namedExports,defaultExport,cache}) Module mocking --experimental-test-module-mocks; load target via dynamic import() after
--experimental-test-coverage Collect coverage print after run
--test-coverage-lines/-branches/-functions=<pct> Failing thresholds stable (≥22.8); non-zero exit below target
--test-coverage-exclude / -include Coverage scope node_modules + test files excluded by default
--test-reporter / --test-reporter-destination Pick + route reporter pair positionally → multiple reporters
spec / tap / dot / junit / lcov Built-in reporters from node:test/reporters
--test-name-pattern / --test-skip-pattern Filter by name regex; both = AND
--test-concurrency / --test-isolation Parallelism / isolation isolation process(default)|none
--test-shard=<i>/<n> / --watch Shard / re-run shard ⊥ watch
t.assert.snapshot / --test-update-snapshots Snapshots <file>.snapshot; custom serializers

Practical patterns

Anti-patterns

Troubleshooting

References