JavaScript testing
Jest
Mocha
dynamic test creation
asynchronous initialization

Jest or Mocha Dynamically create tests based on async initialization

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

Dynamic test generation sounds simple until the list of cases comes from asynchronous work. The core constraint is that Jest and Mocha usually collect tests while the file is being loaded, so data fetched later in beforeAll or before arrives too late to define new test cases.

Why Hooks Cannot Usually Register Tests

Hooks are for setup before test execution, not for creating new tests after discovery has finished. This means code like the following looks reasonable but fails conceptually:

javascript
1let cases = [];
2
3beforeAll(async () => {
4  cases = await loadCases();
5
6  cases.forEach((testCase) => {
7    test(testCase.name, () => {
8      expect(runCode(testCase.input)).toBe(testCase.expected);
9    });
10  });
11});

By the time beforeAll runs, Jest has already scanned the file and recorded which tests exist. Mocha behaves similarly with before.

That is the main mental model to keep in mind: test declaration and test execution happen in different phases.

Load Async Data Before Declaring Tests

If your environment supports ESM and top-level await, load the data first and then declare the tests normally.

javascript
1const cases = await loadCases();
2
3describe("generated cases", () => {
4  test.each(cases)("$name", ({ input, expected }) => {
5    expect(runCode(input)).toBe(expected);
6  });
7});

In Mocha, the same approach works with a simple loop:

javascript
1const cases = await loadCases();
2
3describe("generated cases", () => {
4  for (const testCase of cases) {
5    it(testCase.name, () => {
6      if (runCode(testCase.input) !== testCase.expected) {
7        throw new Error("unexpected result");
8      }
9    });
10  }
11});

This is the cleanest option because each generated case still appears as its own test in the runner output.

Use A Preparation Step In Older Setups

If top-level await is not available, move the asynchronous work to a preparation step that runs before the test file is loaded. The preparation step can write a JSON file, generate a fixture module, or cache the API response somewhere local.

The test file then reads that data synchronously:

javascript
1const fs = require("fs");
2
3const cases = JSON.parse(fs.readFileSync("./tmp/test-cases.json", "utf8"));
4
5cases.forEach((testCase) => {
6  test(testCase.name, () => {
7    expect(runCode(testCase.input)).toBe(testCase.expected);
8  });
9});

This approach is less elegant than top-level await, but it preserves separate pass and fail reporting for each case and works well in continuous integration.

Fall Back To One Async Test When Necessary

Sometimes the data really cannot be loaded before test registration. In that situation, stop trying to dynamically create tests and use one async test that loops over the cases.

javascript
1test("all remote cases", async () => {
2  const cases = await loadCases();
3
4  for (const testCase of cases) {
5    expect(runCode(testCase.input)).toBe(testCase.expected);
6  }
7});

You lose one-test-per-case output, but the behavior is valid everywhere and much easier to reason about than trying to fight the test runner lifecycle.

This tradeoff is often perfectly acceptable for integration or contract tests where the external system is part of the setup cost anyway.

Keep Test Discovery Stable

Even when you can generate tests dynamically, be careful about where the data comes from. Pulling live data from a flaky service during test discovery makes the whole suite unstable. It can also make your test count vary between runs, which is confusing in CI and hard to debug.

If the case list is remote, cache it or snapshot it. Dynamic generation is most useful when the test set is data-driven but still deterministic.

Common Pitfalls

  • Defining test(...) or it(...) inside beforeAll or before.
  • Assuming asynchronous setup hooks run before test discovery.
  • Relying on top-level await in an environment that still executes tests as CommonJS.
  • Pulling volatile remote data during test registration.
  • Generating so many tests that the suite becomes slow and noisy.

Summary

  • Jest and Mocha usually need the full list of tests before hooks run.
  • If cases come from async work, load them before declaration or prepare them in a separate step.
  • Use top-level await when the environment supports it.
  • If early loading is impossible, use one async test that loops through the cases instead of trying to register tests late.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.