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

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.

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.

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.

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

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.

Related

Change process.env variable for single test using JEST [duplicate]

This question already has answers here:
how to reset module imported between tests
(2 answers)
Closed last year.
i have the following code in a file that i'm trying to test:
foo.js
let localEnv = (process.env.LOCAL_ENVIRONMENT).toLowerCase() === 'prod' ? 'prod' : 'stage';
currently, i set this value using setupFiles which points to env.js and it contains:
process.env.LOCAL_ENVIRONMENT = 'prod';
my question is, how do i change process.env.LOCAL_ENVIRONMENT to test (or anything else) in foo.test.js? Just need to do this in a single test, so this line will be covered in my test coverage.
I tried doing something like this but it didn't work...
foo.test.js
test('Nonprod', async () => {
process.env.LOCAL_ENVIRONMENT = 'test';
...
});
You can alter the value as you tried. However you have to take into account that in your original file you only access the variable when you first load this script. In order to make this work you have to do sth like this:
// in your test file foo.test.js
const prev = process.env.LOCAL_ENVIRONMENT
process.env.LOCAL_ENVIRONMENT = 'test'; // from now on the env var is test
const myModule = require('./foo.js'); // foo.js is executed and the var is read as test
process.env.LOCAL_ENVIRONMENT = prev; // change value back
This has some caveheats as you cant test multiple scenarios with this (as the module is only loaded in once).
If you want to test more scenarios you have multiple options:
One would be to split the logic and process.env.LOCAL_ENVIRONMENT apart, for example
function getLocalEnv(env = process.env.LOCAL_ENVIRONMENT) {
return env.toLowerCase() === 'prod' ? 'prod' : 'stage';
}
This function is now very easy to test and doesn't depend on env vars for that anymore
I found the answer on how to reset and re-require the tested module here:
how to reset module imported between tests
The most easier way to test it for a specific test case is to set the test into a describe scope and to apply / remove the env value in beforeAll / afterAll or beforeEach / afterEach hooks depending on your needs.
describe('Test example', () => {
describe('prod (default)', () => {
test('do the thing', () => {
doTheThing(); // process.env.LOCAL_ENVIRONMENT is by default 'prod' because of your setupFiles
});
});
describe('test env', () => {
const oldEnv = process.env.LOCAL_ENVIRONMENT; // or { ...process.env } to copy the whole env
beforeAll(() => {
process.env.LOCAL_ENVIRONMENT = 'test';
});
afterAll(() => {
process.env.LOCAL_ENVIRONMENT = oldEnv; // do not forget to do this
});
test('do the thing', () => {
doTheThing(); // process.env.LOCAL_ENVIRONMENT is 'test' here
});
});
});

Jest onSpy - expected mock function to have been called

I'm struggling with using spyOn as part of testing my utils.js module. I've tried various methods and approaches but all seem to yield the "expected mock function to have been called". For the record, other unit tests work OK, so there shouldn't be any issue with my actual test setup.
Below is a simplified test case with two functions and one test, and I can't even get these to work. Did I misunderstand the spyOn altogether?
// utils.js
function capitalHelper(string){
return string.toUpperCase();
}
function getCapitalName(inputString){
return capitalHelper(inputString.charAt(0)) + inputString.slice(1);
}
exports.capitalHelper = capitalHelper
exports.getCapitalName = getCapitalName
// utils.test.js
const Utils = require('./utils');
test('helper function was called', () => {
const capitalHelperSpy = jest.spyOn(Utils, 'capitalHelper');
const newString = Utils.getCapitalName('john');
expect(Utils.capitalHelper).toHaveBeenCalled();
})
I do ont use spyOn(), but jest.fn() instead for all mock scenario
In your case I would do the following
test('helper function was called', () => {
Utils.capitalHelper = jest.fn((s) => Utils.capitalHelper(s))
const newString = Utils.getCapitalName('john')
expect(Utils.capitalHelper.mock.calls.length).toBe(1)
})
First line could have simply be :
Utils.capitalHelper = jest.fn()
since you don't seem to be testing the returned value in your test :)
You can find more details on jest.fn() on the jest official documentation : https://facebook.github.io/jest/docs/en/mock-functions.html
----------------------- EDIT
I got it : the problem occurs because within your utils.js file, getCapitalName uses the defined function, not the one pointed by the export.
To be able to mock the function in use you could change your utils.js file to
// utils.js
const Utils = {
capitalHelper: string => string.toUpperCase(),
getCapitalName: inputString => Utils.capitalHelper(inputString.charAt(0)) + inputString.slice(1)
}
export default Utils
then the tests I gave before will work

Testing JQuery's "on" method fires callback function

