Tue 31 Jan 2012
Easy mocking with JsTestDriver and SinonJS
Today we want to share a nice integration between JsTestDriver and SinonJS that allows for great stubbing and mocking facilities in your JavaScript tests.
A common thing needed when writing JavaScript unit tests is to mock out an object or the return value of a function. In addition to this, it’s important to restore the original object or function at the end of the test. Let’s check out a pretty trivial example:
Rally.getVersion = function() {
return 2;
};
Rally.isSupported = function(operation) {
return Rally.getVersion() > 1;
};
TestCase('IsSupportedTest', {
'test should support version greater than one': function() {
assertTrue(Rally.isSupported('read'));
},
'test should not support version less than two': function() {
var orig = Rally.getVersion = function() { return 1; };
try {
assertFalse(Rally.isSupported('read'));
} finally {
Rally.getVersion = orig;
}
}
});
Now let’s say you have 20 tests around the Rally.isSupported method. Each of which you might need to clean up any mocked out methods. This would get tedious, so you might leverage the setUp and tearDown methods provided by JsTestDriver’s TestCase:
TestCase('IsSupportedTest', {
setUp: function() {
this.origGetVersion = Rally.getVersion;
},
tearDown: function() {
Rally.getVersion = this.origGetVersion;
},
'test should support version greater than one': function() {
assertTrue(Rally.isSupported('read'));
},
'test should not support version less than two': function() {
Rally.getVersion = function() { return 1; };
assertFalse(Rally.isSupported('read'));
}
});
Another way to do this would be to use the SinonJS framework, specifically the stub() method. This allows you to stub out an existing method on an object, and provide expected return values as well as a variety of methods to assert the method was called:
TestCase('IsSupportedTest', {
setUp: function() {
this.getVersion = sinon.stub(Rally, 'getVersion');
},
tearDown: function() {
this.getVersion.restore();
},
'test should support version greater than one': function() {
this.getVersion.returns(2);
assertTrue(Rally.isSupported('read'));
},
'test should not support version less than two': function() {
this.getVersion.returns(1);
assertFalse(Rally.isSupported('read'));
}
});
This is nice and clean. The restore() method will automatically unwrap the stubbed method, and we can easily control the return values of the stubbed method. But we can make this even cleaner. We can have sinon automatically restore all stubbed and mocked methods upon destruction of the TestCase object. This can be done by leveraging Sinon’s sinon.testCase, which is an integration wrapper between SinonJS and JsTestDriver.
(function() {
var origJstestDriverTestCase = window.TestCase;
var origSinonTestCase = window.sinon.testCase;
window.sinon.testCase = function() {
throw Error('** Do not call sinon.testCase() directly, TestCase() will do this for you! **');
};
var globalSetUp = function() {
// ... global setup stuff goes here
if(this.originalSetUp) {
this.originalSetUp();
}
};
var globalTearDown = function() {
// ... global tear down stuff goes here
if(this.originalTearDown) {
this.originalTearDown();
}
};
window.TestCase = function(name, prototype, optType) {
prototype.originalSetUp = prototype.setUp;
prototype.setUp = globalSetUp;
prototype.originalTearDown = prototype.tearDown;
prototype.tearDown = globalTearDown;
prototype = origSinonTestCase(prototype);
return origJstestDriverTestCase(name, prototype, optType);
};
})();
With this in place, we can now write the following test:
TestCase('IsSupportedTest', {
setUp: function() {
this.getVersion = this.stub(Rally, 'getVersion');
},
'test should support version greater than one': function() {
this.getVersion.returns(2);
assertTrue(Rally.isSupported('read'));
},
'test should not support version less than two': function() {
this.getVersion.returns(1);
assertFalse(Rally.isSupported('read'));
}
});
Wrapping the TestCase’s prototype with sinon.testCase exposes all of the SinonJS methods through the TestCase object – so this.stub(), this.mock(), etc will all work. Additionally, SinonJS will automatically unwrap the stubbed/mocked objects at the end of the test. We’ve also put some logic in to ensure the following will throw an error:
TestCase('IsSupportedTest', sinon.testCase({ // <--- don't need to do this, it's done automatically
setUp: function() {
this.getVersion = this.stub(Rally, 'getVersion');
},
'test should support version greater than one': function() {
this.getVersion.returns(2);
assertTrue(Rally.isSupported('read'));
},
'test should not support version less than two': function() {
this.getVersion.returns(1);
assertFalse(Rally.isSupported('read'));
}
}));
