This is part two of a three-part series introducing my personal approach to JavaScript TDD. In the last article we began creating a small application that loads image data from the Flickr API and displays it in a web page. We began by setting up modules and writing simple unit tests using the Mocha framework. In this article we’ll look at how to test asynchronous network calls (also known as AJAX).

Testing asynchronous network (AJAX) calls

In the last article I joked that I was procrastinating about testing the code where we call the Flickr API. And not without reason. I was procrastinating because testing network calls is a little bit complicated. There are three things that make this tricky:

  1. Testing an API call needs access to the network, which I can’t always guarantee;
  2. Network calls in JavaScript are asynchronous. This means that when we make a network request we interrupt the normal code flow; and
  3. The results from the network call change often. This is the whole point of the network call—but it makes it somewhat difficult to test.

I could go ahead and just write a test that makes the network call and checks what comes back, but this would have a some drawbacks:

  • The data coming back from the live Flickr API changes all the time. Unless I’m careful about how I write my tests, they would pass for maybe a minute before new data broke my test.
  • Making network calls can be slow, and the slower my tests, the less fun TDD becomes.
  • Doing things this way needs an internet connection. I regularly find myself writing code on a bus, or a train, or some other location without (fast) access to the internet.

So, I need to think carefully here about what I want to test. I will create a method called fetchFlickrData() that grabs data from the Flickr API. For this to work, I need to make a network call. But to make a network call, I will be calling some sort of API. The simplest API for this purpose would jQuery’s getJSON() method. getJSON() takes a URL and returns a Promise for the JSON data. If you’re not familiar with Promises, it’s worth taking a moment to get the basic idea.1

Now, to handle this neatly, I need to think like a functional programmer. Network calls involve side effects, making my function impure. But, if I can isolate the impure part (i.e. getJSON()), then I have a pure, testable function. In other words, what if I made getJSON() a parameter that I passed into my function? The signature might look something like this:

fetchFlickrData: function(apiKey, fetch) {
    // Code goes in here
}

In the application code, I would pass $.getJSON as the fetch parameter (more on that later). In my test though, I can pass a fake getJSON() method that always returns a promise for the same data. Then I can check that my function returns exactly what I expect, without making a network call.

The other thing that is tricky about network calls with JavaScript is that they are asyncrhonous. This means that we need some way of telling our test runner (Mocha) to wait until all the tests finish. Mocha provides a parameter to the it() callback called done that allows us to tell Mocha when the test is complete.

Putting all this together, I can write my test like so:

// flickr-fetcher-spec.js
describe('#fetchFlickrData()', function() {
    it(
        'should take an API key and fetcher function argument and return a promise for JSON data.',
        function(done) {
            var apiKey      = 'does not matter much what this is right now',
                fakeData    = {
                    'photos': {
                        'page':    1,
                        'pages':   2872,
                        'perpage': 100,
                        'total':   '287170',
                        'photo':   [{
                            'id':       '24770505034',
                            'owner':    '97248275@N03',
                            'secret':   '31a9986429',
                            'server':   '1577',
                            'farm':     2,
                            'title':    '20160229090898',
                            'ispublic': 1,
                            'isfriend': 0,
                            'isfamily': 0
                        }, {
                            'id':       '24770504484',
                            'owner':    '97248275@N03',
                            'secret':   '69dd90d5dd',
                            'server':   '1451',
                            'farm':     2,
                            'title':    '20160229090903',
                            'ispublic': 1,
                            'isfriend': 0,
                            'isfamily': 0
                        }]
                    }
                },
                fakeFetcher = function(url) {
                    var expectedURL = 'https://api.flickr.com/services/rest/?method=flickr.photos.search&api_key='
                                + apiKey + '&text=pugs&format=json&nojsoncallback=1'
                    expect(url).to.equal(expectedURL)
                    return Promise.resolve(fakeData);
                };
            FlickrFetcher.fetchFlickrData(apiKey, fakeFetcher).then(function(actual) {
                expect(actual).to.eql(fakeData);
                done();
            }
        );

    });
});

I’ve been a little bit clever here, and included an expect() inside the fake fetcher function. This allows me to check that I’m calling the right URL. Let’s run the test:

Sad cat ASCII art. 3 passing tests. 1 failing test. 1) FlickrFetcher #fetchFlickrData() should take an API key and fetcher function argument and return a promise for JSON data.: TypeError: FlickrFetcher.fetchFlickrData is not a function
The cat is sad because there is no function yet.

Stubs

Now that I have a failing test, let’s take a moment to talk about what this is doing. The fakeFetcher() function I’ve used to replace $.getJSON() is known as a stub. A stub is a piece of code that has the same API and behaviour as the ‘real’ code, but with much reduced functionality. Usually this means returning static data instead of interacting with some external resource.

Stubs can replace many different types of code besides network calls. Most often we use them for things functional programmers call side effects. Typical stubs might replace things like:

  • Queries to a relational database;
  • Interaction with the file system;
  • Accepting user input; or
  • Complex computations that take a long time to calculate.

Stubs don’t always have to replace asynchronous or even slow things. It may simply be a piece of code you haven’t written yet. A stub can replace almost anything.

Stubs are an important tool for TDD. They help us to keep tests running fast so our workflow doesn’t slow down. More importantly, they allow us to have consistent tests for things that are inherently variable (like network calls).

Stubs do take a little bit of effort to use well though. For instance, using a stub meant adding an extra parameter to the fetchFlickrData() function. However, if you are using a slightly functional-flavoured style of programming, then you will be thinking about things like side effects and pure functions anyway. I would also argue that making your code testable (whether that’s using stubs or not) is usually worth the effort.

But enough about stubs—back to the code…


