unit-testingmongoosechaisinonproxyquire

How to mock Mongo find using Sinon and Proxyquire


I have the below Middleware class which I want to unit test:

const jwt = require('jsonwebtoken');
const config = require('../config/auth.config.js');
const db = require('../models');
const User = db.user;
const Role = db.role;

isAdmin = (req, res, next) => {
  User.findById(req.userId).exec((err, user) => {
    if (err) {
      res.status(500).send({ message: err });
      return;
    }
    Role.find(
      {
        _id: { $in: user.roles }
      },
      (err, roles) => {
        console.log('Made it here Role');
        console.log(JSON.stringify(roles));
        console.log(roles);
        if (err) {
          console.log(err);
          res.status(500).send({ message: err });
          return;
        }

        for (let i = 0; i < roles.length; i++) {
          if (roles[i].name === 'admin') {
            next();
            return;
          }
        }

        res.status(403).send({ message: 'Require Admin Role!' });
        return;
      }
    );
  });
};

I'm able to mock the User.findById, the config the jsonwebtoken, but I'm not able to correctly stub the Role.find(... This is my current test spec using Mocha and Chai, Sinon and Proxyquire

const chai = require('chai');
const proxyquire = require('proxyquire');
const sinon = require('sinon');
const mongoose = require('mongoose');
chai.should();
var expect = chai.expect;
describe('Verify AuthJWT class', () => {

  let mockAuthJwt;
  let userStub;
  let roleStub;
  let configStub;
  let jwtStub;
  let json;
  let err;
  let json1;
  let err1;

  before(() => {
    userStub = {
      exec: function (callback) {
        callback(err, json);
      }
    };
    roleStub = {
      find: function (query, callback) {
        console.log('Heree');
        return callback(err1, json1);
      }
    };
    configStub = {
      secret: 'my-secret'
    };
    jwtStub = {
      verify: function (token,
        secretOrPublicKey, callback) {
        return callback(err, json);
      }
    };
    mockAuthJwt = proxyquire('../../../middlewares/authJwt.js',
      {
        'User': sinon.stub(mongoose.Model, 'findById').returns(userStub),
        'db.role': sinon.stub().returns(roleStub),
        '../config/auth.config.js': configStub,
        'jsonwebtoken': jwtStub
      }
    );
  });
describe('isAdmin function', () => {
    it('should Pass when user is Admin', (done) => {
      err = null;
      json = { roles: ['5ef3bd3f4144ae5898347e4e'] };
      err1 = {};
      json1 = [{ _id: '5ef3bd3f4144ae5898347e4e', name: 'admin', __v: 0 }];
      let fakeRes = {
        status: sinon.stub().returnsThis(),
        send: sinon.stub()
      };
      let fakeReq = {
        body: { userId: '123', email: 'test@test.com', roles: ['admin'] }
      };

      let fakeNext = sinon.spy();
      mockAuthJwt.isAdmin(fakeReq, fakeRes, fakeNext);
      expect(fakeNext.calledOnce).to.be.true;
      console.log('Status ' + fakeRes.status.firstCall.args[0]);
      done();
    });

Any inside on how to correctly use the proxyquire mock and stub the Role.find method so I can unit test the function correctly.


Solution

  • Based on your case (unit test), you do not need proxyquire at all. You just need chai and sinon.

    This is simplified example on how can be done.

    File middleware.js (just for example file name)

    // @file: middleware.js (This is line 1)
    const db = require('./models'); // Fake model.
    
    const isAdmin = (req, res, next) => {
      const User = db.user; // Define it inside.
      const Role = db.role; // Define it inside.
    
      User.findById(req.userId).exec((err1, user) => {
        if (err1) {
          res.status(500).send({ message: err1 });
          return;
        }
        Role.find({ _id: { $in: user.roles } }, (err2, roles) => {
          if (err2) {
            res.status(500).send({ message: err2 });
            return;
          }
    
          for (let i = 0; i < roles.length; i += 1) {
            if (roles[i].name === 'admin') {
              next();
              return;
            }
          }
    
          res.status(403).send({ message: 'Require Admin Role!' });
        });
      });
    };
    
    module.exports = { isAdmin };
    

    File test / spec: isAdmin.test.js

    const { expect } = require('chai');
    const sinon = require('sinon');
    
    // Load module under test.
    const middleware = require('./middleware');
    // Load module db to create stubs.
    const db = require('./models');
    
    describe('Verify AuthJWT class', function () {
      describe('isAdmin function', function () {
        it('should Pass when user is Admin', function (done) {
          // Fake result user findById.
          const fakeUser = { roles: ['5ef3bd3f4144ae5898347e4e'] };
          const fakeErrUser = null;
          // Create stub for User findById.
          const stubUserFindByID = sinon.stub(db.user, 'findById');
          stubUserFindByID.returns({
            exec: (arg1) => {
              // Inject fakeErrUser and fakeUser result here.
              arg1(fakeErrUser, fakeUser);
            },
          });
    
          // Fake result role find.
          const fakeRole = [{ _id: '5ef3bd3f4144ae5898347e4e', name: 'admin', __v: 0 }];
          const fakeErrRole = null;
    
          // Create stub for Role find.
          const stubRoleFind = sinon.stub(db.role, 'find');
          stubRoleFind.callsFake((arg1, arg2) => {
            // Inject fakeErrRole and fakeRole result here.
            arg2(fakeErrRole, fakeRole);
          });
    
          // Create fake response: empty object because no activity.
          const fakeRes = {};
          // Create fake request.
          // Note: I remove body property!
          const fakeReq = { userId: '123', email: 'test@test.com', roles: ['admin'] };
          // Create fake for next function (fake is sufficient).
          const fakeNext = sinon.fake();
    
          // Call function under test.
          middleware.isAdmin(fakeReq, fakeRes, fakeNext);
    
          // Verify stub user findById get called once.
          expect(stubUserFindByID.calledOnce).to.equal(true);
          // Make sure stub user findById called once with correct argument.
          expect(stubUserFindByID.calledOnceWith(fakeReq.userId)).to.equal(true);
    
          // Verify stub role find get called once.
          expect(stubRoleFind.calledOnce).to.equal(true);
          // Make sure stub role find called with correct argument.
          // Note: alternative style.
          expect(stubRoleFind.args[0][0]).to.deep.equal({
            // Query use fakeUser result.
            _id: { $in: fakeUser.roles },
          });
    
          // Finally for this case: make sure fakeNext get called.
          expect(fakeNext.calledOnce).to.equal(true);
    
          // Do not forget to restore the stubs.
          stubUserFindByID.restore();
          stubRoleFind.restore();
    
          done();
        });
      });
    });
    

    Run it using nyc (to check coverage) and mocha (test runner).

    $ npx nyc mocha isAdmin.test.js --exit
    
    
      Verify AuthJWT class
        isAdmin function
          ✓ should Pass when user is Admin
    
    
      1 passing (7ms)
    
    ---------------|----------|----------|----------|----------|-------------------|
    File           |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
    ---------------|----------|----------|----------|----------|-------------------|
    All files      |    77.27 |       50 |       60 |    76.19 |                   |
     middleware.js |    73.68 |       50 |      100 |    72.22 |    10,11,15,16,26 |
     models.js     |      100 |      100 |        0 |      100 |                   |
    ---------------|----------|----------|----------|----------|-------------------|
    $
    

    That test case only cover success condition (next function get called). I hope the example clear enough and you can continue to create test cases to fully cover function isAdmin based on my example above. Only 3 cases left. Good luck!