RSS
The Testing Goat

Obey the Testing Goat!

TDD for the Web, with Python, Selenium, Django, JavaScript and pals...

Unit testing Ajax calls: if you're not using a mocking library, it's a world of pain

Sat 19 October 2013
By Harry

tl;dr: I found myself going through increasing contortions trying to TDD some JavaScript code with Ajax in. Once I started using sinon.js, all the pain went away. Folks, don’t try to roll your own JavaScript mocks.

I’ve been playing around with Mozilla Persona as an authentication platform, and I knocked together this basic code to interact with their API. You can see it’s quite dense, but fairly readable:

var currentUser = '{{ user.email }}' || null;
var csrf_token = '{{ csrf_token }}';

navigator.id.watch({
  loggedInUser: currentUser,
  onlogin: function(assertion) {
    $.post('/accounts/login', {assertion: assertion, csrfmiddlewaretoken: csrf_token})
    .done(function() { window.location.reload(); })
    .fail(function() { navigator.id.logout();});
  },
  onlogout: function() {
    $.post('/accounts/logout')
    .always(function() { window.location.reload(); });
  }
});

We call a function called watch, passing it in an email address string, and two callbacks for login and logout. Login does a post, refreshes the page if it succeeds, and calls a logout if it fails. Logout just does a post and a refresh. Typical JavaScript, 3 levels of nested callbacks, but it actually reads through quite well

So off I go on my merry way, planning to de-spike this code and re-write it with TDD. Mockin' libraries? We don’t need no stinkin' mockin' libraries. Im’a roll my own, cos you can totes do that in JavaScript:

test("initialize binds sign in button to navigator.id.request", function () {
    var requestWasCalled = false;
    var mockRequestFunction = function() { requestWasCalled = true; };
    var mockNavigator = {
        id: {
            request: mockRequestFunction,
            watch: function () {}
        }
    };

    Superlists.Accounts.initialize(mockNavigator);
    equal(requestWasCalled, false, 'check request not called before click');

    $('#id_login').trigger('click');
    equal(requestWasCalled, true, 'check request called after click');
});

So far, so slightly-awkward-but-not-too-bad. But look how badly things go wrong once you start to try and write tests for more deeply nested callbacks:

test("initialize calls navigator.id.watch", function () {
    var user = 'current user';
    var token = 'csrf token';
    var urls = { login: 'login url', logout: 'logout url'};

    var watchFunctionCalled = false;
    var mockWatchFunction = function (params) {
        equal(params.loggedInUser, user, 'check user');
        equal(params.onlogin, Superlists.Accounts.submitAssertion, 'check login fn'); //<1>
        equal(params.onlogout, Superlists.Accounts.logOut, 'check logout fn'); //<1>
        watchFunctionCalled = true;
    };
    var mockNavigator = { id: { watch: mockWatchFunction } };

    Superlists.Accounts.initialize(mockNavigator, user, token, urls);

    equal(watchFunctionCalled, true, 'check watch function called');

});
  1. You can see I ended up rewriting my anonymous callbacks as named functions in order to make them available to test.

Now, was this the unit tests being useful, forcing me to break up my code into smaller, more self-contained components? I’ll let you judge for yourself what you think of the readability of the new code, compared to the old code:

$(document).ready(function() {

    var accountUrls;
    var csrfToken;
    var personaNavigator;

    var initialize = function (navigator, user, token, urls){
        accountUrls = urls_;
        csrfToken = token;
        $('#id_login').on('click', function () {
            navigator.id.request();
        });

        navigator.id.watch({
            loggedInUser: user,
            onlogin: submitAssertion,
            onlogout: logOut,
        });
    };

    var submitAssertion = function (assertion) {
        $.post(
            accountUrls.login,
            { assertion: assertion, csrfmiddlewaretoken: csrfToken }
        ).done( Superlists.Accounts.refreshPage )
        .fail( Superlists.Accounts.onLoginFailure );
    };

    var logOut = function () {
        $.post(accountUrls.logout).done( Superlists.Accounts.refreshPage );
    };
    var onLoginFailure = function () {
        personaNavigator.id.logout();
    });
    var refreshPage = function () { window.location.reload(); };

    $.extend(window.Superlists, {
        Accounts: {
            initialize: initialize,
            logOut: logOut,
            onLoginFailure: onLoginFailure,
            refreshPage: refreshPage,
            submitAssertion: submitAssertion
        }
    });

});

What the heck happened? At each stage I just tried to make sane, self-contained unit tests, and I end up with this long and, I think, much less readable code! Look at all that painful yanking of variables up into a higher scope, and the contortions I’m putting myself to give them sensible names! Look at all that mess on the Superlists.Accounts namespace!

Doctor, Doctor, it hurts when I do this!

So stop doing that. Near the end of this adventure, I decided it was time to investigate a proper mocking library. sinon.js seemed popular, and guess what — it totally solved all my problems.

