Node's Built-In Test Runner
`node:test`
Node ships a test runner. Zero deps, surprisingly capable, good for CLI scripts and libraries.
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.