Testing Asynchronous Code in Jest

19 min read·Jan 1, 2025

In Jest, testing asynchronous code essentially consists in testing the value of a resolved or rejected promise or the value passed as argument of a callback function.

Testing returned promises

To test the value returned by a fulfilled promise, you can return the Promise object itself and declare your assertions within the callback function of its then() method:

it('should return a fulfilled promise', () => {
  return promise.then(result => {
    expect(result).matcher(value);
  });
});

Where:

  • promise is an instance of the Promise class.

Notes:

  • If the promise is rejected, the test will automatically fail.
  • If the return statement is omitted, your test will complete before the promise resolves.

Example

In this example, the fetchUser() function returns a fulfilled promise containing an object describing a user:

function fetchUser(id) {
  const users = {
    1: {
      name: 'John Doe',
      email: 'john.doe@mail.com'
    }
  };
  const user = users[id];

  return user ? Promise.resolve(user) : Promise.reject(new Error('user_not_found'));
}

it('should return a fulfilled promise', () => {
  const user = {
    name: 'John Doe',
    email: 'john.doe@mail.com'
  };

  return fetchUser(1).then(data => {
    expect(data).toEqual(user);
  });
});

The short syntax

To test the value returned by a fulfilled promise using the short-hand syntax, you can directly return the expect statement and chain it to the resolves matcher:

it('should return a fulfilled promise', () => {
  return expect(promise).resolves.matcher(value);
});

On the other hand, to test the value of a rejected promise, you can use the rejects matcher instead:

it('should return a rejected promise', () => {
  return expect(promise).rejects.matcher(value);
});

Example

In this example, the fetchUser() function returns a rejected promise containing an Error object:

function fetchUser(id) {
  const users = {
    1: {
      name: 'John Doe',
      email: 'john.doe@mail.com'
    }
  };
  const user = users[id];

  return user ? Promise.resolve(user) : Promise.reject(new Error('user_not_found'));
}

it('should return a rejected promise', () => {
  return expect(fetchUser(2)).rejects.toStrictEqual(new Error('user_not_found'));
});

Testing awaited promises

To check the value returned by a fulfilled promise, you can declare an asynchronous callback function using the async keyword, and wait for the unit you want to test to complete using the await keyword:

it('should return a fulfilled promise', async () => {
  const result = await promise;

  expect(result).matcher(value);
});

On the other hand, to check the value returned by a rejected promise, you can wrap the test in a try...catch block:

it('should return a rejected promise', async () => {
  try {
    await promise;
  } catch(error) {
    expect(error).toMatch(value);
  }
});

Example

In this example, the fetchUser() function returns a fulfilled promise containing an object describing a user:

function fetchUser(id) {
  const users = {
    1: {
      name: 'John Doe',
      email: 'john.doe@mail.com'
    }
  };
  const user = users[id];

  return user ? Promise.resolve(user) : Promise.reject(new Error('user_not_found'));
}

it('should return a rejected promise', async () => {
  const user = {
    name: 'John Doe',
    email: 'john.doe@mail.com'
  };
  
  const result = await fetchUser(1);
  expect(result).toEqual(user);
});

The short syntax

To test the value returned by a fulfilled promise, you can also await the expect() statement and chain it to the resolves matcher:

it('should return a fulfilled promise', async() => {
  await expect(promise).resolves.matcher(value);
});

On the other hand, to test the value returned by a rejected promise, you can use the rejects matcher instead:

it('should return a rejected promise', async() => {
  await expect(promise).rejects.matcher(value);
});

Example

In this example, the fetchUser() function returns a rejected promise containing an Error object:

function fetchUser(id) {
  const users = {
    1: {
      name: 'John Doe',
      email: 'john.doe@mail.com'
    }
  };
  const user = users[id];

  return user ? Promise.resolve(user) : Promise.reject(new Error('user_not_found'));
}

it('should return a rejected promise', async () => {
  await expect(fetchUser(2)).rejects.toEqual(new Error('user_not_found'));
});

Testing callback functions

By default, the execution of a test will complete as soon as the execution of the main function completes, before ever calling the callback.

To prevent this from happening, you can pass a single argument to the test's callback function called done(), and invoke it at the end of the callback's execution:

it('should wait for the callback execution', (done) => {
  function cb(data) {
    expect(data).matcher(value);
    done();
  }

  fn(cb);
});

Notes:

  • If the done() function is never called, the test will fail with a timeout error.

  • If the expect() statement fails, it will throw an error that must be wrapped in a try...catch block and passed as argument of the done() handler, as it will otherwise end up in an opaque timeout error that doesn't show what value was received by the expect() statement:

    it('should wait for the callback execution', (done) => {
      function cb(data) {
        try {
          expect(data).matcher(value);
          done();
        } catch(error) {
          done(error);
        }
      }
    
      fn(cb);
    });
    

Example

In this example, the fetchUser() function executes the callback() function passed as argument with an object containing user information or undefined, and an Error object or undefined:

function fetchUser(id, callback) {
  const users = {
    1: {
      name: 'John Doe',
      email: 'john.doe@mail.com'
    }
  };
  const user = users[id];
  const error = !user && new Error('user_not_found') || undefined;

  callback(user, error);
}

it('should wait for the callback execution', (done) => {
  function processUser(data, error) {
    if (error) {
      done(error);
      return;
    }

    try {
      expect(data).toEqual({
        name: 'John Doe',
        email: 'john.doe@mail.com'
      });
      done();
    } catch(error) {
      done(error);
    }
  }

  fetchUser(1, processUser);
});

Summary

Here's a summary of what you've learned in this lesson:

  • The resolves matcher is used to test the value of a fulfilled promise.
  • The rejects matcher is used to test the value of a rejected promise.
  • The done() function is used to indicate the end of execution of a callback function.

Enjoying the courses?

I've made these courses completely free so anyone can learn from them. If they've helped you and you'd like to actively support the work behind BackendBrewery, you can leave a tip:

Support BackendBrewery
Testing Asynchronous Code in Jest | Backend Brewery