(Note: if you aren’t using jasmine and jasmine-jquery to test your javascript go take care of that)
Suppose we’re working on a sliding tile game, and we want to log the moves a player has made. We could create a Logger object to collect the moves, and use a callback in the Game method to update the logger. One of the simplest ways to implement a callback is with standalone function:
Game.after_move = function() {
for (var i = 0; i < this.callbacks.length; i++) {
// it's a function - just execute it, passing the move
this.callbacks[i](this.last_move);
}
}
When the move is finished, the Game object will loop through all its callback functions and execute them. Now we can add our logger as a callback:
Logger.log = function(last_move) {
this.moves.push(last_move);
}
var game = new Game();
var logger = new Logger();
game.add_callback(logger.log); // not quite!
We’ve lost our scope. When we passed the logger.log function to the game, there’s nothing to tie it to the logger instance we created. We can use a closure to maintain the scope:
var game = new Game();
var logger = new Logger();
game.add_callback(function(move){
// we still have access to the outer scope
// in here, so the logger variable still
// holds the Logger we made
logger.log(move);
});
We want to test that our callbacks are being called. Normally a jasmine spy looks like this:
describe("when the user submits the form", function(){
it("submits via ajax", function(){
spyOn(jQuery, 'ajax');
$("form").submit();
expect(jQuery.ajax).toHaveBeenCalled();
});
});
Notice that the spy consists of two parts, an object (jQuery) and a method (ajax). We’re going to need the same thing. And we have it, kinda:
describe("after moving the tile", function(){
it("calls the callback", function(){
var game = new Game();
var logger = new Logger();
spyOn(logger, log);
// we have to use the same closure technique as
// above, to maintain scope
game.add_callback(function(){
logger.log();
});
game.after_move();
expect(logger.log).toHaveBeenCalled();
});
});
However, we shouldn’t use Logger in our Game test. It introduces an unnecessary dependency. Game observers could be anything. So let’s replace the logger with an object just for this test:
describe("after moving the tile", function(){
it("calls the callback", function(){
var game = new Game();
var observer = {callback: function(){}};
// spies always need an object and a method
spyOn(observer, callback);
// closure to maintain scope inside game
game.add_callback(function(){
observer.callback();
});
game.after_move("...description of move...");
expect(observer.callback).toHaveBeenCalled();
});
});
Anything less than this in terms of the observer is insufficient. It must be an object and it must have a method. So there we have it! A simple callback, a relatively straightforward and isolated test, and all scope is maintained by somewhat verbose but easily readable closures. A very serviceable implementation, and one which I use regularly.