patternjavascriptMinor
Unit testing a click event with asynchronous content
Viewed 0 times
clickwithasynchronoustestingcontentunitevent
Problem
I'm writing unit tests for a web application that has quite a lot of UI interactions. I would like some feedback regarding on how to handle click events with asynchronous code.
My goal here is to simluate users interacting with the page. That's why I'm triggering events rather than making function calls.
Here is my idea so far:
Testing the code:
http://jsfiddle.net/0m4t4ndt/2/
My goal here is to simluate users interacting with the page. That's why I'm triggering events rather than making function calls.
Here is my idea so far:
$('#foo').on('click', function(){
var dfd = $.Deferred();
simulateAsync().done(dfd.resolve).fail(dfd.reject);
$(this).data({ promise: dfd.promise() });
});
function simulateAsync(){
var dfd = $.Deferred();
setTimout(function(){
$('body').text('foo');
dfd.resolve();
}, 1000);
return dfd.promise();
}Testing the code:
test('foo text set on body after 1000ms when clicking #foo', function(assert){
var $foo = $('#qunit-fixture').find('#foo'),
done = assert.async();
$foo.trigger('click');
var completePromise = $foo.data('promise');
completePromise.done(function(){
assert.equal($('#bar').text(), 'bar');
done();
});
});http://jsfiddle.net/0m4t4ndt/2/
Solution
Without the real code to be tested, I can only provide an example of how I've tested something similar with QUnit and Sinon.JS, though you don't even need the latter.
You can either use Sinon.JS to mock a response to the AJAX call or substitute your own manual mock for
Mocking JQuery
Let's assume this click event handler that uses the
Here's how you would test this handler.
For safety, I'd recommend saving and restoring
Mocking AJAX
While this does result in a lot of boilerplate sometimes, you can use Sinon's fake server to mock the AJAX responses. The beauty here is that it will match up the request to the response to ensure your AJAX calls (URL, data, type, etc.) are all correct and then supplies a canned response.
Here's the same test above using a fake server.
If you're mocking a lot of JSON AJAX requests, you can create a common fixture to make this a little saner. You can call
Then you simply use this as the module's fixture:
Non-Mocking Solution
As you say in your comment, you want to execute the full AJAX call to your backend service.
Note: This is not unit testing anymore but rather functional or integration testing. Unit testing involves testing each small piece (unit) in isolation of the others: the click handler. Adding in jQuery,
I still recommend doing actual unit tests in addition to integration testing for quicker development as they typically run much faster and can be executed with every change to a source/test file from your IDE.
First, modifying your click handler invalidates any test you run, so we'll need to leave that in place, i.e., no simulated call or extra deferred. Your best bet is to poll for the call to complete with an interval and fail the test if it doesn't complete within X seconds.
```
test('foo text via AJAX set on body after clicking #foo', function(assert) {
var $foo = $('#qunit-fixture').find('#foo'),
$body = $('body'),
done = assert.async(),
timeout = Date.now() + 5000, // timeout after 5s
timer;
$foo.trigger('click');
timer = window.setInterval(function () {
if ($body.text() === 'foobar') {
assert.equal($body.text(), 'foobar');
window.clearInterval(timer);
d
You can either use Sinon.JS to mock a response to the AJAX call or substitute your own manual mock for
$.getJSON or $.ajax. I find the former preferable as it documents the AJAX call and tests the full front-end code, but it's beyond the scope of this question. You could argue that you're needlessly testing jQuery itself, and you'd be right. YMMV :)Mocking JQuery
Let's assume this click event handler that uses the
/load-foo AJAX call to acquire a JSON string. I've omitted the extra deferred for simplicity as your current test of the promise attached to the #foo element is fine.$('#foo').on('click', function(){
$.getJSON('/load-foo', {
foo: 'bar'
}).done(function (foo) {
$('body').text(foo);
});
});Here's how you would test this handler.
test('foo text via AJAX set on body after clicking #foo', function(assert) {
var $foo = $('#qunit-fixture').find('#foo'),
getJSON = $.getJSON;
$.getJSON = function (url, data) {
assert.equal(url, '/load-foo');
assert.equal(data.foo, 'bar');
return new $.Deferred().resolve('"foobar"');
}
$foo.trigger('click');
assert.equal($('body').text(), 'foobar');
$.getJSON = getJSON;
});For safety, I'd recommend saving and restoring
$.getJSON in the module's setup and teardown functions. If you don't, any exception thrown in the test will leave the mock in place.Mocking AJAX
While this does result in a lot of boilerplate sometimes, you can use Sinon's fake server to mock the AJAX responses. The beauty here is that it will match up the request to the response to ensure your AJAX calls (URL, data, type, etc.) are all correct and then supplies a canned response.
Here's the same test above using a fake server.
module('AJAX tests', {
setup: function () {
this.server = sinon.fakeServer.create();
},
teardown: function () {
this.server.restore();
}
});
test('foo text via AJAX set on body after clicking #foo', function(assert) {
var $foo = $('#qunit-fixture').find('#foo');
// setup the expected request with the canned response
this.server.respondWith('/foo',
[ 200, { 'Content-Type': 'text/json' }, '"foobar"' ]
);
$foo.trigger('click');
// handle queued requests immediately
this.server.respond();
assert.equal($('body').text(), 'foobar');
});If you're mocking a lot of JSON AJAX requests, you can create a common fixture to make this a little saner. You can call
this.mockAjax with a number for an HTTP response code, an object to convert it to JSON, or a string as the raw response.var ajaxFixture = {
setup: function () {
this.server = sinon.fakeServer.create();
},
teardown: function () {
this.server.restore();
},
mockAjax: function (url, response) {
var type = { 'Content-Type': 'text/json' };
if (typeof response === 'number') {
this.server.respondWith(url, [ response, type, '' ]);
}
else if (typeof response === 'object') {
this.server.respondWith(url, [ 200, type, JSON.stringify(response) ]);
}
else {
this.server.respondWith(url, [ 200, type, response ]);
}
}
);Then you simply use this as the module's fixture:
module('AJAX tests', ajaxFixture);
test(...);Non-Mocking Solution
As you say in your comment, you want to execute the full AJAX call to your backend service.
Note: This is not unit testing anymore but rather functional or integration testing. Unit testing involves testing each small piece (unit) in isolation of the others: the click handler. Adding in jQuery,
XMLHTTPRequest, and your backend service will make isolating bugs more difficult. But it is useful for making sure all the pieces work together.I still recommend doing actual unit tests in addition to integration testing for quicker development as they typically run much faster and can be executed with every change to a source/test file from your IDE.
First, modifying your click handler invalidates any test you run, so we'll need to leave that in place, i.e., no simulated call or extra deferred. Your best bet is to poll for the call to complete with an interval and fail the test if it doesn't complete within X seconds.
```
test('foo text via AJAX set on body after clicking #foo', function(assert) {
var $foo = $('#qunit-fixture').find('#foo'),
$body = $('body'),
done = assert.async(),
timeout = Date.now() + 5000, // timeout after 5s
timer;
$foo.trigger('click');
timer = window.setInterval(function () {
if ($body.text() === 'foobar') {
assert.equal($body.text(), 'foobar');
window.clearInterval(timer);
d
Code Snippets
$('#foo').on('click', function(){
$.getJSON('/load-foo', {
foo: 'bar'
}).done(function (foo) {
$('body').text(foo);
});
});test('foo text via AJAX set on body after clicking #foo', function(assert) {
var $foo = $('#qunit-fixture').find('#foo'),
getJSON = $.getJSON;
$.getJSON = function (url, data) {
assert.equal(url, '/load-foo');
assert.equal(data.foo, 'bar');
return new $.Deferred().resolve('"foobar"');
}
$foo.trigger('click');
assert.equal($('body').text(), 'foobar');
$.getJSON = getJSON;
});module('AJAX tests', {
setup: function () {
this.server = sinon.fakeServer.create();
},
teardown: function () {
this.server.restore();
}
});
test('foo text via AJAX set on body after clicking #foo', function(assert) {
var $foo = $('#qunit-fixture').find('#foo');
// setup the expected request with the canned response
this.server.respondWith('/foo',
[ 200, { 'Content-Type': 'text/json' }, '"foobar"' ]
);
$foo.trigger('click');
// handle queued requests immediately
this.server.respond();
assert.equal($('body').text(), 'foobar');
});var ajaxFixture = {
setup: function () {
this.server = sinon.fakeServer.create();
},
teardown: function () {
this.server.restore();
},
mockAjax: function (url, response) {
var type = { 'Content-Type': 'text/json' };
if (typeof response === 'number') {
this.server.respondWith(url, [ response, type, '' ]);
}
else if (typeof response === 'object') {
this.server.respondWith(url, [ 200, type, JSON.stringify(response) ]);
}
else {
this.server.respondWith(url, [ 200, type, response ]);
}
}
);module('AJAX tests', ajaxFixture);
test(...);Context
StackExchange Code Review Q#78458, answer score: 6
Revisions (0)
No revisions yet.