Skip to main content

Test Runner

The node:test module facilitates the creation of JavaScript tests. To access it:
import test from 'node:test';
// or
const test = require('node:test');
This module is only available under the node: scheme.

Basic Test Structure

Tests created via the test module consist of a single function that is processed in one of three ways:
  1. Synchronous function - Considered failing if it throws an exception, passing otherwise
  2. Promise-returning function - Considered failing if the Promise rejects, passing if it fulfills
  3. Callback function - Receives a callback; failing if callback receives a truthy first argument, passing if it receives a falsy value

Writing Tests

Basic Examples

import { test } from 'node:test';
import assert from 'node:assert';

test('synchronous passing test', (t) => {
  // This test passes because it does not throw an exception.
  assert.strictEqual(1, 1);
});

test('synchronous failing test', (t) => {
  // This test fails because it throws an exception.
  assert.strictEqual(1, 2);
});

test('asynchronous passing test', async (t) => {
  // This test passes because the Promise returned by the async
  // function is settled and not rejected.
  assert.strictEqual(1, 1);
});

test('failing test using Promises', (t) => {
  // Promises can be used directly as well.
  return new Promise((resolve, reject) => {
    setImmediate(() => {
      reject(new Error('this will cause the test to fail'));
    });
  });
});

test('callback passing test', (t, done) => {
  // done() is the callback function.
  setImmediate(done);
});

Subtests

The test context’s test() method allows subtests to be created, enabling hierarchical test structure:
test('top level test', async (t) => {
  await t.test('subtest 1', (t) => {
    assert.strictEqual(1, 1);
  });

  await t.test('subtest 2', (t) => {
    assert.strictEqual(2, 2);
  });
});
Note: Use await to ensure subtests complete before the parent test finishes. Any outstanding subtests when the parent finishes are cancelled and treated as failures.

describe() and it() Aliases

Tests can also be written using describe() and it() functions:
import { describe, it } from 'node:test';
import assert from 'node:assert';

describe('A thing', () => {
  it('should work', () => {
    assert.strictEqual(1, 1);
  });

  it('should be ok', () => {
    assert.strictEqual(2, 2);
  });

  describe('a nested thing', () => {
    it('should work', () => {
      assert.strictEqual(3, 3);
    });
  });
});

Skipping Tests

Individual tests can be skipped:
// Using skip option
test('skip option', { skip: true }, (t) => {
  // This code is never executed.
});

// Using skip option with message
test('skip option with message', { skip: 'this is skipped' }, (t) => {
  // This code is never executed.
});

// Using skip() method
test('skip() method', (t) => {
  t.skip('this is skipped');
});

TODO Tests

Tests can be marked as TODO (pending implementation):
// Using todo option
test('todo option', { todo: true }, (t) => {
  // This code is executed, but not treated as a failure.
  throw new Error('this does not fail the test');
});

// Using todo() method
test('todo() method', (t) => {
  t.todo('this is a todo test');
  throw new Error('this does not fail the test');
});
TODO tests are executed but not treated as failures. They represent pending implementations or known bugs.

Only Tests

When Node.js is started with the --test-only flag, only tests with the only option will run:
// This test runs when --test-only is used
test('this test is run', { only: true }, async (t) => {
  await t.test('running subtest');
});

// This test is skipped
test('this test is not run', () => {
  throw new Error('fail');
});

describe.only('a suite', () => {
  // All tests in this suite run
  it('this test is run', () => {
    // This code is run.
  });
});

Filtering Tests by Name

Tests can be filtered using command-line options:
# Run tests matching a pattern
node --test --test-name-pattern="test [1-3]"

# Skip tests matching a pattern
node --test --test-skip-pattern="test 4"

# Case-insensitive matching
node --test --test-name-pattern="/test [4-5]/i"

Watch Mode

The test runner supports watch mode:
node --test --watch
In watch mode, the test runner watches for changes to test files and their dependencies. When a change is detected, affected tests are rerun.

Running Tests from Command Line

Invoke the test runner from the command line:
node --test
By default, Node.js will run all files matching these patterns:
  • **/*.test.{cjs,mjs,js}
  • **/*-test.{cjs,mjs,js}
  • **/*_test.{cjs,mjs,js}
  • **/test-*.{cjs,mjs,js}
  • **/test.{cjs,mjs,js}
  • **/test/**/*.{cjs,mjs,js}
