I’ve finally managed to get some traction on writing JavaScript unit tests at work, and the team decided to go with JsTestDriver. I really like JsTestDriver and have managed to get it fully reporting into our Hudson server, so thought I would take the opportunity to document what I’ve done so far.
First off we need to downloaded the latest release of JsTestDriver from here, and set the server running in the background
$ java -jar ~/jstestdriver/js-test-driver.1.2.2.jar --port 4224 --browser open
This runs the server in the background on port 4224 (it can be anything you like) and then opens the default browser (On the mac it is Chrome for me). Running this command should give you the following screen (depending on your default browser):

This now allows us to run JsTestDriver tests in the Chrome browser on port 4224. If you leave that running in the background we can now write our first unit test. Imagine this bit of code which adds a “Postage and Packaging” cost for an online shopping basket.
var basket = {}, basketItemsSetup = [];
var BasketItem = function (desc, price, classHandle) {
this.description = desc;
this.price = price;
this.classHandle = classHandle;
};
basket.addPostage = function () {
var theDataElement = $('input[type=hidden].postage');
var description = $(theDataElement).attr('name');
var price = $(theDataElement).val();
var classHandle = $(theDataElement).attr('class');
basketItemsSetup[basketItemsSetup.length] = new BasketItem(description, price, classHandle);
};
This function is defined in the following Zend Application folder structure: “Application->public->scripts->basket.js”. So we now can create a test file in “Application->tests->public->scripts->basketTest.js”
We need to be able to mock away the html that the function is looking for, and that is simple in JsTestDriver. We can do this using the following notation within a unit test
/*:DOC += <input type="hidden" name="Postage and packaging" value="4.99" class="postage" /> */
So, this is my unit test for the above code, defined in “Application->tests->public->scripts->basketTest.js”:
/**
* Basket Tests
*/
BasketTest = TestCase("BasketTest");
/**
* Setup functionality. Make sure we are in a known state before we run
* the tests
*
* @return void
*/
BasketTest.prototype.setUp = function() {
basketItemsSetup = [];
}
/**
* Test that when you postage it correctly adds a new
* BasketItem to the basketItemsSetup array and that the values
* match what is in the html
*
* @return void
*/
BasketTest.prototype.testAddPostageAppendsToItemsSetupArray = function() {
/*:DOC += <input type="hidden" name="Postage and packaging" value="4.99" class="postage" /> */
basket.addPostage();
assertArray(basketItemsSetup);
assertEquals(1, basketItemsSetup.length);
assertEquals('4.99', basketItemsSetup[0].price);
assertEquals('Postage and packaging', basketItemsSetup[0].description);
assertEquals('postage', basketItemsSetup[0].classHandle);
};
The first line
/**
* Basket Tests
*/
BasketTest = TestCase("BasketTest");
Creates a new test class that we can write our unit tests for. The second set of code
/**
* Setup functionality. Make sure we are in a known state before we run
* the tests
*
* @return void
*/
BasketTest.prototype.setUp = function() {
basketItemsSetup = [];
}
Is called before each test is run, as JsTestDriver is based on the XUnit family. What we have done here is reset the basketItemsSetup array so we now have a predefined state before each test. And the last chunk of code is the unit test itself:
/**
* Test that when you postage it correctly adds a new
* BasketItem to the basketItemsSetup array and that the values
* match what is in the html
*
* @return void
*/
BasketTest.prototype.testAddPostageAppendsToItemsSetupArray = function() {
/*:DOC += <input type="hidden" name="Postage and packaging" value="4.99" class="postage" /> */
basket.addPostage();
assertArray(basketItemsSetup);
assertEquals(1, basketItemsSetup.length);
assertEquals('4.99', basketItemsSetup[0].price);
assertEquals('Postage and packaging', basketItemsSetup[0].description);
assertEquals('postage', basketItemsSetup[0].classHandle);
};
This has the magical mocking of html in there. So first off we define the html which we want our javascript function basket.addPostage() to interrogate. The assertions we want to make are:
- That the basketItemsSetup variable is still an array after the method is called
- That the array has one item in it
- That the BasketItem inside has the correct price, description and classHandle
There is one last thing we need to setup: The JsTestDriver configuration file. This, for us, is defined in “Application->tests->jsTestDriver.conf”:
server: http://localhost:4224
load:
- public/scripts/jquery.js
- ../public/scripts/basket.js
- public/scripts/basketTest.js
plugin:
- name: "coverage"
jar: "/local/services/hudson/common/javascript/jstestdriver/coverage.jar"
module: "com.google.jstestdriver.coverage.CoverageModule"
This configuration states that the tests should be run against localhost on port 4224 and to load the javascript files. The last bit of configuration is to state where the code coverage plugin is. We have that defined in our Hudson application path.
So, lets run the tests
$ cd ~/Application/tests
$ java -jar ~t/jstestdriver/js-test-driver.1.2.2.jar --tests all
.Chrome: Runner reset.
Total 1 tests (Passed: 1; Fails: 0; Errors: 0) (2.00 ms)
Chrome 7.0.517.44 Mac OS: Run 1 tests (Passed: 1; Fails: 0; Errors 0) (2.00 ms)
/Users/bselby/Application/tests/public/scripts/jquery.js: 100.0% covered
/Users/bselby/Application/public/scripts/basket.js: 16.384182% covered
/Users/bselby/Application/tests/public/scripts/basketTest.js: 27.8481% covered
To add more browsers to run the tests against, open up that browser and browse to http://127.0.0.1:4224/

