Built-in Test Runner (node:test)
Since Node.js 18 (stable in 20+), Node ships with a built-in test runner — node: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
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/)
})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
Suites with describe / it
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 |
|---|---|
| Loose equality (use |
| Deep structural equality of objects/arrays |
| Value is truthy |
| fn throws an error matching the pattern |
| Async function rejects |
| String matches a regex |
Built-in mocking
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
})Built-in coverage
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) |
|
|
Assertions |
| Built-in | 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 |