当函数内部需要模块时使用 Sinon 的函数存根

Function stub with Sinon when modules are required inside functions

我正在尝试将函数存根以进行单元测试,但我不确定这是否可行,或者我是否应该在能够执行此操作之前进行更改。我会尽力解释情况

文件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)
}
...
}
...

文件aService.js

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

return {
    ...
    aServiceFunction,
    ...
}

...

function aServiceFunction(a, b) {

  
  ...

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

我想对函数 aControllerFunctionaServiceFunction 进行单元测试。

对于 aControllerFunction 我应该存根 aService.aServiceFunction,对于 aServiceFunction.aServiceFunction 我应该存根 sqlConnection.queryAsyncmodels.aModel.update.

这种结构是否可行,还是我应该先更改它?我尝试这样做,但我发现很难存根,因为要求在函数内部。

为了测试 aControllerFunction,我们需要额外的包 proxyquire. We can use this package to mock function(serviceFactory) exported from a module. This called link seams

例如

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');
  });
});

带有覆盖率报告的单元测试结果:

  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 |                   
-----------------|---------|----------|---------|---------|-------------------