Skip to content

Built-in Test Runner

Since Node 18, you can write and run tests with zero third-party dependencies. The node:test module provides test(), describe(), it(), before/after hooks, and TAP-compatible output. Pair it with node:assert for assertions.

A test file is just a .js (or .mjs) file that imports node:test and node:assert:

math.test.js
import { test, describe, it } from 'node:test';
import assert from 'node:assert/strict';
test('add returns the correct sum', function() {
assert.equal(2 + 3, 5);
});

Run it from the terminal:

Terminal window
node --test math.test.js

Or run all *.test.js files recursively:

Terminal window
node --test

describe groups related tests. it is an alias for test that reads naturally inside describe:

import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
describe('String utilities', function() {
it('trim removes leading and trailing whitespace', function() {
assert.equal(' hello '.trim(), 'hello');
});
it('split divides by delimiter', function() {
assert.deepEqual('a,b,c'.split(','), ['a', 'b', 'c']);
});
});
Terminal window
node --test string-utils.test.js
# TAP output:
# ok 1 - String utilities > trim removes leading and trailing whitespace
# ok 2 - String utilities > split divides by delimiter

Pass an async function to test or it. The runner awaits it — a rejected promise fails the test:

import { test } from 'node:test';
import assert from 'node:assert/strict';
test('fetch-like async test', async function() {
const result = await Promise.resolve(42);
assert.equal(result, 42);
});
import assert from 'node:assert/strict';
assert.equal(actual, expected); // == (strict: ===)
assert.notEqual(actual, expected); // !==
assert.deepEqual(obj1, obj2); // deep structural equality
assert.notDeepEqual(obj1, obj2);
assert.throws(fn, /pattern/); // fn must throw matching error
assert.rejects(asyncFn, /pattern/); // async fn must reject
assert.ok(value); // truthy check

Always prefer node:assert/strict over node:assert — the strict variant uses === for scalar comparisons and avoids subtle type coercion surprises.

The snippet below is a self-contained node:test mini-suite. It runs in node mode so you see real TAP output:

Node.js

Needs the Node.js runtime — open in StackBlitz to run.

Terminal window
# Watch mode — re-runs on file change (Node 22+)
node --test --watch
# Only run tests matching a pattern
node --test --test-name-pattern "Array"
# Code coverage report (Node 22+)
node --test --experimental-test-coverage
# Parallel test files (default in Node 22+)
node --test --test-concurrency=4
Which import gives you `test()` and `describe()` in Node.js without installing anything?
How do you run all `*.test.js` files in the current project recursively?
What happens when an async test function rejects its promise?
Why should you prefer `node:assert/strict` over `node:assert`?