Search code examples
javascriptnode.jsunit-testingsinonstub

Function stub with Sinon when modules are required inside functions


I am trying to stub functions to unit test, but I am not sure if this is possible or I should do changes before I am able to do it. I will try to explain the situation

file aController.js

...
module.exports = (sqlConnection) => {

...

return {
    ...
    aControllerFunction,
    ...
}

function aControllerFunction(req, res, next) {
  const aService = require('../services/aService')(sqlConnection, req.models)
  ...

  aService.aServiceFunction(req.a, req.b)
}
...
}
...

file aService.js

...
module.exports = (sqlConnection, models) => {

return {
    ...
    aServiceFunction,
    ...
}

...

function aServiceFunction(a, b) {

  
  ...

  models.aModel.update(a)
  sqlConnection.queryAsync(`UPDATE... ${a}`)
  ...
}
...
}
...

I want to unit test the functions aControllerFunction and aServiceFunction.

For aControllerFunction I should stub aService.aServiceFunction, and for aServiceFunction.aServiceFunction I should stub sqlConnection.queryAsync and models.aModel.update.

Is this possible with this structure, or should I change it before? I tried to do it, but I find hard to stub because the requires are inside the functions.


Solution

  • In order to testing aControllerFunction, we need additional package proxyquire. We can use this package to mock function(serviceFactory) exported from a module. This called link seams.

    E.g.

    controllers/aController.js:

    module.exports = (sqlConnection) => {
      return {
        aControllerFunction,
      };
    
      function aControllerFunction(req, res, next) {
        const aService = require('../services/aService')(sqlConnection, req.models);
        aService.aServiceFunction(req.a, req.b);
      }
    };
    

    controllers/aController.test.js:

    const proxyquire = require('proxyquire');
    const sinon = require('sinon');
    
    describe('aController', () => {
      it('should pass', () => {
        const sqlConnection = {};
        const req = { models: {}, a: 'a', b: 'b' };
        const aService = { aServiceFunction: sinon.stub() };
        const serviceFactory = sinon.stub().returns(aService);
        const ControllerFactory = proxyquire('./aController', {
          '../services/aService': serviceFactory,
        });
        const { aControllerFunction } = ControllerFactory(sqlConnection);
        aControllerFunction(req);
        sinon.assert.calledWithExactly(serviceFactory, {}, {});
        sinon.assert.calledWithExactly(aService.aServiceFunction, 'a', 'b');
      });
    });
    

    services/aService.js:

    module.exports = (sqlConnection, models) => {
      return {
        aServiceFunction,
      };
    
      function aServiceFunction(a, b) {
        models.aModel.update(a);
        sqlConnection.queryAsync(`UPDATE... ${a}`);
      }
    };
    

    services/aService.test.js:

    const serviceFactory = require('./aService');
    const sinon = require('sinon');
    
    describe('aService', () => {
      it('should pass', () => {
        const sqlConnection = {
          queryAsync: sinon.stub(),
        };
        const models = {
          aModel: {
            update: sinon.stub(),
          },
        };
        const { aServiceFunction } = serviceFactory(sqlConnection, models);
        aServiceFunction('a', 'b');
        sinon.assert.calledWithExactly(models.aModel.update, 'a');
        sinon.assert.calledWithExactly(sqlConnection.queryAsync, 'UPDATE... a');
      });
    });
    

    unit test result with coverage report:

      aController
        ✓ should pass
    
      aService
        ✓ should pass
    
    
      2 passing (35ms)
    
    -----------------|---------|----------|---------|---------|-------------------
    File             | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
    -----------------|---------|----------|---------|---------|-------------------
    All files        |     100 |      100 |     100 |     100 |                   
     controllers     |     100 |      100 |     100 |     100 |                   
      aController.js |     100 |      100 |     100 |     100 |                   
     services        |     100 |      100 |     100 |     100 |                   
      aService.js    |     100 |      100 |     100 |     100 |                   
    -----------------|---------|----------|---------|---------|-------------------