You can also provide specific patterns:
node --test "**/*.test.js" "**/*.spec.js"

Test Hooks

Tests support setup and teardown hooks:
import { describe, it, before, after, beforeEach, afterEach } from 'node:test';

describe('test suite', () => {
  before(() => {
    // Runs once before all tests
  });

  after(() => {
    // Runs once after all tests
  });

  beforeEach(() => {
    // Runs before each test
  });

  afterEach(() => {
    // Runs after each test
  });

  it('test 1', () => {
    // Test code
  });

  it('test 2', () => {
    // Test code
  });
});

Test Reporters

The test runner supports multiple reporters:
# Spec reporter (default)
node --test --test-reporter=spec

# TAP reporter
node --test --test-reporter=tap

# Dot reporter
node --test --test-reporter=dot

# JUnit reporter
node --test --test-reporter=junit

# Multiple reporters
node --test --test-reporter=spec --test-reporter-destination=stdout \
  --test-reporter=junit --test-reporter-destination=junit.xml

Collecting Code Coverage

Collect code coverage with the --experimental-test-coverage flag:
node --test --experimental-test-coverage
The coverage report is printed after all tests complete. You can also generate an lcov report:
node --test --experimental-test-coverage --test-reporter=lcov \
  --test-reporter-destination=lcov.info

Mocking

Function Mocking

import assert from 'node:assert';
import { mock, test } from 'node:test';

test('spies on a function', () => {
  const sum = mock.fn((a, b) => {
    return a + b;
  });

  assert.strictEqual(sum.mock.callCount(), 0);
  assert.strictEqual(sum(3, 4), 7);
  assert.strictEqual(sum.mock.callCount(), 1);

  const call = sum.mock.calls[0];
  assert.deepStrictEqual(call.arguments, [3, 4]);
  assert.strictEqual(call.result, 7);

  mock.reset();
});

Method Mocking

test('spies on an object method', (t) => {
  const number = {
    value: 5,
    add(a) {
      return this.value + a;
    },
  };

  t.mock.method(number, 'add');
  assert.strictEqual(number.add.mock.callCount(), 0);
  assert.strictEqual(number.add(3), 8);
  assert.strictEqual(number.add.mock.callCount(), 1);
});

Timer Mocking

import assert from 'node:assert';
import { mock, test } from 'node:test';

test('mocks setTimeout', (context) => {
  const fn = context.mock.fn();

  // Mock timers
  context.mock.timers.enable({ apis: ['setTimeout'] });
  setTimeout(fn, 9999);
  assert.strictEqual(fn.mock.callCount(), 0);

  // Advance time
  context.mock.timers.tick(9999);
  assert.strictEqual(fn.mock.callCount(), 1);
});

Date Mocking

test('mocks the Date object', (context) => {
  context.mock.timers.enable({ apis: ['Date'] });
  assert.strictEqual(Date.now(), 0);

  // Advance time
  context.mock.timers.tick(9999);
  assert.strictEqual(Date.now(), 9999);
});

Snapshot Testing

Snapshot tests allow values to be serialized and compared against known good values:
import { suite, test } from 'node:test';

suite('suite of snapshot tests', () => {
  test('snapshot test', (t) => {
    t.assert.snapshot({ value1: 1, value2: 2 });
    t.assert.snapshot(5);
  });
});
Generate snapshots with:
node --test --test-update-snapshots test.js

Best Practices

  1. Use descriptive test names - Make it clear what each test is checking
  2. Keep tests focused - Each test should verify one specific behavior
  3. Use subtests for organization - Group related tests together
  4. Clean up after tests - Use after and afterEach hooks
  5. Mock external dependencies - Keep tests isolated and fast
  6. Use watch mode during development - Get instant feedback on changes
  7. Check code coverage - Ensure your tests cover important code paths
  8. Use meaningful assertions - Make test failures easy to understand
// Good: Descriptive name and focused test
test('user.save() should return user id on success', async (t) => {
  const user = new User({ name: 'John' });
  const result = await user.save();
  assert.ok(result.id);
  assert.strictEqual(typeof result.id, 'string');
});

// Bad: Vague name and testing multiple things
test('user test', async (t) => {
  const user = new User({ name: 'John' });
  await user.save();
  await user.delete();
  // What is this test actually verifying?
});