How to test asynchronous errors with Mocha

Asynchronous code is one of the building blocks in Node.js, and as such, we need to test each possible outcome of the functions that use it. Testing custom errors can be a bit tricky, but luckily, native Node.js solutions and external libraries can help. In this short post, I'll cover the basics of asynchronous testing.

It’s often the case in the everyday work of a developer that a function returning a Promise needs to be tested.

It’s usually not an issue to test results when the promise resolves. Testing errors returned by the async function can be a little bit tricky, though.

Example function

Let’s create a dead simple example function and see how we can test both the happy (when the promise resolves) and the sad outcome (i.e. when an error is thrown and the promise is rejected).

I created a file called index.js in the project folder and put the following code in the file:

// index.js

const asyncAddFive = (num) => {
  if (isNaN(num)) {
    return Promise.reject(new Error('Argument must be a number'))
  }

  return Promise.resolve(`The returned value is ${ num + 5 }`)
}

module.exports = asyncAddFive

As I said this function will probably not going to change how you think about the world, but it will do for now.

The function adds 5 to any number that is fed into it, and returns a string with the result. If the argument is not a number, the function returns an error with a gentle warning.

In both cases, the function will return a Promise, which is the whole point of this exercise.

Test setup

As always, I’ll use Mocha and Chai, an excellent assertion library for the tests.

If the reader is not familiar with Mocha and Chai, I wrote some posts about these libraries, please read them first here and here. I also published an article on how to set them up, so I won’t cover these concepts here again.

Let’s create then a file named index.test.js, and place it in the same folder as index.js.

At the top, we need to require chai and our function:

// index.test.js

const { expect } = require('chai')

const asyncAddFive = require('./index')

describe('#asyncAddFive', () => {
  // assertions will come here
})

We can get the expect method using destructuring, instead of writing const expect = chai.expect.

Start a describe block, write the name of the function to be tested in the first argument, then let’s add some assertions in the callback function.

Test the happy path

This is the easier part. Because we know that the function will return the correct result in this case (this is the situation we want to test), we can simply write the following test case inside describe:

it('returns the number plus five if the argument is a number', async () => {
  const num = 12
  const expected = 17
  const msg = await asyncAddFive(num)

  expect(msg).to.eql(`The returned value is ${ expected }`)
})

I’m using async/await and there’s no good reason for not doing so. Although the syntax uses promises in the background, so it’s still asynchronous, it’s much easier to read, because it creates the illusion of a synchronous code. But, again, it’s just illusion, or using the fancy expression, a syntactic sugar, async/await is asynchronous.

As its name suggests, we’ll wait for the return value of asyncAddFive to arrive, before we move on to the next line starting with expect. This way, we can save the return value of the function in a variable called msg.

The assertion (the line starting with expect) is straightforward, we expect that the string asyncAddFive returns will be the same as the one including the expected value of 17.

A quick npm test will show that the test passes.

To make sure that we have written a good assertion, change the value of expected to 18 and run the test again. It should fail this time.

Test the error

Writing a test case for the error is slightly more difficult.

We know that asyncAddFive will throw an error because we’ll call it in a way that it should throw an exception. We’ll feed in an argument other than a number.

We can not only use the try/catch statement in the function, but in the body of the tests, too.

Let’s see how in the next it block in index.test.js:

it('should throw an error if the argument is not a number', async () => {
  try {
    await asyncAddFive('hello')
    throw new Error('This will not run')
  } catch (e) {
    expect(e).to.be.instanceOf(Error)
    expect(e.message).to.eql('Argument must be a number')
  }
})

In the try block, we await asyncAddFive.

Why didn’t I save the return value of the function in a variable this time?

Because we don’t need it. We fed in a string as the argument, and the function should throw an exception. It can be proved by throwing a custom error inside try.

If an error occurs in the try block, the control will immediately be passed to the catch part. Because asyncAddFive throws an error by its argument not being a number, the This will not run exception will not be thrown at all.

Instead, the code will keep running in the catch part. Here, we can expect that the error (e) is an instance of the Error object, and that its message property is what the error has been thrown with (Argument must be a number).

The npm test command will make this test case pass.

As always, it’s a good idea to check if we have written a good test by changing a character or two in the expectation, like changing the word number to string in Argument must be a number. If the test case fails, it’s good, if it still passes, then there’s a problem with the test.

This is a nice and clean way of testing any custom errors in the logic, and I often use this technique in my work. It’s a good practice to validate arguments, types etc. and it’s also a good practice to write test cases to cover these scenarios.

An alternative approach

Chai has an extension called Chai as Promised, which was developed to help developers write tests on promises.

This extension can also be used as way to test asynchronous code.

Setup

It needs a bit of setup though, i.e. we need to add the extensions to Chai.

First, the library needs to be installed as a developer dependency with npm i -D chai-as-promised.

Then, we can set it up in the index.test.js file. No big deal, we only make a small change:

// index.test.js

const chai = require('chai')
const chaiAsPromised = require('chai-as-promised')

chai.use(chaiAsPromised)

const { expect } = chai

We use the use method of Chai and make Chai as Promised available in the file. With this, the methods specifically developed to test promises will be available after expect. There’s no difference how the assertions should be written.

Rewrite the assertions using Chai as Promised

When we test the happy path, i.e. when the function returns the correct answer, we can write something like this:

it('gives back the correct answer of the argument is a number', async () => {
  const num = 12
  const expected = 17

  await expect(asyncAddFive(num)).to.eventually.be.eql(`The returned value is ${ expected }`)
})

We still have an async function inside the it block, and this time we await the whole expect block. The argument of expect will be the asyncAddFive function this time.

The word eventually comes from the Chai as Promised library. eventually refers to the value the promise is fulfilled with, which, in our case, is the The returned value is 17 string. We can then use eventually to unwrap the promise when we want to test if it resolves with the expected value.

What happens if we remove eventually from the assertion? The test will fail:

1) #asyncAddFive
    gives back the correct answer of the argument is a number:
  AssertionError: expected {} to deeply equal 'The returned value is 17'
  at Context.it (.../index.test.js:39:43)

Why? Because by removing eventually, the promised remains wrapped. So let’s quickly write it back.

Assert the error

This is the part where Chai as Promised can offer a simpler syntax, because it doesn’t require try/catch.

Instead, we can simply write just one line:

it('returns an error message if the argument is not a number', async () => {
  await expect(asyncAddFive('hello')).to.be.rejectedWith('Argument must be a number')
})

Another special method is rejectedWith, which we can use to test if the promise rejects with the given value.

With this syntax, the assertion is straightforward and easy to read . We expect that asyncAddFive (a promise), which is called with the string hello, will be rejected with the Argument must be a number message, and this is exactly what happens inside the body of the function.

These assertions could have been written in many different ways. Please refer to the documentation for the list of available methods.

Conclusion

Testing asynchronous code is a frequently required task for developers. It can be done in different ways, like using callbacks with done, promises or async/await.

Using async/await in the tests is the simplest of all as it provides a synchronous way of reading code. Testing errors is as equally important as testing the happy path. The try/catch method is one way to write tests when one decides to use async/await. The disadvantage of this approach is that it’s more verbose and we catch the error instead of expecting it.

The Chai as Promised library is also an option to test asynchronous code and promises, and it can make the code shorter and more test-like, because we expect the error instead of catching it.

Thanks for reading and see you next time.