Unit testing nodejs events - javascript

I'm using a solution explained in this answer to unit test events in my node application.
However, the setTimeout function never calls and so my tests pass when they should fail.
Here is an example:
suite('myTests', function() {
test('myFunction_whenCalled_emitsEvent', function() {
var myClass = new MyClass();
var eventTimeout = setTimeout(function() {
assert(false);
}, 1000);
myClass.on('something', function() {
clearTimeout(eventTimeout);
});
myClass.doSomething(); // this does not emit the 'something' event
});
});
I would expect this to fail, after 1 second as long as the 'something' event is not raised.
I put a breakpoint in the assert(false) line and it is never hit.
Could someone point me in the right direction ? Thanks.

You must use the done callback to show that your test is finished. Something like this:
suite('myTests', function() {
test('myFunction_whenCalled_emitsEvent', function(done) {
var myClass = new MyClass();
myClass.on('something', function() {
done();
});
myClass.doSomething();
});
});
It looks like you are only testing whether the event is emitted. If this is the case, then the whole setTimeout thing is unnecessary. Mocha will itself timeout if it does not get done without the default timeout (2000ms, if I recall correctly).
The way your code was set, Mocha would just schedule your event and then exit the test. Since scheduling the event was successful, Mocha would call the test successful.

Related

Assert that an event was observed

How can I assert, in a QUnit test case, that a specific Backbone event was observed?
The application uses Backbone.js events (Backbone.js version 1.3.3) for communicating between components. A simple view responds to a button click, by triggering a custom event on the event bus:
// foo.js
const WheelView = Backbone.View.extend({
events: {
"click #btn-spin-wheel": "onSpinButtonClick",
},
onSpinButtonClick: function () {
console.log("DEBUG: clicked #btn-spin-wheel");
Backbone.trigger("wheel:spin");
},
});
I want a QUnit test case (QUnit version 1.22.0) that asserts “when this button is selected, event "foo" appears on the event bus”.
The test case will also need to know other aspects of the event (such as optional arguments), so I need a function defined in the test case that the test case arranges as a callback for the specific event.
This is the latest test case I've tried, by making a Sinon (version 1.9.0) spy function for the event callback:
// test-foo.js
QUnit.module("Button “btn-spin-wheel”", {
beforeEach: function (assert) {
this.wheelView = new WheelView();
},
afterEach: function (assert) {
delete this.wheelView;
},
});
QUnit.test(
"Should observe the “wheel:spin” event.",
function (assert) {
assert.expect(1);
const spinWheelButton = document.querySelector(
"#qunit-fixture #btn-spin-wheel");
const eventListener = sinon.spy(function () {
console.log("DEBUG:QUnit: received ‘wheel:spin’ event");
});
Backbone.once("wheel:spin", eventListener);
const done = assert.async();
window.setTimeout(function () {
spinWheelButton.click();
window.setTimeout(function () {
assert.ok(eventListener.calledOnce);
}.bind(this));
done();
}.bind(this), 500);
});
The console.log invocations are to help me understand which functions are being called and which are not. I expect to see both:
DEBUG: clicked #btn-spin-wheel
DEBUG:QUnit: received ‘wheel:spin’ event
Instead, only the click message appears:
DEBUG: clicked #btn-spin-wheel
This is confirmed because the test case fails:
Button “btn-spin-wheel”: Should observe the “wheel:spin” event. (3, 0, 3) 579 ms
1. Assertion after the final `assert.async` was resolved # 578 ms
Source: Assert.prototype.ok#file:///usr/share/javascript/qunit/qunit.js:1481:3
2. failed, expected argument to be truthy, was: false # 578 ms
Expected: true
Result: false
Source: #file://[…]/test-foo.html:50:29
3. Expected 1 assertions, but 2 were run # 579 ms
Source: #file://[…]/test-foo.html:36:13
Source: #file://[…]/test-foo.html:36:13
I have read about QUnit support for asynchronous testing and I am experimenting with different assert.async and setTimeout usages, as suggested in the documentation examples. So far it is to no avail.
How should I use QUnit, Sinon, and Backbone, to assert (the existence, and specific properties of) a specific observed event from the app?
You should be able to register a listener for your event and use assert.async to listen for it, something like this:
var done = assert.async();
Backbone.once("wheel:spin", function(event) {
done();
});
const spinWheelButton = document.querySelector("#qunit-fixture #btn-spin-wheel");
spinWheelButton.click();
It may just be that your code is not behaving as you expect because of the setTimeout calls.
Here you can find further documentation for QUnit's async.
The problem turns out to be an interaction between test cases. The test cases manipulate the listeners and fire events onto the event hub; but all the test cases share the same event hub.
So when the test cases run asynchronously, instead of the test cases being isolated, events fired in one can affect another.
The workaround I have implemented is a custom event queue for each test case, that is managed in the QUnit.module.beforeEach and ….afterEach:
/**
* Set up an event hub fixture during `testCase`.
*
* #param testCase: The QUnit test case during which the fixture
* should be active.
* #param eventHub: The Backbone.Events object on which the fixture
* should exist.
*/
setUpEventsFixture: function (testCase, eventHub) {
testCase.eventHubPrevious = eventHub._events;
eventHub._events = [];
},
/**
* Tear down an event hub fixture for `testCase`.
*
* #param testCase: The QUnit test case during which the fixture
* should be active.
* #param eventHub: The Backbone.Events object on which the fixture
* should exist.
*/
tearDownEventsFixture: function (testCase, eventHub) {
eventHub._events = testCase.eventHubPrevious;
},
By using these in the test module definitions:
QUnit.module("Button “btn-spin-wheel”", {
beforeEach: function (assert) {
setUpEventsFixture(this, Backbone);
this.wheelView = new WheelView();
},
afterEach: function (assert) {
delete this.wheelView;
tearDownEventsFixture(this, Backbone);
},
});
The test cases now can continue to use code that uses the common Backbone object as the event hub, but their events are isolated from each other.

