QUnit
Async Tests
JavaScript Testing
Setup and Teardown
Test Automation

QUnit Async Tests with setup And teardown

Interview Questions practice on Codemia

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

Browse interview questions

Introduction

Async tests in QUnit fail for predictable reasons: state leaks between tests, unresolved promises, and teardown that runs before async work finishes. A stable async test suite needs explicit lifecycle control, not just passing assertions. The pattern is straightforward once setup, cleanup, and timeouts are treated as part of test design.

Core Sections

Build per-test isolation with async hooks

QUnit.module hooks can return promises, so setup and teardown can do real async work without race conditions. The main rule is that every test should get a fresh fixture and cleanup should always run.

javascript
1QUnit.module('profile service', hooks => {
2  let db;
3
4  hooks.beforeEach(async () => {
5    db = await createInMemoryDb();
6    await db.seed([{ id: 1, name: 'Ari' }]);
7  });
8
9  hooks.afterEach(async () => {
10    await db.close();
11  });
12
13  QUnit.test('loads profile by id', async assert => {
14    const profile = await db.getProfile(1);
15    assert.equal(profile.name, 'Ari');
16  });
17});

This structure prevents cross-test coupling. If one test mutates data, it does not contaminate the next test because setup rebuilds state each time.

Use one async style per test body

QUnit supports promise based tests and callback completion with assert.async(). Both are valid, but mixing them in one test often causes double completion bugs. Use async and await for modern APIs and reserve assert.async() for callback-only code.

javascript
1QUnit.test('legacy callback API', assert => {
2  const done = assert.async();
3
4  legacyLookup('user-42', (err, result) => {
5    assert.strictEqual(err, null);
6    assert.equal(result.role, 'admin');
7    done();
8  });
9});

For promise based code, keep the test function itself async and avoid done callbacks. That keeps completion behavior deterministic and easier to debug.

Make teardown robust for failures and early exits

Teardown should clean resources even when assertions fail. Keep test resources reachable from hook scope and never hide cleanup inside test-specific branches.

javascript
1QUnit.module('http client', hooks => {
2  let server;
3
4  hooks.beforeEach(async () => {
5    server = await startMockServer();
6  });
7
8  hooks.afterEach(async () => {
9    if (server) {
10      await server.stop();
11      server = null;
12    }
13  });
14
15  QUnit.test('handles 404 correctly', async assert => {
16    server.reply('/users/9', 404, { message: 'Not found' });
17    const response = await fetchUser(9);
18    assert.equal(response.status, 404);
19  });
20});

That small null check in teardown prevents cleanup code from throwing when setup fails halfway through.

Control time with explicit test timeout and fake timers

Flaky async tests often come from uncontrolled timers. Set QUnit.config.testTimeout to surface hanging tests and use fake timers when the code depends on delays.

javascript
1QUnit.config.testTimeout = 3000;
2
3QUnit.test('retries once after transient failure', async assert => {
4  const clock = sinon.useFakeTimers();
5  const promise = retryingCall();
6
7  clock.tick(1000);
8  await promise;
9
10  assert.ok(true, 'call finished after retry delay');
11  clock.restore();
12});

Without timeout and timer control, intermittent CI slowness can hide race conditions for weeks.

Add targeted diagnostics for async failures

When async assertions fail, logs matter. Capture request ids, timer state, and pending operations in helper utilities. Avoid dumping full global state because noise makes failures harder to inspect.

A practical approach is to wrap high risk async helpers with debug hooks that print only essential context. Keep those hooks off by default and enable them through one environment flag in CI reruns.

Common Pitfalls

  • Starting async work in beforeEach without awaiting completion.
  • Calling done() twice in callback style tests.
  • Combining assert.async() and async test function in one test.
  • Forgetting to restore fake timers or mocked services in teardown.
  • Letting hanging promises run after test completion and affect later tests.

Summary

  • Use async beforeEach and afterEach hooks for reliable fixture lifecycle.
  • Choose one async completion style per test body.
  • Make teardown defensive so cleanup runs after failures.
  • Set explicit timeouts and control timers for deterministic behavior.
  • Add focused diagnostics to reduce triage time for flaky async failures.

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.