NodeJS:如何断言是否使用 sinon 调用了事件回调函数

NodeJS: How to assert if event callback function was called using sinon

如何测试事件侦听器的回调函数是否被调用?例如,我有以下代码,其中 app.js 通过 init.js 控制器初始化应用程序。

main.js 文件有一个 class 扩展和事件发射器,使对象成为事件发射器。

app.js

const initController = require('./init');
async function init() {
    initController.startMain();
}
init();

main.js

const events = require('events'),
ui = require('./ui');

module.exports.getMain = function () {
  class Main extends events.EventEmitter {
    constructor() {
      super();
      this.status = null;
    }
  }
  return new Main();
};

module.exports.init = () => {
  const main = this.getMain();
  ui.init(main);
  this.start(main);
}

module.exports.start = (main) => {
  ui.start(main);
  main.emit('http-init');
  main.emit('http-success');
  main.emit('http-error');
};

ui.js

function init(main) {
    main.on('http-init', onHttpInit.bind(this));
    main.on('http-success', onHttpSuccess.bind(this));
    main.on('http-error', onHttpError.bind(this));
    main.once('app-ready', onAppReady.bind(this));
  };

  function start (main) {};

  function onAppReady() {
    console.log('APP READY');
  };

  function onHttpInit() {
    console.log('HTTP INIT SEQUENCE');
  };

  function onHttpError(error) {
    console.log('HTTP ERROR SEQUENCE');
  };

  function onHttpSuccess() {
      console.log('HTTP SUCCESS SEQUENCE');
  };

  module.exports = exports = {
    init,
    start,
    onHttpInit,
    onHttpError,
    onHttpSuccess,
  };

init.js

exports.startMain = () => {

    console.log('Start application');

    // Load application modules
    const main = require('./main');

    // Start the application
    main.init();
 };

因此,当我 运行 命令 node app.js 时,我看到以下输出

开始申请 HTTP 初始化序列 HTTP 成功序列 HTTP 错误序列

这意味着侦听器处于活动状态并且调用了函数。

ui.tests.js

const sinon = require('sinon'),
    main = require('../main').getMain(),
    proxyquire = require('proxyquire').noPreserveCache().noCallThru();

describe('UI Tests', () => {
    const sandbox = sinon.createSandbox();
    let controller = null;
    before(() => {
        controller = proxyquire('../ui', {});
    })
    describe('Testing Eventlisteners', ()=> {
        afterEach(() => {
            main.removeAllListeners();
        });
        const eventMap = new Map([
        [ 'http-init', 'onHttpInit' ],
        [ 'http-success', 'onHttpSuccess' ],
        [ 'http-error', 'onHttpError']
        ]);
        eventMap.forEach((value, key) => {
            it(`should register an eventlistener on '${key}' to ${value}`, () => {
                const stub = sinon.stub(controller, value);
                controller.init(main);
                main.emit(key);
                sinon.assert.called(stub);
            })
        })
    })
})    

然而,当我 运行 上面的测试时,即使我得到了输出,即函数被调用,然而,sinon assert 总是失败,如下所示:

  UI Tests
    Testing Eventlisteners
HTTP INIT SEQUENCE
      1) should register an eventlistener on 'http-init' to onHttpInit
HTTP SUCCESS SEQUENCE
      2) should register an eventlistener on 'http-success' to onHttpSuccess
HTTP ERROR SEQUENCE
      3) should register an eventlistener on 'http-error' to onHttpError


  0 passing (16ms)
  3 failing

 1) UI Tests
       Testing Eventlisteners
         should register an eventlistener on 'http-init' to onHttpInit:
     AssertError: expected onHttpInit to have been called at least once but was never called
      at Object.fail (node_modules/sinon/lib/sinon/assert.js:106:21)
      at failAssertion (node_modules/sinon/lib/sinon/assert.js:65:16)
      at Object.assert.(anonymous function) [as called] (node_modules/sinon/lib/sinon/assert.js:91:13)
      at Context.it (test/ui.tests.js:25:30)
  2) UI Tests
       Testing Eventlisteners
         should register an eventlistener on 'http-success' to onHttpSuccess:
     AssertError: expected onHttpSuccess to have been called at least once but was never called
      at Object.fail (node_modules/sinon/lib/sinon/assert.js:106:21)
      at failAssertion (node_modules/sinon/lib/sinon/assert.js:65:16)
      at Object.assert.(anonymous function) [as called] (node_modules/sinon/lib/sinon/assert.js:91:13)
      at Context.it (test/ui.tests.js:25:30)

  3) UI Tests
       Testing Eventlisteners
         should register an eventlistener on 'http-error' to onHttpError:
     AssertError: expected onHttpError to have been called at least once but was never called
      at Object.fail (node_modules/sinon/lib/sinon/assert.js:106:21)
      at failAssertion (node_modules/sinon/lib/sinon/assert.js:65:16)
      at Object.assert.(anonymous function) [as called] (node_modules/sinon/lib/sinon/assert.js:91:13)
      at Context.it (test/ui.tests.js:25:30)

