Test Runner IIILoading saved progress…

Test Runner III

Extend a tiny test runner with beforeEach and afterEach hooks — setup that runs before every test and cleanup that runs after every test. The catch is inheritance: a hook registered in an outer describe block must also run for tests nested in inner blocks. This is how Jest, Mocha, and Vitest behave — a beforeEach at the top of a suite applies to every test below it, however deeply nested. A factory returns { describe, test, beforeEach, afterEach, run }; you implement the registration and the run loop. Everything here is synchronous — no async hooks or tests to await.

Signature

// createTestRunner() returns five functions:
//
//   describe(name, fn)    — open a nested suite; fn registers tests/hooks inside it.
//   test(name, fn)        — register one test in the current suite.
//   beforeEach(fn)        — register a setup hook in the current suite.
//   afterEach(fn)         — register a teardown hook in the current suite.
//   run()                 — execute every test. For EACH test:
//                             1. run every beforeEach from the OUTERMOST
//                                enclosing suite inward (outer → inner),
//                             2. run the test body,
//                             3. run every afterEach from the INNERMOST
//                                suite outward (inner → outer).
//                           Returns { passed, failed, total, results },
//                           results: Array<{ name, ok, error? }>.
function createTestRunner(): {
  describe: (name: string, fn: () => void) => void;
  test: (name: string, fn: () => void) => void;
  beforeEach: (fn: () => void) => void;
  afterEach: (fn: () => void) => void;
  run: () => { passed: number; failed: number; total: number; results: object[] };
};

Examples

// Hooks wrap a single test: before, then the body, then after.
const r = createTestRunner();
const log = [];
r.beforeEach(() => log.push('before'));
r.afterEach(() => log.push('after'));
r.test('t', () => log.push('body'));
r.run();
// log → ['before', 'body', 'after']
// An inner test inherits the outer hook, and the order nests like a stack:
// outer setup, inner setup, body, inner teardown, outer teardown.
const r = createTestRunner();
const log = [];
r.beforeEach(() => log.push('outerBefore'));
r.afterEach(() => log.push('outerAfter'));
r.describe('inner', () => {
  r.beforeEach(() => log.push('innerBefore'));
  r.afterEach(() => log.push('innerAfter'));
  r.test('t', () => log.push('body'));
});
r.run();
// log → ['outerBefore', 'innerBefore', 'body', 'innerAfter', 'outerAfter']

Notes

  • Hooks run once per test, not once per suite. Two tests in the same suite each get their own beforeEach and afterEach. If a suite has three tests, its beforeEach runs three times.
  • The order is symmetric. beforeEach runs outermost-first (outer → inner); afterEach runs innermost-first (inner → out). Setup and teardown mirror each other, like opening and closing nested brackets.
  • Multiple hooks in one suite keep registration order. Two beforeEach calls in the same describe run in the order they were declared; likewise two afterEach calls.
  • Inheritance flows down the chain, not across branches. A test sees hooks from itself and every ancestor suite, but a sibling suite's hooks never reach it.
  • A test with no hooks just runs. No setup, no teardown — the body executes on its own.
  • Synchronous only. Hooks and test bodies are plain functions; you never await. Asynchronous hooks are a separate problem.
Loading editor…