Even for JavaScript software developers well-versed in Agile practices, using test-driven development in the development of Node.js-based webservers can be challenging. In this presentation, I identify solutions to some of the most significant challenges to using TDD to build middleware stacks, with a focus on Express and Koa.
1. UNITTESTING NODE.JS
MIDDLEWARE
By Morris Singer
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
express and
ES15
Edition!
2. ABOUT ME
• Senior Software Engineer atVerilume
• I Like:
• Test-Driven Development
• Angular 1 and 2,Aurelia, Ionic, and
React.js, Node.js, and Cordova
3. AGENDA
• Define middleware and why it isn’t just
a fancy term for controllers or
endpoints.
• Review behavior-driven development
principles for unit testing.
• Argue why middleware are behavioral
units.
• Summarize common challenges testing
behavior in Express and Koa.
• Learn and implement a pattern for
Express and Koa Middleware.
• Answer questions. (10 minutes)
14. HTTP RESPONSETESTS
What happens when we add a middleware to the stack?
express
it('should return a 500 error', (done) => {
request({
method: 'POST',
url: 'http://localhost:3000/api/endpoint'
}, (error, response, body) => {
Assert.equal(response.statusCode, 500);
done();
});
});
15. TESTING MID-STACK
How do we pull out these anonymous functions?
express
const middleware = [
function (req, res, next) {
req.message = 'HELLO WORLD';
next();
},
function (req, res, next) {
res.send(req.message.toLowerCase());
}
];
middleware.forEach(app.use);
16. ILLUMINATINGTEST FAILURES
What happens if next() is not called?
express
import {httpMocks} from 'node-mocks-http';
it('should call next()', (done) => {
var req = httpMocks.createRequest(),
res = httpMocks.createResponse();
middleware(req, res, () => {
done();
});
});
17. KNOWING WHENTOTEST
When is the assertion run?
express
import {httpMocks} from 'node-mocks-http';
it('should call next()', () => {
var req = httpMocks.createRequest(),
res = httpMocks.createResponse();
middleware(req, res);
return Assert.equal(req.foo, 'bar');
});
18. TESTING WITH DATA
Where do data come from?
express
app.get('path/to/post', function (req, res, next) {
Post.findOne(params).exec(function (err, post) {
res.json(post);
});
});
19. DEALING WITH POLLUTION
How does one reset the data?
express
it('should update the first post', () => {
/* ... */
});
it('should get the first post', () => {
/* ... */
});
20. MOCKING DEPENDENCIES
How does one cut out the external data source?
express
app.get('endpoint', function (req, res, next) {
request({
method: 'GET',
url: 'http://example.com/api/call'
}, (error, response, body) => {
req.externalData = body;
next();
});
});
22. OVERVIEW
• Pull behavior into middleware and tests.
• Use promises or generators as flow control.
• Return client-server interaction to endpoint.
• Use promises or generators with Mocha.
26. USE PROMISES AS FLOW CONTROL
• Clean, standardized interface between asynchronous
behavior and endpoints.
• Both endpoints and tests can leverage the same mechanism
in the behavior for serializing logic.
express
27. USE PROMISES AS FLOW CONTROL
Old Paradigm
New Paradigm
express
export function middleware (req, res, next) {
/* Define behavior and call res.json(),
next(), etc. */
};
export function behavior () {
return new Promise((resolve, reject) => {
/* Define behavior and resolve or
reject promise. */
};
}
28. USE GENERATORS (WITH CO) AS FLOW CONTROL
• Same interface between asynchronous behavior and
middleware as already used between successive middleware.
• Both endpoints and tests can leverage the same mechanism
in the behavior for serializing logic.
29. CO
Generator based control flow goodness for nodejs and the browser,
using promises, letting you write non-blocking code in a nice-ish way.
https://www.npmjs.com/package/co
30. USE GENERATORS AS LINK BETWEEN
MIDDLEWARE AND ENDPOINTS
Old Paradigm
New Paradigm
export function* middleware (next) {
/* Call with assigned context and
leverage behavior on the Koa context,
yield next, etc.*/
};
export function* behavior () {
/* Define behavior and yield values. */
}
36. USING PROMISES WITH MOCHA
We need:
• A test framework syntax that facilitates easy async testing.
(Supported natively in Mocha since 1.18.0)
• An assertion syntax that we are familiar with. (Assert)
• A set of assertions that facilitate easily writing tests of
promises. (assertPromise)
express
37. USING PROMISES WITH
MOCHA (ASSERT_PROMISE)
Old Paradigm
New Paradigm
express
describe('behavior', () => {
it ('resolves under condition X with result Y', (done) => {
behavior().then(function (done) {
/* Assert here. */
}).finally(done);
});
});
import {assertPromise} from 'assert-promise';
describe('behavior', () => {
it ('resolves under condition X with result Y', () => {
return assertPromise.equal(behavior(), 'value');
});
});
38. USING GENERATORS WITH MOCHA
We need:
• Use the same async flow that Koa leverages (ES15 generators
and co)
• An assertion syntax that we are familiar with. (Assert)
• Mocha syntax that facilitates easily writing tests of generators
with co. (co-mocha)
40. USING PROMISES WITH
MOCHA (CO-MOCHA)
Old Paradigm
(No Co-Mocha)
New Paradigm
describe('behavior', () => {
it ('resolves under condition X with result Y', (done) => {
behavior().then(function () {
/* Assert here. */
}).finally(done);
});
});
describe('behavior', () => {
it ('resolves under condition X with result Y', function* () {
return Assert.equal(yield behavior(), 'value');
});
});
42. Return Client-Server
Interaction to Endpoints
ENDPOINTS
Pull Behavior
into Endpoint
import {behavior} from './behavior.js';
app.use(function (req, res, next) {
behavior()
.then(function () { next(); })
.catch(res.json)
});
express
43. Use Promise as
Flow Control
BEHAVIOR
export function behavior (req, res, next) {
return new Promise(function (resolve, reject) {
/* Define behavior and resolve or reject. */
}
};
express
44. Pull Behavior IntoTests
TEST
Use Promises with Mochaimport {assertPromise} from "assert-promise";
var behavior = require('./behavior.js');
describe('behavior', () => {
it ('resolves under condition X with result Y', () => {
return assertPromise.equal(behavior(), 'value');
});
});
express
45. Return Client-Server
Interaction to Endpoints
ENDPOINTS
Pull Behavior
into Endpoint
import {behavior} from './behavior.js';
app.use(function* (next) {
let message = yield behavior();
this.body = message;
});
46. Use Generators as
Flow Control
BEHAVIOR
export function* behavior (next) {
yield asyncRequest();
return yield next;
};
47. Pull Behavior IntoTests
TEST
var behavior = require('./behavior.js');
describe('behavior', () => {
it ('resolves under condition X with result Y', function* () {
return Assert.equal(yield behavior(), 'value');
});
});