Write the runner

Build the runner, so that nothing about the real ones is mysterious afterwards.

02of 16
≈ 13Min read
5Parts

A test is a function that throws

That is a passing test. Everything a framework adds is about running many of them, not stopping at the first failure, and saying which one broke and why.

the whole idea, with no framework at all
function total(line) {
  return line.qty * line.price;
}

// the test
if (total({ qty: 2, price: 890 }) !== 1780) throw new Error("total is wrong");

console.log("ok");

Collect, then run

it does not run anything — it registers. The file is read top to bottom, the calls to it fill an array, and then the runner walks it. That is why a console.log at the top of a test file runs before every test, and why a variable defined outside a test is shared by all of them.

the shape every runner has
const tests = [];

const it = (name, body) => tests.push({ name, body });

async function run() {
  const results = [];

  for (const test of tests) {
    try {
      await test.body();
      results.push({ name: test.name, ok: true });
    } catch (error) {
      results.push({ name: test.name, ok: false, error: error.message });
    }
  }

  return results;
}

expect, and a message worth reading

The message is the whole value of a matcher. expect(total(line)).toBe(1780) failing with "expected 1782 to be 1780" tells you what happened; assert(total(line) === 1780) failing with "assertion failed" tells you to go and add a print statement.

a matcher is a function that throws a good error
function expect(actual) {
  return {
    toBe(want) {
      if (!Object.is(actual, want)) {
        throw new Error(`expected ${JSON.stringify(actual)} to be ${JSON.stringify(want)}`);
      }
    },
  };
}

describe, and hooks

beforeEach exists because the alternative — building the fixture at the top of the file — means every test shares one object, and a test that modifies it changes the next one. A hook that runs per test gives each one a fresh start, and the order of the tests stops mattering.

grouping is a stack
const suites = [{ name: "", tests: [], before: [] }];

const describe = (name, body) => {
  suites.push({ name, tests: [], before: [] });
  body();                                   // the body runs NOW, filling the new suite
  const done = suites.pop();
  suites.at(-1).tests.push(done);           // and the finished suite joins its parent
};

const it = (name, body) => suites.at(-1).tests.push({ name, body });
const beforeEach = (fn) => suites.at(-1).before.push(fn);
Order-independence is the property to protect

If your suite passes only in one order, it has hidden shared state — and it will fail the day somebody adds a test in the middle, or the runner parallelises. Building fresh state per test is what prevents it.

What the real ones add

Nothing in that list changes what a test is. It is still a function that throws.

  • Discovery — finding *.test.js without you importing them.
  • Isolation — a fresh module registry per file, and parallel workers.
  • Watch mode — re-running only what your change could have affected.
  • Better diffs — a coloured, structural comparison rather than two JSON strings.
  • Mocking of modules, coverage, snapshots, retries, timeouts.

Practice