Doctest.js: A Tutorial

What's it like?

So you've decided to finally get religion when it comes to testing your Javascript code? Or, you feel like testing just isn't as easy as it could be, and want to find a better way to test your Javascript code? Or even: you've thought about or tried doing Test Driven Development but you've found it hard to get going? Let's do this...

Doctest.js is basically example code and then expected output. This is really what most tests look like, but instead of lots of assertEqual(example, expected) this example/expected combination is embedded into the structure of the test.

I'm going to get right into how the test code looks, but to actually use doctest.js you have to setup an HTML file in a specific format. That is described later in the HTML section.

The structure looks like something you've probably seen before. We add one new function, print(), that works a lot like console.log(). Then we have a comment that shows what we expect to be output. A really simple example:

function factorial(n) {
  if (typeof n != "number") {
    throw "You must give a number";
  }
  if (n <= 0) {
    return 1;
  }
  return n * factorial(n-1);
}

print(factorial(4))
// => 25
    

See what I did there? 25 is totally the wrong answer! Also see what happened, the test just ran and told us so! There's also a summary of all the tests; if you do nothing it shows up at the top of the page, but in the interest of introducing the summary, here it is:

You'll notice it shows a failure (or more than one — it's the summary for all the examples in this tutorial). It also has a link to each failure, so you can jump to the problematic section.

Let's look at what we did there: print(factorial(4)) and // => 25 — the output is just a comment that starts with =>.

Testing for error conditions

You can also test errors:

print(factorial(null));
// => Error: You must give a number
When an exception is thrown it will print out Error: (error text) which you can match against. This way you can test for error conditions just like you test how "correct" invocations work. Note that the print() isn't really necessary here, you could do this just as well:
factorial(null);
// => Error: You must give a number

print() and output matching

print() pretty-prints things. This is important, because you have to "expect" the same output that print() produces. You can give multiple arguments, like with console.log.

print({someProperty: 123, something: {a: 1, b: 2}, "foo": 123.1032, "another-property": [1,2,3,4]});
/* =>
{
  "another-property": [1, 2, 3, 4],
  foo: 123.1032,
  someProperty: 123,
  something: {a: 1, b: 2}
}
*/
You might notice that the attributes are alphabetized and are quoted only when necessary. If it's a small object it stays on one line:
print({someProperty: 123});
// => {someProperty: 123}

But sometimes the output is unpredictable; or rather you can predict it will change. When that's the case you can basically put a wildcard in the expected output: ... — that will match anything, including multiple lines. In addition you can use ? to match one word-like-thing (a number, symbol, etc; not " or whitespace or other symbols). You can use it like this:

print({
  date: new Date(),
  timestamp: Date.now()
});

// => {date: ..., timestamp: ?}

You might notice that it passes, but you still get to see the actual output. This is a great way to show information that you can review, without actually testing. For instance, you might be testing something that connects to a server, in that case you might want to do this:

var server = {url: "http://localhost:8000"} // or some calculated value
print(server.url);
// => ...
Now if everything seems breaky you can be 100% sure of what server you are connecting to.

If you have a variable that is dynamic but you still care about the value, you should do something like this:

var date = Date.now();
print(date == date, date);
// => true ...
Think of this pattern of print(x == y) as a kind of assertEqual() equivalent.

Testing async code

This is all well and good, but lots of code in Javascript is asynchronous, meaning that you don't just call a function that returns a value. Doctest.js has an answer to that too: a great answer!

