Visual Regression Testing with PhantomCSS
PhantomCSS is a fantastic project for doing Visual Regression Testing of your web app.
It’s based on PhantomJS, CasperJS and a library written by the same authors as PhantomCSS, called Resemble.js.
Visual Regression Testing
First of all, what is Visual Regression Testing? Well, according to Wikipedia, regression testing is the practice of detecting unwanted changes in a system (regressions), introduced by other, possibly unrelated changes.
Applied to CSS (hence the “visual” part) this means that you take a screenshot of a CSS selector at a moment in time when it looks as expected (your baseline). You then have tests which, on each run, take a screenshot of the same selector against your app, and compare it to the baseline.
The idea
The idea is that if you run these type of tests after introducing changes, you’ll be able to catch regressions automatically. What’s even more awesome, is that you can very easily test across screen sizes (and if you wish, also across rendering engines - cfr. SlimerJS)
But aren’t integration tests enough?
You might wonder: why add another level of testing? Can’t we already test the
structure of the page using tools like Capybara
or CasperJS
itself,
asserting on the HTML structure of the page, and possibly even on the CSS
classes attached to the elements?
You could, but you’d be wrong. Asserting on the HTML structure is a gross
misunderstanging of the scope and goal of integration tests; and even if you
assert that the a div has a .red
class, there’s no way to be sure that the
element is actually red.
The stack
So, with that cleared out of the way, let’s proceed to analyze the stack:
- PhantomJS is a headless WebKit browser, exposing a JavaScript API you can use to drive the browser.
- CasperJS is a library built on top of PhantomJS (and SlimerJS), providing a much easier to use API, especially useful for writing navigation steps and tests. Think Capybara for JavaScript.
- Resemble.js is a library for comparing and diffing images. It allows you to define a threshold, and differences below that threshold are treated as a match. This is useful in many cases, like for when you’re running tests on different OSes that render fonts slightly differently.
What PhantomCSS does is it glues these tools together, and adds its own API for taking screenshots and comparing them to the baselines.
How it works
Example
Here is a fairly typical PhantomCSS test:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
casper.test.begin("Test the example page", function(test) {
casper.start();
casper.then(function() {
casper.viewportSize(1200, 800);
casper.open('http://test.example.com');
}).
then(function() {
phantomcss.screenshot('.come-on-my#selector', 'filename');
});
casper.then(function() {
casper.viewportSize(600, 400);
casper.open('http://test.example.com');
}).
then(function() {
phantomcss.screenshot('.come-on-my#selector', 'filename-small');
});
casper.then(function() {
phantomcss.compareAll();
});
casper.run(function() {
test.done();
});
});
In this example, we’re loading the page twice, at two different resolutions, and taking screenshots of the same selector.
At the end of the test, we’re comparing the saved screenshots with the baselines.
But when have the baselines been saved? Time for some explanation.
The flow
The first time you run a test with PhantomCSS, the library looks into the
baselines directory (where?), looking for a baseline that matches the
filename passed to the screenshot
function. If it doesn’t find one, it will
deduce that we’re running the test for the first time, and save the screenshot
as a baseline. This also means that it will not run any of the compare
steps,
as there’s nothing to compare yet obviously.
On every subsequent run, the baselines directory will be populated with files and PhantomCSS will proceed to make the comparisons.
If you want to create the baselines from scratch, you can either delete the
files manually, or configure the command line parameter for the rebase
function (more on that here).
Structure of a CasperJS test
A full description of the structure of a CasperJS test is out of the scope of this blog post, but a point should be mentioned:
CasperJS tests are written in an asynchronous way, but avoiding the use of
callbacks (and in fact, that’s one of the major advantages of using CasperJS
instead of raw PhantomJS). This means that tests are declared using then()
,
but they’re only executed, in the order in which they were declared, with
run()
.
Configuring PhantomCSS
PhantomCSS configuration is done through the init()
function. This is how I
currenlty configure it:
1
2
3
4
5
6
7
8
phantomcss.init({
libraryRoot: 'vendor/components/phantomcss',
screenshotRoot: 'test/visual/screenshots/baselines',
comparisonResultRoot: 'test/visual/screenshots/results',
failedComparisonsRoot: 'test/visual/screenshots/failures',
rebase: casper.cli.get('rebase'),
mismatchTolerance: 0.1,
});
Most options are self-explanatory. The most interesing one is rebase
, which
is basically saying: when I run the tests using the --rebase
flag,
overwrite the baselines.
How to run tests
To run a test, you invoke casperjs test filename.js
.
It’s also possible to run tests in more than one file, by using globs:
casperjs test *.js
.
Keeping things DRY
If you’re splitting your tests across multiple files (which you should probably
do), you’ll soon find yourself repeating pieces of code over and over - like
the above mentioned .init()
call for example.
I solved this by centralizing my configuration to its own file, which i decided
to call common.js
(great name right?)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
var require = patchRequire(require);
var phantomcss = require('../../vendor/components/phantomcss/phantomcss');
phantomcss.init({
libraryRoot: 'vendor/components/phantomcss',
screenshotRoot: 'test/visual/screenshots/baselines',
comparisonResultRoot: 'test/visual/screenshots/results',
failedComparisonsRoot: 'test/visual/screenshots/failures',
rebase: casper.cli.get('rebase'),
mismatchTolerance: 1,
});
exports.phantomcss = phantomcss;
var viewports = {
'smartphone-portrait': { width: 320, height: 480 },
'smartphone-landscape': { width: 480, height: 320 },
'tablet-portrait': { width: 768, height: 1024 },
'tablet-landscape': { width: 1024, height: 768 },
'desktop-standard': { width: 1280, height: 1024 }
};
exports.set_viewport = function(name) {
var viewport = viewports[name];
return casper.viewport(viewport.width, viewport.height);
};
casper.options.viewportSize = {
width: viewports['desktop-standard'].width,
height: viewports['desktop-standard'].height
};
As you can see, I’m setting some configuration values, defining some helper
functions, and exporting the whole thing using CommonJS’s exports
object.
In your tests you can then do something like
1
2
3
var common = require('support/common');
common.set_viewport('desktop-standard');
common.phantomcss.screenshot();
For more info on how to use modules in CasperJS, check this page. There you’ll also find an explanation for the weird two first lines.