When testing a simple express app / api I like to use supertest.
This can feel like integration testing, and to an extent it is.
But we can stub things out with sinon, and tidy up our tests.
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!
Leave a Reply