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.
// 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[] };
};
// 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']
beforeEach and afterEach. If a suite has three tests, its beforeEach runs three times.beforeEach runs outermost-first (outer → inner); afterEach runs innermost-first (inner → out). Setup and teardown mirror each other, like opening and closing nested brackets.beforeEach calls in the same describe run in the order they were declared; likewise two afterEach calls.await. Asynchronous hooks are a separate problem.