Testing deferreds

18 February 2012 - Wrocław

If you've carefully read my last post and its sample code, you may have spotted how I'm using deferreds for FacebookAdapter. While writing tests for the example, I came up with an idea, how we could test methods that return deferreds.

What are deferreds?

Do you use deferreds in your projects? It's a very good pattern for dealing with asynchronous code. The idea is that async function instead of accepting callbacks as arguments will return a "promise" object. You can bind you own callbacks to promise success/failure and they will run once promise is "resolved" or "rejected".

Here's an example:

@facebook.login().done =>
  @view.showLinksForLoggedIn()
  @view.hideLogin()

class FacebookAdapter
  login: =>
    $.Deferred (dfr) =>
      # do some Facebook async stuff here, and call dfr.resolve() when ready
    .promise()

Once dfr.resolve() is called, every function that you bound to the promise with done() will be called. Conversely, calling dfr.reject() will run callbacks bound with fail() method.

You'll find more detailed explanation in slides from my talk presented at RailsCamp 2011 and of course jQuery docs. There are many implementations of deferred pattern other than the jQuery one, like this one by medikoo (but it's seems targeted at Node.js).

Deferreds vs events

The other common pattern to structure async code is using events. They are great in many cases, like responding to user actions. However, deferreds are better when dealing with async computations or communication:

Back to testing

When dealing with callback-based async code, Jasmine tests usually involve spying on the async method and running callback after the spy was called. There was even an example of this approach in samples for my previous post:

spyOn(facebook, "showWallPostDialog")
# later, when facebook.showWallPostDialog has been called
callback = facebook.showWallPostDialog.mostRecentCall.args[1]
callback()

This is hardly elegant or readable, so I thought we could do better with deferreds. What about this?

spyOn(facebook, "showWallPostDialog").andReturnDeferred()
# later, when facebook.showWallPostDialog has been called
facebook.showWallPostDialog.resolve()

It turns out to be quite easy to implement in Jasmine. When writing the last post, it was quick'n'dirty trick, but today I've put together a cleaner implementation (and I've updated FacebookAdapter mock in example for previous post). I really like Jasmine's simplicity and hackability. It's so easy to hook into the library and create all kinds of things you need.

Please keep in mind we're extending a class we dont own (jasmine.Spy), so I'd advise to be careful, especially when upgrading Jasmine or using other 3rd party extensions.

I hope you've enjoyed reading this post as much as I've enjoyed writing it! :-)

Post scriptum

Have you already bought your ticket for wroc_love.rb? It's going to be this year's best opportunity to drink beers and discuss with other hackers!

Comments