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.
Related
I'm working on the "Approve All" button. The process here is when I click "Approve All," each individual "Approve" button will be triggered as "click" all at once, and then it will send POST requests to the controller. However, when I clicked Approve All button, there was a race condition causing the controller returns Error 500: Internal server error. I have tried using JS setTimeout() with value 1500*iter, but when the iterator gets higher, for example at i = 100, then it would take 1500*100 => 150000ms (150s). I hope that explains the problem clearly. Is there a way to prevent such a case?
Here is my code, I'm using JQuery:
let inspection = $this.parents("li").find("ul button.approve"); // this will get all 'approve' button to be clicked at once
inspection.each((i,e)=>{
(function () {
setTimeout(function () {
$(e).data("note",r);
$(e).click();
}, 1500 * i); // this acts like a queue, but when i > 100, it takes even longer to send POST requests.
})(this,i,e,r);
});
// then, each iteration will send a POST request to the controller.
$("#data-inspection ul button.approve").on("click", function() {
// send POST requests
});
Any help would be much appreciated. Thank you.
That 500 error may also be the server crashing from being unable to process all the requests simultaneously.
What I'd recommend is using an event-driven approach instead of setTimeout. Your 1500ms is basically a guess - you don't know whether clicks will happen too quickly, or if you'll leave users waiting unnecessarily.
I'll demonstrate without jQuery how to do it, and leave the jQuery implementation up to you:
// use a .js- class to target buttons your buttons directly,
// simplifying your selectors, and making them DOM agnostic
const buttonEls = document.querySelectorAll('.js-my-button');
const buttonsContainer = document.querySelector('.js-buttons-container');
const startRequestsEvent = new CustomEvent('customrequestsuccess');
// convert the DOMCollection to an array when passing it in
const handleRequestSuccess = dispatchNextClickFactory([...buttonEls]);
buttonsContainer.addEventListener('click', handleButtonClick);
buttonsContainer.addEventListener(
'customrequestsuccess',
handleRequestSuccess
);
// start the requests by dispatching the event buttonsContainer
// is listening for
buttonsContainer.dispatchEvent(startRequestsEvent);
// This function is a closure:
// - it accepts an argument
// - it returns a new function (the actual event listener)
// - the returned function has access to the variables defined
// in its outer scope
// Note that we don't care what elements are passed in - all we
// know is that we have a list of elements
function dispatchNextClickFactory(elements) {
let pendingElements = [...elements];
function dispatchNextClick() {
// get the first element that hasn't been clicked
const element = pendingElements.find(Boolean);
if (element) {
const clickEvent = new MouseEvent('click', {bubbles: true});
// dispatch a click on the element
element.dispatchEvent(clickEvent);
// remove the element from the pending elements
pendingElements = pendingElements.filter((_, i) => i > 0);
}
}
return dispatchNextClick;
}
// use event delegation to mitigate adding n number of listeners to
// n number of buttons - attach to a common parent
function handleButtonClick(event => {
const {target} = event
if (target.classList.contains('js-my-button')) {
fetch(myUrl)
.then(() => {
// dispatch event to DOM indicating request is complete when the
// request succeeds
const completeEvent = new CustomEvent('customrequestsuccess');
target.dispatchEvent(completeEvent);
})
}
})
There are a number of improvements that can be made here, but the main ideas here are that:
one should avoid magic numbers - we don't know how slowly or quickly requests are going to be processed
requests are asynchronous - we can determine explicitly when they succeed or fail
DOM events are powerful
when a DOM event is handled, we do something with the event
when some event happens that we want other things to know about, we can dispatch custom events. We can attach as many handlers to as many elements as we want for each event we dispatch - it's just an event, and any element may do anything with that event. e.g. we could make every element in the DOM flash if we wanted to by attaching a listener to every element for a specific event
Note: this code is untested
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.
I use mocha for some integration testing and have many test sets.
Each set has initialization tests. When such tests fail, the rest of the set should not run at all, because if one fails then each will fail.
The thing is that I can't avoid such initialization tests, because part of the code/environment is generated by some tool which does not guarantee any correct result.
Is it possible to implement this using mocha ?
Using the BDD interface, the normal way to do this with Mocha is to put anything that sets up the testing environment into before or beforeEach:
describe("foo", function () {
describe("first", function () {
before(function () {
// Stuff to be performed before all tests in the current `describe`.
});
beforeEach(function () {
// Stuff to perform once per test, before the test.
});
it("blah", ...
// etc...
});
describe("second", function () {
before(function () {
// Stuff to be performed before all tests in the current `describe`.
});
beforeEach(function () {
// Stuff to perform once per test, before the test.
});
it("blah", ...
// etc...
});
});
If the before or beforeEach that a test depends on fails, then the test is not run. Other tests that don't depend on it will still run. So in the example above if the callback passed to before in the describe named first fails, the tests in the describe named second won't be affected at all and will run, provided that their own before and beforeEach callbacks don't fail.
Other than this, Mocha is designed to run tests that are independent from each other. So if one it fails, then the others are still run.
I found mocha-steps which basically allow you to write a "chain" of it()s (called step()) and mocha will abort the suite if one of them breaks, thus avoiding the cascade of inevitable failures, and I found pull request 8 marks subsequent steps and subsuites as pending. So I can write:
describe("businessCode()", function() {
step("should be not null", function() {
assert(businessCode() != null)
});
step("should be a number", function() {
assert(typeof businessCode() === 'number');
});
step("should be greater than 10", function() {
assert(businessCode() > 10);
});
describe("thingThatCallsBusinessCode()", function() {
step("should be greater than 10", function() {
assert(thingThatCallsBusinessCode() != null);
});
});
});
If e.g. businessCode() returns a boolean, only the should be a number test will fail; the subsequent ones (and the subsuite will be marked as pending).
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.
I wonder what is a good pattern to use when you could have multiple xmlhttprequests that are part of different processes like (check login, fetch tooltip and display, show sub records/open details).
Your input on my code so far is more than welcome so are some good articles for reference on asynchronyous handling of processes.
Here is what I got so far trying to use a mediator and trying to define a sequence of events to be triggered by the mediator and initiated by a worker for a certain process
var mediator={
events:[],
// bind functions to events, optionally once only if this function doesn't
// need to handle the event during the lifetime of the application
addListener:function(name,processor,onceOnly){
if(!mediator.events[name]){
mediator.events[name]=new Array({
processor:processor,
once: onceOnly ? true : false
});
return;
}
mediator.events[name].push({
processor:processor,
once: onceOnly ? true : false
});
},
trigger:function(name,data){
var i=0;//check if mediator[name] exist
for(i=0;i<mediator.events[name].length;i++){
try{
mediator.events[name][i].processor(data);
// problem is when a use once handler is the 3rd in the chain and the
// second handler fails then the 3rd is never removed
// could trigger an error here that has a cleaner listner
}finally{
if(mediator.events[name][i].once){
mediator.remove(name,mediator.events[name][i]);
}
}
}
},
// removing listener from event
remove:function(name,event){
for(var i=0;i<mediator.events[name].length;i++){
if(mediator.events[name][i]==event){
mediator.events[name].splice(i,1);
return;
}
}
},
// used to provide an event chain through data that will execute a certain
// process
triggerNext:function(data){
// some checks on data
mediator.trigger(data.events[data.index++],data);
}
}
// possible response parsers
var parser=function(type){
var parseLogin=function(data){
console.log(data);
// should call triggerNext here for the worker to be notified.
}
if(type=="loginParser"){
return parseLogin;
}
}
// connects and triggers next
var connector=function(){
this.response="";
this.commObject=null;
this.connect=function(data){
$.get(data.url, function(res) {
data.commObject=this;//maybe you'd like to inpect it
data.response=res;
mediator.triggerNext(data);
});//trigger fail event if failed
};
}
// example of initiating a process
$("document").ready(function(){
//add all listeners that are used during the entire execution
// of the application here
var p=parser("loginParser");
mediator.addListener("checkLogin",p);
//the following is a temporary listener, this code would be in
// a worker object initLogin function.
var c=new connector();
mediator.addListener("connect",c.connect,true);
// the data determines what process will be invoked
// this could be in a worker.initLogin function
var data={
processType:"SendLoginAndCheck",
url:"test.html",
post:"",//could check in the connector.connect to see if post is set
events:["connect","checkLogin"],
//there is no worker.afterLogin but the 3rd event could be finishprocess
//and a worker object's function can be called to process that
index:0
}
//start the process
mediator.triggerNext(data);
});