Unit Testing Visualizations

Yony K.
4 min readAug 10, 2016

--

When we build visualizations that display a small and predictable data set we can usually get away with Looks-Right-Testing (LRT? Anyone? Anyone?). Of course all visualizations have to “look right” but when we start dealing with larger, dynamic datasets, or use our visualizations as UI building blocks we need a way to declare our assumptions and safeguard against regression with unit testing.

To test the visual manipulation parts of our code what we need is a DOM. We could run tests in a browser, but to automate them easily I prefer my build pipeline to remain within Node.

If you look at some of the D3 source code, you’ll see it uses JSDOM to do this. It’s a great project that gives us access to a DOM in Node and works great for creating and removing nodes, and changing attributes. Unfortunately, JSDOM doesn’t support the SVG methods necessary to apply certain transitions, in particular transitions of the transform attribute. So even if you don’t care about testing the transitions themselves, any call to transition.attr(‘transform’, …) in your code will break your test.

So let’s create a hybrid. We’ll write our tests in Node but then bundle them and hand them off to a headless browser to run them and hand us back the results as they’re executed.

Dead Simple Setup

Let’s set up a simple environment. I’m using tape for testing because it’s a zero-configuration, super simple library that just works.

$ npm install --save-dev tape

Say we want to test this function,

// movedot.js
module.exports = function moveDot(selection) {
selection.transition()
.attr('cx', d => d.x)
.attr('cy', d => d.y);
}

We can set up a test like this,

// test.js
let d3 = require('d3'),
test = require('tape'),
moveDot = require('./movedot');
test('moveDot() sets dot position according to data', (t) => {
let dot = d3.select('body')
.append('svg')
.append('circle')
.attr('cx', 0)
.attr('cy', 0);
dot.datum({ x: 10, y: 10 }).call(moveDot);
setTimeout(() => {
t.equal(+dot.attr('cx'), 10, 'Dot moves to new x coord');
t.equal(+dot.attr('cy'), 10, 'Dot moves to new y coord');
t.end();
}, 260);
});

Note that we wait for the transition to complete (default duration is 250ms, but we add a bit of padding) and then declare our assertions. We also need to call end() to notify tape this test is done.

To run this code, we bundle it up and give it to tape-run, a bridge layer between tape and another project called browser-run, which executes code in a browser. It comes with and uses electron.js by default.

$ npm install -g browserify tape-run

And now bundle and run.

$ browserify test.js | tape-run
TAP version 13
# moveDot() sets dot position according to data
ok 1 Dot moves to new x coord
ok 2 Dot moves to new y coord
1..2
# tests 2
# pass 2
# ok

Multiple Transitions

The code above works well when we test the state of our code past a single transition. But if we want to test the state past several transitions sequentially we quickly end up with the dreaded Pyramid of Doom as we wait for each transition to complete before asserting and moving on to the next one.

dot.datum({ x: 0, y: 0 }).call(moveDot);
setTimeout(() => {
t.equal(...);

dot.datum({ x: 1000, y: -1000 }).call(moveDot);
setTimeout(() => {
t.equal(...);

dot.datum({ x: 0, y: 0 }).call(moveDot);
setTimeout(() => {
t.equal(...);
...🚀

We could use Promises but I’ve found a simpler, light-weight syntax already included with D3 to tame this mess.

Enter d3-queue! An easy way of executing asynchronous function at a configurable concurrency. In our case we want a perfectly sequential execution so we configure a queue to execute one function at a time.

let q = d3.queue(1);q.defer(callback => {
dot.datum({ x: 1000, y: -1000 }).call(moveDot);
setTimeout(callback, 260);
});
q.defer(callback => {
t.equal(...);
dot.datum({ x: 0, y: 0 }).call(moveDot);
setTimeout(callback, 260);
});
q.defer(callback => {
t.equal(...);
dot.datum({ x: 10, y: 10 }).call(moveDot);
setTimeout(callback, 260);
});
q.await(t.end);

q.defer() queues up the given function, which the queue then executes only when the previously deferred function completes (again, because we configured concurrency as 1). After making some assertions we set up the next test case and wait for the next transition to complete before calling callback. This signals to the queue that the current function is done and the next deferred function in line can be called. The next function will in turn assert and setup again. Finally, q.await() waits for the queue to deplete before calling a final function with the results. In our case we ignore the results since they are handled by the assertions along the way.

Back to Reality

To be sure, the example above is quite contrived. With it’s thousands of tests you can feel confident that D3 will handle your transitions properly. The point of this exercise isn’t to test transitions. Rather, it’s to allow you to test code that happens to contain transitions. It allows transitions to pass by so you can get to the parts you actually need to be tested.

Another strategy would be to separate your transitions into standalone functions that can be more easily stubbed out. This has the advantage of making transitions more generic and reusable in your app, though it might make your code more verbose than you’d like.

Regardless, you’d do well to separate much of your data manipulation logic from the visual parts. That way you could catch bugs at a granular level before they’re ever translated into DOM elements.

And for everything else, there’s #d3brokeandmadeart.

--

--