This is part three of my three-part series on Javascript Test Driven Development (TDD). In the previous article we discussed how to test asynchronous code and how to use stubs in place of things like network calls. Through the course of the series we have been building a sample application to demonstrate the concepts of TDD. In this article, we work through testing code for generating HTML and manipulating the DOM. We will also finish off the sample application by bringing everthing together, and tweaking it for more flexibility.

Working with HTML strings

So, I now have a module that will fetch a list of photos from Flickr and extract just the data that I want. The next step is to take that data and do something with it—in this case, inject it into a web page. So I’ll create a new module to handle the presentation of the data.

Straight away, I can write a simple test to check that my module exists.

// photo-lister-spec.js
var expect      = require('chai').expect,
    PhotoLister = require('./photo-lister');

describe('PhotoLister', function() {
    it('should exist', function() {
        expect(PhotoLister).not.to.be.undefined;
    });
});

Running the tests for this new module requires me to tweak the command line call I make slightly:

mocha --reporter=nyan photo-lister-spec.js

I run the tests, and it passes, so no code needs to be written yet.1 So, it’s time to do some thinking. I want to take a list of photo objects and convert that into a HTML list of list items containing <figure> elements. Now, any time I’m working with lists, I automatically consider using either map or reduce to process each element one-by-one. So a good place to start would be a function to take single photo object, and transform it into the HTML that I want. So, I write a test:

// photo-lister-spec.js
describe('#photoToListItem()', function() {
    it('should take a photo object and return a list item string', function() {
        var input = {
                title: 'This is a test',
                url:   'http://loremflickr.com/960/593'
            },
            expected = '<li><figure><img src="http://loremflickr.com/960/593" alt=""/>'
                     + '<figcaption>This is a test</figcaption></figure></li>';
        expect(PhotoLister.photoToListItem(input)).to.equal(expected);
    });
});

Note that I’ve used equal() rather than eql() in the assertion. This is because I’m comparing strings rather than objects.

Running the tests, I now have a sad cat (red) because the function does not exist. So I’ll put in the boilerplate module code:

// photo-lister.js
var PhotoLister;

PhotoLister = {
    photoToListItem: function() {}
};

module.exports = PhotoLister;

Running my tests again, and it still fails, so I can keep writing code. And, the simplest easiest way to make this test pass is to just return the expected string. So that’s what I’ll do:

// photo-lister.js
PhotoLister = {
    photoToListItem: function() {
        return '<li><figure><img src="http://loremflickr.com/960/593" alt=""/>'
               + '<figcaption>This is a test</figcaption></figure></li>';
    }
};

Run the test, and it passes. Happy cat (green). So it’s time to refactor, but returning a plain ol’ string isn’t terribly complicated. There’s not much to improve here yet. But, the code is not terribly useful yet either, So I write another test.

// photo-lister-spec.js
describe('#photoToListItem()', function() {
    it('should take a photo object and return a list item string', function() {
        var input = {
                title: 'This is a test',
                url:   'http://loremflickr.com/960/593'
            },
            expected = '<li><figure><img src="http://loremflickr.com/960/593" alt=""/>'
                     + '<figcaption>This is a test</figcaption></figure></li>';
        expect(PhotoLister.photoToListItem(input)).to.equal(expected);

        input = {
            title: 'This is another test',
            url:   'http://loremflickr.com/960/593/puppy'
        }
        expected = '<li><figure><img src="http://loremflickr.com/960/593/puppy" alt=""/>'
                 + '<figcaption>This is another test</figcaption></figure></li>';
        expect(PhotoLister.photoToListItem(input)).to.equal(expected);
    });
});

Run the tests again, and we have a sad cat again (red). So, it’s OK to write some code. In this case, the easiest way to make the test pass is to write the generic code:

// photo-lister.js
PhotoLister = {
    photoToListItem: function(photo) {
        return '<li><figure><img src="' + photo.url + '" alt=""/>'
               + '<figcaption>' + photo.title + '</figcaption></figure></li>';
    }
};

The test passes now, so it’s time to refactor. I’m not a fan of all those concatenation operators, so I’ll replace it with an array join:

// photo-lister.js
PhotoLister = {
    photoToListItem: function(photo) {
        return [
            '<li><figure><img src="',
            photo.url, '" alt=""/>',
            '<figcaption>',
            photo.title,
            '</figcaption></figure></li>'
        ].join('');
    }
};

Now that I have a function for dealing with individual items, I need one to deal with lists. So, I write another test:

describe('#photoListToHTML()', function() {
    it('should take an array of photo objects and convert them to an HTML list', function() {
        var input = [{
                title: 'This is a test',
                url:   'http://loremflickr.com/960/593'
            }, {
                title: 'This is another test',
                url:   'http://loremflickr.com/960/593/puppy'
            }],
            expected = '<ul><li><figure><img src="http://loremflickr.com/960/593" alt=""/>'
                     + '<figcaption>This is a test</figcaption></figure></li>'
                     + '<li><figure><img src="http://loremflickr.com/960/593/puppy" alt=""/>'
                     + '<figcaption>This is another test</figcaption></figure></li></ul>';
        expect(PhotoLister.photoListToHTML(input)).to.equal(expected);
    });
});

Running the test gives an error—sad cat—so, I can write some code:

photoListToHTML: function(photos) {
    return '<ul>' + photos.map(PhotoLister.photoToListItem).join('') + '</ul>';
}

And running the tests again, the cat is happy (green), so it’s time to refactor. Once again, I will remove concatenation operators, simply because I don’t really like them.

