When I got started with D3, I immediately wanted to know how to integrate it into my TDD workflow. My research proved unsatisfying, so I’ve written this post to summarize your current options for testing (yes) and test driving (no) your D3 graph. And, at the end, I’ve offered what might be a glimmer of hope for the future.
Integration Testing
Screenshot comparison is often recommended as the best option for testing D3 graphs, and this is the approach taken by some of the largest open source consumers of D3, such as Rickshaw. This can be a quick and easy solution, but the downsides are clear: you can’t use screenshots for TDD, and you can’t use them to test behavior.
But your D3 graph’s output is an SVG, just another DOM element which you can easily assert against. This approach is used heavily in the tests for D3 itself. This is more granular than screenshot testing, and you can test behavior, but TDD is still a problem.
Some parts of your graph can be easily test driven (e.g. whether or not there’s an SVG in the DOM), but you’ll quickly run into numbers or strings you can’t know until after your code is working. If you want to assert that the y axis spans the right range, you’re going to have to check the tick marks—tick marks that were generated by D3, and which may or may not include the top of your range. If you want to assert on the shape of a path, you’ll need to know the right incantation.
Long story short, you can integration test your D3 graph in several ways, but you won’t be able to use a TDD workflow to do so. Although initially disappointing, this makes sense: much of the work a D3 graph does has to do with the appearance of its output, so parts of D3 testing can look like CSS testing. I don’t know of anyone test driving their CSS, but regression testing it isn’t unheard of. Your strategy with D3 may be similar.
Unit Testing
Currently, unit testing D3 graphs is painful enough I don’t recommend it except as an exercise. This is because there are going to be a lot of calls to D3, and D3’s API is chained.
Your render method, for example, is going to make a few calls to D3’s append, and chained off of each of those calls you’ll have calls to attr, call, data/datum, and probably others. And some of your appends will themselves be chained. Inserting a spy quickly leads to an avalanche of stubs, many of which are dependent on the arbitrary order in which you make your calls.
Here’s me trying to assert that I tried to append an SVG to the DOM, unit style, after render has been fully implemented:
describe("#render", function() {
it("creates an svg", function() {
var d3Spy = jasmine.createSpyObj('d3', ['append']),
svgSpy = jasmine.createSpyObj('svg', ['attr', 'append']),
gSpy = jasmine.createSpyObj('g', ['attr', 'append']),
defsSpy = jasmine.createSpyObj('defs', ['append']);
spyOn(d3, 'select').and.returnValue(d3Spy);
d3spy.append.and.returnValue(svgSpy);
svgSpy.attr.and.returnValue(svgSpy);
svgSpy.append.and.returnValue(gSpy);
gSpy.attr.and.returnValue(gSpy);
gSpy.append.and.returnValue(defsSpy);
defsSpy.append.and.returnValue(//...
initializeGraph();
graph.render();
expect(d3Spy.append).toHaveBeenCalledWith('svg');
});
});
This approach is obviously unacceptable, and my code above doesn’t even work yet. We haven’t stubbed out enough of D3 to get past initializeGraph, and it’d take another 15 or 20 stubs just to get this simple spec green.
As things are today, unit testing your graph’s interactions with D3 does not appear to be a realistic option.
Use a Higher Level Tool
If your graph is simple, you may be able to avoid these problems altogether. High level tools built on top of D3, like Rickshaw, Dimple, or NVD3 can produce similar output in less code, with a smaller dependency surface area.
When I converted a simple D3 graph to Rickshaw, for example, it was half as long, mostly configuration, and I only had three Rickshaw methods to stub out: a constructor, render, and update. Unit testing my graph went from difficult to trivial, and TDD became a possibility.
That said, for the complex graphs that need tests and TDD the most, using a higher level tool isn’t an option.
The Future
Unit testing D3 graphs doesn’t have to be as unpleasant as it is. D3’s API is chained, but those chains follow meaningful patterns that could be followed to write a fake D3 for us to stub in or inject for testing. A good fake may even be able to provide an API simple enough for TDD.
To illustrate what I mean, here’s result of a quick spike:
function d3Spy() {
return d3SelectionSpyGenerator(null, 'selection');
}
function d3SelectionSpyGenerator(parent, name) {
if(parent && parent[name]) {
return parent[name];
}
var spy = jasmine.createSpyObj(name, ['append', 'attr', 'call', 'datum']);
spy.attr.and.returnValue(spy);
spy.call.and.returnValue(spy);
spy.datum.and.returnValue(spy);
spy.append.and.callFake(function(tag) {
return d3SelectionSpyGenerator(this, tag);
});
if(parent) {
parent[name] = spy;
}
return spy;
}
describe("#render", function(){
var f3;
beforeEach(function(){
f3 = d3Spy();
spyOn(d3, 'select').and.returnValue(f3);
initializeGraph();
graph.render();
});
it("creates an svg", function() {
expect(d3.select).toHaveBeenCalledWith('#graphContainer');
expect(f3.append).toHaveBeenCalledWith('svg');
});
it("sets the svg width", function(){
expect(f3.svg.attr).toHaveBeenCalledWith('width', 960);
});
it("applies the margin", function(){
expect(f3.svg.g.attr).toHaveBeenCalledWith('transform', 'translate(40,20)');
});
});
This “works”, for some uses of a very small area of D3’s API. I can imagine TDDing like this. But the fake’s API is small and flawed, and there’s a lot more work to be done.
This spike has convinced me that a fake D3 is possible. If the community were to come together, or a lone hero were to step forward, test driving our complex D3 graphs could become a possibility. Until then, we’ll have to settle for regression testing.