All questions

Test Runner IV

Premium

Test Runner IV

Build a tiny test runner — a stripped-down Jest or Mocha. The catch: your specs and your setup/cleanup hooks can be synchronous OR asynchronous (returning a promise), and your runner must await every one of them. A real suite connects to a database in beforeEach, hits an API inside a test, and tears the connection down in afterEach — all async. A runner that fires those off without awaiting reports nonsense: tests "pass" before their assertions have even run.

createRunner() returns a runner object. You register specs with test(name, fn) (aliased as it) and lifecycle hooks with beforeEach, afterEach, beforeAll, and afterAll. Calling run() executes the whole suite and resolves to a results summary.

Signature

function createRunner(): {
  test(name: string, fn: () => void | Promise<void>): void;
  it(name: string, fn: () => void | Promise<void>): void;   // alias for test
  beforeEach(fn: () => void | Promise<void>): void;
  afterEach(fn: () => void | Promise<void>): void;
  beforeAll(fn: () => void | Promise<void>): void;
  afterAll(fn: () => void | Promise<void>): void;
  run(): Promise<{
    passed: number;
    failed: number;
    total: number;
    results: Array<{
      name: string;
      status: 'pass' | 'fail';
      error?: unknown; // present only when status is 'fail'
    }>;
  }>;
};

A spec passes if its fn resolves without throwing, and fails if it throws (sync) or rejects (async). run() itself must never reject — a failing test is data in the summary, not an exception.

Examples

// Async tests: one passes, one rejects. run() resolves with the tally.
const r = createRunner();
r.test('fetches a user', async () => {
  const user = await Promise.resolve({ id: 1 });
  if (user.id !== 1) throw new Error('wrong id');
});
r.test('rejects on bad input', async () => {
  await Promise.reject(new Error('bad input'));
});

await r.run();
// → {
//     passed: 1,
//     failed: 1,
//     total: 2,
//     results: [
//       { name: 'fetches a user', status: 'pass' },
//       { name: 'rejects on bad input', status: 'fail', error: Error('bad input') },
//     ],
//   }
// Hooks run in a fixed order around each test.
const r = createRunner();
const log = [];
r.beforeAll(() => log.push('beforeAll'));
r.afterAll(() => log.push('afterAll'));
r.beforeEach(() => log.push('beforeEach'));
r.afterEach(() => log.push('afterEach'));
r.test('first', () => log.push('test:first'));
r.test('second', () => log.push('test:second'));

await r.run();
// log === [
//   'beforeAll',
//   'beforeEach', 'test:first',  'afterEach',
//   'beforeEach', 'test:second', 'afterEach',
//   'afterAll',
// ]

Notes

  • Sync OR async, every time. A fn or hook may return nothing (sync) or a promise (async). await works on both — await 42 resolves to 42 — so awaiting every call is safe and correct regardless. Don't branch on the return type.
  • Tests run sequentially, in registration order. Start test N+1 only after test N (and its afterEach) has fully settled. No overlap. This mirrors Jest/Mocha's default and keeps shared setup state predictable.
  • Hook ordering is fixed. beforeAll runs once before the first test. Then for each test: beforeEach → the test → afterEach. afterAll runs once after the last test.
  • Errors are isolated per test. One failing or rejecting test must not stop the others or crash run(). Capture the thrown value, record status: 'fail' with the error, and move on.
  • afterEach runs even when the test fails. Cleanup is not optional — a test that throws halfway through still needs its database connection closed. Run afterEach whether the test passed or threw.
  • A throwing hook fails the affected test, gracefully. If beforeEach throws, that test fails and its body is skipped — but the run continues and run() still resolves.
  • Don't worry about nested describe blocks, per-test timeouts, .only/.skip filtering, or running tests in parallel. Those are out of scope (see the solution's "Going further").

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.

Upgrade to Premium