Doctest/JS: unit testing for Javascript made simple

Author: Ian Bicking <ianb@colorstudy.com>

License & Download

This library is licensed under the GPL. You can download it from the Mercurial repository at http://bitbucket.org/ianb/doctestjs/ with hg clone http://bitbucket.org/ianb/doctestjs/ or download a zip

Bugs may be reported on the issue tracker. Patches are best provided by forking the repository through bitbucket.org, and then submitting a pull request. If you are using doctest/js, or have written about it, consider noting this on the wiki.

Introduction

Doctest/JS is a port of a widely used testing module doctest from the Python world. The original doctest is by Tim Peters.

The idea behind doctest is to demonstrate code usage in such a way that the demonstrations themselves can be tested. But it was soon found that the technique used was more compact and readable than the xUnit style of testing that had become the defacto standard for testing. (In my opinion, an unfortunate defactor standard.)

The basic idea for a doctest is very simple: run an expression and check that its output is what you expected. For example:

$ function factorial(n) {
>     if (n == 0) {
>         return 1;
>     } else {
>         return n * factorial(n-1);
>     }
> }
...
$ factorial(3)
6
Output:
Here we define a function and test it in-place. Typically of course you'd define the functions in your normal .js files (which you'd include as normal with <script>), and then simply use them in the examples.

The format is fairly simple:

Output and repr

Doctest tries to act like a REPL: read an expression or statement, evaluate it, print the result. The basic loop is:

  1. Parse all the examples.
  2. Evaluate an expression.
  3. If the result is null, do nothing and show nothing.
  4. If the expression throws an exception, do writeln('Error: '+exception)
  5. Otherwise writeln(repr(result)).
  6. Compare everything printed to what the example shows
  7. Repeat to bottom (regardless of errors).

Specifically, writeln(str, ...) writes strings. You can use this in your own code if you want, or use something like writeln(someFunction()) to avoid repr.

repr(obj) gets the programmer's representation of an object. The representation of a string is the Javascript literal for that string. The representation of an Array [repr(item0), repr(item1), ...]. (The code for repr is taken from MochiKit.) You can customize what repr does to your own objects by implementing a method __repr__ or repr, for example:

MyClass.prototype.__repr__ = function () {
    return '[MyClass name='+repr(this.name)+']';
};

Exceptions and Logging

It's encouraged that you test exception cases in your code. You do this like:

$ function countTag(parent, tag) {
>     return parent.getElementsByTagName(tag).length;
> }
...
$ countTag(document.getElementById('dt-exception'), 'pre');
0
$ countTag({}, 'pre');
Error: TypeError: parent.getElementsByTagName is not a function
Output:

Unfortunately this hides the traceback. Doctest will try to print the traceback to the logs; it's not as good a traceback, but it might be helpful anyway. If you install Firebug the traceback will be in the console.

Test Page Structure

As mentioned before, pages must contain certain structure. You can have all your tests run together, or you can run pieces individually. You must of course include doctest.js in your file, and then call doctest(). There are optional arguments: doctest(verbosity, elementId, outputId). verbosity defaults to 0. elementId is the id of the <pre> element you want to test. If you don't provide it then all pre elements with class="doctest" are selected. Lastly the output is placed in a pre with the id outputId, or id="doctestOutput if none is given.

Typically you run this code with <button onclick="doctest(...)">test</button>. It can be helpful to include a reload button next to that, since usually when you re-test you want to reload the page first. You can use <button onclick="doctest.reload(this)">reload</button> for this. You can include as many buttons as you want on the page; for instance, each individual pre test block can have a button to test just that block (as do the examples in this document), plus one button to test everything at once.

The file template.html can be copied to setup new tests.

Asynchronous Calls

One problem with this implementation of Doctest is that everything must be synchronous. That is, code using things like XMLHttpRequest will have problems, because Javascript will never stop executing until the test is done, and generally requests like that only start running once Javascript has started running.

It's possible this will be fixed in the future, but until then you'll have to write your code in a synchronous style. You can also set up synchronous mock objects that act similarly to how the asynchronous objects work. For instance, imagine you have a sendRequest(uri, data, callback) routine; in your test you could implement this like:

// Set this variable in your tests to control the mock:
nextRequestResponse = null;
MyModule.sendRequest = function (uri, data, callback) {
    writeln('Requesting '+uri);
    if (data) {
        writeln('Data: '+repr(data));
    }
    callback(nextRequestResponse);
};
This can call code in a different order than in a real situation, but it is usually a good simulation.

For the YUI a mock version of YAHOO.util.Connect.asyncRequest is provided. If you include doctest-yui.js in your test then that function will be replaced with something more suitable for use with Doctest.

To Do

  1. Detail should be displayed (but not matched for) when there is an exception
  2. A difficult-to-implement but important feature would be some way of "pausing" the test so that asyncronous actions can occur. It might look like pause(func), where a timer would call func over and over until it returned true, then restart the tests at that time.
  3. Automatic creation of an output area for the report, if one doesn't exist already.
  4. Easier setup of per-test buttons, etc. Maybe automatic setup.
  5. Some function to run on load that will look at the query string to automatically run some or all tests on the page. And a function that will see if the tests have already been run, and if so then reload the page with that auto-tester, basically combining "reload" and "test" in one button.