← recent

Coffeescript, VowsJS, & Asynchronous Testing

Before releasing my coffeescript RRD library, I needed to write some tests around it (RDT: Release-Driven Testing). I went with Vows (in coffeescript), and I had no problems writing tests around the few synchronous (gasp) functions, but the asynchronous parts just didn’t work. At first, I punted on it, and switched my vows from coffeescript to javascript (which all the docs/examples are in). Later, when I came back to it, I was able to figure out the problem, and a dirty hack to get around it.

The Problem This code is the coffeescript version of a simple vows async test. The code waits 2 seconds, then calls the callback with a static string, and the test asserts that the callback was called with that string. Pretty simple. The test fails, though. Instead of 'results', the second argument to the callback seems to be the return value of setTimeout(). From the debug output, you can see that the assertion is actually being evaluated (and failing) before the callback has triggered.

vows = require('vows')                            
assert = require('assert')

class Downloader                                  
  getAsync: (param, cb) ->                        
    setTimeout () ->                              
      console.log("\nDEBUG: CALLING CALLBACK")    
      cb(undefined, 'results')                    
    , 2000

vows.describe('Downloader').addBatch(             
  'getAsync':                                     
    topic: () ->                                  
      (new Downloader).getAsync('path', @callback)

'has results': (err, result) ->               
      console.log("\nDEBUG: ASSERTION")           
      assert.equal result, 'results'              
).run()

# DEBUG: ASSERTION                                
#                                                 
# ✗                                               
#   getAsync                                      
#     ✗ has results                               
#       » expected 'results',                     
#         got      {                              
#     _idleTimeout: 2000,                         
#     _onTimeout: [Function],                     
#     _idlePrev: {                                
#         repeat: 2,                              
#         _idleNext: ..,                          
#         _idlePrev: ..,                          
#         callback: [Function]                    
#     },                                          
#     _idleNext: {                                
#         repeat: 2,                              
#         _idleNext: ..,                          
#         _idlePrev: ..,                          
#         callback: [Function]                    
#     },                                          
#     _idleStart: Fri, 29 Jul 2011 19:54:24 GMT   
# } (==) // vows.js:93                            
#                                                 
# ✗ Broken » 1 broken (0.005s)                    
#                                                 
# DEBUG: CALLING CALLBACK                         
# DEBUG: ASSERTION                                                              

The Solution Digging through the vows docs, the answer was here:

Note that topics which make use of ‘this.callback’ must not return anything.

This is a problem for coffeescript, which implicitly returns the result of the last evaluation from functions. Since “don’t return anything” is the same as returning undefined, the somewhat dirty solution is to add a one-line undefined at the end of the topic, which is equivalent to not returning anything, and will signal to vows that the tests should be async.

vows = require('vows')                            
assert = require('assert')

class Downloader                                  
  getAsync: (param, cb) ->                        
    setTimeout () ->                              
      console.log("\nDEBUG: CALLING CALLBACK")    
      cb(undefined, 'results')                    
    , 2000

vows.describe('Downloader').addBatch(             
  'getAsync':                                     
    topic: () ->                                  
      (new Downloader).getAsync('path', @callback)
      undefined

'has results': (err, result) ->               
      console.log("\nDEBUG: ASSERTION")           
      assert.equal result, 'results'              
).run()

# DEBUG: CALLING CALLBACK                         
# DEBUG: ASSERTION                                
# ·                                               
# ✓ OK » 1 honored (2.003s)                       

Note: Another solution would be to write your async code another way (perhaps with events.EventEmitter). I do want to play around with that style of async coding sometime, but for the sake of not letting the testing framework determine my code implementation, I wanted to to stick with the callback pattern through this problem.

Update: You can use return undefined or just return to actually prevent coffeescript’s implicit returning for the function. Effectively the same, but probably philosophically more correct. (Thanks michaelchisari & jashkenas).