Tips for Unit Testing D3

July 27, 2016

Note: D3 4.0 has been released, but we haven’t upgraded to it yet, so the syntax in the code examples below is written for D3 3.x. However, the same ideas should hold for the newer version.

If you’ve ever attempted to write unit tests for D3.js code, you’ve undoubtedly noticed some pain points. Many are due to the fact that D3 code is typically chained. You might call the append method on a selection, then chain calls to attr, data, call, or other methods on the element(s) appended. This makes it difficult to stub or spy on certain functions, since you may need to make multiple nested stubs to be able to assert that one part of the chain is called correctly. You could instead test the SVG output of a chain of D3 calls, but several methods result in hard-to-reason-about output, like the d attribute of a path generated with d3.svg.line, d3.svg.area, or some other path generator. Tests that assert that a d attribute looks correct can be difficult to read and maintain, especially if you want to test the output of larger datasets.

Instead, we suggest you focus on testing that your data is structured correctly and that your logic works as expected in isolation. This brings us to our first tip.

Test the logic in your callbacks in isolation

There are two approaches for testing logic in callbacks on selections. One is to test that the correct attributes were set for selections with a given dataset. The other is to separate out the callbacks and test them in isolation. The former involves switching up the data and testing what was appended, or what attributes were set, each time. The latter is a simpler way to test your logic against several test cases. The guideline we follow is to separate out callbacks into their own functions and use Function.prototype.bind() if we need to pass any additional variables to them.

Here’s an example:

We can easily test the logic for determining cx and cy by simply testing those two functions rather than calling renderCircles with different datasets and confirming that each appended circle has the correct values each time. However, it’s still a good idea to have some test cases that confirm that circles are appended and have cx and cy set correctly, but we don’t need to have these tests be as comprehensive since we can unit test our callbacks more extensively.

For path data generators, test accessors directly

D3 includes some generators to simplify the construction of a path’s d attribute (like d3.svg.line, d3.svg.area, and d3.svg.arc). These helpers allow you to specify accessors that build the path (e.g., the functions that generate x- and y- coordinates for each datum in your dataset.) However, it is difficult to test that the d attribute for a created path is correct. Instead, it’s simpler to test that your path generator is set up correctly. To do so, separate out the code that instantiates your generator into its own function. Then, in your test, you can get the accessors from the generator and test different data points against them.

Here’s an example:

This way, you can easily test that your generator returns the correct coordinates for different data points, instead of feeding the renderLines function different datasets and confirming that the likely-complicated d attribute string looks correct each time. For reference, this is what the latter testing approach might look like for the same renderLines function:

As you can see, the tests becomes harder to understand and maintain with more data points, as the d attribute gets longer. And this is for a path with straight lines; it could become more complicated if we have smoothing, arcs, bezier curves, etc.

For D3 utility classes, assert on getters instead of spying/stubbing

For utility classes like d3.scale.linear or d3.svg.axis, write functions that returns the appropriate objects (scale, axis, etc.) This way, you can assert that getter methods on those objects return the values you expect.

Here’s an example:

Conclusion

D3 is a very large library with a lot of functionality. I’ve provided examples on only a small subset of that functionality, but these tips should extend to everything else. The key to testing D3 without frustration is decomposing code to enable us to test our logic in isolation.