January 17, 2014

Testing Connect middleware

Background

Connect and its bigger brother Express are extremely popular general servers for Node. Connect takes an onion approach: it is all about layers. Each layer can answer a request, modify it or pass it to the next layer to be answered. This architecture makes it very intuitive and powerful. Each layer is called middleware and generally all one needs is to write a JavaScript function with this signature to create custom middleware:

function logUrl(req, res, next) {
    console.log('request url', req.url);
    // pass everything unchanged to next middleware
    next();
}

To allow configuring the middleware, it is a convention to return the actual worker function from a function that accepts configuration options:

function logMatchingUrls(pattern) {
    return function (req, res, next) {
        if (pattern.test(req.url)) {
            console.log('request url', req.url);
        }
        next();
    };
}

We can use same middleware multiple times, for example lets log all JavaScript and CSS requests while serving all files from folder public

var app = connect()
    .use(logMatchingUrls(/\.js$/i))
    .use(logMatchingUrls(/\.css$/i))
    .use(connect.static('public'))
    .use(function(req, res){
        res.end('hello world\n');
    })
http.createServer(app).listen(3000);

Testing middleware

Creating custom middleware is so easy, that the Connect community created more than 60 layers in addition to the 18 that are included with Connect itself. If you are thinking about creating your own middleware, here is how to make testing it as simple as writing it. This example is based on the connect-slow middleware I wrote to slow down serving some requests to ease debugging website loading and performance problems.

Here is a typical example for connect-slow - delay serving jquery.js by 5 seconds to look at how the website behaves during these 5 seconds.

var slow = require('connect-slow');
var app = connect()
    .use(slow({
        url: /\.jquery\.js$/i,
        delay: 5000
    }))
    .use(connect.static('public'));
http.createServer(app).listen(3000);

There are 3 levels of testing one could do for this middleware:

  1. Small unit testing to make sure invalid arguments are handled properly.
  2. Medium sized testing with mock request, response objects and next function.
  3. End to end testing with actual connect stack running and separate live requests.

I decided to only create tests to check invalid inputs (1) and perform end to end testing (3). I skipped creating mock objects, because they introduce more complexity and would essentially look like end to end tests.

Small unit testing

I used my own testing runner gt that is pretty much compatible with QUnit syntax, runs natively on Node, and provides code coverage via istanbul integration.

Here are the small tests that make sure an error is thrown if url is not a RegExp, or delay is negative number. You can see the entire test file

gt.test('url should be a regexp', function () {
  gt.throws(function () {
    slow({
      url: '.html'
    });
  }, 'AssertionError');
});

gt.test('delay should be positive', function () {
  gt.throws(function () {
    slow({
      delay: -100
    });
  }, 'AssertionError');
});

I did not want to create the medium mock tests, but I still wanted to make sure the function returned by slow() is a valid middleware function that expects 3 arguments. So I added a unit test to check if the returned value is a function with arity 3

gt.test('valid parameters', function () {
  var fn = slow({
    url: /\.jpg$/i,
    delay: 2000
  });
  gt.arity(fn, 3, 'middleware expects 3 arguments');
});

End to end testing

Lets validate that the middleware actually delays answering some requests but not the others. This is an example of asynchronous testing and some testing frameworks are better at this than others. gt provides lots of support for async testing, most relevant for this problem that it provides setupOnce and teardownOnce methods that can create and tear down a Connect stack before running multiple unit tests

var request = q.denodeify(require('request'));
var port = 3440;
var msg = 'hello world';
var url = 'http://localhost:' + port + '/something';
gt.module('connect-slow some resources', {
  setupOnce: function () {
    var app = connect()
    .use(connect.logger('dev'))
    .use(slow({
      url: /\.slow$/i,
      delay: 500
    }))
    .use(sendMessage);
    this.server = http.createServer(app).listen(port);
  },
  teardownOnce: function () {
    this.server.close();
    delete this.server;
  }
});

gt.async('.slow requests are slow', function () {
  var start = new Date();
  request(url + '/foo.slow')
  .then(function (data) {
    gt.equal(data[0].statusCode, 200, 'code 200');
    var end = new Date();
    var ms = end - start;
    gt.ok(ms >= 500, 'server responded in', ms, 'not in 500ms');
  })
  .fail(function (err) {
    gt.ok(false, err);
  })
  .finally(function () {
    gt.start();
  });
});

gt.async('other requests are still fast', function () {
  var start = new Date();
  request(url + '/foo.html')
  .then(function (data) {
    gt.equal(data[0].statusCode, 200, 'code 200');
    var end = new Date();
    var ms = end - start;
    gt.ok(ms >= 0 && ms < 150, 'server responded in', ms);
  })
  .fail(function (err) {
    gt.ok(false, err);
  })
  .finally(function () {
    gt.start();
  });
});

Result

You can try running the tests yourself:

npm install connect-slow
cd node_modules/connect-slow
# install devDependencies needed for testing
npm install
npm test

The tests should pass, the code coverage is printed after the tests

connect-slow tests and coverage

You can open cover/lcov-report/index.html and see code coverage line by line

connect-slow code coverage

Bonus - pre-git

Remember, achieving passing tests is important initially, but is extremely important in the future whenever any changes to the code are made. I use my own pre-git project that installs Node hooks to run before git commit and git push commands. For example, to make sure tests pass before each commit, I add the following to package.json

"pre-commit": "npm test"

To make sure the code pushed to master does not depend on unlisted dependencies, the pre-push hook is more stringent

"pre-push": [
    "rm -rf node_modules",
    "npm install",
    "npm test"
]

Using these safety mechanisms, you will keep your middleware working happily in the future.

author

Follow Gleb Bahmutov @twitter, see his projects at glebbahmutov.com