Running the tests, I get an error, but that’s still a sad cat (red), so I can write some code. In this case, returning the expected result isn’t that simple. I have two expect() calls in there, so I have to call the fetcher function as well as return a promise for the data. In this case it’s easiest to write the general code straight-up:

// flickr-fetcher
fetchFlickrData: function(apiKey, fetch) {
    var url = 'https://api.flickr.com/services/rest/?method=flickr.photos.search&api_key='
            + apiKey + '&text=pugs&format=json&nojsoncallback=1'
    return fetch(url).then(function(data) {
        return data;
    });
}

Run the test again, and the cat is happy (green). So it’s time to refactor.

This time there are two things I want to refactor. First of all, there’s no need to use .then() in the fetchFlickrData() function. So I refactor to take out the redundant code:

fetchFlickrData: function(apiKey, fetch) {
    var url = 'https://api.flickr.com/services/rest/?method=flickr.photos.search&api_key='
            + apiKey + '&text=pugs&format=json&nojsoncallback=1'
    return fetch(url);
}

Running the tests again, everything still passes. But I would also like to refactor my test code. Mocha actually provides two ways to handle asynchronous code. The first is the done() function as we saw before. The second is specifically for Promises. If you return a Promise from your test, Mocha will automatically wait for it to either resolve or reject:

// flickr-fetcher-spec.js
describe('#fetchFlickrData()', function() {
    it(
        'should take an API key and fetcher function argument and return a promise for JSON data.',
        function() {
            var apiKey      = 'does not matter much what this is right now',
                fakeData    = {
                    'photos': {
                        'page':    1,
                        'pages':   2872,
                        'perpage': 100,
                        'total':   '287170',
                        'photo':   [{
                            'id':       '24770505034',
                            'owner':    '97248275@N03',
                            'secret':   '31a9986429',
                            'server':   '1577',
                            'farm':     2,
                            'title':    '20160229090898',
                            'ispublic': 1,
                            'isfriend': 0,
                            'isfamily': 0
                        }, {
                            'id':       '24770504484',
                            'owner':    '97248275@N03',
                            'secret':   '69dd90d5dd',
                            'server':   '1451',
                            'farm':     2,
                            'title':    '20160229090903',
                            'ispublic': 1,
                            'isfriend': 0,
                            'isfamily': 0
                        }]
                    }
                },
                fakeFetcher = function(url) {
                    var expectedURL = 'https://api.flickr.com/services/rest/?method=flickr.photos.search&api_key='
                                + apiKey + '&text=pugs&format=json&nojsoncallback=1'
                    expect(url).to.equal(expectedURL)
                    return Promise.resolve(fakeData);
                };
            return FlickrFetcher.fetchFlickrData(apiKey, fakeFetcher).then(function(actual) {
                expect(actual).to.eql(fakeData);
            }
        );

    });
});

Running my refactored code, the tests still pass, so it’s on to the next step.

Building up

At this point, I need to stop and think. There is one final thing to test before I can declare the FlickrFetcher module done: Do the pieces fit together OK? Can I make a network call, get back the results, and transform them into the format I want? It would be most convenient if I could do all this with one function.

So, I write a test:

describe('#fetchPhotos()', function() {
    it('should take an API key and fetcher function, and return a promise for transformed photos', function() {
        var apiKey   = 'does not matter what this is right now',
            expected = [{
                title: 'Dog goes to desperate measure to avoid walking on a leash',
                url:   'https://farm2.staticflickr.com/1669/25373736106_146731fcb7_b.jpg'
            }, {
                title: 'the other cate',
                url:   'https://farm2.staticflickr.com/1514/24765033584_3c190c104e_b.jpg'
            }],
            fakeData = {
                'photos': {
                    'page':    1,
                    'pages':   2872,
                    'perpage': 100,
                    'total':   '287170',
                    'photo':   [{
                        id:       '25373736106',
                        owner:    '99117316@N03',
                        secret:   '146731fcb7',
                        server:   '1669',
                        farm:     2,
                        title:    'Dog goes to desperate measure to avoid walking on a leash',
                        ispublic: 1,
                        isfriend: 0,
                        isfamily: 0
                    }, {
                        id:       '24765033584',
                        owner:    '27294864@N02',
                        secret:   '3c190c104e',
                        server:   '1514',
                        farm:     2,
                        title:    'the other cate',
                        ispublic: 1,
                        isfriend: 0,
                        isfamily: 0
                    }]
                }
            },
            fakeFetcher = function(url) {
                var expectedURL = 'https://api.flickr.com/services/rest/?method=flickr.photos.search&api_key='
                            + apiKey + '&text=pugs&format=json&nojsoncallback=1'
                expect(url).to.equal(expectedURL)
                return Promise.resolve(fakeData);
            };

        return FlickrFetcher.fetchPhotos(apiKey, fakeFetcher).then(function(actual) {
            expect(actual).to.eql(expected);
        });
    });
});

Note that I am still using a fake fetcher function as an external dependency. Running the test, I get an error. The cat is sad, so I can write some code.

Because I’m just calling two functions, it’s just as easy to write the general case as it is to return the expected value.

fetchPhotos: function(apiKey, fetch) {
    return FlickrFetcher.fetchFlickrData(apiKey, fetch).then(function(data) {
        return data.photos.photo.map(FlickrFetcher.transformPhotoObj);
    });
}

Running the test again, my test passes—happy cat (green). So it is time to refactor. But, since this function is just three or four (depending how you count it) function calls, there’s not much to refactor.2 So, for the moment, I have completed my first module.

So, what have we covered? In this article we covered two main topics: Testing asynchronous code and using stubs to standardise things like network calls. The next article will focus on working with HTML and the DOM.