Testing JQuery's "on" method fires callback function - javascript

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.

Related

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

Unit Testing with Jasmine - Can not spyOn mocked method

I am writing a javascript library that calls a method on another js lib.
Most of the time i would create a mock function of the 3rd party library and spy on it. However, it doesn't seem to work.
For example:
mymain.js
export const checkForExternalFunc = () => {
try {
return com.externalFunc
} catch (error) {
return false
}
}
mymain_spec.js
import { checkForExternalFunc } from './src';
describe('checkForExternalFunc', () => {
let com = com || {};
com.externalFunc = function () {
return true;
};
it('return the function when com.externalFunc is present', () => {
spyOn(com, "externalFunc");
let check = checkForExternalFunc();
expect(check).toBe(jasmine.Any(function));
});
})
and this would give me an error
ReferenceError: com is not defined
Function in 3rd part library
var com = com || {};
com.externalFunc = function () {
// return something
};
Any suggestion how i can approach this? Also i have researched a little on Stub with Sinon but not sure how to use it properly. Any help will be appreciated. Thanks!!
Note: I setup project with webpack + babel, karma, jasmine.
Thanks #AdityaBhave for pointing out. I only need to make sure my mock function and the actual one are actually the same. Please see comment above.

How to spyon jquery selector [duplicate]

When it comes to spying on jQuery functions (e.g. bind, click, etc) it is easy:
spyOn($.fn, "bind");
The problem is when you want to spy on $('...') and return defined array of elements.
Things tried after reading other related answers on SO:
spyOn($.fn, "init").andReturn(elements); // works, but breaks stuff that uses jQuery selectors in afterEach(), etc
spyOn($.fn, "merge").andReturn(elements); // merge function doesn't seem to exist in jQuery 1.9.1
spyOn($.fn, "val").andReturn(elements); // function never gets called
So how do I do this? Or if the only way is to spy on init function how do I "remove" spy from function when I'm done so afterEach() routing doesn't break.
jQuery version is 1.9.1.
WORKAROUND:
The only way I could make it work so far (ugly):
realDollar = $;
try {
$ = jasmine.createSpy("dollar").andReturn(elements);
// test code and asserts go here
} finally {
$ = realDollar;
}
Normally, a spy exists for the lifetime of the spec. However, there's nothing special about destroying a spy. You just restore the original function reference and that's that.
Here's a handy little helper function (with a test case) that will clean up your workaround and make it more usable. Call the unspy method in your afterEach to restore the original reference.
function spyOn(obj, methodName) {
var original = obj[methodName];
var spy = jasmine.getEnv().spyOn(obj, methodName);
spy.unspy = function () {
if (original) {
obj[methodName] = original;
original = null;
}
};
return spy;
}
describe("unspy", function () {
it("removes the spy", function () {
var mockDiv = document.createElement("div");
var mockResult = $(mockDiv);
spyOn(window, "$").and.returnValue(mockResult);
expect($(document.body).get(0)).toBe(mockDiv);
$.unspy();
expect(jasmine.isSpy($)).toEqual(false);
expect($(document.body).get(0)).toBe(document.body);
});
});
As an alternative to the above (and for anyone else reading this), you could change the way you're approaching the problem. Instead of spying on the $ function, try extracting the original call to $ to its own method and spying on that instead.
// Original
myObj.doStuff = function () {
$("#someElement").css("color", "red");
};
// Becomes...
myObj.doStuff = function () {
this.getElements().css("color", "red");
};
myObj.getElements = function () {
return $("#someElement");
};
// Test case
it("does stuff", function () {
spyOn(myObj, "getElements").and.returnValue($(/* mock elements */));
// ...
});
By spying on the window itself you have access to any window properties.
As Jquery is one of these you can easily mock it as below and return the value you require.
spyOn(window, '$').and.returnValue(mockElement);
Or add a callFake with the input if it needs to be dynamic.

Javascript/Typescript 'this' scope

