Unit testing JavaScript code when Ajax gets in the way
Join the DZone community and get the full member experience.
Join For FreeThe previous part of this article is here, but it's not required for understanding the current topic.
Unit testing is one of the best tools I have in my box to aid the design of my code. Yet I'm not always in the environment where unit testing is easy: JavaScript frameworks are maturing, and isolation of tests presents the same issues of other languages: global state and external interactions (plus asynchronous behavior).
One of the obvious external interactions is Ajax calls, performed by whatever library you happen to be using. This article is the story of how we got to unit test JavaScript client code by stubbing Ajax calls; although then we moved to a end-to-end testing approach for other reason, at least this know-how will be kept here and won't be lost.
Mechanics
JavaScript is a kind of open language, in the Ruby open classes sense. You can substitute almost any method exposed by a public Api:
jQuery.val = function() { ... };
This ability means that if we know which library calls the code under test is using, we can test it even if it was not designed with Dependency Injection like we'll prefer in PHP or Java. Since parts of the code may not even be under our control, this is often the only choice.
This form of isolation is obtained by tampering with global state, so it may be dangerous. The teardown phases become fundamental, at least until the testing frameworks get better at sandboxing the tests.
Stubbing the library calls
A first choice for testing code that performs Ajax calls in it is to stub the library method it's using. If you are in the 0.01% of people who instantiates XMLHttpRequest objects directly, it's easy to wrap this creation into a method you can stub out later.
For jQuery, the target is the jQuery.ajax method. In the case of Ext JS, it is the Ext.Ajax.request. However, in the latter case Ext.Ajax.request is called with so many different options by other Ext JS components that is going to be difficult to write a general purpose stub.
The mechanics are the following:
- save the old method in a variable (var oldAjax = jQuery.ajax;)
- Substitute it with a closure that saves the callback in a variable. This closure may check its arguments (becoming a kind of mock).
- Use the callback to pass back a canned response, written in literal JSON, XML or whatever you return from the server side.
- Restore the old method (teardown phase) with a new assignment.
Stubbing XMLHttpRequest
An alternative way to stub Ajax calls is to stub out the creation of XMLHttpRequest. Since XMLHttpRequest is just a global object of the class Function, it can be substituted. Moreover, you don't have to do it by hand: Sinon is a JavaScript library that does exactly what we need. It works also in Explorer since checks also for ActiveXObject; it provides an higher level Api, so that you don't have to rewrite the whole object and all its methods.
Sinon provides various testing utility methods which let you use more than one workflow: a list of correspondences between URLs and responses, or single expectations. It's just a .js file, so it will work with whatever testing framework you're using:
var fakeXhr = sinon.useFakeXMLHttpRequest(); var requests = []; fakeXhr.onCreate = function (xhr) { requests.push(xhr); }; jQuery.ajax({ url: "/my/page" }); // this should go in an always executed tearDown method fakeXhr.restore(); assertEquals("/my/page", this.requests[0].url);
When multiple Ajax calls have to be mocked, the Api changes a little:
var server = sinon.fakeServer.create(); server.respondWith("responseText content"); // also configurable by URL var called = false; callback = function(text) { assertEquals("responseText content", text); called = true; }; jQuery.ajax({ url: "/my/page", success: callback }); server.respond(); // should go in the tearDown phase server.restore(); assertTrue(called);
The server.respond() call eliminates asychronicity from the test: after that, you're sure that the Ajax handler functions have been called. You can proceed in making assertions or continuing the test.
Ext JS currently requires a larger step, since it checks via polling if a request is complete instead of relying on callbacks (don't ask me why): setInterval should be stubbed as well.
var oldSetInterval = setInterval; callbacks = []; setInterval = function (callback, delay) { callbacks.push(callback); }; // stub and call code under test as before... setInterval = oldSetInterval; for (i in callbacks) { callbacks[i].call(); } // assertions...
This time, callbacks[i].call() avoids the asynchronicity: the Ext handlers, which should check the response text and code are called immediately, after the Ajax stubbing has been performed.
Playing with setTimeout, setInterval and the other time-related functions may be dangerous if the testing framework does not support it; it may be calling them for internal purposes, like avoiding an infinite test. The issue was preminent in jsTestDriver, but was solved in 2009: now jsTestDriver makes its own copies of setInterval and other sensitive functions before starting to run the test cases. If you're using jsTestDriver, you're free to play with these global functions.
Conclusions
Testing JavaScript code is always possible, and when there is more logic involved than an Ajax call, stubbing helps isolating the system under test. Testing with real Ajax calls won't even be possible in my favourite environment, jsTestDriver, as the code is served from a webserver on localhost and not from the real application. Indeed the solutions describes in this article are appropriate for unit testing, not end-to-end or functional one.
Opinions expressed by DZone contributors are their own.
Comments