A Mock for {set,clear}{Timeout,Interval}

Here’s a potential JSSpec spec for Sequentially.trickle.map:

describe('Sequentially.trickle.map', {
  'should apply to all the elements': function() {
    Sequentially.trickle.map(
      ['a', 'b', 'c'],
       function(x) { return x + 1 },
       1,
       function(result) {
         value_of(result.join(',')).should_be('a1,b1,c1');
       });
    });
  }
});

This doesn’t work. The problem is that Sequentially.trickle.map is asynchronous (it defers most of its computation – including the invocation of the callback – via setTimeout). This means that should_be isn’t called until after the spec has returned. If it succeeds, this isn’t a problem, but if it fails, JSSpec can’t associate it with the failing spec – worse, JSSpec will have already have marked it successful.

Here’s the version that I actually used:

describe('Sequentially.trickle.map', {
  'should apply to all the elements': function() {
    withMockTimers(function() {
      Sequentially.trickle.map(
        ['a', 'b', 'c'],
         function(x) { return x + 1 },
         1,
         function(result) {
           value_of(result.join(',')).should_be('a1,b1,c1');
         });
      });
    }
});

withMockTimers temporarily replaces setTimeout and friends with its own deferred execution system, so that it can make sure to call them all before it returns. Get it here.

Limitations

This approach has its limits. It doesn’t mock new Date to pretend that more time has passed, so whether it works or not will depend on how your code uses the timers (if it keeps an interval running or re-submitting a timeout until an amount of time measured on new Date has passed, it will probably get a “script running slowly” error). And, I don’t know how kosher it is to replace setTimeout – this let me test against Firefox 2.0 and Safari 3.1; I haven’t tried on Opera and MSIE. Nonetheless, it got me what I wanted here – unit tests for the new methods in Sequentially.

(I’ve got a more involved implementation that patches the test suite to run callbacks within an emulation of the dynamic scope of the original test function, but it’s tricky, and I haven’t got it integrated with JSSpec – or any other browser JavaScript implementation – yet.)