The Built-In Test Runner

Node.js ships a test runner: write a file named *.test.js, run node --test, and read the exit code.

Syntaxnode --test [path ...]

A test is code that runs your other code and checks the answer. Node.js 24 can do this on its own. The module is node:test and the command is node --test. There is nothing to install and no dependency to add to package.json.

How a run works

Put tests in a file whose name the runner recognises, such as cart.test.js. Run node --test with no arguments and it searches the project for those names: *.test.js, *-test.js, *_test.js, test-*.js, test.js, and anything inside a test/ directory. The same patterns apply with .cjs and .mjs extensions. Give it a path and it runs exactly that path.

Each test file runs in its own process: a separate running copy of Node.js with its own memory. More than one file can run at the same time, so two files must not both claim one fixed port. The report is written to stdout, the process's normal output stream. When the run finishes the process hands back an exit code, a number meaning how it went: 0 if everything passed, 1 if anything failed. That number is what a CI pipeline checks.

What trips people up

The runner became stable in Node.js 20; under Node.js 18 it printed an experimental warning. The default report format changed in Node.js 23: output to a non-terminal used to be TAP and is now the readable spec format. Pass --test-reporter=tap when a tool expects the old one.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Add tests to a service that has no test framework and no room for new dependencies.
  • Wire node --test into a CI step that must fail the build when any test fails.
  • Re-run one failing file on every save while you fix it, instead of the whole suite.

More examples

A test file, and the script that runs it

The usual layout: the test file sits beside the file it tests and imports from it. With the script in place, both `npm test` and `npm run test` run it — `npm test` is a shorthand for the same command. Reach for this on day one of any project, before there is anything else to configure.

Example · javascript
// --- cart.js : the code under test
export function total(items) {
  return items.reduce((sum, item) => sum + item.price * item.qty, 0);
}

// --- cart.test.js : the test file, sitting next to it
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { total } from './cart.js';

test('multiplies price by quantity', () => {
  assert.equal(total([{ price: 2, qty: 3 }]), 6);
});

// --- package.json
// {
//   "type": "module",
//   "scripts": { "test": "node --test" }
// }

The flags you reach for while writing tests

Watch mode while you are fixing one thing, a name pattern to run a single failing test instead of the suite, and a junit report when the CI system wants a file rather than console output. The reporter can be sent to stdout or to a path.

Example · bash
# run every test file the runner can find
node --test

# re-run on save (--watch, available since Node.js 19.2.0)
node --test --watch

# only the tests whose name matches this regular expression
node --test --test-name-pattern="price"

# one file, with coverage numbers (still experimental in Node.js 24)
node --test --experimental-test-coverage cart.test.js

# machine-readable output for CI to collect
node --test --test-reporter=junit --test-reporter-destination=report.xml

Running the runner from code with run()

Use this when you need the results as data rather than as text: posting failures to a dashboard, writing your own report format, or running a chosen subset of files from a script. Setting process.exitCode keeps the CI contract intact, because run() does not fail the process for you.

Example · javascript
import { run } from 'node:test';
import path from 'node:path';

// run([options]) returns a stream of events instead of printing a report.
const stream = run({
  files: [path.resolve('cart.test.js')],
  concurrency: 1, // one file at a time
});

stream.on('test:pass', (event) => console.log('pass:', event.name));
stream.on('test:fail', (event) => {
  console.log('fail:', event.name);
  process.exitCode = 1; // make the whole process report failure
});

Discussion

  • Be the first to comment on this lesson.