我不知道为什么即使函数至少被调用一次,测试也会失败,当我 运行 测试。

我试过 stub.should.have.been.called;。然而,通过这个测试,它并没有真正通过测试,因为 stub.should.have.been.called;stub.should.not.have.been.called; 都通过了测试,而不是后者未通过测试。

有人知道这个测试失败的原因吗?感谢您的帮助。

您是在存根之前注册主要回调,所以存根函数不是调用的函数,只是原始函数。尝试在您的 it 函数中颠倒顺序:

        eventMap.forEach((value, key) => {
            it(`should register an eventlistener on '${key}' to ${value}`, () => {
                const stub = sinon.stub(controller, value);
                controller.init(main);
                main.emit(key);
                sinon.assert.called(stub);
            })
        })

您似乎还需要 main 而没有使用 proxyquire。那么它永远不会拿起存根。几个解决方案:1)返工 main 以将 UI 作为参数(即依赖注入),在这种情况下,您的测试可以将存根传递给 main;或 2) require main with proxyquire 这样你就可以强制它要求存根版本。如果您需要更多详细信息,请告诉我。

好的,我不关心 sinon,但这个笑话具有相同的功能,称为 mock functions

jest 由于导出 https://medium.com/@DavideRama/mock-spy-exported-functions-within-a-single-module-in-jest-cdf2b61af642 也面临同样的问题。由于您在 main.js 中导出 getMaininitstart 并使用 getMainstart init.

里面

改为尝试移动 getMainstart 以分离模块并导出和测试它。如果问题仍然出现请告诉我

您 运行 const stub = sinon.stub(controller, value); 存根 ui 模块导出的值。这确实改变了模块导出的值,但问题出在 ui 模块中的这段代码:

function init(main) {
    main.on('http-init', onHttpInit.bind(this));
    main.on('http-success', onHttpSuccess.bind(this));
    main.on('http-error', onHttpError.bind(this));
    main.once('app-ready', onAppReady.bind(this));
}

从这段代码的角度来看 module.exports 被你的调用改变了 sinon.stub(controller, value) 这不会改变上面代码中的符号 onHttpInitonHttpSuccess 等符号,因为这些符号是 ui 模块 范围内的局部符号。您可以随意改变 module.exports:它仍然对上面的代码没有影响。

您可以将代码更改为:

function init(main) {
    main.on('http-init', exports.onHttpInit.bind(this));
    main.on('http-success', exports.onHttpSuccess.bind(this));
    main.on('http-error', exports.onHttpError.bind(this));
    main.once('app-ready', exports.onAppReady.bind(this));
}

你可以直接使用exports,因为你用module.exports = exports = ...

module.exportsexports赋了相同的值

此更改应该可以解决您 运行 遇到的直接问题。但是,我会在这里修改测试方法。您的测试标题为 should register an eventlistener on '${key}' to ${value} 但实际上您正在测试的不仅仅是事件侦听器已注册,而且事件传播有效。实际上,您正在测试 EventEmitter 负责提供的功能。我会更改测试以存根 main object 的 on 方法,并验证是否已使用适当的值调用它。然后测试将测试它实际宣传的内容。

经过一周的提问和测试,我找到了解决办法。这是 DDupont 和 Louis 解决方案的组合。首先更改如下,在ui.js文件中,在bind

中添加this.
function init(main) {
    main.on('http-init', this.onHttpInit.bind(this));
    main.on('http-success', this.onHttpSuccess.bind(this));
    main.on('http-error', this.onHttpError.bind(this));
    main.once('app-ready', this.onAppReady.bind(this));
  };

正如 DDupont 所说,在单元测试中,将 controller.init(main) 移到存根

之后
eventMap.forEach((value, key) => {
            it(`should register an eventlistener on '${key}' to ${value}`, () => {
                const stub = sinon.stub(controller, value);
                controller.init(main);
                main.emit(key);
                sinon.assert.called(stub);
            })
        })

感谢大家的帮助。