Unit Testing Express and Koa Middleware in ES2015

  • View
    1.617

  • Download
    1

  • Category

    Software

Preview:

Citation preview

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