When testing a simple express app / api I like to use supertest.
This can feel like integration testing, and to an extent it is.
Let’s look at an example.
Suppose we have a simple API with the following router:
var express = require('express'), app = express(); var router = express.Router(); app.use('/', require('./router')); module.exports = app;
var express = require('express'), router = express.Router(), authentication = require('./authentication'); router.all('*', authentication.ensureAuthenticated); router.get('/', function(req, res, next) { return res.send('Hello'); }); router.post('/', function(req, res, next) { return res.send('Posted'); }); module.exports = router;
To run the app, we have a separate server.js:
var app = require('./app'); var port = process.env.PORT || 8080; app.listen(port, function() { console.log('Running on port ' + port); });
This makes loading our app much easier in our supertest tests, as we’ll see later on.
Ok, nothing complex going on here – We can see we have some authentication middleware on all of our routes:
router.all('*', authentication.ensureAuthenticated);
This is some very basic authentication, for the purposes of our demo:
module.exports.ensureAuthenticated = function(req, res, next) { var authToken = req.get('x-auth-token'); if (!authToken) return res.sendStatus(401); next(); }
Ok, all is good, our app runs, and if we don’t supply an authentication token in the header, we’re returned a 401, otherwise, we’re presented with ‘Hello’
Our tests currently look like this:
(see the full test at this version here)
describe('GET /', function() { describe('with authentication header set', function() { it('returns hello', function(done) { agent .get('/') .set('X-Auth-Token', 'xyz123') .expect(200) .end(function(err, res) { if (err) return done(err); assert(res.text == 'Hello') done(); }); }); }); describe('without authentication header set', function() { it('returns a 401', function(done) { agent .get('/') .expect(401) .end(function(err, res) { if (err) return done(err); done(); }); }); }); });
These all pass
The testing for with / without authentication header set stuff bothers me;
For a start, it’s a cross cutting concern – it shouldn’t be tested like this.
Plus – we’re integration testing our authentication middleware.
What happens if/when we make this more complex?
Much better would be to stub this out.
So, by changing our test slightly as follows:
var ensureAuthenticatedSpy; before(function() { //important to stub before we load our app ensureAuthenticatedSpy = sinon.stub(authentication, 'ensureAuthenticated'); //this ensures we call our next() function on our middleware ensureAuthenticatedSpy.callsArg(2); agent = require('supertest') .agent(require('../app')); }); afterEach(function() { //assert that our middleware was called once for each test sinon.assert.calledOnce(ensureAuthenticatedSpy); ensureAuthenticatedSpy.reset(); }) describe('GET /', function() { it('returns hello', function(done) { agent .get('/') .set('X-Auth-Token', 'xyz123') .expect(200) .end(function(err, res) { if (err) return done(err); assert(res.text == 'Hello') done(); }); }); }); describe('POST /', function() { it('returns hello', function(done) { agent .post('/') .set('X-Auth-Token', 'xyz123') .expect(200) .end(function(err, res) { if (err) return done(err); assert(res.text == 'Posted') done(); }); }); });
We can create a stub of our ensureAuthenticated middleware.
Important here, is to note the removal of the “with authentication header set” tests… we don’t need them anymore! That’s taken care of by
afterEach(function() { //assert that our middleware was called once for each test sinon.assert.calledOnce(ensureAuthenticatedSpy);
Hope this helps!