10LOC
Node.jsintermediate

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. If fn throws — including a failed assert.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.equal from node:assert/strict uses strict (===) comparison — node:assert's non-strict variants use loose == and are best avoided.
  • Run it with node --test snippet.ts (or point --test at a directory to run every matching file); no separate CLI package needed.

Gotchas

  • File discovery is convention-based: node --test looks for files matching patterns like *.test.ts, *-test.ts, or anything under a test/ directory. A file just named snippet.ts won't be picked up automatically — name real test files accordingly.
  • t.test() subtests must be awaited; 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:test does ship t.mock, but it's less featureful. For heavy mocking-based suites, a dedicated framework may still be the better trade.

Related

  • assert.strict vs assert — 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 lets node --test run .ts files directly without a separate build step.