Search code examples
javascriptexpressecmascript-6sinonsinon-chai

How to properly test an Express controller method with Mocha, Chai, and Sinon


I'm pretty new to using Sinon. I have the following test I've written, and it fails because res.status always comes back as not called.

import chai from 'chai';
import 'chai/register-should';
import sinon from 'sinon';
import sinonChai from 'sinon-chai';
import { db } from '../../models';
import * as loginController from '../../controllers/login';

chai.use(sinonChai);

describe('Login controller', () => {

  describe('post function', () => {
    let findOne, req, status, send, res;

    beforeEach(() => {
      findOne = sinon.stub(db.User, 'findOne');
      findOne.resolves(null);
      req = { body: { email: 'test@test.com', password: 'testpassword' }};
      status = sinon.stub();
      send = sinon.spy();
      res = { send: send, status: status };
      status.returns(res);
      loginController.post(req, res);
    });
    afterEach(() => {
      findOne.restore();
    });
    it('should return a 401 status for an invalid email', (done) => {
      res.status.should.be.calledWith(401);
      findOne.restore();
      done();
    });

  });
});

The method in the controller right now is pretty simple. It uses a sequelize findOne method first. If it doesn't find a matching email it should throw a 401. Here's what that looks like:

export function post(req,res) {
  const email = req.body.email;
  const password = req.body.password;

  db.User.findOne({
    where: {email: email}
  }).then(user => {
    if (user) {
      // Other stuff happens here
    } else {
      res.status(401).send('That email address does not exist in our system.');
    }
  }).catch((error) => {
    res.status(500).send(error.message);
  });
}

When I run the test it does get to the else statement where it should be returning the status, but the test fails and when I check the log it says that the res.status wasn't ever called.


Solution

  • The problem here is that the spec is synchronous and doesn't take a promise into account.

    It makes sense to return a promise for testability reasons:

    export function post(req,res) {
      ...
      return db.User.findOne(...)
      ...
    }
    

    This can be naturally done if route handler is async function.

    Since Mocha supports promises, the specs can use async functions instead of done callback as well:

    it('should return a 401 status for an invalid email', async () => {
      const handlerResult = loginController.post(req, res);
      expect(handlerResult).to.be.a('promise');
    
      await handlerResult;
      res.status.should.be.calledWith(401);
    });