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: http://mochajs.org/
- Chai Assertions: http://chaijs.com/api/assert/
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:
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.