Trust is earned, not given

A different perspective

2022-08-09 · Projects

Node.js, part 17: the built-in test runner and watch mode

Part 17from the Node.js series · 24 parts in all

Jest, Mocha, Vitest, ts-node, nodemon, chokidar — an ordinary Node project used to begin with a page of devDependencies that existed to run and re-run its own code. Node 18 made the test runner and the watcher built-ins. Part 17 is what the built-in runner does well, and the line where a framework still wins.

A test file, no framework

// test/pricing.test.js
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const { total } = require('../src/pricing');

describe('total', () => {
  test('adds tax and shipping', () => {
    assert.equal(total({ items: 100, tax: 8, shipping: 5 }), 113);
  });

  test('rejects a negative quantity', async () => {
    await assert.rejects(() => total({ items: -1 }), /quantity/);
  });
});
node --test                    # finds test/**/*.test.js and friends
node --test --test-only        # run only tests marked with test.only
node --watch server.js         # restart on change, built in
node --watch --test            # watch mode for tests

That is the whole setup: no config file, no transform, no globals injected into the global scope. Assertions are node:assert, which gained a strict mode — worth using, since assert.equal is loose and assert.strictEqual is not the same thing.

What it does better than a framework

Where a framework still wins

Mocking, rich snapshot testing, fake timers, and a browser-like DOM. The built-in runner has a mocking API now, but if you are testing React components you want Vitest or Jest and the DOM environment with them. The sensible split: use the built-in runner for library and service logic, keep a framework for the parts that genuinely need one — and note that you no longer install a test runner for a small package at all. Next: pinning the package manager itself.