For some reason, it is all too easy not to give the client-side JavaScript code in a web project the same level of care that the “real code” server-side gets. I’m sure I’m not the only who have found myself having to searching through .js files with several hundred (if not thousands) of lines of badly formatted JavaScript code with global variables and functions all over the place, trying to track down some elusive bug. These days, I have a zero-tolerance for code without test coverage – why should I treat my JavaScript any differently? JavaScript is code too…
Unit Test Frameworks for JavaScript
There are quite a few unit test frameworks available for JavaScript - Mark Levison recently posted a nice comparison between a few of them over on his blog. I’ve decided to give a few of the most interesting ones a go, and try to find out which one I like the most, blogging about my experiences along the way. The first framework I’m going to have a look at is YUI Test.
Giving It a Go
To get some real-life experience with unit testing JavaScript using YUI Test, I decided to implement an inline editing component. It should work like this:
- 1. User clicks on the element to edit (for example a <span/>).
- 2. The element is converted into an <input type=’text’ /> element.
- 3. The user edits the text and hits the ENTER key.
- 4. The <input/> element is converted back to the original element type, containing the edited text.
Using the YUI Test framework, here’s my first test, which will verify that clicking an element which is editable turns it into a text input (step 1-2). Note that I’ve omitted some of the initialization code etc for the framework – you can download a complete working example at the end of this post.
var testCase0 = new YAHOO.tool.TestCase(
{
name: "When clicking editable span",
setUp: function()
{
var span = $('#span1'); // find the span to edit
this.editor = new InlineEditor(span);
YAHOO.util.UserAction.click(span[0]); // simulate mouse click to begin editing
},
test_span_is_turned_into_input: function()
{
var input = $('#span1'); // find the element in the document
// element should now be replaced with an input element
YAHOO.util.Assert.isInstanceOf(HTMLInputElement, input[0]);
}
});
Note that I’m also using jQuery here (the $ stuff), as I’m not familiar with the rest of the YUI framework (which probably has similar functionality for finding elements etc – I’m just too lazy to look it up :p).
To make this test pass, we need some markup:
<html>
<body>
<div>
<h1>Test Page</h1>
<span id="span1">Click to edit me!</span>
</div>
</body>
</html>
Next, we need to implementing enough of the InlineEditor to make the first test pass:
InlineEditor = function(span) {
this.span = $(span);
var self = this;
this.span.click(function() { self.beginEdit(); });
}
InlineEditor.prototype =
{
span: null,
input: null,
beginEdit: function() {
var self = this;
self.input = $("<input type='text' name='title' />");
self.input.attr('id', self.span.attr('id')); // copy id across
self.input.val(self.span.text());
self.span.replaceWith(self.input);
self.input.select();
}
}
Running the tests, we can see that we’re OK so far:

The next requirement is that when the user changes the text and hits ENTER, the original element is shown again, updated with the new text (step 3-4). Lets write a test for that:
var testCase1 = new YAHOO.tool.TestCase(
{
name: "When hitting enter key while editing",
setUp: function()
{
var span = $('#span2'); // find the span to edit
var editor = new InlineEditor(span);
// simulate user clicking the element to begin editing
YAHOO.util.UserAction.click(span[0]);
this.expected = 'test';
editor.input.val(this.expected); // simulate user changing text
// simulate the user hitting enter to end editing
YAHOO.util.UserAction.keydown(editor.input[0], { keyCode: 13 });
},
test_text_is_updated: function()
{
var elem = $('#span2');
// the document should now contain a span element
YAHOO.util.Assert.isInstanceOf(HTMLSpanElement, elem[0]);
// span should have the edited value set
YAHOO.util.Assert.areEqual(this.expected, elem.text());
}
});
To make this pass, we must complete the implementation of our InlineEditor:
InlineEditor.prototype =
{
span: null,
input: null,
beginEdit: function()
{
var self = this;
self.input = $("<input type='text' name='title' />");
self.input.attr('id', self.span.attr('id')); // copy id across
self.input.val(self.span.text());
self.span.replaceWith(self.input);
// handle keydown event to watch for user hitting 'enter'
self.input.keydown(function(e)
{
switch (e.keyCode)
{
case 13: // enter
self.endEdit(); break;
}
});
self.input.select();
},
endEdit: function()
{
var self = this;
// update text
self.span.text(self.input.val());
// restore click handler
self.span.click(function() { self.beginEdit(); });
// switch elements again
self.input.replaceWith(self.span);
}
}
With this in place, we can run our tests again and verify that things are OK:
First Impressions of Unit Testing JavaScript (with YUI Test)
I can say right away that it just feels right to also have unit tests for the JavaScript code in a project. These days the client-side logic is becoming more and more complex, and is often just as important to the project as the server-side business logic.
I’ve just barely scratched the surface of the features in YUI Test in this post – check out it’s excellent documentation pages to learn more; the asynchronous test support looks especially cool. Apparently a complimentary mocking library is in the works as well.
Unit testing JavaScript is certainly not without complications. The biggest issue I’ve had so far, is making each test run in isolation. YUI Test doesn’t refresh the browser window between running each test, which means that it is easy for tests to contaminate each others state if you’re not careful (especially true for tests that interact with the DOM, like my example does). This is a feature that I would really like to see, and one I’ll be looking for in other frameworks.
Another obvious obstacle is how to make these tests part of a continuous integration environment. I haven’t looked into it yet, but YUI Test seems to have some great extensibility points for reporting test result either as XML or JSON. I can imagine that writing a custom build task for Team Foundation Server which kicks off the tests and parses the results would be perfectly possible to implement.
Demo Code
If you want to play about with this stuff for yourself, you can download the demo code that I wrote for the examples in this post.