Streamline Mocha/SuperTest REST tests with async functions

By: (plus.google.com) +David Herron; Date: 2017-11-17 22:53

Tags: Node.js »»»» Asynchronous Programming

Mocha is an excellent unit testing framework for Node.js, and SuperTest is an excellent library for testing REST services. SuperTest is based on SuperAgent which makes it pretty darn easy to write code interfacing with a REST service. The problem with this combination is the test code can get pretty darn caught up in boilerplate callback functions, if you use Mocha and SuperTest the way its documented. Fortunately it's pretty easy to make SuperTest return a Promise, which then plays well with async functions, and Mocha is easily able to use an async function in a test scenario. So let's get started.

To review the structure of a Mocha test suite, it goes something like this:

const assert = require('chai').assert;
describe('Test Suite Name', function() {
  describe('Test Area', function() {
    it('should have behavior #1', function(done) {
      assert.equal(testedValue, expectedValue);
      done();
    });
    it('should have behavior #2', function(done) {
      assert.equal(testedValue, expectedValue);
      done();
    });
    it('should have behavior #3', function(done) {
      assert.equal(testedValue, expectedValue);
      done();
    });
  });
});

Tests are run using the Mocha command as so:

$ mocha test-suite.js

This example uses Chai Assertions because they're very nice and useful.

Mocha as used above is okay when the tested code is synchronous and things are straight-forward. But I've found while writing test code for the REST service I'm creating, that things can become quite convoluted if you use the callback-oriented test framework, and the callback-oriented usage of SuperTest.

That was a long sentence, and an example should help instead:

describe('Weather Profile', function() {

    let pvSystem;
    before(function(done) {
        request(appurl).post('/systems')
        .set('Content-Type', 'application/json')
        .set('Acccept', 'application/json')
        .timeout({ response: 100000, deadline: 100000 })
        .send(require('./data/systems/system-1.js'))
        .then(results => { 
            
            if (results.error !== null 
                && typeof results.error !== 'undefined' 
                && results.error !== false) {
                console.log(util.inspect(results.error));
            }
            assert.ok(results.status === 200);
            pvSystem = results.body;
            // console.log(util.inspect(pvSystem));
            utilsystem.checkSystem1(pvSystem);
            done();
        })
        .catch(err => { 
            console.error(err);
            done(err); 
        });
    });

    var weatherID0;
    var weatherID1;
    var weatherID2;
    it('should retrieve all Weather Profiles from System', function(done) {
        request(appurl).get(`/systems/${pvSystem.identifier}/weathers`)
        .timeout({ response: 100000, deadline: 100000 })
        .end(function(err, response) {
            if (err) throw err;
            if (response.error !== null 
                && typeof response.error !== 'undefined' 
                && response.error !== false) {
                console.log(util.inspect(response.error));
            }
            assert.ok(response.status === 200);
            // console.log(util.inspect(response.body));
            assert.equal(response.body.length, 1);
            utilweather.checkWeatherProfile1(response.body[0]);
            weatherID0 = response.body[0].measurementID;
            done();
        });
    });
});

Here's an example from the tests I've written -- it's not too bad, but there's still some callback boilerplate. This test suite contains some more convoluted code that's even worse. Overall we can improve this considerably by using async functions with the await keyword to wait on promises from the SuperTest library.

The test suite follows a pattern that may be common practice. In a before hook it pushes a test object into the database, and then runs a few manipulations of that object. The pvSystem object is semi-global within this set of tests, so we can easily refer to it in every test. In particular we need its ID to pass as a parameter to further REST calls. The weatherID0/1/2 objects are used similarly.

First, Mocha can use an async function as so:

before(async function() {
    ...
});

The code within that async function can then use the await keyword, thrown exceptions are automatically caught by the async function, and a number of other good things.

let pvSystem;
before(async function() {
    let results = utiltest.checkResponse(
        await request(appurl).post('/systems')
        .set('Content-Type', 'application/json')
        .set('Acccept', 'application/json')
        .timeout({ response: 100000, deadline: 100000 })
        .send(require('./data/systems/system-1.js'))
        .catch(err => { 
            console.error(err);
            throw err; 
        })
    );
    assert.ok(results.status === 200);
    pvSystem = results.body;
    // console.log(util.inspect(pvSystem));
    utilsystem.checkSystem1(pvSystem);
});

Isn't this much easier to read? But, what happened?

Instead of function(done) { ... } there is no done argument, which causes Mocha to expect to receive a Promise, and the async keyword is used, which signifies among other things that the function will return a Promise.

First off I defined a module, utiltest.js, containing this:

const util = require('util');

module.exports.checkResponse = function(response) {
    if (response.error !== null 
        && typeof response.error !== 'undefined' 
        && response.error !== false) {
        console.log(util.inspect(response.error));
    }
    return response;
};

That one test appears all over the place in the test suite, so it's best to localize the code to one place. We simply require that into every test script, and call it as utiltest.checkResponse(...) as shown here. It returns the object it's given so it's easy to use in-line.

Next, we call the request function in a model which causes it to return a Promise. That is - SuperAgent builds on the SuperTest API, so the best source for SuperAgent documentation is SuperTest. In the SuperTest documentation it says that if a request thing does not include a .end call, it will instead return a Promise which can be used with a .then. See here: (visionmedia.github.io) http://visionmedia.github.io/superagent/#request-basics

That's nice, except the .then is still buried inside a callback. Better is to use this within an async function and to use the await keyword. That's what we've done here.

The await keyword then causes the value from the request to be returned as its value. So what would have been this:

request(appurl).post('/systems')
    .set('Content-Type', 'application/json')
    .set('Acccept', 'application/json')
    .timeout({ response: 100000, deadline: 100000 })
    .send(require('./data/systems/system-1.js'))
    .then(results => {
        handle results here
    })
    .catch(err => { 
        console.error(err);
        done(err); 
    })

Is now much easier to deal with and you don't have nested callback functions.

The entire new-and-improved code reads as so:

describe('Weather Profile', function() {

    let pvSystem;
    before(async function() {
        let results = utiltest.checkResponse(
            await request(appurl).post('/systems')
            .set('Content-Type', 'application/json')
            .set('Acccept', 'application/json')
            .timeout({ response: 100000, deadline: 100000 })
            .send(require('./data/systems/system-1.js'))
            .catch(err => { 
                console.error(err);
                throw err; 
            })
        );
        assert.ok(results.status === 200);
        pvSystem = results.body;
        // console.log(util.inspect(pvSystem));
        utilsystem.checkSystem1(pvSystem);
    });

    var weatherID0;
    var weatherID1;
    var weatherID2;
    it('should retrieve all Weather Profiles from System', async function() {
        let response = utiltest.checkResponse(
            await request(appurl).get(`/systems/${pvSystem.identifier}/weathers`)
                .timeout({ response: 100000, deadline: 100000 })
                .catch(err => { 
                    console.error(err);
                    throw err; 
                })
        );
                
        assert.ok(response.status === 200);
        // console.log(util.inspect(response.body));
        assert.equal(response.body.length, 1);
        utilweather.checkWeatherProfile1(response.body[0]);
        weatherID0 = response.body[0].measurementID;
    });
});

Notice that this still uses a .catch block. That's so we get an output of any thrown errors to aid the programmer in diagnosing test failures.