The built-in node:test runner in 10 lines
Published October 16, 2026
import { test } from "node:test";
import assert from "node:assert/strict";
const add = (a: number, b: number) => a + b;
test("add sums two positive numbers", () => {
assert.equal(add(2, 3), 5);
});
test("add handles negatives", async (t) => {
await t.test("negative plus positive", () => assert.equal(add(-2, 3), 1));
});What
node:test is Node's built-in test runner. test() registers a test, t.test() inside it registers a subtest, and node:assert/strict gives you assertions — no test framework installed, none required.
Why it matters
For a long time "add a test" in a Node project meant "add a dependency" — Jest, Mocha, Vitest, ava, all reasonable, all extra install size and config surface for what's often a handful of assertions. node:test has been stable since Node 20 and needs nothing beyond Node itself: no config file, no separate test runner binary, no plugin to make TypeScript work — current Node LTS strips types from .ts files natively, so node --test can run this file as written.
It's not trying to replace Jest for a large suite with snapshot testing and elaborate mocking. For a library, a utility module, or anywhere you just need "does this function do what it claims," it's a real test suite for the cost of two imports.
How it works
test(name, fn)registers a top-level test. Iffnthrows — including a failedassert.equal— the test fails; otherwise it passes.t.test(name, fn)inside a test callback registers a subtest, scoped under its parent; the parent test doesn't finish until all its subtests do, and any subtest failure fails the parent too.assert.equalfromnode:assert/strictuses strict (===) comparison —node:assert's non-strict variants use loose==and are best avoided.- Run it with
node --test snippet.ts(or point--testat a directory to run every matching file); no separate CLI package needed.
Gotchas
- File discovery is convention-based:
node --testlooks for files matching patterns like*.test.ts,*-test.ts, or anything under atest/directory. A file just namedsnippet.tswon't be picked up automatically — name real test files accordingly. t.test()subtests must beawaited; an un-awaited subtest still running when its parent returns gets cancelled and reported as a failure.- There's no built-in mocking library as rich as Jest's —
node:testdoes shipt.mock, but it's less featureful. For heavy mocking-based suites, a dedicated framework may still be the better trade.
Related
assert.strictvsassert— always prefer the strict variant; the non-strict one predates ES2015 and its loose equality footguns aren't worth the shorter import path.--experimental-strip-types— the flag (unflagged as of Node 23+) that letsnode --testrun.tsfiles directly without a separate build step.