Now we can capture the new browser, in this instance: Firefox. Click on “Capture The Browser”:

Now if we run the tests again we should get the output for 2 tests (1 for chrome, 1 for firefox):
$ cd ~/Application/tests
$ java -jar ~t/jstestdriver/js-test-driver.1.2.2.jar --tests all
Chrome: Runner reset.
Firefox: Runner reset.
..Chrome: Runner reset.
Firefox: Runner reset.
Total 2 tests (Passed: 2; Fails: 0; Errors: 0) (4.00 ms)
Chrome 7.0.517.44 Mac OS: Run 1 tests (Passed: 1; Fails: 0; Errors 0) (3.00 ms)
Firefox 3.6.12 Mac OS: Run 1 tests (Passed: 1; Fails: 0; Errors 0) (4.00 ms)
/Users/bselby/Application/tests/public/scripts/jquery.js: 100.0% covered
/Users/bselby/Application/public/scripts/basket.js: 16.384182% covered
/Users/bselby/Application/tests/public/scripts/basketTest.js: 27.8481% covered
Great, but now we need a coverage report. To do this add —testOutput=build to the command
$ cd ~/Application/tests
$ java -jar ~t/jstestdriver/js-test-driver.1.2.2.jar --tests all --testOutput=build
Chrome: Runner reset.
Firefox: Runner reset.
..Chrome: Runner reset.
Firefox: Runner reset.
Chrome: Runner reset.
Firefox: Runner reset.
..Chrome: Runner reset.
Firefox: Runner reset.
Total 2 tests (Passed: 2; Fails: 0; Errors: 0) (4.00 ms)
Chrome 7.0.517.44 Mac OS: Run 1 tests (Passed: 1; Fails: 0; Errors 0) (3.00 ms)
Firefox 3.6.12 Mac OS: Run 1 tests (Passed: 1; Fails: 0; Errors 0) (4.00 ms)
See how the output has changed, and that is because the coverage information has now been placed in “Application->tests->build->jsTestDriver.conf-coverage.dat”. This now needs converting into a html document so we can easily understand it. To do this we can use genhtml
$ cd ~/Application/tests
$ ~/tools/genhtml -o build/coverage build/jsTestDriver.conf-coverage.dat
Reading data file build/jsTestDriver.conf-coverage.dat
Found 3 entries.
Found common filename prefix "/Users/bselby/Application"
Writing .css and .png files.
Generating output.
Processing file public/scripts/basket.js
Processing file tests/public/scripts/basketTest.js
Processing file tests/public/scripts/jquery.js
Writing directory view page.
Overall coverage rate:
lines......: 20.2% (52 of 257 lines)
functions..: no data found
branches...: no data found
If we open this in our browser we get:

I hope this gives an overview of how to write JavaScript unit tests using JsTestDriver. I will follow up with an article of getting this running in Eclipse as a “builder” and also how to set it up running in Hudson. Sadly I cannot get the Eclipse plugin to work in Helios on Snow Leopard or Windows so I have defined a builder instead