Testing async functions with mocks and mocha in JavaScript

I’ve recently gone down the road of testing all my code using Mocha and Chai, and I aim for 100% code coverage. My current library does a HTTP connection to a backend and I’m hoping to use node-fetch for that. But how do you test a piece of asynchronous code that uses promises or callbacks?

Let’s take a look at my code under test:

import fetchImpl from 'node-fetch';

export default class Client {
    constructor(baseUrl, options = {}) {
        const defaultOptions = {
            fetch: fetchImpl
        }
        
        this.prvOptions = Object.assign({}, defaultOptions, options);
        this.prvBaseUrl = baseUrl;
    }
    
    fetch(relativeUrl, options = {}) {
        const defaultOptions = {
            method: 'GET'
        }
        
        let fetchOptions = Object.assign({}, defaultOptions, options);
        return this.prvOptions.fetch(`${baseUrl}${relativeUrl}`, fetchOptions);
    }
}

This is a much shortened version of my code, but the basics are there. Here is the important thing – I set a default option that includes an option for holding the fetch implementation. It’s set to the “real” version by default and you can see that in line 6. If I don’t override the implementation, I get the node-fetch version.

Later on, I call client.fetch('/foo'). The client library uses my provided implementation of fetch or the default one if I didn’t specify.

All this logic allows me to substitute (or mock) the fetch command. I don’t really want to test the functionality of fetch – I just want to ensure I am calling it with the right parameters.

Now for the tests. My first problem is that I have asynchronous code here. fetch returns a Promise. Promises are asynchronous. That means I can’t just write tests like I was doing before – they will fail because the response wouldn’t be available during the test. The mocha library helps by providing a done call back. The general pattern is this:

    describe('#fetch', function() {
        it('constructs the URL properly', function(done) {
            client.fetch('/foo').then((response) => {
                    expect(response.url).to.equal('https://foo.a.com/foo');
                    done();
                })
                .catch((err) => {
                    done(err);
                });
        });
    });

You might remember the .then/.catch pattern from the standard Promise documentation. Mocha provides a callback (generally called done). You call the callback when you are finished. If you encountered an error, you call the callback with the error. Mocha uses this to deal with async tests.

Note that I have to handle both the .then() and the .catch() clause. Don’t expect Mocha to call done for you. Ensure all code paths in your test actually call done appropriately.

This still has me calling client.fetch without an override. I don’t want to do that. I’ve got this ability to swap out the implemenetation. I have a mockfetch.js file that looks like this:

export default function mockfetch(url, init) {
    return new Promise((resolve, reject) => {
        resolve({url: url, init: init});
    });
}

The only thing that the mockfetch method does is create a promise that is resolved and returns the parameters that were passed in the resolution. Now I can finish my test:

    describe('#fetch', function() {
        let clientUrl = 'https://foo.a.com';
        let clientOptions = {fetch: mockfetch};
        let client = new AzureMobileClient(clientUrl, clientOptions);

        it('constructs the URL properly', function(done) {
            client.fetch('/foo')
                .then((response) => {
                    expect(response.url).to.equal('https://foo.a.com/foo');
                    done();
                })
                .catch((err) => {
                    done(err);
                });
        });
    });

Note that my mockfetch does not return anything resembling a real response – it’s not even the same object type or shape. That’s actually ok because it’s designed for what I need it to do – respond appropriately for the function under test.

There are three things here:

  1. Construct your libraries so that you can mock any external library calls
  2. Use the Mocha “done” parameter to handle async code
  3. Create mock versions of those external library calls

This makes testing async code easy.