Even for JavaScript software developers well-versed in Agile practices, using test-driven development in Node.js and Express can be challenging. In this presentation, I identify solutions to some of the most significant challenges to using TDD with Express, including mocking data in MongoDB / Mongoose, using promises to control asynchronous testing in Mocha with Chai, and separating concerns to write robust and enduring test suites.
Unit Testing Express Middleware with Mocha and Promises
1. express + mocha
UNIT TESTING EXPRESS
MIDDLEWARE
By Morris Singer
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
2. ABOUT ME
• Senior Software Engineer
Cengage Learning
• Expertise:
• Sencha Touch
• Angular.js and Node.js
• Cordova / PhoneGap
• Ruby on Rails
3. AGENDA
• Define Express Middleware and why it isn’t just
a fancy term for controllers or endpoints.
• Review behavior-driven development principles
for unit testing.
• Argue why Express Middleware are behavioral
units.
• Summarize common challenges testing behavior
in Express.
• Review Promises with the Q.js library.
• Learn and implement a pattern for Promise-based
Express Middleware.
• Build tests for common scenarios using Mocha,
Chai, Chai as Promised, and Mockgoose.
• Answer questions. (10 minutes)
5. A SIMPLE CASE
One Middleware Per Endpoint
app.get('hello.txt', function (req, res, next) {
res.send(200, 'Hello World!');
});
“Why is it called ‘Middleware’ anyway?”
6. MORE COMPLEX CASES
Two Ways of Stacking Middleware
app.get('hello.txt',
function (req, res, next) {
req.message = 'Hello World!';
next();
},
function (req, res, next) {
res.send(200, req.message);
}
);
app.get('hello.txt',
function (req, res, next) {
req.message = 'Hello World!';
next();
});
!
app.get('hello.txt',
function (req, res, next) {
res.send(200, req.message);
});
7. THE MIDDLEWARE STACK
app.get('hello.txt',
function (req, res, next) {
req.message = 'Hello World!';
next();
},
function (req, res, next) {
res.send(200, req.message);
}
);
GET
generateMessage
sendMessage
res.send
9. MIDDLEWARE
IS BEHAVIOR
Middleware:
• Define reusable components.
• Create, modify, and store public
variables.
• Send responses to clients.
• Comprise node packages.
11. HTTP RESPONSE TESTS
it('should return a 500 error', function (done){
request({
method: 'POST',
url: 'http://localhost:3000/api/endpoint'
}, function (error, response, body){
expect(response.statusCode).to.equal(500);
done();
});
});
What happens when we add a middleware to the stack?
12. TESTING MID-STACK
app.get('hello.txt',
function (req, res, next) {
req.message = 'Hello World!';
next();
});
!
app.get('hello.txt',
function (req, res, next) {
res.send(200, req.message);
});
How do we pull out these anonymous functions?
13. ILLUMINATING TEST FAILURES
var httpMocks = require('node-mocks-http');
!
it('should call next()', function (done){
var req = httpMocks.createRequest(),
res = httpMocks.createResponse();
!
middleware(req, res, function () {
done();
});
});
What happens if next() is not called?
14. KNOWING WHEN TO TEST
var httpMocks = require('node-mocks-http');
!
it('should call next()', function (done){
var req = httpMocks.createRequest(),
res = httpMocks.createResponse();
!
middleware(req, res);
!
expect(req.foo).to.equal('bar');
});
When is the assertion run?
15. TESTING WITH DATA
app.get('path/to/post', function (req, res, next) {
Post.findOne(params).exec(function (err, post){
res.json(200, post);
});
});
Where do data come from?
16. DEALING WITH POLLUTION
it('should update the first post', function (){
/* ... */
});
!
it('should get the first post', function (){
/* ... */
});
How does one reset the data?
17. MOCKING DEPENDENCIES
app.get('endpoint', function (req, res, next) {
request({
method: 'GET',
url: 'http://example.com/api/call'
}, function (error, response, body) {
req.externalData = body;
next();
});
});
How does one cut out the external data source?
18. WRITING ROBUST TESTS
app.get('hello.txt',
function (req, res, next) {
req.message = 'Hello World!';
next();
});
!
app.get('hello.txt',
function (req, res, next) {
res.send(200, req.message);
});
What if someone adds a middleware?
20. PYRAMID OF DOOM
queryDatabase(params, function (result) {
makeRequestOfThirdPartyService(result, function (result) {
writeFile(result, function (handle) {
sendFileOverHttp(handle, function (result) {
},
function (err) {
// Handle Error
});
},
function (err) {
// Handle Error
});
},
function (err) {
// Handle Error
});
});
21. PROMISES TO THE RESCUE
queryDatabase()
.then(makeRequestOfThirdPartyService)
.then(updateDatabase)
.then(writeFile)
.then(sendFileOverHttp)
.catch(function (err) {
// Handle Errors
}).done();
22. WHAT IS A PROMISE
A promise is:
• a delegate
• for an asynchronous action
• that:
• collects references to callbacks
• maintains state, and
• provides a mechanism for chaining.
23. THEN, CATCH, FINALLY, DONE
myPromise()
.then(function (result) {
!
})
.catch(function (err) {
!
})
.finally(function () {
!
})
.done();
Data sent, received,
read, written, etc.
Problems
No matter what
24. THE FLIP SIDE
var Q = require('q');
!
function myPromise() {
var deferred = Q.defer();
!
if (conditionX) {
!
deferred.resolve('Result');
!
} else {
!
deferred.reject(new Error());
!
}
!
return deferred.promise;
}
Triggers then().
Passes ‘Result’
Triggers catch().
Passes new Error()
25. PUTTING IT ALL TOGETHER
var Q = require('q');
!
function myPromise() {
var deferred = Q.defer();
!
if (conditionX) {
!
!
} else {
!
!
}
!
return deferred.promise;
}
myPromise() !
.then(function (result) {
!
}) !
.catch(function (err) {
!
}) !
.finally(function () {
!
}) !
.done();
deferred.resolve(‘Result’);
deferred.reject(new Error());
26. THE LIFE OF A PROMISE
Pending
Fulfilled
then()
finally()
Rejected
catch()
finally()
27. THE PROMISE CHAIN
Start a new promise chain Continue the chain End the chain
.then()
.catch()
.finally()
Q.defer().promise
Q.when()
Q.promise()
Q.fcall()
.done()
Return a promise
28. IN PRACTICE
Promise B Promise D Promise F
Q.promise()
.then().then().then().catch().finally().done()
Promise A Promise C Promise E
29. DO NOT BREAK
YOUR CHAINS
Otherwise, your user may be left hanging…
30. NOT BREAKING CHAINS
var Q = require('q');
!
Q.when(function () {})
.then(function (result) {
var deferred = Q.defer();
!
/* Do async and call deferred.resolve()
and deferred.reject(). */
!
return deferred.promise;
})
.then(function (result) {
var deferred = Q.defer();
!
/* Do async and call deferred.resolve()
and deferred.reject(). */
!
return deferred.promise;
})
.catch(function (err) {
!
})
.done();
Resolving here
calls the referenced function, passing the result as an argument.
Rejections of either promise result in the referenced function called with err
and uncaught rejections are thrown as errors here.
33. OVERVIEW
• Pull middleware into endpoints and tests.
• Mock req and res.
• Use promises as link between middleware and endpoints.
• Return client-server interaction to endpoint.
• Use promises with Mocha.
34. PULL MIDDLEWARE INTO
ENDPOINTS, TESTS
Endpoint
Middleware Middleware
Test
Endpoint
Middleware Middleware
Test Test
!
!
Old Paradigm
"
!
New Paradigm
35. PULL MIDDLEWARE INTO
ENDPOINTS, TESTS
app.get('example/uri', function (req, res, next) {
/* Middleware implementation */
}, function (req, res, next) {
/* Middleware implementation */
});
var middleware = {
first: function (req, res, next) {},
second: function (req, res, next) {}
};
app.get('example/uri',
middleware.first,
middleware.second);
!
!
Old Paradigm
"
!
New Paradigm
36. MOCK REQ, RES
• We need a way to call our
middleware functions
directly.
• Our middleware functions
expect req and res to be
passed as arguments.
• So, we mock req and res.
module npm
node-mocks-http https://www.npmjs.org/
package/node-mocks-http
express-mocks-http https://www.npmjs.org/
package/express-mocks-http
37. MOCK REQ, RES
it ('should do something', function (done) {
var requestParams = { uri: 'http://path.to/endpoint',
method: 'POST' };
!
request(requestParams, function (error, response, body) {
expect(response.body).to.equal( /* Expected Data */ );
done();
});
});
it ('resolves under condition X with result Y', function () {
!
var req = httpMocks.createRequest(),
res = httpMocks.createResponse();
!
/* Call middleware(req, res) and assert. */
!
});
!
!
Old Paradigm
"
!
New Paradigm
38. USE PROMISES AS LINK BETWEEN
MIDDLEWARE AND ENDPOINTS
• Clean, standardized
interface between
asynchronous middleware
and endpoints.
• Both endpoints and tests
can leverage the same
mechanism in the
middleware for serializing
logic.
then
39. USE PROMISES AS LINK BETWEEN
MIDDLEWARE AND ENDPOINTS
module.exports = function (req, res, next) {
!
/* Define middleware behavior and
call res.json(), next(), etc. */
};
var Q = require('q');
module.exports = function (req, res) {
var deferred = Q.defer();
/* Define middleware behavior and
resolve or reject promise. */
return deferred.promise;
};
!
!
Old Paradigm
"
!
New Paradigm
40. RETURN CLIENT-SERVER
INTERACTION TO ENDPOINT
Endpoint
Req
Res
Middleware
Req
Res
Client
Endpoint
Req
Res
Middleware
Req
Res
Client
!
!
Old Paradigm
"
!
New Paradigm
41. RETURN CLIENT-SERVER
INTERACTION TO ENDPOINT
var middleware = {
first: function (req, res, next) {},
second: function (req, res, next) {}
};
app.get('example/uri',
middleware.first,
middleware.second);
var middleware = require('./middleware.js');
app.get('example/uri', function (req, res, next) {
middleware.first(req, res)
.then(function () { next(); })
.catch(res.json)
.done();
}, middleware.second(req, res)
.then(function () { next(); })
.catch(res.json)
.done();
});
!
!
Old Paradigm
"
!
New Paradigm
42. USING PROMISES WITH
MOCHA (CHAI-AS-PROMISED)
We need:
• A test framework syntax that
facilitates easy async testing.
mocha
(Supported natively in Mocha
since 1.18.0)
• An assertion syntax that we are
familiar with. (Chai)
• A set of assertions that facilitate
easily writing tests of promises.
(Chai-As-Promised) then
43. USING PROMISES WITH
MOCHA (CHAI-AS-PROMISED)
describe('middleware', function () {
it ('resolves under condition X with result Y', function () {
!
var req = httpMocks.createRequest(),
res = httpMocks.createResponse();
!
middleware(req, res).then(function (done) {
/* Assert here. */
}).finally(done).done();
!
});
!
!
Old Paradigm
"
!
New Paradigm
describe('middleware', function () {
it ('resolves under condition X with result Y', function () {
!
var req = httpMocks.createRequest(),
res = httpMocks.createResponse();
!
return expect(middleware(req, res)).to.eventually.equal('value');
!
});
45. ENDPOINTS
Pull Middleware
into Endpoint
Return Client-Server
Interaction to Endpoints
var middleware = require('./middleware.js');
app.get('example/uri', function (req, res, next) {
middleware(req, res)
.then(function () { next(); })
.catch(res.json)
.done();
});
46. MIDDLEWARE
Use Promise as Link
Between Middleware and
Endpoints
var Q = require('q');
module.exports = function (req, res) {
var deferred = Q.defer();
/* Define middleware behavior and
resolve or reject promise. */
return deferred.promise;
};
47. Pull Middleware Into Tests
TEST
Use Promises with Mocha
var httpMocks = require('node-mocks-http'),
chai = require('chai'),
chaiAsPromised = require('chai-as-promised');
chai.use(chaiAsPromised);
Mock Req, Res
var middleware = require('path/to/middleware');
var req, res;
beforeEach(function (done) {
req = httpMocks.createRequest(),
res = httpMocks.createResponse();
});
describe('middleware', function () {
it ('resolves under condition X with result Y', function () {
return expect(middleware(req, res)).to.be.fulfilled.then(function () {
/* Assert */
});
});
it ('rejects under condition X with error Y', function () {
return expect(middleware(req, res)).to.be.rejectedWith('Error String');
});
});
48. TESTING WITH DATA
Mocking Mongoose and Using Fixtures to Build a Robust
and Effective Test Suite
49. THE PROBLEM WITH DATA
We need a solution where:
• Testing does not depend on the environment,
• Data travel with the repo,
• The database can easily be reset to an initial data
set.
50. (MONGODB + MONGOOSE)*
* Solutions are available for other setups. You can also roll your own,
without too much heartache.
51. THE HIGH LEVEL
• Mock MongoDB with in-memory database that can be
reset between tests and thrown away after tests run.
(Mockgoose)
• Write test data as code that can move with the repo.
(Fixtures)
• Build test harness that loads fixtures into mock
database before tests run.
52. MOCKING MONGOOSE
var mongoose = require('mongoose');
var mockgoose = require('mockgoose');
mockgoose(mongoose);
53. CODING DATA IN FIXTURES
module.exports.User = [
{ name: 'Maeby' },
{ name: 'George Michael' }
];
54. LOADING FIXTURES
var loader = require('pow-mongoose-fixtures');
!
var users = require('users.js'); /* User fixtures */
!
beforeEach(function (done) {
loader.load(users);
done();
});
!
/* Build Tests Here */
55. TDD EXERCISES
Use TDD in Pairs to Complete the
Accompanying TDD / Express Exercises
57. IMPROVEMENTS?
• There are still some shortcomings in this approach,
though it is better than other approaches I have seen.
• Particularly, there are still some failure modes that will
just timeout.
• If you can improve on this pattern, PLEASE let me
know!
58. GET IN TOUCH
# @morrissinger
$ linkedin.com/in/morrissinger
% morrissinger.com
& github.com/morrissinger