In a previous blog post, I talked about the QUnit unit testing library for JavaScript. In that post, I showed how to use it with a date library I was messing around with. If I were you I would review that previous blog post; it will help in what’s to come.
photo © 2011 Derek Gavey | more info (via: Wylio)Recently, while I was at the Tech·Ed conference working in the DevExpress booth, someone asked me about how to test JavaScript functions that were asynchronous in nature. By this I mean that the function only completes its work asynchronously using a callback. Immediate examples of such functions are those using setTimeout
and those using AJAX calls to get data from the server.
Imagine the following scenario: you’re writing a JavaScript function that accepts a string, sends a request to a server using AJAX with that string, and gets back a string array containing customer names that match that string. Obviously the function that gets the returned string is a callback function and the original function completes immediately the request is sent. You have written a “mock” function on the server that’ll get called and that returns a predefined set of strings. How on earth do you unit test your JavaScript function? Using the information I provided last time, a naïve test JavaScript file might look like this:
jQuery(function () { module("jmbAsync"); test("myAjax success", function () { var success = function (result) { ok(true); // 4 more tests }, error = function (errorType, message) { ok(false, "error called"); }; expect(5); jmbAsync.getCustomerNames("foo", error, success); }); });
To expand: we define a test that calls my special function getCustomerNames
, passing in the text to match (I’m using “foo” in this example) and a function to be called in case of an error, and one in case of success. I’m expecting 5 checks to be made and to pass.
Unfortunately my unit test as written will fail miserably. It will complete well before the server has had time to complete processing the request and return the result and then for the JavaScript run-time to call the success
callback. What to do? Ideally we want to ask QUnit to pause (is that even possible?) or wait for the callbacks to be called before reporting the results.
The answer is to use the asyncTest
function instead. Let’s see how this works.
First of all let me write a getCustomerNames
function. Since in this post I’m really not interested in writing server code at all, I’ll mock it all up in JavaScript instead and use setTimeout
to mimic the delay between client and server.
(function () { jmbAsync = {}; jmbAsync.getCustomerNames = function (search, error, success) { var processResult = function () { if (search === "error") { error("ajaxError", "request failed"); } else if (search === "foo") { var result = ["food", "fool", "foot"]; success(result); } }; setTimeout(processResult, 250); }; })();
So: if the search string is “error” I call the error callback; if it’s “foo” I create a three element array and call the success callback with it. Of course, all this happens one quarter of a second after being called. (To elaborate: the outer function calls the inner function by calling setTimeout
and passing the inner function to it. For convenience, we’ll call this method of calling a function, delay-calling the function. The outer function returns immediately after that. The inner function, processResult
, 1/4 of a second later does the work of testing the passed in search string, available to it through the magic of closures.) We’re now set up with an asynchronous function we can test.
Before seeing the test code, we should learn a little bit how QUnit works under the hood. This will give us an understanding about how two new functions, stop
and start
, work and hence how we can use them in our async unit tests.
When we call test
, as we did previously, QUnit will create an internal Test
object and push it onto an internal queue, ready for processing. Internally, it will also delay-call a function with a very short delay (13 milliseconds, one tick) to pop tests off this queue and process them. In other words, QUnit uses the event loop built into the JavaScript run-time to execute the tests we write. QUnit stops once there are no more test objects to process (that is, the queue is empty). In essence, calling a series of test
statements will queue them all up as objects on the internal queue. The first one will also delay-call the queue processor. Once all the test
calls are complete (they will all execute immediately) the JavaScript run-time will kick off the internal processor and this will dequeue the various tests and run them.
When we write normal synchronous test cases, we don’t notice all this frantic paddling under the hood. Instead we just ‘see’ the tests executing and we get the results immediately. No problem.
Brilliant. I’m sure now that you know this, you’re getting some idea about how this can be altered to support testing async functions.
There are two methods to help. stop
will set a flag to stop the internal processor from dequeuing test objects and processing them. Instead, it will delay-call itself with the same short fuse as before and terminate. (Note that this means the internal processor is continually delay-calling itself until the flag is cleared. This does not matter: using the delay-call means the browser is fully responsive and we won’t trigger the “script is taking too long” browser error.)
start
does the opposite: it will clear the flag. The next time the internal processor is called by the run-time (the short timeout has expired), it’ll start processing the test objects in the queue again.
Using these two new functions, our test code now looks like this:
jQuery(function () { module("jmbAsync"); test("myAjax success", function () { var success = function (result) { ok(true); // 4 more tests start(); }, error = function (errorType, message) { ok(false, "error called"); start(); }; stop(); expect(5); jmbAsync.getCustomerNames("foo", error, success); }); });
The first thing we do in our test
callback is to call stop()
. As mentioned before, this will stop the internal processor from running any test cases. We then do the same as before: we set the expectation of 5 checks to pass, and call the asynchronous function under test. Note the changes to the success and error callbacks. I’ve added a call to start
in both (you can put it anywhere in the function: remember all it does is to clear a flag; the internal processor won’t get started until we’ve finished).
What this does then is, essentially, to force QUnit to wait until the async function’s callbacks are executed and completed. It’s like magic. We’ve turned an asynchronous process into a synchronous one.
Notice that when you write tests for async functions, you always find yourself calling stop()
immediately in the test callback. To help deal with this duplication, QUnit provides the asyncTest
function that does it automatically.
Here’s the full set of tests I wrote to test my fake getCustomerNames function:
jQuery(function () { module("jmbAsync"); asyncTest("myAjax success", function () { var success = function (result) { ok(true); ok(result, "check resulting object is not undefined/null"); equal(result[0], "food", "check 1st element to be food"); equal(result[1], "fool", "check 2nd element to be fool"); equal(result[2], "foot", "check 3rd element to be foot"); start(); }, error = function (errorType, message) { ok(false, "error called"); start(); }; expect(5); jmbAsync.getCustomerNames("foo", error, success); }); asyncTest("myAjax error", function () { var success = function (result) { ok(false, "success called"); start(); }, error = function (errorType, message) { ok(true); start(); }; expect(1); jmbAsync.getCustomerNames("error", error, success); }); });
Notice that in using asyncTest
, I no longer have to call stop
. It’s all taken care of for me. If you want to run these tests yourself, click here.
Now playing:
Sting - Saint Agnes and the Burning Train
(from The Soul Cages)
No Responses
Feel free to add a comment...
Leave a response
Note: some MarkDown is allowed, but HTML is not. Expand to show what's available.
_emphasis_
**strong**
[text](url)
`IEnumerable`
* an item
1. an item
> Now is the time...
Preview of response