Bites

Better asynchronous jasmine-node specs

Alon Salant on

Update 2/5/2012: as of version 1.0.15 jasmine-node includes this enhancement to support asynchronous specs. The original post follows…

We’re using jasmine-node for BDD-style testing of our node apps. It’s not an amazing implementation of jasmine for node but it gets the job done.

One of my issues with jasmine-node is its awkward support for asynchronous tests with the global asyncSpecWait and asyncSpecDone methods.

Inspired by the BDD style supported by Mocha, I decided that our jasmine specs should support asynchronous specs in the same style where it, beforeEach and afterEach wait until the spec is finished if passed a function that expects a done callback.

describe 'User', ->
  describe '#save()', ->
    it 'should save without error', (done) ->
      user = new User('Luna')
      user.save (error) ->
        expect(error).toBeNull()
        done();

The done callback will fail the spec if passed an error, so even more succinctly:

describe 'User', ->
  describe '#save()', ->
    it 'should save without error', (done) ->
      user = new User('Luna')
      user.save(done)

In order to modify the behavior of the jasmine spec methods, I wrote a spec helper that monkey patches it, beforeEach and afterEach to run with asynchronous support if the spec is written to expect a done handler. Here’s a Gist of my file async_helper.coffee that I have in my spec/support folder.

# Monkey patch jasmine.Env to wrap spec ('it', 'beforeEach', 'afterEach')
# in async handler if it expects a done callback
withoutAsync = {}
for jasmineFunction in [ "it", "beforeEach", "afterEach"]
  do (jasmineFunction) ->
    withoutAsync[jasmineFunction] = jasmine.Env.prototype[jasmineFunction]
    jasmine.Env.prototype[jasmineFunction] = (args...) ->
      specFunction = args.pop()
      # No async callback expected, so not async
      if specFunction.length == 0
        args.push specFunction
      else
        args.push -> asyncSpec(specFunction, @)

      withoutAsync[jasmineFunction].apply @, args

# Run any function, failing the current spec if there is an error or it times out
asyncSpec = (specFunction, spec, timeout = 1000) ->
  done = false
  spec.runs ->
    try
      specFunction (error) ->
        done = true
        spec.fail(error) if error?
    catch e
      # if we hit an exception before any async code, mark the spec done
      done = true
      throw e
  spec.waitsFor ->
    done == true
  , "spec to complete", timeout

The use of Function.length to check how many arguments a function expects was a new and very handy trick that made this spec style possible. Much better!!