ImaginativeThinking.ca


A developers blog

How the Heck do you test Async code with Mocha?

By: Brad

Hey Brad I have a function which returns a promise, how the heck do I test that with Mocha?

TL;DR Either call the done callback or simply return the promise. Checkout Things to Watch out For when testing Promises

No worries I can help, the mocha testing framework does support testing asynchronous code. The only catch is that you need to tell mocha that the code you are testing is asynchronous or it will assume the code to be synchronous.

How Mocha tests work

As talked about in What the Heck is Mocha mocha runs each test defined via the it() within a try-catch and any it() tests which throws are marked as failed tests. However since Async code by their nature will be executed after the it() function is invoked the try-catch won’t exist by the time the code under test is actually executed. So how does mocha detect a failed assertion for async code?

Because mocha was made for testing Node code and Node is primarily designed as asynchronous code mocha defiantly support testing asynchronously; in fact it has two different ways of doing that: callbacks and Promises.

Callbacks

The first of the two ways mocha has to test asynchronous code is the done callback. Back in the day before Promises Node was all about callbacks. An async function generally took a callback function as an input parameter which the async function was to call when it finished processing. If the async function was successful it would call the callback with any optional parameters to pass down stream. If the async function ran into an error it would pass the error as the first parameter to the call back function.

function myAsyncFunc(callback) {
  // do something async
  ...
  callback(err); // if error
  ...
  callback(null, 42); // if success
}

mocha is setup to work in the same way, when it invokes the function you passed into the it() function it will pass in a callback function for you to call; if the test passed just call the callback, if the test failed then pass the error into the callback.

it('my test', (done) => {
 const result = 5 + 5;

  if (result !== 10) {
    return done(new Error(`expect 10 got ${result}`));
  }
  done();
});

Not the greatest example above I know as it is just a sync function but I think that adequate shows off how the done callback is to be used.

A better example might be if the code we were testing was event based then we’d use the done callback to let mocha know when the code has finished and if it passed or failed.

it('my test', (done) => {
  doWork()
    .on('error', done) // If error event emitted invoke done with error
    .on('finish', done); // If finish event emitted invoke done
});

mocha will wait until the done callback is called, if neither the error or finish events above are emitted then mocha will timeout which by default will happen after 2 seconds.

mocha can tell if you wrote your test with the done callback or not and mark those with the callback as async. So it('my test', () => {...}); is a sync test and it('my test', (done) => {...}); is an async test. Just make sure that if you include the done callback that you call it otherwise your test will timeout.

Promises

With the introduction of Promises into JavaScript/Node which promise to completely replace callbacks mocha was updated to support that form of async code.

Instead of working with a done callback mocha can accept a Promise object and check if the Promise ends up being fulfilled or rejected. A rejected Promise is interpreted as a failed test.

To use Promises simply return the promise.

it('my test', () => {
  return doWorkReturnPromise(5);
});

That is it, the doWorkReturnPromise() function will return a Promise. If that promise is fulfilled the test will pass, if not the test will fail.

Note: If the code you are testing returns a promise and you intend to use the Promise to tell mocha if the code worked or not then do not use the done callback. The mixing of done callback and Promises are not supported; mocha will log warnings in the terminal if it detects a test which returns both a Promise and takes in a done callback.

Validating Error Cases

When working with promises and your dealing with the happy path cases where you expect a function to return a fulfilled promise the above test is fine. If something goes wrong and the promise is rejected the test will fail; However, the error message might be non-descriptive.

What about the un-happy path case? If you want to make sure your code is rejecting how do you make sure your test passes when the Promise rejects?

You could simply catch the error, which will cause the Promise to be fulfilled.

const { expect } = require('chai');

it('my test', () => {
  return doWorkReturnPromise(5)
    .catch((err) => {
      expect(err).to.have.property('message', 'boom');
    });
});

This test would pass if the doWorkReturnPromise() function rejected with an Error that has the message ‘boom’. Any other rejection would fail the test because the expect would throw a new error which would reject the promise again.

But what if the doWorkReturnPromise() didn’t reject?

Oops, the test would still pass. The Promise is fulfilled which is all mocha knows to care about. So how do we make sure the Promise is rejecting?

