Remember when you gave unit testing your Javascript a serious go only to find yourself Googling “mocking jQuery ajax” or “moment.js unit test”? Remember that feeling of disgust after your test finally passed only to find you racked up a dozen lines of setup code? It’s no surprise many of us give up on writing tests in frustration after fighting these challenges.
Dependencies
The modern in-browser development ecosystem is looking more and more like server and desktop development of the last few decades with complex state, interactions, and integrations. We’re using static analysis tools, more intricate editor setups, build automation, continuous integration, operations tooling, and more.
As this evolution continues, we have an opportunity to reach back into the archives of software development and learn from design patterns, principles of code structure, and techniques for testing at different levels of our applications. The research we find is invaluable, thoughtful, and often…pretty DRY (pun very much intended). Many of those patterns, we’ve noticed, involve dependencies in our software and how they intertwingle and corrupt—especially in tests. That is, many software patterns exist to help organize decoupled, distinct pieces of functionality. We’d like to save you a little research time and give you a catchy phrase to throw around the office: Mow Your Own Yard.
Mow Your Own Yard
That’s right, the next time you receive a Pull Request with setTimeout mocked or an assertion on jQuery.ajax arguments, simply reply Mow Your Own Yard and get back to sipping your cappuccino with your feet propped up. Ok, maybe that’s not the best way to handle a PR.
The Art of Managing Dependencies
At Sparkbox, we’ve taken to using this phrase to describe what we test and how we organize our code. Mow Your Own Yard is our way of saying, test your own code, not jQuery, moment.js, React, etc., by pushing those pieces to the edges. We feel it’s an easy mantra to hold that helps make testing more natural and software more stable in the face of change.
Enough jibber-jabber, let’s look at an example. Imagine you rely on jQuery.ajax to retrieve some activity and then use moment.js to present that information to your users, including a lastUpdated
note. That’s not hard, so you write something along the lines of this:
$(() => {
$.ajax({
url: "https://rawgit.com/cromwellryan/d1fbb3e79f873ad5758c/raw/a21a988004e81eda5a9144b2663227d29dbcb0d4/activity.json"
}).done( (data) => {
var activities = data.activity;
var formatted = activities.map( (activity) => {
return {
"displayName": `${activity.user.firstName} ${activity.user.lastName} (${activity.user.username})`,
"formattedDateTime": moment(activity.written_at).fromNow(),
"activity": activity.activity
};
});
formatted.forEach( (activity) => {
var $li = $('<li/>');
$li.text(`${activity.formattedDateTime}, ${activity.displayName} ${activity.activity}`);
$('ul.js-activity').append($li);
});
});
})
Maybe you rely on Handlebars, Knockout, or React to actually push information into the DOM, but the problem is nearly unchanged: get some data, transform that data into something comprehensible, and push it into the DOM.
Unfortunately, writing tests for the happy and failure paths of such an implementation would require either a reliable server, which you could inform how to respond, or stubbing jQuery’s ajax—yuck! Using moment.js for the current date/time doesn’t do us any favors either because we can’t reliably test something that is changing under us.
Creating Code Suburbs
By making a few minor changes to push troublesome dependencies to the edges, we end up with the following:
var myApp = {
formatActivities(data, asOf) {
var activities = data.activity;
asOf = asOf || moment(); // optional arguments make testing easier... sometimes
var formatted = activities.map( (activity) => {
return {
"displayName": `${activity.user.firstName} ${activity.user.lastName} (${activity.user.username})`,
"formattedDateTime": moment(activity.written_at).from(asOf),
"activity": activity.activity
};
});
return formatted.map(myApp.formatActivityMessage);
},
formatActivityMessage(activity) {
return `${activity.formattedDateTime}, ${activity.displayName} ${activity.activity}`;
}
};
$(() => {
$.ajax({
url: "https://rawgit.com/cromwellryan/d1fbb3e79f873ad5758c/raw/a21a988004e81eda5a9144b2663227d29dbcb0d4/activity.json"
}).done( (data) => {
var formatted = myApp.formatActivities(data);
formatted.forEach( (message) => {
var $li = $('<li/>');
$li.text(message);
$('ul.js-activity').append($li);
});
});
})
We’ve isolated the interesting parts of our code, formatActivities
and formatActivityMessage
, from those of our neighbors’ jQuery and moment.js. In doing so, we’ve given ourselves a seam in which to test different scenarios without the corrupting influence of Ajax, DOM Elements, or Promises. Here we see an example of potential data being transformed and tested:
my_app_spec.js:
describe('myApp', () => {
describe('.formatActivityMessage', () => {
it('shows all data', () => {
var activity = {
"displayName": "Skippy John Jones",
"formattedDateTime": "Earlier today",
"activity": "Flew to space"
}
var result = myApp.formatActivityMessage(activity);
expect(result).toEqual("Earlier today, Skippy John Jones Flew to space");
});
});
describe('.formatActivities', () => {
it('displayName happy path', () => {
var activities = [{
"user": { firstName: "Skippy", "lastName": "John Jones", "userName": "skippyJJ" },
"written_at": "2012-04-23T18:25:43.511Z",
"activity": "Space flight"
}];
var result = myApp.formatActivities(activities);
expect(result[0].displayName).toEqual("Skippy John Jones (skippyJJ)");
});
});
});
myApp.js:
var myApp = {
formatActivities(data, asOf) {
var activities = data.activity;
asOf = asOf || moment(); // optional arguments make testing easier... sometimes
var formatted = activities.map( (activity) => {
return {
"displayName": `${activity.user.firstName} ${activity.user.lastName} (${activity.user.username})`,
"formattedDateTime": moment(activity.written_at).from(asOf),
"activity": activity.activity
};
});
return formatted.map(myApp.formatActivityMessage);
},
formatActivityMessage(activity) {
return `${activity.formattedDateTime}, ${activity.displayName} ${activity.activity}`;
}
};
Same Problems, New Platform
Despite what we might think are uncharted territories of development on a platform based on browsers, markup, standards, and a funny language, the recognized patterns of software development still prove applicable and invaluable. As we march toward ES6, I suspect concepts like modules will put even more demands on relearning many of these lessons.
Recommended Further Reading
The Internet is abound with resources, opinions, and thoughts on software principles and patterns. Here are some of the resources I’ve found helpful over the years. Even popular frameworks like Redux and ReactJS are reusing popular patterns discovered decades ago!
Dofactory continues to provide a comprehensive list of pattern descriptions and example.
Ward Cunningham, creator of the wiki, has a thoughtful collection of annotated design patterns.
Design Patterns: Elements of Reusable Object-Oriented Software, also known as the Gang of Four, is a timeless collection of solutions to commonly occurring design problems.
Architectural Patterns and Styles collects a number of infrastructural patterns that can help a system evolve effectively and efficiently.