I was tasked with researching Javascript testing in the browser. Saw this video, whose message is simple: decouple your code to be more testable. This is a testing 1.0 concept put in the context of Javascript/Ajax. So I set out to try out their methodology. I wrote a piece of code to do live updating in respond to keypresses in a field. It started out very ad-hoc so I refactored it to be an object:
function LiveUpdater(inputField, resultElement, url){
this.inputField = inputField;
this.url = url;
this.lastValue = null;
this.resultElement = resultElement;
var self = this;
this.pe = new PeriodicalExecuter(function(){
if (self.lastValue != self.inputField.value){
self.lastValue = self.inputField.value
new Ajax.Updater(self.resultElement, self.url, {
method: 'get',
parameters: { query: self.inputField.value }
});
}
}, 0.5);
But this still isn't testable. To follow their methodology, I would have to break this up into 3 objects: the view, the controller, and the data source. I started out on that path - using jsMock(those guys used a home grown mock library) - but it soon became apparent that my little chunk of managable code would have to more than double in size just to be testable, it was to heavy for my taste.
But it doesn't have to be this way. jsMock has the limitation that you cannot mock only certain methods on an object, it's basically all or nothing. Therefore, if you want to test the expectation or stub out a method, you must refactor it out as an object on its own. I've long gotten used to rspec and it doesn't have this limitation. It turns out it's not hard to fix this for jsMock, it's a small library - merely 300 some lines of code - and I was able to add this ability and give it a more rspec style of writing stubs and expectations. Long story short, here's my new code:
function LiveUpdater(inputField, resultElement, queryParameterKey, url){
this.lastValue = null;
var self = this;
this.getParams = function(){
var params = {}
params[queryParameterKey] = inputField.value
return params
}
this.update = function(){
new Ajax.Updater(resultElement, url, {
method: 'get',
parameters: self.getParams()
});
}
this.check = function(){
if (self.lastValue != inputField.value){
self.lastValue = inputField.value
self.update();
}
}
this.start = function(){
new PeriodicalExecuter(self.check, 0.5);
}
}
And my tests(using Thomas Fuchs' unittest framework, although you could also use jsUnit):
new Test.Unit.Runner({
setup: function() {
JSMock.extend(this)
myField = {value:"hello"}
this.lu = this.makeMockable(new LiveUpdater(myField, document.createElement('div'), 'query', ''))
},
teardown: function() {
this.verifyMocks()
},
testCheckShouldUpdateInitially: function() { with(this) {
lu.shouldReceive('update').with_no_args()
lu.check()
}},
testCheckShouldNotUpdateWhenNothingHasChangedTheSecondTime: function() { with(this) {
lu.stub('update').andReturn(null)
lu.check()
lu.shouldNotReceive('update').with_no_args()
lu.check()
}},
testParamsShouldBeRight: function() { with(this) {
assert(mapEqu({query:'hello'}, lu.getParams()))
}}
}, {testLog: "testlog"});
As you can see I refactored the activity into 4 methods. Only 2 of them are tested: check() and getParams(), there other 2 are basically direct third party library calls.
To show off some other things you can do:
function Robot(){
var self = this
this.move = function(){
if (self.facingWall())
self.turnToTheLeft()
else
self.goStraight()
}
this.goStraight = function(){
}
this.facingWall = function(){
}
this.turnToTheLeft = function(){
}
}
...
testShouldTurnToTheLeftWhenFacingWall: function() { with(this) {
robot.stub('facingWall').andReturn(true)
robot.shouldReceive('turnToTheLeft').with_no_args()
robot.move()
}},
testShouldNotGoStraightWhenFacingWall: function() { with(this) {
robot.stub('facingWall').andReturn(true)
robot.shouldNotReceive('goStraight').with_no_args()
robot.move()
}},
...
function Boxer(){
var self = this
this.health = 10
this.hit = function(opponent, power){
opponent.hurt(power)
}
this.hurt = function(points){
self.health -= points
}
}
...
testGettingHitShouldHurtOpponentBySameAmountAsPower2: function(){ with(this){
boxer.shouldReceive('hurt').with(5)
new Boxer().hit(boxer, 5)
}},
Here's a zip file for the code: jsMockB.zip. I may put it up on svn somewhere later.