w3resource

Testing Asynchronous Code


When writing JavaScript codes, most times you will want to write asynchronously. In the case where you have code that runs asynchronously, Jest will need to know when the code it is testing has completed, before it can move to another test. Jest provides several ways to handle this.

Callbacks

Callbacks are the most common asynchronous pattern.

For instance, say that you have a fetchData(callback) function that will fetch some data and calls callback(data) when it completes. You then want to test that this returned data is just the string 'peanut butter'.

By default, Jest tests will complete once they reach the end of their execution. That means that the test below will not work as intended:

// Do not do this!
test('the data will be peanut butter', () => {
  function callback(data) {
    expect(data).toBe('peanut butter');
  }

  fetchData(callback);
});

The problem in the code above is that the test completes as soon as fetchData completes, before the callback is ever called.

An alternate form of test exists that fixes this. Rather than putting the test in a function with an empty argument, you need to use a single argument that is called done. Jest waits until the done callback is called before finishing the test.

test('the data will be peanut butter', done => {
  function callback(data) {
    expect(data).toBe('peanut butter');
    done();
  }

  fetchData(callback);
});

In the case where done() is never called, the test fails, which is exactly what you want to happen.

Promises

If your code makes use promises, a simpler way to handle asynchronous tests exists. All you need to do is to return a promise from your test, and Jest waits for that promise to resolve. In the case where the promise is rejected, the test automatically fails.

For instance, if fetchData, rather than using a callback, returns a promise that we expect to resolve to the string 'peanut butter'. We can test it with:

test('the data will be peanut butter', () => {
  return fetchData().then(data => {
    expect(data).toBe('peanut butter');
  });
});

Ensure that you return the promise - if you omit the return statement, your test completes before the promise returned from fetchData resolves and then() will have a chance to execute the callback.

In the case where you expect a promise to be rejected you need to use the .catch method. Ensure you add expect.assertions to verify that a certain number of assertions are called. Otherwise a fulfilled promise will not fail the test.

test('the fetch will fail with an error', () => {
  expect.assertions(1);
  return fetchData().catch(e => expect(e).toMatch('error'));
});

.resolves / .rejects

You may also use the .resolves matcher in your expect statement, and Jest waits for that promise to resolve. In the case where the promise is rejected, the test automatically fails.

test('the data will be peanut butter', () => {
  return expect(fetchData()).resolves.toBe('peanut butter');
});

Ensure that you return the assertion-if this return statement is omitted, your test completes before the promise returned from fetchData is resolved and then() will have a chance to execute the callback.

If a promise is expected to be rejected, you need to use the .rejects matcher. It will work analogically to the .resolves matcher. In the case where the promise is fulfilled, the test automatically fails.

test('the fetch will fail with an error', () => {
  return expect(fetchData()).rejects.toMatch('error');
});

Async/Await

Alternatively, you can make use of async and await in your tests. If you want to write an async test, all you need to do is to use the async keyword in front of the function passed to test. For instance, the same fetchData scenario can be tested with the code below:

test('the data will be peanut butter', async () => {
  const data = await fetchData();
  expect(data).toBe('peanut butter');
});
test('the fetch will fail with an error', async () => {
  expect.assertions(1);
  try {
    await fetchData();
  } catch (e) {
    expect(e).toMatch('error');
  }
});

Of course, you could combine async and await with .resolves or .rejects.

test('the data will be peanut butter', async () => {
  await expect(fetchData()).resolves.toBe('peanut butter');
});

test('the fetch will fail with an error', async () => {
  await expect(fetchData()).rejects.toThrow('error');
});

In such cases, async and await are just syntactic sugar for the same logic as the promises example uses.

None of these forms can be said to superior to the others, and you can mix and match them across your codebase or even within a single file. It only depends on which style makes your tests simpler.



Inviting useful, relevant, well-written and unique guest posts