unit testing
asynchronous methods
software testing
async development
testing best practices

How do I unit test asynchronous methods nicely?

Interview Questions practice on Codemia

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

Browse interview questions

Introduction

Testing asynchronous methods cleanly is mostly about making completion deterministic. A good async unit test does not sleep for arbitrary amounts of time and hope for the best; it waits explicitly for the operation to finish and keeps external timing, network calls, and background scheduling under control.

The First Rule: Make Completion Observable

An async method is easiest to test when it exposes a clear completion mechanism such as a promise, an async function, or a callback. Once the test framework can wait for that completion signal, the test becomes much less fragile.

With Jest and async/await, the clean pattern is to await the call directly:

javascript
1async function fetchValue() {
2  return Promise.resolve(42);
3}
4
5test("fetchValue resolves to 42", async () => {
6  const value = await fetchValue();
7  expect(value).toBe(42);
8});

That is much better than calling the method and then sleeping for a fixed delay. The test waits for the actual completion rather than guessing how long the operation might take.

Return the Promise or Await It

A common mistake in async tests is forgetting to return the promise or await it. Then the test can finish before the asynchronous work actually runs.

Correct promise-based style:

javascript
1function fetchValue() {
2  return Promise.resolve(42);
3}
4
5test("fetchValue resolves", () => {
6  return fetchValue().then((value) => {
7    expect(value).toBe(42);
8  });
9});

Correct async style:

javascript
1function fetchValue() {
2  return Promise.resolve(42);
3}
4
5test("fetchValue resolves", async () => {
6  const value = await fetchValue();
7  expect(value).toBe(42);
8});

Both are fine. The key is that the test runner must know there is unfinished async work.

Control Dependencies, Not Time

The nicest async tests isolate the method from real time and real network activity. If your method depends on an API client, database call, or message queue, replace that dependency with a controlled fake or mock.

javascript
1async function loadUser(apiClient) {
2  const user = await apiClient.getUser();
3  return user.name.toUpperCase();
4}
5
6test("loadUser formats the returned name", async () => {
7  const fakeApi = {
8    getUser: jest.fn().mockResolvedValue({ name: "mark" }),
9  };
10
11  const result = await loadUser(fakeApi);
12
13  expect(result).toBe("MARK");
14  expect(fakeApi.getUser).toHaveBeenCalledTimes(1);
15});

This gives you a fast, reliable test because the dependency resolves immediately and predictably.

Avoid sleep-Style Tests

Bad async tests often look like this idea:

  • start the method,
  • wait 100 milliseconds,
  • then hope the result is ready.

That approach is brittle because:

  • it slows the test suite,
  • it still can fail on a busy machine,
  • and it hides the real completion condition.

If your only way to test a method is by sleeping and polling, the method or its dependencies probably need a better interface.

Test Failures Explicitly

Async code often has both success and failure paths. The failure path should be tested just as directly.

javascript
1async function fetchUser(apiClient) {
2  return apiClient.getUser();
3}
4
5test("fetchUser rejects on API failure", async () => {
6  const fakeApi = {
7    getUser: jest.fn().mockRejectedValue(new Error("network error")),
8  };
9
10  await expect(fetchUser(fakeApi)).rejects.toThrow("network error");
11});

That keeps error handling under test without resorting to log inspection or timing hacks.

Common Pitfalls

  • Forgetting to return or await the asynchronous operation in the test.
  • Using arbitrary delays instead of waiting for the real completion signal.
  • Hitting real network or database dependencies in unit tests.
  • Testing only the success case and ignoring rejections or callback errors.
  • Writing async code that exposes no clean completion point, which makes tests awkward by design.

Summary

  • Async methods are easiest to test when completion is explicit and deterministic.
  • Return promises or await them so the test framework knows when the test is actually finished.
  • Mock dependencies instead of depending on real time or real external systems.
  • Avoid sleep-style tests because they are slow and fragile.
  • Test both success and failure paths with the same direct async patterns.

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.