NodeJSBuilt-in Test Runner (node:test)

Built-in Test Runner (node:test)

Since Node.js 18 (stable in 20+), Node ships with a built-in test runnernode:test — and an assertion library — node:assert. Together they cover the majority of testing needs with zero dependencies: no Jest, no Mocha, no install step. You write tests using test() and describe(), assert with assert, and run them with node --test. For new projects that don't need Jest's heavy feature set, this is increasingly the default choice. This page covers the API, running tests, and the built-in mocking and coverage features.

A first test

math.test.js

JS
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { add, divide } from './math.js'

test('add sums two numbers', () => {
  assert.equal(add(2, 3), 5)
})

test('divide throws on division by zero', () => {
  assert.throws(() => divide(10, 0), /cannot divide by zero/)
})

Bash
node --test                       # runs all *.test.js / *.test.mjs files
node --test --watch               # re-run on file changes
node --test test/unit/            # run a specific directory
node --test --test-name-pattern="add"   # filter by test name
`node --test` auto-discovers test files — no config, no install
Node's runner automatically finds files matching `*.test.js`, `*.test.mjs`, `*.test.cjs` (and files in a `test/` directory). There's no configuration file to write and nothing to install — it's part of the Node binary. This zero-setup property makes it ideal for libraries (no dev-dependency burden on consumers) and for projects that want to avoid the maintenance of a separate test framework.
Suites with describe / it

JS
import { describe, it, before, after, beforeEach } from 'node:test'
import assert from 'node:assert/strict'

describe('UserService', () => {
  let service

  before(() => { /* one-time setup before all tests in this suite */ })
  after(() => { /* one-time teardown */ })
  beforeEach(() => { service = new UserService() })  // fresh instance per test

  it('creates a user with a hashed password', async () => {
    const user = await service.create({ email: 'a@x.com', password: 'pw' })
    assert.notEqual(user.passwordHash, 'pw')   // not stored in plaintext
  })

  it('rejects a duplicate email', async () => {
    await service.create({ email: 'a@x.com', password: 'pw' })
    await assert.rejects(
      () => service.create({ email: 'a@x.com', password: 'pw2' }),
      /already exists/,
    )
  })
})
The node:assert API

Assertion

Checks

assert.equal(a, b)

Loose equality (use assert/strict for ===)

assert.deepEqual(a, b)

Deep structural equality of objects/arrays

assert.ok(value)

Value is truthy

assert.throws(fn, /regex/)

fn throws an error matching the pattern

assert.rejects(promise, /regex/)

Async function rejects

assert.match(str, /regex/)

String matches a regex

Import `node:assert/strict` so `equal` uses `===`, not `==`
The plain `node:assert` module's `equal` uses loose (`==`) comparison, which has the usual JavaScript coercion surprises (`'1' == 1` is true). Always import `node:assert/strict` (or use `assert.strict`) so `equal` and `deepEqual` use strict comparison. This avoids tests that pass for the wrong reason — a string `'5'` matching a number `5`.
Built-in mocking

JS
import { test, mock } from 'node:test'
import assert from 'node:assert/strict'

test('sends a notification after creating an order', () => {
  const notifier = { send: mock.fn() }     // a mock function
  const service = new OrderService(notifier)

  service.createOrder({ item: 'book' })

  assert.equal(notifier.send.mock.callCount(), 1)
  assert.deepEqual(notifier.send.mock.calls[0].arguments, ['order created'])
})

// Mock timers:
test('retries after a delay', (t) => {
  t.mock.timers.enable({ apis: ['setTimeout'] })
  // ...code that uses setTimeout...
  t.mock.timers.tick(1000)    // advance time without waiting
})
`node:test` has built-in `mock.fn()`, method mocking, and fake timers
The runner includes a mocking API — `mock.fn()` for mock functions, `mock.method(obj, 'name')` to replace a method, and `mock.timers` to fake `setTimeout`/`setInterval`/`Date`. This covers most needs without a separate library like Sinon. Mocks created via the test context (`t.mock`) are automatically restored after the test, preventing leakage between tests. [Mocking](/nodejs/mocking) covers the patterns in depth.
Built-in coverage

Bash
node --test --experimental-test-coverage

# Output includes a coverage table:
# ----------------------------------------------------------------
# file        | line % | branch % | funcs % | uncovered lines
# ----------------------------------------------------------------
# math.js     |  92.31 |   85.71  |  100.00 | 14-15
# ----------------------------------------------------------------
node:test vs Jest vs Mocha

node:test

Jest

Mocha

Install

None (built-in)

npm i -D jest

npm i -D mocha

Assertions

node:assert

Built-in expect

Bring your own (Chai)

Mocking

Built-in

Built-in

Bring your own (Sinon)

Snapshots

No

Yes

No

Best for

Libraries, lean projects

Apps wanting everything

Flexible/custom setups

Choose node:test for zero-dependency simplicity; Jest when you want snapshots and a rich ecosystem
`node:test` is excellent for libraries (no dependency burden) and projects that value a small footprint. [Jest](/nodejs/jest) wins when you want snapshot testing, a vast plugin ecosystem, and an all-in-one experience. [Mocha](/nodejs/mocha-chai) suits teams who want to compose their own stack. All three express the same fundamentals — `describe`/`it`, setup/teardown, assertions — so moving between them is mostly mechanical.
Next
The most popular all-in-one framework: [Testing with Jest](/nodejs/jest).