Search code examples
javascriptnode.jsunit-testingsinonsinon-chai

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


How can I test if a callback function from a event listener is called? For example, I have the following code where app.js initializes the application through the init.js controller.

The main.js file has a class which extends and Event Emitter, making the object an event emitter.

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

So, when I run the command node app.js, I see the following output

Start application HTTP INIT SEQUENCE HTTP SUCCESS SEQUENCE HTTP ERROR SEQUENCE

which means that the listeners are active and that the functions are called.

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

However, when I run the above test, even though I get the output, i.e. the functions were called, however, sinon assert always fails saying the below:

  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)

I do not know why the tests fail even though the function was called at least once, which is seen by the outputs HTTP INIT SEQUENCE, HTTP SUCCESS SEQUENCE and HTTP ERROR SEQUENCE when I run the tests.

I tried doing stub.should.have.been.called;. With this the tests pass, however, it's not really passing the tests as both stub.should.have.been.called; or stub.should.not.have.been.called; pass the test regardless, instead of the latter failing the test.

Anybody know the reason for this failing test? Thank you for any help.


Solution

  • You run const stub = sinon.stub(controller, value); to stub the values exported by your ui module. This does change the values exported by the module, but the problem is with this code inside your ui module:

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

    From the perspective of this code module.exports is mutated by your calls sinon.stub(controller, value) but this does not change the values of the symbols onHttpInit, onHttpSuccess, etc. symbols in the code above because these are symbols that are local to the scope of the ui module. You can mutate module.exports as much as you want: it still has no effect on the code above.

    You could change your code to this:

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

    You can use exports directly because you assign the same value to both module.exports and exports with module.exports = exports = ...

    This change should fix the immediate issue you ran into. However, I'd modify the testing approach here. Your tests are titled should register an eventlistener on '${key}' to ${value} but really what your are testing is not merely that an event listener has been registered but that event propagation works. In effect, you are testing the functionality of that EventEmitter is responsible for providing. I'd change the tests to stub the on method of your main object and verify that it has been called with the appropriate values. Then the test would test what it actually advertises.