I am trying to test following code with Jasmine:
import $ from 'jquery';
export function createCheckIcon (handleClick) {
return $('<span>').attr('data-element', 'check').addClass('icon--check').on('click', handleClick);
}
export function createCrossIcon (handleClick) {
return $('<span>').attr('data-element', 'cross').addClass('icon--cross').on('click', handleClick);
}
My test looks like this:
import $ from 'jquery';
import { createCrossIcon, createCheckIcon } from './input-icons';
describe('input icons', () => {
let handleMock = { handleClick: () => true };
it('can create a cross icon', () => {
let $crossIcon = createCrossIcon();
expect($crossIcon).toHaveAttr('data-element', 'cross');
expect($crossIcon).toHaveClass('icon--cross');
});
it('cross icon handles click event', () => {
let $crossIcon = createCrossIcon(handleMock.handleClick);
spyOn(handleMock, 'handleClick');
$crossIcon.trigger('click');
expect(handleMock.handleClick).toHaveBeenCalled();
});
it('can create a check icon', () => {
let $checkIcon = createCheckIcon();
expect($checkIcon).toHaveAttr('data-element', 'check');
expect($checkIcon).toHaveClass('icon--check');
});
});
Unfortunately my test will fail with:
Expected spy handleClick to have been called.
Maybe I still have a basic misunderstanding how to test if the click handle was fired. Does anyone see where my problem is? I also tried jasmine-jquery to get this working but still no luck.
The problem
You have a misconception of how variables work in JavaScript.
You see, when you pass handleClick in createCrossIcon(handleMock.handleClick)
you are actually passing a link to the function, not the function itself. And then you pass this link to the .on method.
Then you set a Jasmine spy - spyOn(handleMock, 'handleClick') and what happens is Jasmine replaces the handleClick property of handleMock object with a utility function that remembers all the times it was called.
But jQuery will never know about your intentions, because it has already decided to call the original function on click.
The solution
What you need to do is simply change the order of the lines so that spyOn will come before creating an icon:
spyOn(handleMock, 'handleClick');
let $crossIcon = createCrossIcon(handleMock.handleClick);
This way you will pass a spy as a callback, not the actual callback function.

Custom browser actions in Protractor

