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');
});
-
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!
Comments
comments powered by Disqus