cusejuice
cusejuice

Reputation: 10681

Unit test node controller/promises using express-validator

I'm using the "express-validator" middleware package to validate some parameters for this exampleController endpoint. What would be the best way to stub out this controller for unit tests? I keep getting errors like:

TypeError: errors.isEmpty is not a function

router

var controller = require('./controllers/exampleController.js');
var express = require('express');
var router = express.Router();

router.get('/example', controller.exampleController);

exampleController.js

exports.doSomething = function(req, res, next) {
  var schema = {
    'email': {
      in: 'query',
      isEmail: {
        errorMessage: 'Invalid Email'
      }
    },
    'authorization': {
      in: 'headers',
      // custom test
      isValidAuthToken: {
        errorMessage: 'Missing or malformed Bearer token'
      }
    }
  };

  // Validate headers/query params
  req.check(schema);

  // Handle response
  req.getValidationResult()
    .then(function(errors) {
      if (!errors.isEmpty()) {
        return res.status(400).json({ error: 'Bad Request' });
      } else {

        var context = {
          email: req.query.email,
        };

        return res.render('index', context);
      }
    })
};

test

var chai = require('chai');
var sinonChai = require('sinon-chai');

chai.Should();
chai.use(sinonChai);
global.sinon = require('sinon');

var sinonStubPromise = require('sinon-stub-promise');
sinonStubPromise(sinon);

var rewire = require('rewire');
var exampleController = rewire('../controllers/exampleController.js');

var errorsResponse = [{ 
  param: 'email',
  msg: 'Invalid Email',
  value: undefined
}];

describe('exampleController', function() {
    var req;
    var res;

    beforeEach(function() {
      req = {
        headers: {},
        query: {},
        check: sinon.spy(),
        getValidationResult: sinon.stub().returnsPromise()
      };
      res = {
        status: sinon.stub().returns({
          json: json
        }),
        render: sinon.spy()
      };
    });

    afterEach(function() {
      req.query = {};
    });

    context('when missing email query param', function() {
      beforeEach(function() {
        req.getValidationResult.resolves(errorsResponse);
        exampleController.doSomething(req, res);
      });

      it('should call status on the response object with a 400 status code', function() {
        res.status.should.be.calledWith(400);
      });

      it('should call json on the status object with the error', function() {
        json.should.be.calledWith({ error: 'Bad Request' });
      });
    });
  });
});

Upvotes: 1

Views: 1572

Answers (1)

chrysanthos
chrysanthos

Reputation: 1418

The way you have structured the unit test for validating a controller is not really consistent. I will try to present you the issues and workarounds in detail, but before we move on have a look at this great article on unit testing Express controllers.

Ok, so regarding the initial error you presented TypeError: errors.isEmpty is not a function that was due to a malformed response object you had setup for stubbing the getValidationResult() method.

After printing out a sample response object from this method you will notice that the correct structure is this:

{ isEmpty: [Function: isEmpty],
  array: [Function: allErrors],
  mapped: [Function: mappedErrors],
  useFirstErrorOnly: [Function: useFirstErrorOnly],
  throw: [Function: throwError] }

instead of your version of the response:

var errorsResponse = [{ 
  param: 'email',
  msg: 'Invalid Email',
  value: undefined
}];

isEmpty() is a top-level function and you should have used an array attribute for storing the errors list.

I'm attaching a revamped version of your controller and test scenario so that you can correlate it with the best practices presented in the aforementioned article.

controller.js

var express = require('express');
var router = express.Router();

router.get('/example', function(req, res) {
  var schema = {
    'email': {in: 'query',
      isEmail: {
        errorMessage: 'Invalid Email'
      }
    }
  };

  // Validate headers/query params
  req.check(schema);

  // Handle response
  req.getValidationResult()
    .then(function(errors) {

      if (!errors.isEmpty()) {

        return res.status(400).json({
          error: 'Bad Request'
        });
      } else {

        var context = {
          email: req.query.email,
        };

        return res.render('index', context);
      }
    });
});


module.exports = router;

test.js

'use strict';

const chai = require('chai');
const sinon = require('sinon');
const SinonChai = require('sinon-chai');

var sinonStubPromise = require('sinon-stub-promise');
sinonStubPromise(sinon);

chai.use(SinonChai);
chai.should();

var mockHttp = require('node-mocks-http');

var controller = require('./controller.js');

describe.only('exampleController', function() {

  context('when missing email query param', function() {

    var req;
    var res;

    beforeEach(function() {

      // mock the response object
      // and attach an event emitter
      // in order to be able to
      // handle events
      res = mockHttp.createResponse({
        eventEmitter: require('events').EventEmitter
      });

    });

    it('should call status on the response object with a 400 status code',
      (done) => {

        // Mocking req and res with node-mocks-http
        req = mockHttp.createRequest({
          method: 'GET',
          url: '/example'
        });

        req.check = sinon.spy();

        var errorsResponse = {
          isEmpty: function() {
            return false;
          },
          array: [{
            param: 'email',
            msg: 'Invalid Email',
            value: undefined
          }]
        };

        // stub the getValidationResult()
        // method provided by the 'express-validator'
        // module
        req.getValidationResult = sinon.stub().resolves(errorsResponse);

        // spy on the response status
        sinon.spy(res, 'status');
        sinon.spy(res, 'json');

        // called when response
        // has been completed
        res.on('end', function() {
          try {
            // assert status and JSON args
            res.status.should.have.been.calledWith(400);
            res.json.should.have.been.calledWith({error: 'Bad Request'});
            done();
          } catch (e) {
            done(e);
          }
        });

        // Call the handler.
        controller.handle(req, res);
      });

  });

});

A few points to notice in the updated version of the test.

  • Instead of manually constructing request / response objects, you should better use a library that's already there for this job. In my version I'm using 'node-mocks-http' which is pretty much a standard when it comes to Express.
  • When testing controllers, instead of manually calling the service method it's better to use the natural routing mechanism through the mocked HTTP request object. This way you can cover both happy & sad routing paths
  • Using a common HTTP req / res mocking library, means less work for you - all you need to do is extend the factory objects with non-standard functions (e.g. getValidationResult() from express-validator) and add your spies / stubs seamlessly
  • Finally, the library supports attaching event listeners on response events that otherwise you could not simulate manually. In this example, we're listening for the end event from the response object that is called after the return res.status(400).json({error: 'Bad Request'}); method has been called in your controller.

Hope I've cleared things up a bit :)

Upvotes: 4

Related Questions