asynchronous JavaScript
testing JavaScript functions
JavaScript promises
Jasmine testing
PhantomJS

How to test an asynchronous JavaScript function Promises, Jasmine, PhantomJS

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 JavaScript with Jasmine requires explicit signaling so the test runner knows when work is complete. In Promise-based code, flaky tests usually come from missing returns, incorrect done usage, or timers not being controlled. Even in older PhantomJS-based pipelines, reliable async testing is possible with disciplined patterns.

Promise Tests by Returning the Promise

In Jasmine, the cleanest pattern is to return the Promise from the spec.

javascript
1function fetchValueAsync() {
2  return new Promise((resolve) => {
3    setTimeout(() => resolve(42), 30);
4  });
5}
6
7describe('fetchValueAsync', function () {
8  it('resolves expected value', function () {
9    return fetchValueAsync().then(function (value) {
10      expect(value).toBe(42);
11    });
12  });
13});

When you return the Promise, Jasmine waits automatically. No manual callback is required.

Using done for Callback-Style Interop

If the tested path mixes callbacks and Promises, use done carefully.

javascript
1it('handles async callback path', function (done) {
2  fetchValueAsync()
3    .then(function (value) {
4      expect(value).toBeGreaterThan(0);
5      done();
6    })
7    .catch(function (err) {
8      done.fail(err);
9    });
10});

Always call done.fail on errors, otherwise failures can be swallowed and tests may hang.

Testing Rejections Explicitly

Error paths deserve first-class tests.

javascript
1function fetchWithError() {
2  return Promise.reject(new Error('network failed'));
3}
4
5it('rejects with message', function () {
6  return fetchWithError().then(function () {
7    fail('expected rejection');
8  }).catch(function (err) {
9    expect(err.message).toBe('network failed');
10  });
11});

This verifies contract behavior and prevents false positives from untested rejection logic.

Working with Timers in Legacy Environments

In PhantomJS-era test stacks, timer behavior can differ from modern headless Chromium setups. Keep timer-dependent code deterministic using Jasmine clock where possible.

javascript
1describe('delayed update', function () {
2  beforeEach(function () {
3    jasmine.clock().install();
4  });
5
6  afterEach(function () {
7    jasmine.clock().uninstall();
8  });
9
10  it('fires callback after delay', function () {
11    var called = false;
12    setTimeout(function () {
13      called = true;
14    }, 1000);
15
16    jasmine.clock().tick(1000);
17    expect(called).toBe(true);
18  });
19});

This removes real-time waiting and stabilizes CI runs.

Structuring Async Test Utilities

Large suites benefit from helper wrappers for repetitive Promise assertions, timeout handling, and mock setup. Consistent helpers reduce copy-paste mistakes and make failure output easier to interpret.

Also keep each test focused on one async behavior. Combining multiple asynchronous branches in one spec increases flakiness and debugging time.

CI Stability Techniques

Async tests fail in CI when environment timing differs from local machines. Build tests to be timing-agnostic and deterministic.

Recommended practices:

  • avoid magic delays such as waiting arbitrary milliseconds
  • isolate network by mocking Promise-returning APIs
  • assert one async outcome per test
  • use explicit timeouts only as failure guards

Example with a mocked async dependency:

javascript
1function loadUser(api) {
2  return api.fetch().then(function (u) {
3    return u.name;
4  });
5}
6
7it('maps fetched user name', function () {
8  var api = {
9    fetch: function () {
10      return Promise.resolve({ name: 'Mina' });
11    }
12  };
13
14  return loadUser(api).then(function (name) {
15    expect(name).toBe('Mina');
16  });
17});

By controlling async sources directly, you reduce flaky timing differences between PhantomJS and modern local browsers.

It also improves test runtime.

And makes failures clearer.

Common Pitfalls

A common pitfall is forgetting to return the Promise from a Jasmine spec. The test may pass before assertions run.

Another issue is calling done more than once in complex control flow. This can produce confusing failures.

Developers also mix fake timers with real Promise microtasks without understanding ordering. Keep timer and Promise handling explicit.

Finally, do not rely on arbitrary sleep calls in tests. Deterministic synchronization is faster and more reliable.

Summary

  • Return Promises from Jasmine specs whenever possible.
  • Use done only when necessary and always handle failure paths.
  • Test both resolve and reject behavior.
  • Control timers for deterministic async tests.
  • Keep specs small and focused to reduce flakiness in CI.

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.