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:
- Deferred represents *one* instance of computation. You don't need to worry about your callback being run multiple times unexpectedly (like, for example, if someone called facebook.login() again and you were bound to event like facebook:loggedIn).
- The callbacks will run even if you bind to already resolved promise - you don't have to care whether something is still going on or just finished.
- It's very easy to run things in parallel and wait for all computations to finish - using $.when(oneDeferred, anotherOne, ...).done(doMyStuff).
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 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! :-)
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!