UNIT TESTING NODE.JS MIDDLEWARE
By Morris Singer
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
express and
ES15 Editio
n!
ABOUT ME
• Senior Software Engineer at Verilume
• I Like:
• Test-Driven Development
• Angular 1 and 2, Aurelia, Ionic, and React.js, Node.js, and Cordova
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)
MIDDLEWAREBuilding Your Product, One Layer at a Time
A SIMPLE CASEOne Middleware Per Endpoint
express
app.use(function (req, res, next) { res.send('hello world'); });
A SIMPLE CASE
“Why is it called ‘Middleware’ anyway?”
app.use(function* (next) { this.body = 'hello world'; });
• One Middleware Per Endpoint
MORE COMPLEX CASESTwo Ways of Stacking Middleware
Variadic Iterative
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);app.use(...middleware);
MORE COMPLEX CASESTwo Ways of Stacking Middleware
Variadic Iterativeapp.use(...middleware);
const middleware = [ function* (next) => { this.message = 'HELLO WORLD'; return yield next; }, function* (next) => { this.body = this.message.toLowerCase(); } ];
middleware.forEach(app.use);
THE MIDDLEWARE STACK
GET
Done
Generate Message
Send Lowercase Message
express
app.use(
function (req, res, next) { req.message = 'HELLO WORLD'; next(); },
function (req, res, next) { res.send(req.message.toLowerCase()); }
);
THE MIDDLEWARE STACK
GET
Done
Generate Message
Assign Lowercase to Body
app.use(
function (next) { this.message = 'HELLO WORLD'; return yield next; },
function (next) { this.body = this.message.toLowerCase(); }
);
A B C
1
D E F
2
G H I
3
GET /
TEST BEHAVIOR
COMMON CHALLENGESOr, Why Node Developers Often Avoid TDD
HTTP RESPONSE TESTS
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(); }); });
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);
ILLUMINATING TEST 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(); }); });
KNOWING WHEN TO TEST
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'); });
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); }); });
DEALING WITH POLLUTION
How does one reset the data?express
it('should update the first post', () => { /* ... */ });
it('should get the first post', () => { /* ... */ });
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(); }); });
MIDDLEWARE + SEPARATION OF CONCERNS + FLOW CONTROLThe “Eureka” Moment
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.
PULL BEHAVIOR INTO MIDDLEWARE, TESTS
Endpoint
Test
BehaviorBehavior
BehaviorBehavior
Endpoint
TestTest
Old Paradigm
New Paradigm
PULL BEHAVIOR INTO ENDPOINTS
Old Paradigm New Paradigm
N.B.: This only looks like a lot more code…express
const middleware = [ function (req, res, next) { /* Behavior */ }, function (req, res, next) { /* Behavior */ } ];
app.use(...middleware);
const behavior = { first: function () {}, second: function () {} };
const middleware = [ function (req, res, next) { behavior.first(); next(); }, function (req, res, next) { behavior.second(); next(); } ];
app.use(...middleware);
PULL BEHAVIOR INTO ENDPOINTS
Old Paradigm New Paradigm
const middleware = [ function* (next) { /* Behavior */ }, function* (next) { /* Behavior */ } ];
app.use(...middleware);
const behavior = { first: function* () {}, second: function* () {} };
const middleware = [ function* (next) { yield behavior.first(); return yield next; }, function* (next) { yield behavior.second(); return next; } ];
app.use(...middleware);
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
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. */ }; }
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.
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
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. */ }
RETURN CLIENT-SERVER INTERACTION TO ENDPOINT
Endpoint
Res
Req
Behavior
Res
ReqClient
Endpoint
Value
Object
Behavior
Value
ObjectClient
Old Paradigm
New Paradigm
RETURN CLIENT-SERVER INTERACTION TO ENDPOINTOld Paradigm New Paradigm
express
const middleware = [ function (req, res, next) {}, function (req, res, next) {} ];
app.use(...middleware);
const behavior = [ function () {}, function () {} ];
const middleware = [ function (req, res, next) { behavior[0](/* Pass objects, values */) .then(function () { next(); }) .catch(res.json); }, function (req, res, next) { behavior[1](/* Pass objects, values */) .then(function () { next(); }) .catch(res.json); } ];
app.use(...middleware);
RETURN CLIENT-SERVER INTERACTION TO ENDPOINTOld Paradigm New Paradigm
express
const middleware = [ function (req, res, next) {}, function (req, res, next) {} ];
app.use(...middleware);
const behavior = [ function () {}, function () {} ];
const middleware = behavior.map((func) => { return function (req, res, next) { func() .then(function () { next(); }) .catch(res.json); } };
app.use(...middleware);
RETURN CLIENT-SERVER INTERACTION TO ENDPOINTOld Paradigm New Paradigm
express
const middleware = [ function (req, res, next) {}, function (req, res, next) {} ];
app.use(...middleware);
const behavior = [ function () {}, function () {} ];
const middleware = behavior.map((func) => { return function(args) { return function (req, res, next) { func() .then(function () { next(); }) .catch(res.json); } } };
app.use( middleware[0](/* Pass objects, values */), middleware[1](/* Pass objects, values */) );
RETURN CLIENT-SERVER INTERACTION TO ENDPOINTOld Paradigm New Paradigm
const middleware = [ function* (next) {}, function* (next) {} ];
app.use(...middleware);
const behavior = [ function* () {}, function* () {} ];
const middleware = [ function* (next) { yield behavior[0](/* Pass objects, values */); return yield next; }, function* (next) { yield behavior[1](/* Pass objects, values */); return yield next; } ];
app.use(...middleware);
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
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'); }); });
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)
CO-MOCHA
Enable support for generators in Mocha tests using co.
https://www.npmjs.com/package/co-mocha
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'); }); });
PUTTING IT ALL TOGETHER“Detroit Industry” by Diego Rivera
Return Client-ServerInteraction to Endpoints
ENDPOINTS
Pull Behaviorinto Endpoint
import {behavior} from './behavior.js'; app.use(function (req, res, next) { behavior() .then(function () { next(); }) .catch(res.json) });
express
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
Pull Behavior Into Tests
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
Return Client-ServerInteraction to Endpoints
ENDPOINTS
Pull Behaviorinto Endpoint
import {behavior} from './behavior.js'; app.use(function* (next) { let message = yield behavior(); this.body = message; });
Use Generators as Flow Control
BEHAVIOR
export function* behavior (next) {
yield asyncRequest(); return yield next;
};
Pull Behavior Into Tests
TEST
var behavior = require('./behavior.js');
describe('behavior', () => { it ('resolves under condition X with result Y', function* () { return Assert.equal(yield behavior(), 'value'); }); });
QUESTIONS
GET IN TOUCH
! @morrissinger
" linkedin.com/in/morrissinger
# morrissinger.com
$ github.com/morrissinger
Top Related