I am working with Ionic2 and Meteor. I do however have a Javascript/Typescript issue relating to the scope of the this object.
I have read that I should use bind when I don't have handle on this at the appropriate level.
I probably don't understand the concept, because I try the following, but get an error trying to call a function.
this.subscribe('messages', this.activeChat._id, this.senderId, () => {
this.autorun(() => {
let promiseMessages: Promise<Mongo.Collection<Message>> = this.findMessages();
promiseMessages.then((messageData: Mongo.Collection<Message>) => {
messageData.find().forEach(function (message: Message) {
setLocalMessage.bind(message);
});
});
});
and
private setLocalMessage(message: Message): void {
this.localMessageCollection.insert(message);
}
I get the following error when I try build the app:
ERROR in ./app/pages/messages/messages.ts
(72,19): error TS2304: Cannot find name 'setLocalMessage'.
UPDATE
Thank you for the advise below.
I am now using the following, and it works.
let promiseMessages: Promise<Mongo.Collection<Message>> = this.findMessages();
promiseMessages.then((messageData: Mongo.Collection<Message>) => {
messageData.find().forEach((message: Message) => {
this.setLocalMessage(message);
});
});
I have read that I should use bind when I don't have handle on this at the appropriate level.
That's a bit outdated now, better have a look at How to access the correct `this` context inside a callback? these days which also shows you how to use arrow functions.
You're getting the error message because setLocalMessage is not a variable but still a property of this so you have to access it as such. There are basically three solutions in your case:
bind
messageData.find().forEach(this.setLocalMessage.bind(this));
the context argument of forEach (assuming it's the Array method):
messageData.find().forEach(this.setLocalMessage, this);
another arrow function:
messageData.find().forEach((message: Message) => {
this.setLocalMessage(message);
});
There are a few things wrong here.
In ES6 (and thus TypeScript), you need to refer to instance members using explicit this, such as this.setLocalMessage. Just writing setLocalMessage is invalid no matter where the code is.
Inside a function, the this object will probably not be what you expect anyway. You need to capture the this object from outside the function and put it in a variable, like so:
this.subscribe('messages', this.activeChat._id, this.senderId, () => {
this.autorun(() => {
let self = this;
let promiseMessages: Promise<Mongo.Collection<Message>> = this.findMessages();
promiseMessages.then((messageData: Mongo.Collection<Message>) => {
messageData.find().forEach(function (message: Message) {
self.setLocalMessage(message);
});
});
});
Alternatively, you can use an arrow expression, in which this is the same as what it is in the code around it:
this.subscribe('messages', this.activeChat._id, this.senderId, () => {
this.autorun(() => {
let promiseMessages: Promise<Mongo.Collection<Message>> = this.findMessages();
promiseMessages.then((messageData: Mongo.Collection<Message>) => {
messageData.find().forEach(message => this.setLocalMessage(message));
});
});
});
It's not an issue of TypeScript itself. Without it, the code will just fail at runtime.

Reuse of functions

I think I know the theory behind the solution but I am having trouble implementing it. Consider following piece of code:
this.selectFirstPassiveService = function () {
getFirstPassiveService().element(by.tagName('input')).click();
}
this.clickAddTask = function () {
getFirstActiveService().element(by.tagName('a')).click();
}
this.selectTask = function () {
getFirstActiveService()
.element(by.tagName('option'))
.$('[value="0"]')
.click();
}
this.saveTask = function () {
getFirstActiveService().element(by.name('taskForm')).submit();
}
getFirstActiveService = function () {
return services.filter(function (elem) {
return elem.getAttribute('class').then(function (attribute) {
return attribute === 'service active ';
});
}).first();
}
getFirstPassiveService = function () {
return services.filter(function (elem) {
return elem.getAttribute('class').then(function (attribute) {
return attribute === 'service passive ';
});
}).first();
}
};
To minimalize code duplication, I created two functions:
* getFirstActiveService()
* getFirstPassiveService()
My spec goes as follows:
it('Select service', function () {
servicePage.selectFirstPassiveService();
servicePage.clickAddTask();
servicenPage.selectTask()();
});
Both clickAddTask() and selectTask() use the function called getFirstActiveService(). Everything runs fine in clickAddTask() but when I use the function in selectTask(), some elements (which are present) cannot be found by protractor.
Here goes my theory, every command in getFirstActiveService() is queued in the control flow when the function is called in clickAddTask() and is then executed. When reusing the function in selectTask() the commands aren't queued, the instance created in clickAddTask() is used and therefore, some elements cannot be found since the DOM has changed since then.
Now first question: Is my theory correct?
Second question: How can I fix this?
Thanks in advance!
Cheers
I refactored it a bit and it works now.
The problem was in the test itself, not in the reuse of functions.

Categories

Resources