Unit Testing Express Routes with Mongoose and Jasmine

I’ve been working for a while on a project called Repcoin. It’s a web application that uses Express, NodeJS, Mongoose, and MongoDB on the backend. As the Repcoin codebase has grown, we’ve become increasingly dependent on unit tests for sanity’s sake. One way to reassure that the code paths in our routes work is by unit testing with Jasmine.

Every request that hits our backend goes to the appropriate router. Consider a simplified version of the UserRouter, which handles queries related to the User Schema:

var UserHandler = require('../handlers/user.js');

module.exports = function(router) {
  router.get('/users/:user_id', UserHandler.users.userId.get);
};

From here, the request is sent to the UserHandler, which takes care of the real logic. This route might look like this:

var User = require('../models/User.js');

var UserHandler = {
  ........
    userId: {
      get: function(req, res) {
        User.findById(userId).exec().then(function(user) {
          return res.status(200).send(user);
        }, function(err) {
          return res.status(500).send(err);
        });
      },
    },
  ........
};

module.exports = UserHandler;

It would be nice if we could unit test the paths here and make sure that a response will certainly be returned. But, there’s some gritty code to mock up.

First is the Mongoose promise returned by findById(). The promise returned by calling exec() is executed by calling then(), which takes a callback for success and a callback for failure.

The other issue comes from the request and response objects. Express packs these variables with functionality, but we just need some simple mocks. Here’s how we can solve this:

Jasmine provides beforeEach() and afterEach(), which will run before and after a given test or set of tests. By declaring our req and res at the top of the file, we can mock the functionality we want like this:

var req, res;
beforeEach(function() {
  req = {
    query: {},
    params: {},
    body: {},
  };

  res = {
    status: jasmine.createSpy().andCallFake(function(msg) {
      return this;
    }),
    send: jasmine.createSpy().andCallFake(function(msg) {
      return this;
    })
  };
});

afterEach(function() {
  expect(res.status.callCount).toEqual(1);
  expect(res.send.callCount).toEqual(1);
});

Now, we have simple req and res objects that will perform the functionality we need. We can confirm that the calls to send() and status() happen with the proper arguments on a per-test basis, which I will show in a moment.

The other issue was the Mongoose promise. We can create a mock promise that looks like this:

var userPromise = {
  exec: function() {
    return {
      then: function(cbS, cbF) { return cbS({ username: 'Matt', _id: '123' }); };
    };
  },
};

This userPromise will execute successfully. If we wanted to have the promise fail, we could do that like this:

var userPromise = {
  exec: function() {
    return {
      then: function(cbS, cbF) { return cbF('Error!!'); };
    };
  },
};

Now we finally have our mocked up variables. We can include these in any of our tests to make sure errors are handled properly. A success test might look like so:

describe('get: ', function() {
  it('successfully gets the user', function() {
    spyOn(User, 'findById').andReturn(userPromise);
    req.params = { user_id: '123' };
    UserHandler.users.userId.get(req, res);
    expect(User.findByIdPublic.callCount).toEqual(1);
    expect(res.status).toHaveBeenCalledWith(200);
    expect(res.send).toHaveBeenCalledWith({ username: 'Matt', _id: '123' });
  });
});

And that’s all there is to it. A more full-stack test that takes advantage of the Express middleware could be done with a tool like supertest, but this is a good way to unit test a single function.

If you’re interested in digging deeper into the Repcoin code, check out the Github.