Headless Raphael Testing
I’m going to debunk two popular myths today: Testing Javascript outside the browser is impossible and testing graphics libraries is impossible. At Hoopla, Mat and I have found through experience that these myths are untrue. We have a test suite running 157 tests in jSpec and Rhino that tests every graph and chart that we draw on our site. Today, I’m going to show you how to unit test the code for this (be sure to click the box):
First, you’re going to need the JSpec gem, which includes Rhino with it. Assuming you have Ruby installed, run this on your command line:
$ gem install jspec
Now we need to setup a jspec directory:
$ jspec init explosion
Awesome, now let’s make a test file:
// in ./explosion/spec/unit/spec.js
describe('Explosion', function () {
var explosion;
before_each(function () {
// Raphael is a mock, we'll discuss that in a minute
Raphael.clear();
// Make our explosion object
explosion = new Explosion("explosion-draw-here");
});
it('should draw the text on init', function () {
// textCalls will give us all the textCalls we made.
var text = Raphael.textCalls;
expect(text[0]).to(eql, [200, 100, "Raphael"]);
expect(text[1]).to(eql, [200, 120, "Awesome!"]);
});
it('should explode', function () {
explosion.explode();
var animate = Raphael.animateCalls;
expect(animate[0]).to(eql, [{rotation: 90, x: 30, y: 30},
1000, "bounce"]);
// We only test the first three args, because testing
// the callback is too hard for this example :-)
expect(animate[1].slice(0,3)).to(eql,
[{rotation: 180, x: 300, y: 170}, 1000, "<>"]);
});
it('should reform', function () {
explosion.reform();
var animate = Raphael.animateCalls;
expect(animate[0]).to(eql, [{rotation: 360, x: 200, y: 100},
1300, "<"]);
expect(animate[1]).to(eql, [{rotation: 360, x: 200, y: 120},
1300, ">"]);
});
});
There’s a few things going on above. First, we’re setting up our Raphael mock at the beginning of each test. We’ll define the mock in a minute, but suffice to say that we’re not going to actually call Raphael in our test. Second, we have three tests. The first tests that we wrote out some text, the second that we can explode, and the third that we will reform. A number of constants are hardcoded here for purposes of simplifying the example, but in real life code hardcoding is bad.
So about that Raphael mock… let’s take a look at how it’s implemented:
var Raphael = function () {
var self = {
attr: function () {
return this;
},
text: function () {
// store text calls
var args = Array.prototype.slice.call(arguments, 0);
Raphael.textCalls.push(args);
return this;
},
animate: function () {
// store animate calls
var args = Array.prototype.slice.call(arguments, 0);
Raphael.animateCalls.push(args);
return this;
}
};
Raphael.textCalls = Raphael.textCalls || [];
Raphael.animateCalls = Raphael.animateCalls || [];
return self;
};
Raphael.clear = function () {
Raphael.textCalls = [];
Raphael.animateCalls = [];
};
As you can see, this mock just stores the various method invocations into arrays. In the implementation below, you’ll see us make calls to text() and animate(). On the page, these will be called on Raphael proper. In tests, they will be called on our Raphael mock, where we’ll store the invocation and assert that it was called correctly. This allows us to run these test without an actual browser, because we never actually use the real Raphael library in our code. Similarly, this approach can be applied to jQuery (something that infact do at Hoopla).
So now let’s take a look at the implementation:
// in ./explosion/lib/yourlib.js
var Explosion = function (div) {
this.r = Raphael(div, 400, 200);
this.labels = {};
this.drawText();
};
Explosion.prototype = {
drawText: function () {
this.labels.raphael = this.r.text(200, 100, "Raphael").
attr({font: "14px Helvetica, Arial, sans-serif"});
this.labels.awesome = this.r.text(200, 120, "Awesome!").
attr({font: "14px Helvetica, Arial, sans-serif"});
},
explode: function () {
var self = this;
this.labels.raphael.animate({rotation: 90, x: 30, y: 30},
1000, "bounce");
this.labels.awesome.animate({rotation: 180, x: 300, y: 170},
1000, "<>", function () {
setTimeout(function () {
self.reform();
}, 400);
});
},
reform: function () {
this.labels.raphael.animate({rotation: 360, x: 200, y: 100},
1300, "<");
this.labels.awesome.animate({rotation: 360, x: 200, y: 120},
1300, ">");
}
};
Much like last week’s post, we initialize a Raphael object and use it to draw. The three functions from the test are here. We know they work because of the unit tests.
The final step is to run the tests:
$ jspec run --rhino # Run the tests once
$ jspec --rhino # Run the tests each time we change a file
Hopefully this post has shown that it is possible to do headless testing of graphical Javascript libraries. As I said before, we’re using this strategy heavily, and it’s working great for us. By mocking out Raphael (and jQuery) we’re able to run our entire test suite headless in less than a second. That means we can code awesome, graphically rich sites in a fraction of the time it takes other people. I think that’s pretty awesome.
Update: I’ve written a follow-up post showing how to use Mat’s RecorderMock.js to clean up the Raphael mock.
blog comments powered by Disqus