A little while ago, I wrote about an exciting new JavaScript feature called WebWorkers. These offer developers the first real shot at developing truly multi-threaded application code, much like what is available in Java. Unfortunately, they are not widely adopted by browser vendors. Having said that, there are plenty of methods in JavaScript today that execute asynchronously, such as setTimeout(), Ajax calls, as well as event-driven processes. These examples all share one thing in common in that they have to wait until the current execution thread comes to an end and surrenders to the next event. As a result, there is going to be a time lag before the asynchronous code executes, be it a button click, XHR Callback, or setTimeout codeblock. And that makes asynchronous processes hard to test. Fortunately, the Jasmine JS testing library comes equipped with a couple of tools for handling callbacks, which is the subject of today’s article.
Spies Described
I discussed Jasmine Spies in my Spy on JavaScript Methods Using the Jasmine Testing Framework article. Spies are used to mock an object or function. Behind the scenes, Jasmine creates a proxy object that takes the place of the real object so that we can then define what methods are called and their returned values from within our test method. Mocks can then be utilized to retrieve run-time statistics on the spied function such as:
- How many times the spied function was called.
- What was the value that the function returned to the caller.
- How many parameters the function was called with.
Spies are ideal to use for testing Ajax calls where you don’t care about the practical implementation of the HttpXmlRequest or the network connection.
Here is a method that uses jQuery to retrieve JSON-format data from the server. It accepts two arguments: the callback is an object containing callback functions; the configuration variable contains Ajax related attributes such as the URL and the timeout in milliseconds:
function sendRequest(callbacks, configuration) { $.ajax({ url: configuration.url, dataType: "json", success: function(data) { callbacks.checkForInformation(data); }, error: function(data) { callbacks.displayErrorMessage(); }, timeout: configuration.remainingCallTime }); }
We would like to know:
- Whether or not the correct URL was passed to the $.ajax object.
- That a successful result causes the checkForInformation(data) method to be called.
- That an error results in displayErrorMessage() executing.
To do that, we’ll start by setting the configuration object:
describe("Ajax Tests", function() { var configuration = { url: "ProductData.json", remainingCallTime: 30000 }; });
Here is the test that verifies the URL:
it("should make an Ajax request to the correct URL", function() { spyOn($, "ajax"); sendRequest(undefined, configuration); expect($.ajax.mostRecentCall.args[0]["url"]).toEqual(configuration.url); });
This turns out to be quite a trivial affair because we don’t care what the results of the Ajax call are or that it is even made at all! In fact, to prove the point, I called sendRequest() without any callback methods. The ajax() method spy doesn’t need a callback function because the call doesn’t do anything. All we want to do is verify that the url property of the spy’s mostRecentCall is what we set it to, which you’ll see shortly, it is.
The onsuccess() Test
The next test verifies that a successful result causes the checkForInformation(data) method to be called. Here, we want to simulate a successful Ajax result. To do that, we chain the spyOn() constructor to the andCallFake() method, passing in an anonymous function that calls the Ajax success() event handler. This time around, we’ll need some fake methods to check whether or not they were called, so we replace both checkForInformation() and displayErrorMessage() with spies. After calling sendRequest(), we can use the expect() method to confirm that checkForInformation() was in fact called, while displayErrorMessage() was not:
it("should receive a successful response", function() { spyOn($, "ajax").andCallFake(function(e) { e.success({}); }); var callbacks = { checkForInformation: jasmine.createSpy(), displayErrorMessage: jasmine.createSpy(), }; sendRequest(callbacks, configuration); expect(callbacks.checkForInformation).toHaveBeenCalled(); //Verifies this was called expect(callbacks.displayErrorMessage).not.toHaveBeenCalled(); //Verifies this was NOT called });
The onerror() Test
Our last test ascertains that an error results in displayErrorMessage() executing. Again, the andCallFake() method is chained to the spyOn() constructor call so that Ajax object’s error() method is explicitly invoked. Then, in optimistic fashion, expect() is called with the displayErrorMessage() method and chained to toHaveBeenCalled() for verification:
it("should receive an Ajax error", function() { spyOn($, "ajax").andCallFake(function(e) { e.error({}); }); var callbacks = { displayErrorMessage : jasmine.createSpy() }; sendRequest(callbacks, configuration); expect(callbacks.displayErrorMessage).toHaveBeenCalled(); });
Just be sure to point the CSS and script tags to your installation of Jasmine. A hosted version of jQuery is referenced as well, so don’t try the demo offline.
Conclusion
As mentioned above, using Jasmine spies is an excellent technique for testing asynchronous Ajax calls where you don’t care about the practical implementation of the HttpXmlRequest or the network connection. However, there are times when that is precisely what you want to verify! In those instances, a better approach is to use the Jasmine runs() and waitFor() methods, which were both created especially for that purpose. We’ll take a look at some practical examples in an upcoming article.