English |  Español |  Français |  Italiano |  Português |  Русский |  Shqip

Developing a D3.js Edge

5. Unit Testing / Test Suites

  • Build a test suite using the Jasmine BDD framework
  • Test the bar chart plugin developed in previous chapters

Building Testable Code

Implementing the reusable API will help with building testable code. We will assume you already know what Test-Driven Development (TDD) is, or at least what is a unit test. TDD forces you to write modular units, that are totally decoupled from other modules, where the logic is usually hidden behind a clean API. Using a test suite framework or a simple assert function, you write down the contract (the way to use your module from the public API), in a way that is meaningful and readable for a user. Here is one unit test example:

001:it('should add a number to another'function() {
002:    expect(addFunctionToTest(23)).toBe(5);
003:});

TDD is more than just a way to document your code. You can be more aggressive in your refactoring when you know that a test will fail, since you will be informed if you break the contract other modules potentially rely upon. A good test suite, though, is the best documentation you can have. If you've never read the D3 test suite, now is a good time to do so.

In this chapter we will write a simple test suite for the reusable chart plugin API we discussed in the previous chapter (Chapter 4) to show some of its key features in action. We will use the Jasmine BDD framework, but it should be easy to port this test suite to any other TDD/BDD framework. Keep in mind that this is not a guide about how to write a good test suite, but instead will illustrate and document topics we have discussed so far in this book.

A Test Suite for our Bar Chart Plugin


 Source code is available in code/chapter05

 


A test suite usually starts with a description and the initialization of some commonly used variables, as well as with an HTML fixture that is used as a sandbox.

001:describe('Reusable Bar Chart Test Suite'function() {
002:  var barChart, dataset, fixture;
003:
004:  beforeEach(function () {
005:    dataset = [10203040];
006:    barChart = d3.edge.barChart();
007:    fixture = d3.select('body').append('div').classed('test-container'true);
008:  });
009:
010:  afterEach(function () {
011:    fixture.remove();
012:  });
013:});

Let's first test some basic usage. We only need one line of code (and two more for the test harness itself). We bind some data to the DOM fixture and call our chart function. Note that the d3.datum() method is like d3.selection.data(), but doesn't compute a data join, which we don't need in this case:

001:it('should render a chart with minimal requirements'function() {
002:  fixture.datum(dataset).call(barChart);
003:  expect(fixture.select('.chart')).toBeDefined(1);
004:});

Setters are also used as getters when no arguments are provided; this is where we test for that dual behavior:

001:it('should provide getters and setters'function() {
002:  var defaultWidth = barChart.width();
003:  var defaultEase = barChart.ease();
004:
005:  barChart.width(1234).ease('linear');
006:
007:  var newWidth = barChart.width();
008:  var newEase = barChart.ease();
009:
010:  expect(defaultWidth).not.toBe(1234);
011:  expect(defaultEase).not.toBe('linear');
012:  expect(newWidth).toBe(1234);
013:  expect(newEase).toBe('linear');
014: });

Here we just want to demonstrate that some functions are not accessible from the API:

001:it('should scope some private and some public fields and methods'function() {
002:  expect(barChart.className).toBeUndefined();
003:  expect(barChart.ease).toBeDefined();
004:  expect(typeof barChart.ease).toBe('function');
005:});

We update an attribute on the chart by using a setter and calling the chart function again. Here, we only test if the new attribute has been set inside the module, and not if the new width was actually applied to the chart. This is slightly more involved and is sometimes better tested with browser automation and automated screenshot comparison tools:

001:it('should update a chart with new attributes'function() {
002:  barChart.width(10000);
003:  fixture.datum(dataset)
004:    .call(barChart);
005:
006:  barChart.width(20000);
007:  fixture.call(barChart);
008:
009:  expect(barChart.width()).toBe(20000);
010:});

Updating the data is slightly different. We have to bind the new data to the DOM before calling our chart function. It's not good practice for a unit test to have too much knowledge about the internal working of a module, but here we illustrate that the data, once bound to the DOM, is available under the special __data__ field of the DOM object. Another trick to mention here is the use of [0][0] to access the first member of a selection, illustrating that this selection is an array of DOM elements:

