Saturday, September 6, 2008

Mocking in JavaScript

This post is kind of like a postmortem for my JSMock project. The project does not follow this post (but that may chane).

I ran into this issue repeatedly when I was writing tests in JavaScript: The function I want to test, makes calls to a function I don't want to be called (window.alert, Effect.BlindUp, new SomeObject, ... etc). Often I would write tests like:

testSomething: function() {
  window.setTimeout = function(){};
  var timer = setTimeout(function(){ alert('hi'); }, 100);
  assert(timer);
}

The problem with this is it leaves window.setTimeout as a empty function, and later calls to window.setTimeout don't do anything. What we need to do is store the original function before we override it, then restore it at the end of the test:

testSomething: function() {
  var Original_Window_SetTimeout = window.setTimeout;
  window.setTimeout = function(){};
  var timer = setTimeout(function(){ alert('hi'); }, 100);
  assert(timer);
  window.setTimeout = Original_Window_SetTimeout;
}

Cool! Now lets try to automate this:

Object.prototype.mock = function(funcName, block){
  var originalFunc = this[funcName];
  this[funcName] = function(){};
  block();
  this[funcName] = originalFunc;
}
...
testSomething: function() {
  window.mock('setTimeout', function(){
    var timer = setTimeout(function(){ alert('hi'); }, 100);
    assert(timer);
  });
}

Handy! But what if I want to test that window.setTimeout was called? We can do this by adding a variable to our new mock of window.setTimeout. See the wasCalled variable:

Object.prototype.mock = function(funcName, block){
  var originalFunc = this[funcName],
      that = this;
  that[funcName] = function(){ that[funcName].wasCalled = true; };
  that[funcName].wasCalled = false;
  block();
  that[funcName] = originalFunc;
}
...
testSomething: function() {
  window.mock('setTimeout', function(){
    var timer = setTimeout(function(){ alert('hi'); }, 100);
    assert(timer);
    assert(window.setTimeout.wasCalled);
  });
}

Of course you can set other variables if you would like, such as a callCount; but once the mock is finished, these variables are gone. Lastly I'm going to toss in a few things for fun. I'm going to add a reference to the original function, and have the mock return the mocked function.

Object.prototype.mock = function(funcName, block){
  var originalFunc = this[funcName],
      that = this,
      mock = that[funcName] = function(){
        that[funcName].callCount++;
      };
  that[funcName].callCount = 0;
  block(originalFunc);
  that[funcName] = originalFunc;
  return(mock);
}
...
testSomething: function() {
  var mock = window.mock('setTimeout', function(origSetTimeout){
    var timer = setTimeout(function(){ alert('hi'); }, 100);
    assert(timer);
    origSetTimeout(); // actually call window.setTimeout
  });
  assertEqual(1, mock.callCount);
}

If you would like further explanation, examples, or would like to suggest a topic, let me know.

No comments: