Reputation: 993
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.
Upvotes: 2
Views: 1630
Reputation: 993
After a week of questions and tests, I have found a solution. It was a bit of a combination of solutions from DDupont and Louis. The first change the following, in the ui.js file, add this.
to the bind
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));
};
And like DDupont said, in the unit test, move controller.init(main)
after the stub
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);
})
})
Thank you for all the help.
Upvotes: 0
Reputation: 151401
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.
Upvotes: 1
Reputation: 1938
Ok I do not about sinon, but the jest has same functionality called mock functions.
And jest has faced the same issue due to export https://medium.com/@DavideRama/mock-spy-exported-functions-within-a-single-module-in-jest-cdf2b61af642. Because of your export getMain
,init
and start
in main.js and using getMain
and start
inside init
.
Instead try to move getMain
and start
to separate module and export and test it. Let me know if issues still appears
Upvotes: 0
Reputation: 451
you are registering the main callbacks prior to stubbing, so the stubbed functions are not what is called, only the original functions. Try reversing the order in your it function:
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);
})
})
It also appears you are requiring main without using proxyquire. So then it would never be picking up the stub. A couple solutions: 1) rework main to take UI as a argument (i.e. dependency injection) in which case your tests could pass the stub to main; or 2) require main with proxyquire so you can force it to require the stubbed version. Let me know if you need more details.
Upvotes: 1