Long ago, in the late days of the first Internet boom, before jQuery, before Underscore, before Angular, there was a web application built by a large corporation. This application was written as a server-side application using server-side technology like Java or PHP. A tiny seed of JavaScript was added to some of the pages of this application to give it a little sizzle.
Over the ages, this tiny bit of JavaScript grew like kudzu. Most of it was embedded in the HTML in
12. SOLUTION #2: WE NEED A REWRITE!
A new tiger team is selected. Everyone wants to be on this team because it’s a
green-field project. They get to start over and create something truly beautiful.
But only the best and brightest are chosen for the tiger team. Everyone else must
continue to maintain the current system.
Now the two teams are in a race. The tiger team must build a new system that
does everything that the old system does. Not only that, they have to keep up
with the changes that are continuously being made to the old system.
Management will not replace the old system until the new system can do
everything that the old system does.
This race can go on for a very long time. I’ve seen it take 10 years. And by the
time it’s done, the original members of the tiger team are long gone, and the
current members are demanding that the new system be redesigned because it’s
such a mess.
Martin, Robert C. (2008-08-01). Clean Code: A Handbook of Agile Software Craftsmanship (p. 5).
Pearson Education (USA). Kindle Edition.
16. THE SIZZLE
I have code in the HTML or in an associated
JavaScript file that does some sort of fancy UI thing
in a dated way.
Problem
Solution Refactor to use modern CSS and test manually.
There is very little point in putting automated tests
around visual effects.
18. CODE IN THE HTML
I have code in the HTML. It cannot be loaded into a
test harness to test it.
Problem
Solution Move code a .js file and add a script tag if needed.
Surround code in question with a test if possible or
extract a method and test that. If there is code in
event attributes in the HTML, used jQuery to bind
to those events instead.
19. index.html
<script>
function doAwesomeThing() {
thing.doAwesomeStuff("Awesome App is Awesome");
}
</script>
<a onclick="doAwesomeThing()">Awesome Thing</a>
index.html
<script src="awesome.js"></script>
<a id="awesomeThing">Awesome Thing</a>
awesome.js
$('#awesomeThing').on('click', onAwesomeThingClicked);
function onAwesomeThingClicked() {
thing.doAwesomeStuff("Awesome App is Awesome");
}
awesome-spec.js
describe("awesome thing", function() {
beforeEach(function() {
spyOn(thing, 'doAwesomeStuff');
});
it("does awesome stuff when clicked", function() {
onAwesomeThingClicked();
expect(thing.doAwesomeStuff).toHaveBeenCalledWith('Awesome App is Awesome');
});
});
20. DOM INTERACTION
A lot of the code interacts with the DOM and I
don’t have a DOM in my unit test.
Problem
Solution You can have a DOM in your unit tests. Mock out
HTML elements using Jasmine jQuery. Use jQuery
to trigger events and to validate that the DOM is
modified in the expected way.
22. 10,000 GLOBAL FUNCTIONS
I have functions everywhere, man!Problem
Solution Put a test around the global function. Move the
global function to a Module and replace the global
function with a variable that points to the Module.
Later, when no more code is accessing the global,
remove the global variable.
23. app.js
function transmogrify() {};
function transmute() {};
function transform() {};
app.js
var Mutators = {};
Mutators.transmogrify = function() {};
Mutators.transmute = function() {};
Mutators.transform = function() {};
var transmogrify = Mutators.transmogrify();
var transmute = Mutators.transmute();
var transform = Mutators.transform();
app-spec.js
describe("Mutators.transform", function() {
it("has mapping to legacy function", function() {
expect(transform).toBe(Mutators.transform);
});
it("does stuff", function() {
expect(Mutators.transform()).toBe(true);
});
});
24. NO UNITS
There are no units to test.Problem
Solution Carefully extract a function and place tests around
that function. Be careful not to create a 10,000
Global Functions problem.
25. app.js
$(function() {
var id = $('#id').val();
$.get('data/' + id, function(data) {
$('#name').val(data.name);
$('#rank').val(data.rank);
$('#serialNo').val(data.serial);
});
});
app.js
$(function() {
fetchAndUpdateDom();
});
function fetchAndUpdateDom() {
var id = $('#id').val();
$.get('data/' + id, function(data) {
updateDom(data);
});
};
function updateDom(data) {
$('#name').val(data.name);
$('#rank').val(data.rank);
$('#serialNo').val(data.serial);
}
26. JQUERY EVERYWHERE
I have jQuery calls inline everywhere in my code.Problem
Solution Using the Module pattern, migrate the jQuery that
interacts with the DOM into Passive Views. Migrate
the jQuery that makes AJAX calls into Adapters.
Place tests around these Modules.
27. app.js
function fetchAndUpdateDom() {
var id = $('#id').val();
$.get('data/' + id, function(data) {
$('#name').val(data.name);
$('#rank').val(data.rank);
$('#serialNo').val(data.serial);
});
};
app.js
var view = {
id : function() { return $('#id').val(); },
name : function(val) { $('#name').val(val); },
rank : function(val) { $('#rank').val(val); },
serialNo : function(val) { $('#serialNo').val(val); }
};
var data = {
fetch : function(id, callback) {
$.get('data/' + id, callback);
}
};
function fetchAndUpdateDom() {
data.fetch(view.id(), function(data) {
view.name(data.name); view.rank(data.rank); view.serialNo(data.serialNo);
});
}
28. REALLY UGLY FOR LOOPS
These nested for loops are hideous!Problem
Solution Use Underscore, lodash, or the built in array
functions like forEach, find, reduce, and map.
29. app.js
var values = [1,2,3,4,5], sum = 0, evens = [], doubles = [], i;
for(i = 0; i < values.length; i++) {
sum += values[i]
}
for(i = 0; i < values.length; i++) {
if (values[i] % 2 == 0) evens.push(values[i]);
}
for(i = 0; i < values.length; i++) {
doubles.push(values[i] *2);
}
app.js
var values = [1,2,3,4,5];
var sum = values.reduce(function(prev, value) {
return prev + value;
}, 0);
var evens = values.filter(function(value) {
return value % 2 === 0;
});
var doubles = values.map(function(value) {
return value * 2;
});
30. REALLY LONG FUNCTIONS
This function is too long!Problem
Solution Place a test around the function and refactor it to a
Revealing Module.
31. app.js
function doMaths(x, y) {
var add = x + y;
var sub = x - y;
var multi = x * y;
var div = x / y;
return { add: add, sub: sub, multi: multi, div: div };
};
app.js
var mather = (function() {
var _x, _y;
function doMaths(x, y) {
_x = x; _y = y;
return { add: add(), sub: sub(), multi: multi(), div: div() };
}
function add() { return _x + _y; }
function sub() { return _x - _y; }
function multi() { return _x * _y; }
function div() { return _x / _y; }
return { doMaths : doMaths };
})();
mather.doMaths(x, y);
32. ALMOST DUPLICATION
I have all these functions that do almost the same
thing but I can’t break them down.
Problem
Solution Use a Lambda Factory to create the functions for
you.
33. app.js
function getFirstNumberTimes(x) { return Number($('#first').val()) * x; }
function getSecondNumberTimes(x) { return Number($('#second').val()) * x; }
function getThirdNumberTimes(x) { return Number($('#third').val()) * x; }
app.js
function getFirstNumberTimes(x) { return getNumberTimes('#first', x); }
function getSecondNumberTimes(x) { return getNumberTimes('#second', x); }
function getThirdNumberTimes(x) { return getNumberTimes('#third', x); }
function getNumberTimes(id, x) {
return Number($(id).val()) * x;
}
app.js
function getTimesFn(id) {
return function(x) {
var val = $(id).val();
return Number(val) * x;
};
}
var getFirstNumberTimes = getTimesFn('#first');
var getSecondNumberTimes = getTimesFn('#second');
var getThirdNumberTimes = getTimesFn('#third');
34. SIDE-EFFECTS
When testing an object, I get side effects.Problem
Solution This is really just the problem of using globals.
JavaScript’s dynamic nature makes global objects
easy to implement. Avoid this and create Factory
Functions instead of global Modules.
35. app.js
var statefullThing = (function() {
var _name = "Alice";
function getName() { return _name; }
function setName(name) { _name = name; }
return { getName: getName, setName: setName };
})();
app-spec.js
describe("statefulThing", function() {
var subject = statefullThing;
it("has a settable name", function() {
subject.setName('Bob');
expect(subject.getName()).toBe('Bob');
});
it("has expected default name", function() {
expect(subject.getName()).toBe('Alice');
});
});
app.js
function statefuleThingFactory() {
var _name = "Alice";
function getName() { return _name; }
function setName(name) { _name = name; }
return { getName: getName, setName: setName };
};
var statefulThing = statefulThingFactory();
app-spec.js
describe("statefulThing", function() {
var subject;
beforeEach(function() {
subject = statefulThingFactory();
});
it("has a settable name", function() {
subject.setName('Bob');
expect(subject.getName()).toBe('Bob');
});
it("has expected default name", function() {
expect(subject.getName()).toBe('Alice');
});
});
36. PYRAMIDS OF DOOM
My AJAX calls are nested really deeply and I have
some timing issues with them.
Problem
Solution Use Promises to remove the nesting and force the
call order to what is needed.
37. app.js
$.get('orders.json', function(orders) {
var order = findOrder(orders);
updateDom(order);
$.get('items/' + order.id, function(items) {
var item = findItem(items);
updateDom(item);
$.get('details/' + item.id, function(details) {
udpateDom(details);
});
});
});
app.js
get('orders.json').then(function(orders) {
var order = findOrder(orders);
updateDom(order);
return get('items/' + order);
}).then(function(items) {
var item = findItem(items);
updateDom(item);
return get('details/' + item.id);
}).then(function(details) {
updateDom(details);
});