Testing Asynchronous JavaScript
There seems to be a common misconception in the JavaScript community that testing asynchronous code requires a different approach than testing ‘regular’ synchronous code. In this post I’ll explain why that’s not generally the case. I’ll highlight the difference between testing a unit of code which supports async behavior, as opposed code which is inherently asynchronous. I’ll also show how promise-based async code lends itself to clean and succinct unit tests which can be tested in a clear, readable way while still validating async behaviour.
18 September 2013
Async-compatible code vs. Intrinsically async code
Most ‘asynchronous’ code which we write as JavaScript developers is not intrinsically asynchronous. For example, let’s look at a simple ajax operation implemented using JQuery:
var callback = function(){ alert('I was called back asynchronously'); }; $.ajax({ type: 'GET', url: 'http://example.com', done: callback });
You might look at that and say “that's asynchronous code”. It has a callback, right? Actually it's not inherently asynchronous, it just supports the potential to be used in an async context. Because $.ajax
provides the result of the AJAX call to its caller via callbacks rather than by using a return value (or exception) it is able to implement its operations asynchronously, but in fact it could choose not to. Imagine this fake version of $.ajax
:
function ajax( opts ){ opts.done( "fake result" ); }
This is obviously not a complete XHR implementation ;) but more importantly it is also not an asynchronous implementation. By calling the done
callback directly from within the method implementation itself we're flattening a potentially asynchronous operation into a fully synchronous operation. What effect would that have on clients of ajax
? If we called our fake ajax method like this:
console.log( 'calling ajax...' ); ajax({ done: function(){ console.log( 'callback called' ); } }); console.log( '...called ajax' );
What we'd see would be output along the lines of:
calling ajax... callback called ...called ajax
As you can see, our log entries are written in the order they were defined, because the fake ajax
function we're using is a synchronous implementation of a potentially async API. All of this code will be executed by the runtime synchronously, in a single turn through the event loop 1.
1:
There is a strong argument that even if you can resolve an async operation inline you should still preserve a consistent call sequence by deferring your response to a subsequent turn around the event loop (see the next footnote for more on that!) Deferring execution of a callback is usually achieved using setImmediate
or similar.
Another way to think about this is that method calls which operate synchronously are a special case of the more general asynchronous case. Synchronous code is just async code where the result is returned within the context of the originating call.
Testing async-supporting code
Hopefully I have succeeded in demonstrating that most of the JavaScript code we write isn't inherently async, it just supports async behaviour because it calls async-capable APIs - those that use callbacks (or promises). But why should we care about this? The code we write will always be used in an async context, right? Well, no, not when we want to write unit tests for our async-supporting code.
Let's continue with an example using $.ajax
. Imagine we're writing a function that fetches a JSON description of the current user from a URL and creates a local User object based on that JSON. Our implementation might look something like this:
function parseUserJson(userJson) { return { loggedIn: true, fullName: userJson.firstName + " " + userJson.lastName }; }; function fetchCurrentUser(callback) { function ajaxDone(userJson) { var user = parseUserJson(userJson); callback(user); }; return $.ajax({ type: 'GET', url: "http://example.com/currentUser", done: ajaxDone }); };
In fetchCurrentUser
we initiate a GET request to a url, supplying an ajaxDone
callback which will be executed asynchronously once the request comes back with a response. In that callback we take the JSON we got back from the response, parse it using the parseUserJson
function to create a (rather anemic) User domain object. Finally, we call the callback that was initially passed in to fetchCurrentUser
, passing the user object as a parameter.
Let's look at how we would we write a unit test for this code (there is a list of the tools and libraries used during testing at the bottom of this post). For example, how would we test that fetchCurrentUser
parses JSON into the appropriate User object? Before realizing the distinction between intrinsically async code and async-supporting code we might think that we need some sort of asynchronous unit test to test this asynchronous code. But now we understand that the code we're testing isn't inherently async. We can flatten its execution into a synchronous flow for testing purposes. Let's see what that might look like:
describe('fetchCurrentUser', function() { var simulatedAjaxResponse = { firstName: "Tomas", lastName: "Jakobsen" }; $.ajax = function(ajaxOpts) { var doneCallback = ajaxOpts.done; doneCallback(simulatedAjaxResponse); }; function fetchCallback(user) { expect(user.fullName).to.equal("Tomas Jakobsen"); }; fetchCurrentUser(fetchCallback); }); });
The first thing we do is to replace $.ajax
with a stub function which allows us to simulate an ajax response coming back (messing with $.ajax
directly is pretty gross, but I don't want to get into dependency management in this particular post so we'll hold our nose). We then invoke the fetchCurrentUser
function which is the subject of our test. Because fetchCurrentUser
needs to support async fetching it takes a callback. Our goal in this test is to inspect the eventual result of calling fetchCurrentUser
, which means we need to provide a callback to it which will receive the user object which is eventually created. That callback uses Chai's expect-style assertions to verify that the user's fullName
property has been initialized correctly.
It's important to note that this test will execute in a totally synchronous fashion. Just as with our previous example it will complete within a single turn of the event loop.
The catch
There is a catch with this testing approach. What happens if we accidentally comment out an important line in our production code like so:
function fetchCurrentUser(callback) { function ajaxDone(userJson) { var user = parseUserJson(userJson); //callback(user); }; return $.ajax({ type: 'GET', url: "http://example.com/currentUser", done: ajaxDone }); };
This function as it stands won't work correctly. It'll never call the callback passed in to fetchCurrentUser
, meaning no user object will ever be returned. You'd expect that our test would verify this, since it's explicitly checking the value of the user's fullName
property.
However, that's not the case. The test will continue to pass. Why is that? Well, we put our assertions inside the callback which is never executed! The bug means that parts of our test are never called and thus never exercised.
A naive approach to solving this might look something like:
describe('fetchCurrentUser', function() { it('creates a parsed user', function() { var simulatedAjaxResponse = { firstName: "Tomas", lastName: "Jakobsen" }; $.ajax = function(ajaxOpts) { var doneCallback = ajaxOpts.done; doneCallback(simulatedAjaxResponse); }; function fetchCallback(user) { expect(user.fullName).to.equal("Tomas Jakobsen"); callbackCalled = true; }; var callbackCalled = false; fetchCurrentUser(fetchCallback); expect(callbackCalled).to.be.true; }); });
This is the same test as before, except now we're also tracking whether the callback was ever called. This test will now correctly detect that our callback is not being exercised and will fail, but it's a bit clunky. If we're using Mocha as our test runner (as opposed to Jasmine for example) then we have a slightly better option:
describe('fetchCurrentUser', function() { it('creates a parsed user', function(done) { var simulatedAjaxResponse = { firstName: "Tomas", lastName: "Jakobsen" }; $.ajax = function(ajaxOpts) { var doneCallback = ajaxOpts.done; doneCallback(simulatedAjaxResponse); }; function fetchCallback(user) { expect(user.fullName).to.equal("Tomas Jakobsen"); done(); }; fetchCurrentUser(fetchCallback); }); });
Note that our it
block now takes a done
parameter. If Mocha sees that your it
block expects a parameter then it will treat the test as an async test. It will provide a done
function to your test, and in return your test is required to call done()
to tell Mocha that the test is complete. The call to done()
is our test marking the end of its control flow. This means that Mocha is able to detect whether a test reached the end of its control flow, and fail the test if that doesn't happen. Note this also means your tests can be truly asynchronous if required and execute through multiple turns of the run loop.
This simple way of explicitly indicating when callback-centric test code has concluded is one reason why some people to prefer Mocha over Jasmine. Jasmine does support async tests too, but the mechanism is considerably clunkier than the done()
function.
Good, but not great
OK, so we've seen that it's very possible to write unit tests for callback-oriented async-supporting code. However I'll be the first to admit that the tests aren't very easy to read. It feels like there's a lot of plumbing code, and the callback-centric nature of the code leaks into our tests. I consider this a classic case of our tests helping inform us of a 'code smell'. If the tests look bad or are hard to write then there's probably something wrong with the way the code under test was designed.
Next up I'll argue that switching from callbacks to Promises will help us reduce this code smell, leading to cleaner code with correspondingly pleasant tests.
A whirlwind introduction to Promises
I'm going to switch from callback-oriented async code to promise-oriented for the rest of this article. I find that Promises lead to an implementation which models the control flow of async-supporting code in a declarative way. Likewise I think Promises make it easier to reason about unit tests for async-supporting code. I'll give a very brief overview of Promises here. If you haven't used them before then I strongly recommend learning more about them as a first step towards using them to improve your own async code.
I like to think of Promises as an object-oriented version of callbacks, with some bonus features tacked on. With traditional callback-oriented code when you call an async function you pass in a callback which will be invoked once the async operation completes. In that one function call you are both asking for some async work to be performed and also specifying what's next to do once the work is complete. With Promises the request for an async operation is broken apart from what to do afterwards. You invoke the async operation as before, but the caller does not provide a callback as an argument to the async function. Instead, the async function returns a Promise object to the caller. The caller then registers a callback onto that Promise. You call the function to invoke the async operation, and then you separately say what you want to do after the operation completes by interacting with the Promise which the function returns.
So instead of this callback-oriented code:
var callback = function(){ alert('I was called back asynchronously'); }; someAsyncFunction( "some", "args", callback );
you would do this with promise-oriented code:
var callback = function(){ alert('I was called back asynchronously'); }; var promise = someAsyncFunction( "some", "args" ); promise.done( callback );
In most cases you would use jQuery-style method chaining and anonymous functions, resulting in something like:
someAsyncFunction( "some", "args" ).done( function(){ alert('I was called back asynchronously'); });
That's just the very basic functionality that a Promise library provides - there's a lot more to take advantage of. I covered Promises in a bit more detail in a previous blog post. I'd encourage you to read that for more information. That post also includes a more complex example which shows how some of the more advanced features of a Promise library can help remove some of the tedious async plumbing from your code, helping keep the focus on the actual problem being solved.
Dominic Denicola also does a great job of explaining why Promises are so useful in this post. Highly recommended reading.
Porting our async code to promises
In our previous example we had the following callback-oriented implementation:
function parseUserJson(userJson) { return { loggedIn: true, fullName: userJson.firstName + " " + userJson.lastName }; }; function fetchCurrentUser(callback) { function ajaxDone(userJson) { var user = parseUserJson(userJson); callback(user); }; return $.ajax({ type: 'GET', url: "http://example.com/currentUser", done: ajaxDone }); };
Here's what a promise-oriented implementation would look like:
function parseUserJson(userJson) { return { loggedIn: true, fullName: userJson.firstName + " " + userJson.lastName }; }; function fetchCurrentUser() { return Q.when($.ajax({ type: 'GET', url: "http://example.com/currentUser" })).then(parseUserJson); };
As before fetchCurrentUser
initiates a GET request to a url, but rather than passing a callback directly to the $.ajax
function we instead take the promise returned from that function and chain the parseUserJson
function onto it. In this way we arrange for the JSON response coming from the $.ajax
call to flow through to our parser function - where it is transformed into a parsed User object - and then continue to flow through to whatever further promise pipeline had been set up by the caller to fetchCurrentUser
.
Note that I'm using the excellent Q library to enhance the less-than-perfect $.Deferred
promise implementation that JQuery will return from the $.ajax(...)
call. The post from Dominic which I referenced earlier also discusses what's missing from $.Deferred
in more detail.
I find this promise-oriented implementation easier to read than the callback-oriented code, and by replacing the primitive callbacks with a promise object the options for extending how it works are much greater. We can take the promise returned from something like $.ajax
and then build upon it to construct a pipeline of operations which operate on a value, transforming it as it moves through the pipeline.
The tests for this promise-oriented implementation should also illustrate that it leads to more readable code:
describe('fetchCurrentUser', function() { it('creates a parsed user', function(done) { var simulatedAjaxResponse = { firstName: "Tomas", lastName: "Jakobsen" }; $.ajax = function(){ return Q(simulatedAjaxResponse); } var userPromise = fetchCurrentUser(); userPromise.then(function(user) { expect(user.fullName).to.equal("Tomas Jakobsen"); done(); }); }); });
This test is conceptually identical to the callback-oriented test we had before. We replace $.ajax
with a fake implementation that just returns a hardcoded simulated response, wrapped in a pre-resolved Q promise. Then we call the fetchCurrentUser
function. Finally we verify that what comes out the other end of our promise pipeline has the appropriate .fullName
property.
I'd argue that this promise-oriented form is easier to read, and easier to refactor. There's more though! Because promises act as an encapsulation of an async operation we can also enhance our test runner with nice libraries like chai-as-promised which will let us refactor our test code into:
describe('fetchCurrentUser', function() { it('creates a parsed user', function(done) { var simulatedAjaxResponse = { firstName: "Tomas", lastName: "Jakobsen" }; $.ajax = function(){ return Q(simulatedAjaxResponse); } var fetchResult = fetchCurrentUser(); return expect( fetchResult ).to.eventually .have.property('fullName','Tomas Jakobsen') .notify(done); }); });
We can take this further. With the addition of mocha-as-promised we no longer need to explicitly tell Mocha when our test's control flow has concluded:
describe('fetchCurrentUser', function() { it('creates a parsed user', function() { var simulatedAjaxResponse = { firstName: "Tomas", lastName: "Jakobsen" }; $.ajax = function(){ return Q(simulatedAjaxResponse); } var fetchResult = fetchCurrentUser(); return expect( fetchResult ).to.eventually .have.property('fullName','Tomas Jakobse'); }); });
Here we've gotten rid of the done
function trick. Instead we pass the promise chain that represents the asynchronous control flow of our test back to the Mocha test runner, where the mocha-as-promised extensions are able to do the background plumbing of making sure that the promise chain concludes before moving on to the next test. This is such a smart feature that it comes built in to other test runners such as Buster.
This is a good example of why promises are so powerful. By reifying the concept of async control flow we can actually pass the control flow back to the test runner where it can then operate on that control flow directly. Crucially, our tests don't really need to know what the framework is doing. They just pass the control flow back to the caller and let it take over.
Essentially promises allow us to separate the concern of invoking an operation from the concern of handling the result of that operation. That means we can have our test code simulate one half and test how our production code handles the other half.
Testing inherently async code
I've illustrated that we can test async-supporting code in an essentially synchronous way, but what if you want to test code that really is inherently async?
Mocha's support for async testing means that it is very possible to test truly asynchronous code using the same techniques I've been showing for testing async-supporting code in a synchronous way. However, it's actually rare for 'normal' JavaScript code to be inherently async. Inherently async means code which explicitly abandons its turn around the event loop - for example by calling setTimeout
- or code which calls a native non-blocking API such as XMLHttpRequest
. How often do we write code that does that directly? Not often I'd say2. We integrate with code that does that in the form of libraries like JQuery, but as I've demonstrated in this post we can still test that integration in a synchronous way since that integration code is not inherently async.
2:
I'm sneakily glossing over the fact that a promises implementation that complies with Promises/A+ - such as Q - is required to NOT resolve within a single turn of the event loop.
If you don't know what that means, don't worry about it too much. If you do, then forgive me for not going down that rat hole. I'd argue that conceptually promised-oriented code is still flattened out to a synchronous order even if it completes in two turns rather than one.
Aggressively limit the number of truly asynchronous tests you write and maintain
I would assert that there are few cases where you need to unit test inherently async code, unless you are writing a low-level library. You may need to test code that rubs up against libraries that are inherently async, but you won't often be creating that sort of code yourself, and thus won't often find a need to write tests for that sort of code. In fact, my advice is to aggressively limit the number of truly asynchronous tests you write and maintain. Martin Fowler's excellent article on non-deterministic tests does a thorough job of explaining why these types of tests tend to be detrimental to the overall health of your test suite.
If you do find yourself writing significant portions of inherently async code then it's likely you need to take a step back and identify a small, containable area of your code which handles all that icky async stuff. It's code that's hard to write and hard to test. Isolate it, and test the snot out of it with a different type of test (i.e. an integration test). Gerald Meszaros's documentation of the Humble Object pattern provides a nice explanation of some ways to isolate your truely async code in a clean way. Another wonderful source of advice on testing and containing this sort of tricky code is the GOOS book, which talks about testing of asynchronous code at various levels in some detail.
At the end of the day I'd guess that most JavaScript 'unit tests' which need async functionality are in fact higher-level integration tests which are calling out to databases, the DOM, web APIs, etc. Those sort of tests are good and valuable, but it's important to understand that they're a different type of test and that you'll almost certainly get better value from a larger suite of more isolated unit tests. But that's a post for another day.
Colophon: tools and libraries used
In my code examples I used the Q promises implementation. For testing I used the Mocha test runner paired with the Chai test assertion library. I enhanced this testing setup with the mocha-as-promised and chai-as-promised libraries. I ran the tests on node.js, using npm to declare and install the tools and libraries mentioned above.
All test code was isolated from any need for a DOM to be present and also didn't require JQuery (since we always replaced $.ajax
with a test double).
Footnotes
1: be consistently async
There is a strong argument that even if you can resolve an async operation inline you should still preserve a consistent call sequence by deferring your response to a subsequent turn around the event loop (see the next footnote for more on that!) Deferring execution of a callback is usually achieved using setImmediate
or similar.
2: Promises/A+ spec compliance
I'm sneakily glossing over the fact that a promises implementation that complies with Promises/A+ - such as Q - is required to NOT resolve within a single turn of the event loop.
If you don't know what that means, don't worry about it too much. If you do, then forgive me for not going down that rat hole. I'd argue that conceptually promised-oriented code is still flattened out to a synchronous order even if it completes in two turns rather than one.
Significant Revisions
18 September 2013: second edition, adding coverage of promises
03 September 2013: release first edition