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.The full solution is part of Premium
Walkthrough, edge cases, complexity notes, and the runnable editor unlock with a Premium subscription.
Submissions are part of Premium
Unlock community code, comments, reactions, and framework-specific approaches.
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.The full solution is part of Premium
Walkthrough, edge cases, complexity notes, and the runnable editor unlock with a Premium subscription.
Submissions are part of Premium
Unlock community code, comments, reactions, and framework-specific approaches.
Unlock the solution & editor
Runnable editor + tests
Solve in the browser with instant Jest feedback.
Detailed solutions
Walkthroughs, edge cases, and complexity notes.
Multi-framework variants
React, Vue, Vanilla, Angular — same question, different stacks.