Doctest.js: A Humane Javascript Test Framework

Doctest.js is a test runner and testing framework for Javascript.

Doctest uses a novel approach to testing: example and expected result. Each test is a chunk of code that prints out results and side effects, and then the expected result is matched against that to see if the test passed or failed.

An example (note: these are live examples — you can also try your own via the live demo):

function capitalize(words) {
  return words.replace(/\b[a-z]/g, function (m) {
    return m[0].toUpperCase();
  });
}

print(capitalize('some words'));
// => Some Words

print(capitalize('some 4ward words'));
// => Some 4ward Words

This is similar to something like assertEqual(capitalize('some words'), 'Some Words') — there's a kind of "equal" check every time you print something. Instead of doing stuff then testing every detail of what happened or what was returned, testing almost happens for you — you print out what you are interested in, and you can even punt: you can start by simply exercising everything that matters, and then inspecting that what happens is what you expect, and copying those results into the test. For instance:

function getProperties(obj) {
  var result = [];
  for (i in obj) {
    result.push(i);
  }
  result.sort();
  return result;
}

print(getProperties({b: 1, a: 2}));
// =>

print(getProperties("a"));
// =>

Look: the first example worked great, ["a", "b"]. The second example though wasn't right at all. "0"? Once we have a better idea we can adjust those tests:

function getProperties(obj) {
  var result = [];
  for (i in obj) {
    if (obj.hasOwnProperty(i)) {
      result.push(i);
    }
  }
  result.sort();
  return result;
}

print(getProperties({b: 1, a: 2}));
// => ["a", "b"]

print(getProperties("a"));
// => []
Well, that wasn't quite enough, but this kind of incremental development is exactly why doctest.js is so helpful: it shows you what you've done, it shows you both presence and absence. Most test frameworks only give you tools to test what is there, and not ensure that the things you don't want aren't there.

Async testing made easy!

The other great feature of doctest.js is how it lets you test async code.

Async code is a bit tricky for all test runners. When you run the test, the result of the test won't be known until after you wait some time. Test runners might have ways to pause the test process while the test code completes. They also need ways to test that everything you wanted to happen will happen. Something that is often tricky in test code is to make sure that some callback was called — it's relatively easier to test that the callback was called correctly.

Here doctest's ability to test both what's present and what's missing shines. And the test runner also gives you a great way to serialize your asynchronous tests.

The core feature here is wait() — this lets you register a callback that will tell the test runner when this block of code is fully finished. An example:

var now = Date.now();
var done = false;

setTimeout(function () {
  done = true;
  print('The timeout finished!', Date.now() - now);
}, 300);

wait(function () {return done;});
// => The timeout finished! ...
(Notice we use ... to ignore the specific number that is printed: ellipsis act as a kind of wildcard)

With Doctest you don't have to use fake setTimeout or fake async anywhere — you always use the real thing, and it's nearly as easy to test as async code. Frameworks that make this code synchronous are cheating you, because asynchronous code is the hardest of code and deserves to be tested accurately. But when a test framework makes synchronous easy and asynchronous hard, it's all too easy when in the depth of test development to take shortcuts.

Mocking with Spy

In addition there's a simply mocking framework in doctest with Spy. This lets you create a function that records over time it is called — and each time it is called it prints out how it is called. It also makes it easy to wait on the Spy to be called:

var button = $('<button></button>');
$('body').append(button);
button.click(Spy('button.click'));
button.click();
Spy('button.click').wait();
/* =>
<button />.button.click({...})
*/
You'll notice here that we also ignored the details of the object passed to the callback. We could use wildcards to match no part or any specific part of the argument called. Also note that the value of this is shown: people usually forget that this is a kind of implicit argument to many functions, but again doctest makes the implicit explicit. Also note that the actual output is always displayed, so you can use this to inspect aspects of the environment even if you don't want to test all fo them.

Lineage

Doctest is based on the Python doctest module originally written by Tim Peters. Spy was inspired some by Jasmine's Spy class, and carries over ideas from MiniMock.

If you've used Python's doctest and found it annoying or not widely useful for doctest: doctest.js fixes all those problems: see this post for more.