You could put a .then() before the .catch() with an assertion which always fails so that if this promise ever fulfills throw an exception but that exception would end up in your .catch() and if your .catch() isn’t checking the error value again your test would pass when it should fail.

Instead of writing a bunch of code your self to detect failure to reject would it not be nice if there was an assertion method to do just that? Enter a chai plugin called chai-as-promised. This plugin adds more assertion helpers to the chai library which know how to check Promises.

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

const { expect } = chai;
chai.use(chaiAsPromised);

it('my test', () => {
  return expect(doWorkReturnPromise(5))
    .to.eventually.be.rejectedWith('boom');
});

By installing the chai-as-promised module into chai via the chai.use() method we now have access to the .eventually.be.rejected, .eventually.be.rejectedWith(), and .eventually.be.fulfilled assertion functions.

The above test is now explicitly stating that the doWorkReturnPromise() function must reject, if it does not the assertion method will force a rejection stating that it expected a rejection but got a fulfillment. It also converts rejections into fulfillment if the expectation was to get a rejection.

Although for the happy path tests you can get by with just returning the promise as we did above, by using chai-as-promised even on tests expecting a fulfillment the message you get in the terminal when the test fails is much better.

it('my test', () => {
  return expect(doWorkReturnPromise(5))
    .to.eventually.be.fulfilled;
});

it('my test', () => {
  return doWorkReturnPromise(5)
});

If you have other things to check on the value of the promise you can always attach a .then() to the assertion to validate the value.

// If the promise is rejected the .then() is not called
it('my test', () => {
  return expect(doWorkReturnPromise(5))
    .to.eventually.be.fulfilled
    .then((value) => {
        expect(value).to.equal(42);
    });
});

// If the promise is fulfilled the .then() is not called
// because chai-as-promised threw since it didn't get a rejection
it('my test', () => {
  return expect(doWorkReturnPromise(5))
    .to.eventually.be.rejectedWith('boom')
    .then((err) => {
        expect(err).to.have.property('code', 404);
    });
});

Things to Watch Out For

When testing async code there are a few things to watch out for that might leave you thinking your tests are passing when they are really not.

Forgetting to return the Promise

When using Promises with mocha it is important to return the Promise at the end of the test, otherwise mocha will assume the test is sync and not wait to see if the Promise fulfilled or rejected.

it('my test', () => {
  const p = doWorkReturnPromise(5);
  expect(p).to.eventually.be.fulfilled;
});

Since we are not actually returning the Promise p here mocha assumes the test is synchronous and since there are no exceptions being thrown reports the test as a success. The problem is that if the doWorkReturnPromise() function did reject mocha is no longer listening for it so what you end up with is an unhandled rejection in the terminal and a bunch of “passed” tests.

Not Checking Why your Test Rejected

The same can be said for not checking why your test threw. If your writing an unhappy path test which is expecting the code under test to throw or reject but you fail to actually verify that the thrown error or rejected promise is the error you were expecting your test could hide unexpected errors.

it('my test', () => {
  return expect(doWorkReturnPromise(5))
    .to.eventually.be.rejected;
});

This test would pass for any rejection even rejections caused by TypeErrors resulting from faulty input or code trying to access properties off of undefined. These are most likely not the errors you were looking for.

There you have it, that is how you test asynchronous code with mocha, you either return the promise or call the done callback. Just make sure not to do both in the same test and if your using Promises don’t forget to return the promise and always check the error to ensure it was the error you were expecting.

Until next time think imaginatively and design creatively

Brad

My interest in computer programming started back in high school and Software Development has remained a hobby of mine ever since. I graduated as a Computer Engineering Technologist and have been working as a Software Developer for many years. I believe that software is crafted; understanding that how it is done is as important as getting it done. I enjoy the aesthetics in crafting elegant solutions to complex problems and revel in the knowledge that my code is maintainable and thus, will have longevity. I hold the designation Certified Technician (C.Tech.) with the Ontario Association of Computer Engineering Technicians and Technologists (OACETT), have been certified as a Professional Scrum Master level 1 (PSM I) and as a Professional Scrum Developer level 1 (PSD I) by Scrum.org as well as designated as an Officially Certified Qt Developer by the Qt Company. For more on my story check out the about page here

Feel free to write a reply or comment.