photoListToHTML: function(photos) {
    return ['<ul>', photos.map(PhotoLister.photoToListItem).join(''), '</ul>'].join('');
}

So I now have some code to generate a full HTML list as a string. As you can see, unlike asynchronous code or network calls, testing string manipulation is relatively straightforward. And since HTML is just plain text, writing tests for code that generates HTML strings is also relatively straightforward. At some point though, we need to get that string to render in the browser, so we have to interface with the DOM.

Working with the DOM

Now that I’ve got my list ready to go, it would be great if I could check that it gets added to the page. But the catch is, that up ‘til now, I’ve been working purely in Node, without a browser. I’ve done this deliberately as:

  • Tests run much faster on the command line;
  • It encourages me to think about how I can keep my code flexible; and
  • Mocha gives me that fun Nyan Cat reporter on the command line.

Without a browser though, I can’t use jQuery or regular DOM methods to check that everything is working. Fortunately there is a very handy node module called cheerio that will emulate much of the jQuery API for us. This means that I can test my functions that manipulate the DOM without loading up a headless browser or completely changing my testing approach.

To get started, I need to install cheerio, by running npm:

npm install cheerio --save-dev

Now that we’ve got cheerio installed, we can use it to create a fake jQuery with a fake DOM:

// photo-lister-spec.js
var cheerio = require('cheerio');

// … snip …

describe('#addPhotosToElement()', function() {
    it('should take an HTML string of list items and add them to an element with a given selector', function() {
        var $        = cheerio.load('<html><head></head><body><div id="mydiv"></div></body></html>'),
            list     = '<ul><li><figure><img src="http://loremflickr.com/960/593" alt=""/>'
                     + '<figcaption>This is a test</figcaption></figure></li>'
                     + '<li><figure><img src="http://loremflickr.com/960/593/puppy" alt=""/>'
                     + '<figcaption>This is another test</figcaption></figure></li></ul>',
            selector = '#mydiv',
            $div     = PhotoLister.addPhotosToElement($, selector, list);
        expect($div.find('ul').length).to.equal(1);
        expect($div.find('li').length).to.equal(2);
        expect($div.find('figure').length).to.equal(2);
        expect($div.find('img').length).to.equal(2);
        expect($div.find('figcaption').length).to.equal(2);
    });
});

Here I have created a fake DOM with just one <div> in the body of the document, and wrapped it up with cheerio. I pass that to my function as if it were jQuery, and then I expect addPhotosToElement() to return a jQuery-like object. I run a test to check that each of the elements I expect to be there exists. This gives me a failing test. And now I have a fake test, I can write some code:

addPhotosToElement: function($, selector, list) {
    return $(selector).append(list);
}

By passing $ in as a parameter I have access to the fake DOM as if it were jQuery operating in a browser. And with this code all the tests pass. The cat is happy so it is time to refactor—but I don’t think I could make this any more simple than it already is.

So for now, my modules are done. There are just a few final touches needed to make them operate nicely in a browser.

Putting it together in a web page

So far, we have been (deliberately) doing everything in Node, and not in a browser. This is good, but the whole point of this module is to display photos in a browser, not just to make tests pass. So I need to make a few tweaks to the code so that it will run in both environments.

This is a form of refactoring. Each time I make a change I will re-run my tests to make sure they still pass.

The first thing I’ll do is wrap a conditional around the module.exports so that the browser won’t throw an error if I just include the code in a web page. I could, of course, use something like Browserify or Webpack to package these up (and if you can, I highly recommend you do), but it’s nice to make them work either way. If I just want to throw the code in something like CodePen, for example, I’d prefer not to do a full Webpack setup:

// flickr-fetcher.js
if ((typeof module !== 'undefined') && (typeof module.exports !== 'undefined')) {
    module.exports = FlickrFetcher;
}
// photo-lister.js 
if ((typeof module !== 'undefined') && (typeof module.exports !== 'undefined')) {
    module.exports = PhotoLister;
}

To run all my tests at once, I use the following code:

$ mocha --reporter=nyan ./*-spec.js

…and the cat is still happy.

The final thing I would like to do is provide an interface that takes away the need to pass in jQuery.getJSON if jQuery is present as a global variable. To do this, I’m going to make use of the built in bind() function method found in most implementations of JavaScript.

//flickr-fetcher.js
fetchFlickrData: function(apiKey, fetch) {
    if ((!fetch) && (typeof jQuery !== 'undefined')) {
        fetch = jQuery.getJSON.bind(jQuery);
    }
    var url = 'https://api.flickr.com/services/rest/?method=flickr.photos.search&api_key='
            + apiKey.toString() + '&text=pugs&format=json&nojsoncallback=1'
    return fetch(url);
}

I can now use these functions in the browser without having to rely on a packaging system, and I don’t have to bother passing in jQuery to the fetchPhotos() function. This gives me more flexibility and makes the API more accessible.

And with that, the application is nearly done. All that remains is to piece the two modules together. To see that in action, I recommend you check out the demonstration in CodePen, but the relevant code is summarised below:

FlickrFetcher.fetchPhotos('8060d4cdac3ceb86af470aae29af3a56')
    .then(PhotoLister.photoListToHTML)
    .then(function(photosHTML) {
        PhotoLister.addPhotosToElement($, '#mydiv', photosHTML);
    });

So, across three articles we’ve covered my general approach to JavaScript TDD; including asynchronous tests, stubbing out network calls, and working with HTML and the DOM. In this article we looked in particular at working with HTML and using the cheerio package in place of jQuery to make tests work without a browser. There is, of course, a whole lot more to TDD, and this series has barely scratched the surface, but I sincerely hope it has been helpful.