async testing
Jasmine framework
JavaScript
testing strategies
web development

Testing async function with jasmine

Interview Questions practice on Codemia

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

Browse interview questions

Introduction

Asynchronous code is everywhere in JavaScript, from HTTP calls to timers and event streams. Jasmine supports several async testing styles, but each style has specific failure modes if used incorrectly. This guide shows reliable patterns for Promise-based and callback-based code, with examples you can run directly.

Test Promises with async and await

For modern JavaScript, async and await is the cleanest approach. Jasmine waits for the returned Promise to settle.

javascript
1function fetchUser(api, id) {
2  return api.get(`/users/${id}`).then((res) => res.data);
3}
4
5describe("fetchUser", () => {
6  it("returns user data", async () => {
7    const api = {
8      get: jasmine.createSpy("get").and.resolveTo({ data: { id: 7, name: "Ana" } })
9    };
10
11    const result = await fetchUser(api, 7);
12
13    expect(api.get).toHaveBeenCalledWith("/users/7");
14    expect(result.name).toBe("Ana");
15  });
16});

If the Promise rejects and you do not handle it, Jasmine marks the spec as failed automatically.

Test Callback APIs with done

Some legacy APIs use callbacks instead of Promises. In that case, Jasmine done is still useful.

javascript
1function loadConfig(readFile, callback) {
2  readFile("config.json", (err, content) => {
3    if (err) return callback(err);
4    callback(null, JSON.parse(content));
5  });
6}
7
8describe("loadConfig", () => {
9  it("parses json from callback", (done) => {
10    const fakeReadFile = (name, cb) => {
11      cb(null, '{"env":"test"}');
12    };
13
14    loadConfig(fakeReadFile, (err, cfg) => {
15      if (err) return done.fail(err);
16      expect(cfg.env).toBe("test");
17      done();
18    });
19  });
20});

Always call done.fail(error) on error paths so failures are visible.

Control Timer-Based Async Logic

For code using setTimeout or setInterval, use Jasmine fake clock to avoid slow tests.

javascript
1function debounceExample(fn) {
2  let timer = null;
3  return (value) => {
4    clearTimeout(timer);
5    timer = setTimeout(() => fn(value), 300);
6  };
7}
8
9describe("debounceExample", () => {
10  beforeEach(() => jasmine.clock().install());
11  afterEach(() => jasmine.clock().uninstall());
12
13  it("calls fn once after delay", () => {
14    const spy = jasmine.createSpy("spy");
15    const debounced = debounceExample(spy);
16
17    debounced("A");
18    debounced("B");
19
20    jasmine.clock().tick(299);
21    expect(spy).not.toHaveBeenCalled();
22
23    jasmine.clock().tick(1);
24    expect(spy).toHaveBeenCalledOnceWith("B");
25  });
26});

This gives deterministic timing without setTimeout in test code.

Test Rejection and Error Paths Explicitly

Async bugs often hide in unhappy paths, so write dedicated rejection tests.

javascript
1function save(api, payload) {
2  return api.post("/save", payload).then((r) => r.status);
3}
4
5describe("save", () => {
6  it("handles rejected promise", async () => {
7    const api = {
8      post: jasmine.createSpy("post").and.rejectWith(new Error("network"))
9    };
10
11    await expectAsync(save(api, { id: 1 })).toBeRejectedWithError("network");
12  });
13});

expectAsync makes rejection assertions concise and readable.

Keep Async Tests Deterministic

Stable async tests usually follow a few simple rules.

  • Do not rely on real network access. Stub dependencies and resolve promises locally.
  • Avoid shared mutable state across specs.
  • Keep one async style per test. Do not combine done and async in the same spec.
  • Use explicit timeouts only when necessary and keep them short.

If a test suite is flaky, start by removing real timers and real I/O.

Tune Timeouts for CI Stability

Async tests that pass locally can fail in CI due to slower workers. Set a reasonable default timeout and override only when a spec really needs more time.

javascript
beforeAll(() => {
  jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000;
});

Avoid extreme values that hide performance regressions. If a test needs long timeouts regularly, it usually indicates real external I/O or poor isolation.

For browser-based suites, ensure microtask queues are flushed consistently between tests. For Node-based suites, reset spies and fake timers in afterEach so one spec does not leak state into another.

Common Pitfalls

  • Forgetting to return or await a Promise, causing false-positive passing tests.
  • Calling done twice, which can produce confusing failures.
  • Using done in a test that already returns a Promise.
  • Depending on actual wall-clock delays instead of fake timers.
  • Not testing rejection paths, which leaves error handling unverified.

Summary

  • Prefer async and await for Promise-based APIs.
  • Use done only for callback-style APIs and call done.fail on errors.
  • Use jasmine.clock for deterministic timer tests.
  • Assert both success and failure paths with expectAsync.
  • Keep tests isolated from real network and timing dependencies.

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.