Testing style set using Mocha

I'm trying to test some "style set" functions, and I'm facing issues because I need to wait until the element if fully rendered, then read its property and check if it was sucesfully changed. I know that this is pretty obvious, but I was wondering if there is a way to do the same but without waiting.
For example, I want to test this function:
function changeWidth() {
document.getElementById('test').style.width = '200px';
}
And I use this test in Mocha:
it('Width should be 200px', () => {
changeWidth();
assert.equal(document.getElementById('test').style.width, '200px');
});
That assertion will always returns false. The following test will work:
it('Width should be 200px', () => {
changeWidth();
window.setTimeout( () => {
assert.equal(document.getElementById('test').style.width, '200px');
}, 1000);
});
It has to be a better way to accomplish the same without using timeouts. Can someone guide me? Thanks!
There are two things I noted:
No, you cannot force a render to happen synchronously. The browser decides, so the test will be async and a bit messy. That being said, there are (a bit) more elegant answers than setTimeout.
Your test is synchronous, while the logic is asynchronous. That means your test will always pass, as the assertion is called after the test has finished (see this). You need to pass a callback to the test that can be called when the test has finished.
You could do the tests somewhat cleaner by using requestAnimationFrame. Just create a helper that will run each function passed to it in a seperate animation frame and you will be guaranteed separate render phases.
function runSteps(fns) {
if(!fns.length) return;
var current = fns[0];
var rest = fns.slice(1);
requestAnimationFrame(function() {
current();
runSteps(rest);
})
}
// note the `done` argument - it is what makes Mocha able to know
// when the test is finished. See https://stackoverflow.com/questions/20748918/cannot-run-mocha-js-in-synchronous-mode
it('should run dom render steps chronologically', function(done) {
function assertWidthChanged(){
assert.equal(
document.getElementById('test').style.width,
'200px'
);
}
runSteps([
changeWidth,
assertWidthChanged,
done
]);
}

How to test event listeners using QUnit

I'm using pure Javascript (no JQuery) and I'm trying to get QUnit to test my function that is only invoked via an event, i.e. it's an event listener.
So the function I wish to test is of the form:
(function() {
function the_one_i_want_to_test() {
// do stuff
}
window.addEventListener('load', function() {
var some_element = ...;
some_element.addEventListener('click', the_one_i_want_to_test);
});
})();
I know I could expose the function to test it, but it is only ever used as an event listener and I don't want to pollute the global namespace.
That's why I am trying to kill two birds with one stone by manually triggering a "click" event and then checking for desired effects (in my tests).
The problem is that the event handler doesn't seem to be executing at all, either because QUnit doesn't wait for it before performing the effects checks, or for some other reason.
So my QUnit test code looks like this:
QUnit.test('test function', function(assert) {
var some_element = ...;
var event = document.createEvent('Event');
event.initEvent('click', true, true);
some_element.dispatchEvent(event);
assert.ok(...assert one or more effects of running "the_one_i_want_to_test"...);
});
I have tried including JQuery just for the tests to use ".trigger()", but that doesn't seem to help.
The event listener executes fine/normally on the production page, just not in the tests.
Any ideas on why the event listener doesn't seem to be running?
In Javascript, event handlers are not executed immediately when the event is raised. They are queued up on a list, and execute after the current code block is done. By that time, the test is over.
You have to use the setTimeout trick to queue up your validation code so that it executes after the handler. When the delay is 0 (or unspecified), the code will be queued for execution as soon as possible, meaning right after the event handler gets a chance to run.
As Amit says, you also need to use the assert.async() method to get a done() function that you'll call after your assertions. This makes QUnit.test wait until you invoke it before moving on to the next test.
Try something like this:
QUnit.test('test function', function(assert) {
var some_element = ...;
var event = document.createEvent('Event');
event.initEvent('click', true, true);
some_element.dispatchEvent(event);
var done = assert.async();
setTimeout(function() {
assert.ok(...assert one or more effects of running "the_one_i_want_to_test"...);
done();
});
});
Ensure that the code you wish to test is actually included in your web page.
I have been running in a headless environment and didn't get any runtime errors (apart from the failed assertions). With the help of #Amit (to bring me to my senses), I tried running the tests in a browser and discovered the code was not even being included in the page.

How to test for my Javascript callback with Jasmine?

I need to test if specific methods are called when user scrolls the window to a certain point. In my source code I have windows listener attached, something like:
$(window).on("scroll.singleJob",function(e)
{
// code here, check if the window is scrolled past certain point etc. and then I need to call this method
LozengesPanel.makeFixed();
}
Now, in my Jasmine test I'm trying to confirm that the method is being called when the window is scrolled to a certain point. So I set up the test:
describe("SingleJob page", function() {
beforeEach(function() {
loadFixtures('my_fixture.html');
});
it("panel sticks to top when page scrolled down", function() {
spyOn(mycompany.singleJobTestable.LozengesPanel, "makeFixed");
window.scroll(0,1000);
expect(mycompany.singleJobTestable.LozengesPanel.makeFixed).toHaveBeenCalled();
});
});
But the test fails, all I get is Expected spy makeFixed to have been called.
How can I trigger window scroll so I can test methods inside of this callback?
EDIT:
Finally it all makes sense.. It seems that scroll event was put in a tasks queue only to be executed after the current thread finishes. Adding $(window).trigger("scroll"); did the trick. I posted short blog post about it that explains the issue http://spirytoos.blogspot.com.au/2014/02/testing-windowscroll-with-qunitjasmine.html
EDIT: This answer does not satisfy the question. See the comments for the reason.
Actually, it looks like you are triggering the scroll event from your Jasmine spec. I tried very similar code, which I include below. However, my expect still fails, like yours (I'm still getting familiar with Jasmine, so I can't explain with certainty why that is).
var fun = {
scrollEventCallback: function() {
console.log('scroll event triggered');
}
};
$(window).on('scroll', fun.scrollEventCallback);
describe("A test suite", function() {
it("should trigger f", function() {
spyOn(fun, "scrollEventCallback");
$(window).trigger('scroll'); // my callback function is executed because it logs to the console
expect(fun.scrollEventCallback).toHaveBeenCalled(); // this fails anyway
});
});
Maybe your window.scroll(0, 1000) is not actually pushing the viewport low enough to trigger your Lozenges.makeFixed() call. That would be the case if the page (your fixture, I think) wasn't long and it didn't actually have anywhere to scroll.
Also, I got code similar to your provided code to work. The expect(...) succeeds. It is pasted below.
var LozengesPanel = {
makeFixed: function() {
console.log('makeFixed was called with its original function definition');
}
};
$(window).on("scroll.singleJob",function(e) {
LozengesPanel.makeFixed();
});
describe("A test suite", function() {
it("should trigger callback", function() {
spyOn(LozengesPanel, "makeFixed");
$(window).trigger('scroll'); // nothing is logged to the console
expect(LozengesPanel.makeFixed).toHaveBeenCalled(); // succeeds
});
});

Simple Jasmine Async Test with Inverted waitsFor

I'm trying to write an async test, which I've never done before. In english the test says this:
create a timer object with a callback
if the timer were to be started, it'd trigger after 500ms
the timer shouldn't be started
confirm the callback isn't called by waiting 1000ms and checking if it was called
So, from reading the docs, and looking at some other code, I think this is how I should write it.
it("should create a timer not start", function (done) {
var fail = false, timer;
// if this test is passing, this should do nothing
runs(function(){
timer = new Timer(function () {
fail = true;
timer.pause();
}, 500);
});
// if this test is passing, fail should never be true
waitsFor(function(){
return fail;
}, 1000);
// this should be called after 1 second because the previous times out
runs(function(){
expect(fail).toBeFalsy();
});
});
However waitsFor is timing out, because fail should never be true. I need waitsFor to wait the full second, and then the expect statement can run, but I need the timeout to be a good thing, not a failure (which Jasmine reports it as).
How do I do this with Jasmine?
Edit 2017-07-24
Based on #Yavin5's comment, This answer is valid for Jasmine v1.
For upgrading to Jasmine v2 please reference the documentation. https://jasmine.github.io/2.0/upgrading.html#section-9
For Jasmine V2 Documentation visit https://jasmine.github.io/2.0/introduction.html#section-Asynchronous_Support
You need to add a flag that that will become true when the async operation has completed. That is what the waitsFor is waiting for. I would suggest something like
it("should create a timer not start", function (done) {
var fail = false, timer, flag;
// if this test is passing, this should do nothing
runs(function(){
flag = false;
timer = new Timer(function () {
fail = true;
timer.pause();
}, 500);
setTimeout(function() {
flag = true;
}, 1000);
});
// if this test is passing, fail should never be true
waitsFor(function(){
return flag;
}, 1100);
// this should be called after 1 second because the previous times out
runs(function(){
expect(fail).toBeFalsy();
});
});

Categories