30
votes

I am completely new to D3JS and would like to understand the testing strategies for D3 JS.

To elaborate little more on question - consider I have a simple page that shows a line graph using a TSV file.

Java Script Code:

function LineManager() {}
function LineProperties() { // Line Properties }
LineManager.prototype.draw = function(properties) { 
  // D3 code to draw a line with the given properties.
}

I am not able to think of test cases to be considered for writing unit tests. Here is a sample test that I wrote ..

it("should throw an exception if line graph properties are not set.", function() {
        expect(lineManager.draw.bind(lineManager)).toThrow("Line Graph properties not set");
    });

it("It should have single line chart", function() {
    lineManager.draw(properties);
    expect(lineManager.countLines()).toEqual(1);
});

I have written unit tests to make sure the TSV file is getting generated correctly. But does it make sense to write a unit test to see if the data is getting rendered correctly? Isn't that more of a d3js unit test rather than unit test for my function?

So my question is - what tests should be considered for charts generated by d3js?

4
I have always unit tested any part which does something with the data which will then be draw with d3. So for instance if you was creating a cumulative line graph I would check that [0,5,2,1] is transformed to [0,5,7,8] which is then applied to a selection and dawn (and that unusual cases are handled gracefully). Would be interested to see what other do.Christopher Hackett
One way to do it would be to use Selenium.Lars Kotthoff
You probably need to review my answer , I have edited it. As I disagree with your answer a little bit and have explained reason. Hope it will be helpful.Hafiz

4 Answers

16
votes

I think I got the answer to my own question. Will try to explain it here.

It is not possible to validate whether the graph is plotted correctly by JS function written using D3JS. For this we may have to use Phantom.js or similar framework as mentioned by Chrisopher. I was not worried about making sure D3JS is plotting graph correctly, as any ways it is D3JS functionality and my code can safely assume D3JS is doing its work.

My worry is more of whether the data passed to D3JS is correct and as per my requirement. It is very much possible to make sure the properties of the graph are set correctly by creating Spy objects. I am providing a sample unit test covering test cases for a JS code plotting a Circle using D3JS.

CircleManager.js

function CircleManager() {};

CircleManager.prototype.draw = function(radius) {
    var svg = d3.select("body")
            .append("svg");

    svg.attr("width", 100)
            .attr("height", 100);    

    var circle = svg.append("circle");
    circle.style("stroke", "black")
        .style("fill", "white")
        .attr("r", radius)
        .attr("cx", 50)
        .attr("cy", 50);
};

CircleManagerSpec.js

describe("draw", function() {
    it("Constructs an svg", function() {
        var d3SpyObject = jasmine.createSpyObj(d3, ['append', 'attr']);

        // Returns d3SpyObject when d3.select method is called
        spyOn(d3, 'select').andReturn(d3SpyObject);

        var svgSpyObject = jasmine.createSpyObj('svg', ['append', 'attr', 'style']);

        // Returns svgSpyObject when d3.select.append is called.
        d3SpyObject.append.andReturn(svgSpyObject);


        d3SpyObject.attr.andCallFake(function(key, value) {
            return this;
        });

        svgSpyObject.append.andReturn(svgSpyObject);

        svgSpyObject.attr.andCallFake(function(key, value) {
            return this;
        });

        svgSpyObject.style.andCallFake(function(key, value) {
            return this;
        });

        var circleManager = new CircleManager();
        circleManager.draw(50);
        expect(d3.select).toHaveBeenCalledWith('body');
        expect(d3SpyObject.append).toHaveBeenCalledWith('svg');
        expect(svgSpyObject.attr).toHaveBeenCalledWith('r', 50);
        expect(svgSpyObject.attr).toHaveBeenCalledWith('width', 100);
        expect(svgSpyObject.attr).toHaveBeenCalledWith('height', 100);
        expect(svgSpyObject.style).toHaveBeenCalledWith('stroke', 'black');
        expect(svgSpyObject.style).toHaveBeenCalledWith('fill', 'white');
    });
});

Hope this helps.

8
votes

Testing strategy

The strategy I end up using to test d3.js code is to create helper functions to manage my data and settings. I then unit test these functions. So for charts I would check every functionality dealing with data, every function to set width, legends etc...

Concerning drawing functions it can get trickier but with testing frameworks such as buster.js it can be quite easy to implement these too. A good way of testing a chart would be to count the number of bars/lines in the page, check that legends are printing etc.

I would not try to check that the chart is visually the same because visually checking that the end result is the same is easiest. However, when writing the drawing functions, one should be very attentive to what happens on updates (will changing the data draw twice as many lines? are selectors right? ...)


Javascript testing

A great book on javascript testing is: Test Driven Javascript Development. It provides lots of examples and strategies to test javascript code. Most of them can be directly applied to d3.js code.


Tools

I recently looked for solutions for unit testing d3.js code and I ended up using the following tools:

buster.js

Buster.js is a very complete framework for unit testing javascript code in multiple browsers.

phantom.js

Phantom.js is a headless WebKit scriptable with a JavaScript API.

This means that it makes it easy to run automated tests on javascript without needing to use browsers such as chrome, safari etc..


EDIT: I would now use jasmine for unit testing and selenium (through the Saucelabs service maybe) for end to end testing.

7
votes

I think you should consider this: http://busypeoples.github.io/post/testing-d3-with-jasmine/

And it really seems to make sense. I have read the others' answers but I am little bit disagree with them. I think we not only check if right function is called or not but we can check much more than that. Checking only some function call are good at unit testing level but not enough. Such test cases written by developer will be based on developer's understanding like these functions are called or not. But whether these methods should be called or not, this thing can be only checked by going at another level because unlike other work, here are code is making something not returning something that can be just checked and make sure everything is correct.

We obviously don't need to check whether D3 is doing its work correctly or not. So we can use D3 inside our testing code. But D3 renders SVG and we can check things like if svg have elements where expected. Again it is not going to test whether SVG is showing and rendering properly or not. We are going to check if SVG have elements which are expected and they are set as expected.

For example: If this is bar chart, we can check the number of bars. As in example in above link here it is check that.

    // extend beforeEach to load the correct data...
beforeEach(function() {
    var testData =  [{ date: '2014-01', value: 100}, { date: '2014-02', value: 140}, {date: '2014-03', value: 215}];
    c = barChart();
    c.setData(testData);
    c.render();
});

describe('create bars' ,function() {
    it('should render the correct number of bars', function() {
        expect(getBars().length).toBe(3);
    });

    it('should render the bars with correct height', function() {
        expect(d3.select(getBars()[0]).attr('height')).toBeCloseTo(420);
    });

    it('should render the bars with correct x', function() {
        expect(d3.select(getBars()[0]).attr('x')).toBeCloseTo(9);
    });

    it('should render the bars with correct y', function() {
        expect(d3.select(getBars()[0]).attr('y')).toBeCloseTo(0);
    });
});

// added a simple helper method for finding the bars..
function getBars() {
    return d3.selectAll('rect.bar')[0];
}

Some people probably gonna say that we are going to use D3 inside testing code? Again we should remember that purpose of test writing here is not to test D3 but our logic and SVG code that is compiled in response to our code.

This is just a way and jasmine is something that is helping us in writing test, you can also go into more detail and in different scenarios. You can make domain and check datapoints width height to cross check if they result into data which were given to render.

I think I am clear if not then check this link : http://busypeoples.github.io/post/testing-d3-with-jasmine/

Here write of this article have explained things in detail with how you can use jasmine. Also I think I am still gone into detail. If only unit testing is required at different js functions level then there are a lot more things which can be tested without going into elements detail.

4
votes

Probably worth mentioning Jest snapshot testing. Jest/snapshots are popular for React UI components, as they're also difficult to test. With D3 you could generate an SVG take a snapshot and verify as your codebase evolves that you continue to generate the same output.

function makeChart(id, dataset) {
  const chart = d3.select(id)
      .append("svg")
      .selectAll("circle")
      .data(dataset)
      .enter().append("circle")
      .style("stroke", "gray")
      .style("fill", "black")
      .attr("r", 40)
      .attr("cx", 50)
      .attr("cy", 20);
  return chart;
}

it('renders correctly', () => {
  const myChart = makeChart('#example', [1,2,3])
  expect(myChart.node()).toMatchSnapshot();
});

This test is untested

A snapshot can be any output a string "Foo Bar" a JSON object {foo: "bar"} etc.

Basically you have to run a test once that contains .toMatchSnapshot. Jest will then manage the generation and store a copy that it'll compare in future tests.

The concept is similar to VCR in Ruby testing. Record and replay.