Jasmine failed to spy function called within the async.waterfall block

I have a node.js app, using async utilities, to break nested callbacks.

And I'm trying to spy functions which enclosed by async.waterfall in my jasmine specs, but always get failures.

The following code can reproduce the error:

async = require 'async'

app = hi: ->

fn = ->
  # app.hi() # works
  async.waterfall [
    (cb) ->
      app.hi() # doesn't work
      cb null
  ], (err) ->

describe 'jasmine', ->
  beforeEach ->
      spyOn app, 'hi'
  it 'test async.waterfall', ->
    spyOn app, 'hi'
    fn()
    expect(app.hi).toHaveBeenCalled()

The failure messages:

Failures:

  1) jasmine test async.waterfall
   Message:
     Expected spy hi to have been called.
   Stacktrace:
     Error: Expected spy hi to have been called.
    at new jasmine.ExpectationResult (/Volumes/ws/prj/litb/crm/node_modules/jasmine-node/lib/jasmine-node/jasmine-1.3.1.js:114:32)
    at null.toHaveBeenCalled (/Volumes/ws/prj/litb/crm/node_modules/jasmine-node/lib/jasmine-node/jasmine-1.3.1.js:1235:29)
    at null.<anonymous> (/Volumes/ws/prj/litb/crm/tests/job/indexSpecs.coffee:51:29)
    at jasmine.Block.execute (/Volumes/ws/prj/litb/crm/node_modules/jasmine-node/lib/jasmine-node/jasmine-1.3.1.js:1064:17)
    at jasmine.Queue.next_ (/Volumes/ws/prj/litb/crm/node_modules/jasmine-node/lib/jasmine-node/jasmine-1.3.1.js:2096:31)
    at jasmine.Queue.start (/Volumes/ws/prj/litb/crm/node_modules/jasmine-node/lib/jasmine-node/jasmine-1.3.1.js:2049:8)
    at jasmine.Spec.execute (/Volumes/ws/prj/litb/crm/node_modules/jasmine-node/lib/jasmine-node/jasmine-1.3.1.js:2376:14)
    at jasmine.Queue.next_ (/Volumes/ws/prj/litb/crm/node_modules/jasmine-node/lib/jasmine-node/jasmine-1.3.1.js:2096:31)
    at jasmine.Queue.start (/Volumes/ws/prj/litb/crm/node_modules/jasmine-node/lib/jasmine-node/jasmine-1.3.1.js:2049:8)
    at jasmine.Suite.execute (/Volumes/ws/prj/litb/crm/node_modules/jasmine-node/lib/jasmine-node/jasmine-1.3.1.js:2521:14)
    at jasmine.Queue.next_ (/Volumes/ws/prj/litb/crm/node_modules/jasmine-node/lib/jasmine-node/jasmine-1.3.1.js:2096:31)
    at onComplete (/Volumes/ws/prj/litb/crm/node_modules/jasmine-node/lib/jasmine-node/jasmine-1.3.1.js:2092:18)
    at jasmine.Suite.finish (/Volumes/ws/prj/litb/crm/node_modules/jasmine-node/lib/jasmine-node/jasmine-1.3.1.js:2478:5)
    at null.onComplete (/Volumes/ws/prj/litb/crm/node_modules/jasmine-node/lib/jasmine-node/jasmine-1.3.1.js:2522:10)
    at jasmine.Queue.next_ (/Volumes/ws/prj/litb/crm/node_modules/jasmine-node/lib/jasmine-node/jasmine-1.3.1.js:2106:14)
    at onComplete (/Volumes/ws/prj/litb/crm/node_modules/jasmine-node/lib/jasmine-node/jasmine-1.3.1.js:2092:18)
    at jasmine.Spec.finish (/Volumes/ws/prj/litb/crm/node_modules/jasmine-node/lib/jasmine-node/jasmine-1.3.1.js:2350:5)
    at null.onComplete (/Volumes/ws/prj/litb/crm/node_modules/jasmine-node/lib/jasmine-node/jasmine-1.3.1.js:2377:10)
    at jasmine.Queue.next_ (/Volumes/ws/prj/litb/crm/node_modules/jasmine-node/lib/jasmine-node/jasmine-1.3.1.js:2106:14)
    at null._onTimeout (/Volumes/ws/prj/litb/crm/node_modules/jasmine-node/lib/jasmine-node/jasmine-1.3.1.js:2086:18)
    at Timer.listOnTimeout [as ontimeout] (timers.js:110:15)

But it passes, if the function call (which should be a spy) is outside the waterfall block.

I'd like to know if there's something wrong with my code? or it's not supported by jasmine or async?

Since waterfall is asynchronous, the spec is completing without realizing it should wait for the waterfall to happen. You can use jasmine's async support to work around this, although jasmine's async stuff is considered to not be that great.

it 'test async.waterfall', ->
  spy = spyOn app, 'hi'
  runs ->
    fn()

  waitsFor ->
    spy.callCount > 0

  runs ->
    # kind of redundant at this point, 
    # the waitsFor already asserted this
    expect(app.hi).toHaveBeenCalled()

another approach is to make setTimeout and/or `setInterval not be asynchronous in your test environment. This has drawbacks too:

beforeEach ->
  # I'm sure jasmine's spys can handle this too
  # I use sinon myself, not as familiar with jasmine's spies
  @realSetTimeout = window.setTimeout
  window.setTimeout = (fn, delay) -> fn()

afterEach ->
  window.setTimeout = @realSetTimeout

it 'test async.waterfall', ->
  # as you have it now