`node:test`

Node's Built-In Test Runner

`node:test`

Node ships a test runner. Zero deps, surprisingly capable, good for CLI scripts and libraries.

4 min read Level 1/5 #nodejs#testing#node-test
What you'll learn
  • Author tests with node:test
  • Use assertion library
  • Run with node --test

Node 22 has a built-in test runner — no install needed. For libraries and CLIs, it’s often enough.

A First Test

// math.test.mjs
import { test } from "node:test";
import assert from "node:assert/strict";
import { add } from "./math.mjs";

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

test("add: works with negatives", () => {
  assert.equal(add(-2, 3), 1);
});

Run

node --test

Node auto-discovers *.test.mjs, *.test.js, test/*.mjs, test/*.js (and .cjs equivalents). Or pass specific files: node --test math.test.mjs.

Assertions

import assert from "node:assert/strict";

assert.equal(actual, expected);        // ===
assert.deepEqual(actual, expected);    // structural
assert.match(string, /pattern/);
assert.throws(() => doBadThing(), /error message/);
await assert.rejects(async () => doBadAsync(), /error/);

Always use node:assert/strict (the strict variant) — non-strict allows coercion (1 == "1") and bites you.

Suites

import { describe, it } from "node:test";

describe("User", () => {
  it("has a name", () => {
    assert.equal(new User("Ada").name, "Ada");
  });

  it("has an id", () => {
    assert.ok(new User("Ada").id);
  });
});

Hooks

import { before, after, beforeEach } from "node:test";

before(async () => { await db.connect(); });
after(async () => { await db.disconnect(); });
beforeEach(() => seedTestData());

Watch Mode

node --test --watch

Re-runs on file changes.

When to Reach for Vitest Instead

node:test is great. But for frontend or full-stack projects, Vitest is more featureful — UI, snapshots, mocks, coverage, plugins, JSX support. Next lesson.

Vitest →