For our example we'll use XMLHttpRequest, a common source of asynchronosity. We'll test a request (just a loopback request, but if you are testing a foreign service you'd need CORS access). When we instantiate and setup the request we don't have anything really to test — we want to test what happens when the request completes.

To do this we'll use wait() — when this function is called the test runner will wait at the point where it sees // =>, for a certain amount of time or until a certain condition is met. Only then will it compare all the output that has happened to what was expected, and run the next chunk of test.

You can use this like: wait(function () {return true when done}) or wait(millisecondsToWait). We'll use the first form, which is almost always better, since it allows the test to continue more quickly. Tests also always time out eventually (by default the timeout is 5000 milliseconds, i.e., 5 seconds — by convention everything in Javascript is in milliseconds).

var endpoint = location.href;
print(endpoint);
// => ...

var req = new XMLHttpRequest();
req.open("GET", endpoint);
req.onreadystatechange = function () {
  if (req.readyState != 4) {
    // hasn't actually finished
    return;
  }
  print("Result:", req.status, req.getResponseHeader('content-type'));
};
req.send();

wait(function () {return req.readyState == 4;});

print("Current state:", req.readyState);

/* =>
Current state: 1
Result: 200 text/html
*/
I put in something tricky there to try to clarify what wait() really does. You'll notice there's a call to wait() that makes sure that req.readyState == 4 (that's the code that means the request is finished). But right after when we do print(req.readyState) it shows a readyState of 1. That's because the entire block is printed (from the previous // => output up until the next one). But the test runner keeps collecting output and doesn't run the next section until that wait() clause returns true.

Another thing to note is that wait() needs to be called when that block of code is run — it can't be inside a function that isn't called. That said, if you write test helper functions (and you should!), it often works well to put those calls in the helper function. We'll see an example of that next...

The Spy

Note: the next example will use some jQuery, just for the heck of it, though there is no special support for jQuery or other frameworks in Doctest.

If you use these tools you might end up writing code like this quite a lot:

// We've embedded a button just below this element
var button = $('#example-button');
// Just to highlight what we're working with:
button.css({border: '1px dotted #f00'});
button.click(function () {
  print('Button clicked');
});

// Now we test that our event handler gets called when we do an artificial click of the button:
button.click();
// => Button clicked

But maybe we are curious about the arguments passed to that handler — even though we ignored the arguments, there was one passed. And we might want to show what this is bound to; this is kind of like an invisible extra argument passed to every function invocation. We could make a fancier print() statement there. But instead, there's also a handy tool for tracking calls: Spy.

An example:

var button = $('#example-button2');
button.css({border: '1px dotted #00f'});
button.click(Spy('button.click'));
button.click();

// => ...

That's a lot more information! Let's break it down:

<button id="example-button2" style="border: 1px solid rgb(0, 0, 255); " type="button">Example Button 2</button>.button.click({ ...
There's two bits of information here. The first is the value (in blue) of this, which is the #example-button2 element. You'll notice it shows the HTML of the element. If you want Spy to ignore this you can use Spy('button.click', {ignoreThis: true}).

The second value (in green) is the name we gave the Spy when we created it. Note that Spy names are also identifiers, that is, Spy('button.click') === Spy('button.click').

Next of course is all the arguments. There's a lot of arguments there. They are... interesting. You'll notice some references to ...recursive... which is what you get when you have self-referencing data structures. But maybe you want to test just a little of that structure without testing all of it. You might do something like this:

// Spy('button.click') fetches the same Spy we were using before, which still has all its call information
// .formatCall() shows the way the Spy was last called.
print(Spy('button.click').formatCall());

/* =>
<button...</button>.button.click({
  currentTarget: <button...
  ...
  timeStamp: ?,
  type: "click"
})
*/

So we've tested that the type is click, that it has a timeStamp (though not the value) and that the currentTarget is a button (presumably the button we bound it to). We still get to see all the other information, we just aren't testing it. This can be helpful in the future when you realize there's more you want to test — you can look at the test output and transcribe more into the test. Or when something fails later you might want to inspect that output to make sure everything is what you expect (and when you see something unexpected that's also a great time to expand your test).

Spies have a bunch of options, and act as a kind of mock object as well. You can pass in options as the second argument, like Spy('name', {options...}). Some highlights:

applies
This is a function that the Spy "wraps". So if you do Spy('click', {applies: function (event) {this.remove(); return false;}}) then you'll get the same output printed, but you'll also run this.remove().
writes
If you set this to false then it won't automatically print out the calls. The values of the calls will still be recorded, and you can use aSpy.formatCall() to see them.
ignoreThis

Lots of code binds this without intending too. It's really easy in Javascript to do this. For instance, if you do handlers[i]() then this will be handlers. (Instead you might do var handler = handlers[i]; handler())

Anyway, sometimes you don't care about this, and using {ignoreThis: true} lets you do that.

returns
If you want the Spy to return a value when its called, give the value here. Normally it returns undefined.
throwError
This makes the Spy throw the given error anytime it is called.
wait
If you use Spy('name', {wait: true}) then the test will wait until the Spy has been called. This is a pretty common pattern. It's basically the same as Spy('name').wait().

You can set values like Spy.defaultOptions.writes = false if you want to set one of these by default.

If you want to inspect how the Spy has been called, you can check a few attributes:

.called
True once this Spy has been called.
.self and .selfList
This is the value of this, or .selfList contains a history for each call.
.args and .argList
The arguments the function was called with, or .argList is a history of arguments.

console.log

This isn't a feature you have to do anything about, it's just there for you, so I'm just going to point it out.

When you use console.log (or any of its friends, like console.warn) those messages will be captured (in addition to going to the log as normal), and the output will be shown in the specific test where they happened. A quick example:

function enumProps(object) {
  console.log('obj', object);
  var result = {}
  for (var attr in object) {
    if (typeof object[attr] == "number" && attr.toUpperCase() == attr) {
      result[object[attr]] = attr;
    }
  }
  return result;
}

print(enumProps($('#example-button')[0]));

// => {...}

You can think of it a little like print() goes to stdout, and console.log() goes to stderr.

Giving Up

Tests often require some feature or setup to be usable at all. When it's not setup right you'll just get a bunch of meaningless failures. For this reason there's a way to abort all your tests. If you call Abort() then no further tests will be run. If you want to connect to a server, for instance, you might check that the server is really there, and if not then just abort the rest of the tests. For example:

$.ajax({
  url: '/ping',
  success: Spy('ping', {wait: true, ignoreThis: true}),
  error: function () {
    Abort("Server isn't up");
  }
});

// => ping(...)

Setting Up Your HTML

I wanted to show you all the cool features of doctest first, but you can't actually use any of them unless you set up a test runner page. Luckily the page is pretty simple. Let's say you've put doctest.js into doctest/:

<DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>My Test</title>
    <script src="doctest/doctest.js"></script>
    <link href="doctest/doctest.css" rel="stylesheet">
    <script src="mylibrary.js"></script>
  </head>
  <body class="autodoctest">

  A test:

<pre class="test">
test goes here
</pre>

  </body></html>

Mostly it's just boilerplate: you have to include doctest.js and doctest.css and of course any libraries or dependencies of the thing you are testing. You also must use <body class="autodoctest"> — that's what tells doctest.js you want to find and run tests right away.

Each test then is in a <pre class="test">. You might not want to actully write your tests inside the HTML, and instead put them in a separate Javascript file. To do that use:

<pre class="test" href="./my_tests.js"></pre>
This will load the test code from ./my_tests.js and inline it into the element. This is how I personally write most of my tests, though when moving between a narrative and tests (as I am doing in this tutorial) it is nice to keep the tests together with the descriptions.

Note that specifically when you use href="URL.js" you can split the tests into sections by including the comment // == SECTION Your Section Header Name in the included file, and you'll get multiple elements with headers automatically.

A pass/fail summary is automatically added to the top of the page, though you can use <div id="doctest-output"></div> to position it someplace specific (as we did in this tutorial).

Feedback?

Was something in this tutorial confusing? Is there a testing problem or pattern you think this tutorial should talk about? Please give feedback in the form of an new issue. Thanks!

Live Demo...