Look how much more readable the tests are:

test("initialize calls navigator.id.watch", function () {
    Superlists.Accounts.initialize(mockNavigator, user, token, urls);
    equal(mockNavigator.id.watch.calledOnce, true, 'check watch function called');
});


test("watch sees current user", function () {
    Superlists.Accounts.initialize(mockNavigator, user, token, urls);
    var callArgs = mockNavigator.id.watch.firstCall.args[0];
    equal(callArgs.loggedInUser, user, 'check user');
});

test("onlogin does ajax post to login url", function () {
    Superlists.Accounts.initialize(mockNavigator, user, token, urls);
    var onloginCallback = mockNavigator.id.watch.firstCall.args[0].onlogin;
    onloginCallback();
    equal(requests.length, 1, 'check ajax request');
    equal(requests[0].method, 'POST');
    equal(requests[0].url, urls.login, 'check url');
});

test("onlogin sends assertion with middleware token", function () {
    Superlists.Accounts.initialize(mockNavigator, user, token, urls);
    var onloginCallback = mockNavigator.id.watch.firstCall.args[0].onlogin;
    var assertion = 'browser-id assertion'
    onloginCallback(assertion);
    equal(
        requests[0].requestBody,
        $.param({ assertion: assertion, csrfmiddlewaretoken: token }),
        'check POST data'
    );
});

test("onlogin post failure should do navigator.id.logout ", function () {
    mockNavigator.id.logout = sinon.spy();
    Superlists.Accounts.initialize(mockNavigator, user, token, urls);
    var onloginCallback = mockNavigator.id.watch.firstCall.args[0].onlogin;
    server = sinon.fakeServer.create();
    server.respondWith([403, {}, "permission denied"]);

    onloginCallback();
    equal(mockNavigator.id.logout.called, false, 'should not logout yet');

    server.respond()
    equal(mockNavigator.id.logout.called, true, 'should call logout');
});

That last one is testing a callback nested fully 3 levels deep, and it’s still totally readable. Sinon’s fakeServer makes checking callbacks on Ajax requests a breeze. Sure, there’s still a bit too much boilerplate, the fact that .requestBody is URL-encoded and the only way to check send POST params is a little annoying for example, but with this kind of testing I can get right back to writing code the way I had it in the first place.

var initialize = function (navigator, user, token, urls) {
    $('#id_login').on('click', function () {
        navigator.id.request();
    });

    navigator.id.watch({
        loggedInUser: user,
        onlogin: function (assertion) {
            $.post(
                urls.login,
                { assertion: assertion, csrfmiddlewaretoken: token }
            )
            .done(function () { window.location.reload(); })
            .fail(function () { navigator.id.logout(); } );
        },
        onlogout: function (assertion) {
            $.post(urls.logout)
            .always(function () { window.location.reload(); });
        }
    });
};

Perfectly readable.

But isn’t TDD supposed to make you break up monolithic code blocks?

So that’s a question - one of the supposed advantages of unit testing is that, when you find yourself struggling to write tests, you often find yourself re-writing your code so that is uses several, smaller, self-contained, more easily testable components, and your code is improved as a result.

In this case though, my code was definitely not being improved — or at least, that’s what I think. Would anyone disagree?

Ultimately I think it was a matter of using the wrong tool for the job. Once I had a decent mocking library, I was able to get back to good-looking code and good-looking tests.

Another thing is that nested callbacks are just quite a natural part of client-side JavaScript. GUI / UI / Async code just isn’t the same as server-side code I guess, so I shouldn’t be surprise if it follows slightly different patterns of readability. What do you think?

Anyway folks, if you’re not using a mocking library to test your Ajax code, you should definitely try one. sinon.js worked well for me, there are others out there in the wonderfully diverse JS testing ecosystem.

More info on Mozilla Persona, unit testing javascript and TDD in my book, chapter 14 of which prompted this write-up. You should check it out. It’s got a great joke with Henry Ford in it, which I’m particularly proud of.

Comments

comments powered by Disqus
Read the book

The book is available both for free and for money. It's all about TDD and Web programming. Read it here!

Reviews & Testimonials

"Hands down the best teaching book I've ever read""Even the first 4 chapters were worth the money""Oh my gosh! This book is outstanding""The testing goat is my new friend"Read more...

Resources

A selection of links and videos about TDD, not necessarily all mine, eg this tutorial at PyCon 2013, how to motivate coworkers to write unit tests, thoughts on Django's test tools, London-style TDD and more.

Old TDD / Django Tutorial

This is my old TDD tutorial, which follows along with the official Django tutorial, but with full TDD. It badly needs updating. Read the book instead!

Save the Testing Goat Campaign

The campaign page, preserved for history, which led to the glorious presence of the Testing Goat on the front of the book.