001:it('should update a chart with new data'function() {
002:  fixture.datum(dataset)
003:    .call(barChart);
004:
005:  var firstBarNodeData1 = fixture.selectAll('.bar')[0][0].__data__;
006:
007:  var dataset2 = [1000];
008:  fixture.datum(dataset2)
009:    .call(barChart);
010:
011:  var firstBarNodeData2 = fixture.selectAll('.bar')[0][0].__data__;
012:
013:  expect(firstBarNodeData1).toBe(dataset[0]);
014:  expect(firstBarNodeData2).toBe(dataset2[0]);
015:});

Now let's render two charts in two <div>s. We first verify if they are both there with a different configuration:

001:it('should render two charts with distinct configuration'function() {
002:  fixture.append('div')
003:    .datum(dataset)
004:    .call(barChart);
005:
006:  var dataset2 = [400300200100];
007:  var barChart2 = d3.edge.barChart().ease('linear');
008:
009:  fixture.append('div')
010:    .datum(dataset2)
011:    .call(barChart2);
012:
013:  var charts = fixture.selectAll('.chart');
014:
015:  expect(charts[0].length).toBe(2);
016:  expect(barChart2.ease()).not.toBe(barChart.ease());
017:});

With this in mind, let's look at a less obvious usage example. A reusable chart can be used as a component in another chart, as is the case with the d3.axis component. Here we add a chart inside another chart:

001:it('can be composed with another one'function() {
002:  fixture.datum(dataset)
003:    .call(barChart);
004:
005:  var barChart2 = d3.edge.barChart();
006:
007:  fixture.selectAll('.chart')
008:    .datum(dataset)
009:    .call(barChart2);
010:
011:  var charts = fixture.selectAll('.chart');
012:
013:  expect(charts[0].length).toBe(2);
014:  expect(charts[0][1].parentElement).toBe(charts[0][0]);
015:});

Another interesting usage we didn't describe earlier is the use of _selection.each, which is used to loop through the selection. Its task is to build a chart for each element of the selection set that was passed to the chart module. Next, we will pass an array of arrays. The .each loop will add a child <div> to the container for each of the three sub-arrays:

001:it('should render a chart for each data series'function() {
002:  var dataset = [[1234], [5678], [9101112]];
003:
004:  fixture.selectAll('div.container')
005:    .data(dataset)
006:    .enter().append('div')
007:    .classed('container'true)
008:    .datum(function(d, i){return d;})
009:    .call(barChart);
010:
011:  var charts = fixture.selectAll('.chart');
012:
013:  expect(charts[0].length).toBe(dataset.length);
014:  expect(charts[0][0].__data__).toBe(dataset[0]);
015:  expect(charts[0][1].__data__).toBe(dataset[1]);
016:  expect(charts[0][2].__data__).toBe(dataset[2]);
017:});

We call to the chart only once, but internally it is built three times, each time with a different dataset. This gives you small multiples for free!

Testing Events: Using Spies

Callbacks on events are a bit harder to test. We use a spy for this task and watch for it, since it is called with the expected arguments when the event is triggered. The tricky part is to trigger the event. D3.js comes to the rescue, since the custom events we bound with d3.dispatch are available as properties of the DOM object. Note how in this specific example they exist under the name __onmouseover():

001:it('should trigger a callback on events'function() {
002:  fixture.datum(dataset)
003:    .call(barChart);
004:
005:  var callback = jasmine.createSpy("filterCallback");
006:  barChart.on('customHover', callback);
007:
008:  var bars = fixture.selectAll('.bar');
009:  bars[0][0].__onmouseover();
010:  var callBackArguments = callback.argsForCall[0][0];
011:
012:  expect(callback).toHaveBeenCalled();
013:  expect(callBackArguments).toBe(dataset[0]);
014:});

Voilà! We have illustrated some features provided by the reusable API using a unit test suite. It's always a good idea to add a unit test when you share a plugin, and it is mandatory when working on the D3.js core. 

Summary

If you made it this far into this chapter, you are probably geeky enough to already be convinced by the virtues of TDD/BDD. So let's go on to the next chapter and build a real-world application using the reusable API.

There has been error in communication with Booktype server. Not sure right now where is the problem.

You should refresh this page.