Monday, February 26, 2007

JavaScript unit tests

Athena has had Nit for some time now. Nit comes in handy when code which interacts in complex ways with the runtime needs to be tested (that is, when you really want Firefox running your code, to make sure your code works with Firefox, because Firefox does something ridiculously insane that you need to account for). This isn't really the common case for unit testing, though. In the common case, you want nice, cleanly factored units, each with a well defined test suite with nice, simple tests which don't require a web browser to run.

A little while ago jml threw together an xUnit implementation for JavaScript to include in Nevow with Athena. Here's a sample of the result:

// Copyright (c) 2006 Divmod.
// See LICENSE for details.

* Tests for L{Mantissa.ScrollTable.PlaceholderModel}

// import Divmod.UnitTest
// import Mantissa.ScrollTable

Mantissa.Test.TestPlaceholder.PlaceholderTests = Divmod.UnitTest.TestCase.subclass(
* Set up a placeholder model with an initial placeholder which extends as far
* as C{totalRows}.
* @type totalRows: integer
* @rtype: L{Mantissa.ScrollTable.PlaceholderModel}
function setUp(self) {
var totalRows = 5;
self.model = Mantissa.ScrollTable.PlaceholderModel();
self.model.registerInitialPlaceholder(totalRows, null);

* Set up a placeholder model with as many placeholders are there are entries
* in C{ranges}, each start and stop indices equal to the first and second
* entries in the corresponding pair.
* @param ranges: pairs of [start index, stop index]
* @type ranges: sorted C{Array} of C{Array}
* @rtype: L{Mantissa.ScrollTable.PlaceholderModel}
function createPlaceholderLayout(self, ranges) {
var model = Mantissa.ScrollTable.PlaceholderModel();
for (var i = 0; i < ranges.length; i++) {
model.createPlaceholder(ranges[i][0], ranges[i][1], null));
return model;

* Test L{Mantissa.ScrollTable.PlaceholderModel.createPlaceholder}
function test_createPlaceholder(self) {
var p = self.model.createPlaceholder(0, 1, null);

self.assertIdentical(p.start, 0, "expected a start of 0");
self.assertIdentical(p.stop, 1, "expected a stop of 1");
self.assertIdentical(p.node, null, "expected a null node");

p = self.model.createPlaceholder(5, 11, 6);

self.assertIdentical(p.start, 5, "expected a start of 5");
self.assertIdentical(p.stop, 11, "expected a stop of 11");
self.assertIdentical(p.node, 6, "expected a node of 6");

// ...

These can be run with trial, although the integration still has a couple obvious defects:

exarkun@boson:~$ trial xmantissa.test.test_javascript
Running 1 tests.
test_createPlaceholder ... [OK]
test_dividePlaceholder ... [OK]
test_findFirstPlaceholderIndexAfterRowIndexMultiplePlaceholders ... [OK]
test_findFirstPlaceholderIndexAfterRowIndexMultiplePlaceholders2 ... [OK]
test_findFirstPlaceholderIndexAfterRowIndexMultiplePlaceholders3 ... [OK]
test_findFirstPlaceholderIndexAfterRowIndexMultiplePlaceholders4 ... [OK]
test_findFirstPlaceholderIndexAfterRowIndexMultiplePlaceholders5 ... [OK]
test_findFirstPlaceholderIndexAfterRowIndexOnePlaceholderNeg ... [OK]
test_findFirstPlaceholderIndexAfterRowIndexOnePlaceholderNeg2 ... [OK]
test_findPlaceholderIndexForRowIndexMultiplePlaceholders ... [OK]
test_findPlaceholderIndexForRowIndexMultiplePlaceholdersNeg ... [OK]
test_findPlaceholderIndexForRowIndexOnePlaceholder ... [OK]
test_registerInitialPlaceholder ... [OK]
test_removedRow ... [OK]
test_removedRowNeg ... [OK]
test_replacePlaceholder ... [OK]

Ran 16 tests in 2.435s

PASSED (successes=16)

This kind of testing means faster development cycles, better code factoring, and a more reliable final product.


  1. Thanks for being so inspiring. TDD really is exhilarating when you do it right. We have some tests at linden, but not nearly enough, and they are almost never run. We have tests in c++, python, ruby, perl and probably php. Who knows when which ones run. It would be great to add javascript to that list, and it would be super awesome to run them all with just one script.

    Are the tests you are running above running by pushing js through a python server into a web browser?

  2. They're run with the standalone spidermonkey interpreter :)

    There's some plan to spiff up nit (so that it has more of the xUnit stuff and encourages better testing practices), at which point it will probably become possible to run both nits and this kind of test in a browser. The standalone runner will probably hang around though, since it's a lot faster than starting up a browser.

    Other future plans include getting debugger/profiling/coverage support into the js runner.

  3. Ok, now that I read it again, I see that they are just model tests. I guess since I read "ScrollTable", I assumed they were actually testing some ui functionality as well.

    Using a standalone js interpreter is good too, though :-) Nice!