Successfully reported this slideshow.
Your SlideShare is downloading. ×

Unit Testing Express Middleware

Ad

express + mocha 
UNIT TESTING EXPRESS 
MIDDLEWARE 
By Morris Singer 
This work is licensed under a Creative Commons Attrib...

Ad

ABOUT ME 
• Senior Software Engineer 
Cengage Learning 
• Expertise: 
• Sencha Touch 
• Angular.js and Node.js 
• Cordova ...

Ad

AGENDA 
• Define Express Middleware and why it isn’t just 
a fancy term for controllers or endpoints. 
• Review behavior-d...

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Ad

Loading in …3
×

Check these out next

1 of 58 Ad
1 of 58 Ad

Unit Testing Express Middleware

Download to read offline

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.

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.

Advertisement
Advertisement

More Related Content

Advertisement

Unit Testing Express Middleware

  1. 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. 2. ABOUT ME • Senior Software Engineer Cengage Learning • Expertise: • Sencha Touch • Angular.js and Node.js • Cordova / PhoneGap • Ruby on Rails
  3. 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)
  4. 4. EXPRESS MIDDLEWARE Building Your Product, One Layer at a Time
  5. 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. 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. 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
  8. 8. TEST BEHAVIOR
  9. 9. MIDDLEWARE IS BEHAVIOR Middleware: • Define reusable components. • Create, modify, and store public variables. • Send responses to clients. • Comprise node packages.
  10. 10. COMMON CHALLENGES Or, Why Back End Node Developers Often Avoid TDD
  11. 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. 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. 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. 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. 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. 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. 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. 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?
  19. 19. PROMISES Links in a Chain of Async Operations
  20. 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. 21. PROMISES TO THE RESCUE queryDatabase() .then(makeRequestOfThirdPartyService) .then(updateDatabase) .then(writeFile) .then(sendFileOverHttp) .catch(function (err) { // Handle Errors }).done();
  22. 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. 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. 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. 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. 26. THE LIFE OF A PROMISE Pending Fulfilled then() finally() Rejected catch() finally()
  27. 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. 28. IN PRACTICE Promise B Promise D Promise F Q.promise() .then().then().then().catch().finally().done() Promise A Promise C Promise E
  29. 29. DO NOT BREAK YOUR CHAINS Otherwise, your user may be left hanging…
  30. 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.
  31. 31. Always return a promise or call done(). Period.
  32. 32. EXPRESS + Q The “Eureka” Moment
  33. 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. 34. PULL MIDDLEWARE INTO ENDPOINTS, TESTS Endpoint Middleware Middleware Test Endpoint Middleware Middleware Test Test ! ! Old Paradigm " ! New Paradigm
  35. 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. 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. 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. 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. 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. 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. 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. 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. 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'); ! });
  44. 44. THE NEW PARADIGM Looking at the Whole Picture
  45. 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. 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. 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. 48. TESTING WITH DATA Mocking Mongoose and Using Fixtures to Build a Robust and Effective Test Suite
  49. 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. 50. (MONGODB + MONGOOSE)* * Solutions are available for other setups. You can also roll your own, without too much heartache.
  51. 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. 52. MOCKING MONGOOSE var mongoose = require('mongoose'); var mockgoose = require('mockgoose'); mockgoose(mongoose);
  53. 53. CODING DATA IN FIXTURES module.exports.User = [ { name: 'Maeby' }, { name: 'George Michael' } ];
  54. 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. 55. TDD EXERCISES Use TDD in Pairs to Complete the Accompanying TDD / Express Exercises
  56. 56. QUESTIONS
  57. 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. 58. GET IN TOUCH # @morrissinger $ linkedin.com/in/morrissinger % morrissinger.com & github.com/morrissinger

×