The problem:
In one of our tests we have a "long click"/"click and hold" functionality that we solve by using:
browser.actions().mouseDown(element).perform();
browser.sleep(5000);
browser.actions().mouseUp(element).perform();
Which we would like to ideally solve in one line by having sleep() a part of the action chain:
browser.actions().mouseDown(element).sleep(5000).mouseUp(element).perform();
Clearly, this would not work since there is no "sleep" action.
Another practical example could be the "human-like typing". For instance:
browser.actions().mouseMove(element).click()
.sendKeys("t").sleep(50) // we should randomize the delays, strictly speaking
.sendKeys("e").sleep(10)
.sendKeys("s").sleep(20)
.sendKeys("t")
.perform();
Note that these are just examples, the question is meant to be generic.
The Question:
Is it possible to extend browser.actions() action sequences and introduce custom actions?
Yes, you can extend the actions framework. But, strictly speaking, getting something like:
browser.actions().mouseDown(element).sleep(5000).mouseUp(element).perform();
means messing with Selenium's guts. So, YMMV.
Note that the Protractor documentation refers to webdriver.WebDriver.prototype.actions when explaining actions, which I take to mean that it does not modify or add to what Selenium provides.
The class of object returned by webdriver.WebDriver.prototype.actions is webdriver.ActionSequence. The method that actually causes the sequence to do anything is webdriver.ActionSequence.prototype.perform. In the default implementation, this function takes the commands that were recorded when you called .sendKeys() or .mouseDown() and has the driver to which the ActionSequence is associated schedule them in order. So adding a .sleep method CANNOT be done this way:
webdriver.ActionSequence.prototype.sleep = function (delay) {
var driver = this.driver_;
driver.sleep(delay);
return this;
};
Otherwise, the sleep would happen out of order. What you have to do is record the effect you want so that it is executed later.
Now, the other thing to consider is that the default .perform() only expects to execute webdriver.Command, which are commands to be sent to the browser. Sleeping is not one such command. So .perform() has to be modified to handle what we are going to record with .sleep(). In the code below I've opted to have .sleep() record a function and modified .perform() to handle functions in addition to webdriver.Command.
Here is what the whole thing looks like, once put together. I've first given an example using stock Selenium and then added the patches and an example using the modified code.
var webdriver = require('selenium-webdriver');
var By = webdriver.By;
var until = webdriver.until;
var chrome = require('selenium-webdriver/chrome');
// Do it using what Selenium inherently provides.
var browser = new chrome.Driver();
browser.get("http://www.google.com");
browser.findElement(By.name("q")).click();
browser.actions().sendKeys("foo").perform();
browser.sleep(2000);
browser.actions().sendKeys("bar").perform();
browser.sleep(2000);
// Do it with an extended ActionSequence.
webdriver.ActionSequence.prototype.sleep = function (delay) {
var driver = this.driver_;
// This just records the action in an array. this.schedule_ is part of
// the "stock" code.
this.schedule_("sleep", function () { driver.sleep(delay); });
return this;
};
webdriver.ActionSequence.prototype.perform = function () {
var actions = this.actions_.slice();
var driver = this.driver_;
return driver.controlFlow().execute(function() {
actions.forEach(function(action) {
var command = action.command;
// This is a new test to distinguish functions, which
// require handling one way and the usual commands which
// require a different handling.
if (typeof command === "function")
// This puts the command in its proper place within
// the control flow that was created above
// (driver.controlFlow()).
driver.flow_.execute(command);
else
driver.schedule(command, action.description);
});
}, 'ActionSequence.perform');
};
browser.get("http://www.google.com");
browser.findElement(By.name("q")).click();
browser.actions().sendKeys("foo")
.sleep(2000)
.sendKeys("bar")
.sleep(2000)
.perform();
browser.quit();
In my implementation of .perform() I've replaced the goog... functions that Selenium's code uses with stock JavaScript.
Here is what I did (based on the perfect #Louis's answer).
Put the following into onPrepare() in the protractor config:
// extending action sequences
protractor.ActionSequence.prototype.sleep = function (delay) {
var driver = this.driver_;
this.schedule_("sleep", function () { driver.sleep(delay); });
return this;
};
protractor.ActionSequence.prototype.perform = function () {
var actions = this.actions_.slice();
var driver = this.driver_;
return driver.controlFlow().execute(function() {
actions.forEach(function(action) {
var command = action.command;
if (typeof command === "function")
driver.flow_.execute(command);
else
driver.schedule(command, action.description);
});
}, 'ActionSequence.perform');
};
protractor.ActionSequence.prototype.clickAndHold = function (elm) {
return this.mouseDown(elm).sleep(3000).mouseUp(elm);
};
Now you'll have sleep() and clickAndHold() browser actions available. Example usage:
browser.actions().clickAndHold(element).perform();
I think it is possible to extend the browser.actions() function but that is currently above my skill level so I'll lay out the route that I would take to solve this issue. I would recommend setting up a "HelperFunctions.js" Page Object that will contain all of these Global Helper Functions. In that file you can list your browser functions and reference it in multiple tests with all of the code in one location.
This is the code for the "HelperFunctions.js" file that I would recommend setting up:
var HelperFunctions = function() {
this.longClick = function(targetElement) {
browser.actions().mouseDown(targetElement).perform();
browser.sleep(5000);
browser.actions().mouseUp(targetElement).perform();
};
};
module.exports = new HelperFunctions();
Then in your Test you can reference the Helper file like this:
var HelperFunctions = require('../File_Path_To/HelperFunctions.js');
describe('Example Test', function() {
beforeEach(function() {
this.helperFunctions = HelperFunctions;
browser.get('http://www.example.com/');
});
it('Should test something.', function() {
var Element = element(by.className('targetedClassName'));
this.helperFunctions.longClick(Element);
});
});
In my Test Suite I have a few Helper files setup and they are referenced through out all of my Tests.
I have very little knowledge of selenium or protractor, but I'll give it a shot.
This assumes that
browser.actions().mouseDown(element).mouseUp(element).perform();
is valid syntax for your issue, if so then this would likely do the trick
browser.action().sleep = function(){
browser.sleep.apply(this, arguments);
return browser.action()
}

Chai Sinon, should.have.been flags are undefined?

Many apologies for the title, I'm still thinking of a better way to explain this.
I'm having an issue in Chai when calling should.have.been. , where the propert always returns undefined. The weird thing is I'm doing a console.log(should.have.been) , and it prints the full object, but for some reason I can't access the flags.
example.js under test
var var BaseController = function (model, routesFn) {
var router = require('express').Router();
routesFn(router);
return router;
};
test.js
var controller = require('../lib/controller.js')
,assert = require("assert")
,chai = require('chai')
,sinon = require('sinon')
,should = chai.should();
describe('baseController', function() {
it('should have something..', function() {
var routesFn = sinon.spy();
controller(null, routesFn);
routesFn.should.have.not.been.called; //notice the 'not'.
//Should fail here, but called is undefined
assert.equal(routesFn.called, true);
});
});
So, I'm able to verify that the callback 'routesFn' is invoked by using the assert.equa(), but for some reason the called property is showing undefined. I was initially trying to create a test using the chai should have been called syntax, and later realized that my tests would pass even when I didn't have the logic implemented.
When I do a console.log of the object I see this
console.log(routesFn.should.have.not.been);
{ __flags:
{ ssfi: [Function: shouldGetter],
object:
{ [Function: proxy]
reset: [Function],
invoke: [Function: invoke],
named: [Function: named],
...
called: true
}
}
When I do a console out of 'called' , it prints undefined. Obviously I can't access the nested property.
console.log(routesFn.should.have.not.been.called); //expecting to print true
//prints
undefined
So, am I missing something in the initial setup? I was thinking I need to add something else to the 'should' variable initialization, but can't find anything on google.
Chai itself does not have support for what you are trying to do.
I presume what you call example.js should be controller.js and should be something like:
module.exports = function (model, routesFn) {
var router = require('express').Router();
routesFn(router);
return router;
};
Otherwise, I can't see how you code can work at all. Using the module above, the only thing I need to change to your test.js file is to add this after the require calls you already have, and after the call to chai.should():
var sinonChai = require('sinon-chai');
chai.use(sinonChai);
sinon-chai is what adds support for should.have.not.been.called. With this in place, your test fails as you expect.

Categories

Resources