This tutorial is for Dojo 1.6 and may be out of date.

Up to date tutorials are available.

D.O.H. Test Suite

Learn how to set up a suite of unit tests with the DOH test harness, and run them.

Introduction

Having a suite of tests that prove each unit of your application works is a good way to ensure you’re not building a house of cards. Writing code is no fun if every change and addition you make can quietly break something. Unit tests run quickly and painlessly against your code to build confidence and peace of mind. Your unit tests make explicit how you expect something to work; likewise, the absence of tests for some expected or assumed behavior indicates that this behavior is tentative and a potential source of errors. Even a thin suite of tests maps out your knowns and your known-unknowns.

The Problem

We need an overall, at-a-glance “health check” for a codebase—one that combines individual DOH tests into a test suite that can be run easily both inside and outside a browser.

The Solution

Create test modules that allow individual tests to be run en-masse in the DOH test runner.

Test Suite Screenshot A suite of tests in the DOH browser test runner

View Testsuite Demo

Discussion

DOH is the Dojo Toolkit’s testing utility, and the test runners it includes provide a neat way to run collections of tests and give you aggregated results.

To see how this works, we’ve created a fairly simple piece of functionality—a module that injects an “author bar” into the page. It fetches JSON data, picks out the info for the configured author, and renders it to the DOM. There’s plenty of opportunity for error there, so let’s first see it in action before we look at how to test it.

View Author Bar Demo

Defining Tests

The Author Bar has a simple API. Each of the steps in its setup is represented by a method. The normal order of operation looks like this:

demo.authorBar
	  .update()
	  |
	  .loadData()
	  |
	  |  request authors data
	  '---------------------> +--------+
	                          |        |
	                          | server |
	     JSON data response   |        |
	  .<--------------------- +--------+
	  |
	  .onDataLoad()
	  |
	  | validate data, pick out author item
	  |
	  .render()
	  |
	  | format content
	  |
DOM <-----'

Notice that the only part of this sequence which really needs a DOM is the render method. Also, note that while loadData will normally make an XHR request, onDataLoad just needs data—it doesn’t care where it came from. We can use these kind of distinctions to limit the variables at play in any given test, and to focus in on just the narrow set of conditions we care about.

For this example, tests are broken down into 3 separate files:

  • api.js tests the authorBar at an API level. It checks if the methods get called in the correct sequence, and if they give back the expected results when provided known data.
  • data.js tests the data handling and makes sure that both good and bad data follow the expected codepaths.
  • test_authorBar.html is an HTML page that loads Dojo, the demo.authorBar module (the test module), and the doh.runner module (the test harness).

We can test all of these aspects of the subject module by running each test in isolation first, then confirming the result by running them in aggregate.

Helpers

DOH is a pretty simple test harness, and as you write tests you’ll see your own patterns and opportunities for code reuse. In this solution, all our test files also require a demo.tests.util module, which defines a couple of handy things for us:

  • demo.tests.util.mockMethod A simple API to temporarily replace a method’s implementation with a mock function to be used in the context of a test. Like dojo.connect, it returns a handle object which can be passed into its partner demo.tests.util.mockMethod to restore the original behavior.
  • demo.tests.util.Fixture A helper class for defining individual test fixtures. Instances have the same setUp, runTest and tearDown methods DOH expects, but this extension allows tests to stay DRY by including some repeated steps in a shared setUp/tearDown on the prototype.

The demo.tests.data test module gives an example of using a fixture class to ensure the dojo.config.authorName is reset in between tests to avoid any unintended bleed of effects from one test to another:

// paraphrased for brevity...
var author = { name: "Someone" };

// define a subclass of the Fixture class
var TF = dojo.declare(util.Fixture, {
	setUp: function() {
		// make sure the configured authorName is a
		// known value at the start of every test
		dojo.config.authorName = author.name;
	}
});

// usage:
doh.register("group name", [
	new TF("test name", function() {
		// runTest - the test function
		doh.is(
			author.name,
			dojo.config.authorName,
			"Item name matches the config authorName"
		);
	})
	// more tests using the same fixture class
]);

Mocking methods

It is often a good idea to test which path is being taken during execution. Does bad data produce a call to the error handling method? Does an XHR request produce a call to the load handler method? The simple mechanism we use to do this is defined in the demo.tests.util module:

mockMethod: function(obj, methName, fn) {
	var orig = obj[methName];
	var handle = [obj, methName, orig];
	obj[methName] = fn;
	return handle;
},
unMockMethod: function(handle) {
	handle[0][handle[1]] = handle[2];
}
// putting mocking to use
var success = false;
var renderHdl = mockMethod(demo.authorBar, "render", function(){
	success = true;
});
try{
	demo.authorBar.onDataLoad([author]);
	doh.t(success, "render method is called when onDataLoad is passed good data");
}finally{
	unMockMethod(renderHdl);
}

Here, we temporarily replace the authorBar’s render method with our own function which just updates the success variable. We can then assert that success must be true for the test to pass. Finally, the handle returned by mockMethod is fed back into unMockMethod to restore the original behavior, regardless of whether or not the test was successful.

Running Tests in the DOH Test Harness

Let’s briefly review how to run DOH through the browser. The browser runner is at util/doh/runner.html. Code in that page looks for a testModule parameter in the query string which specifies which test module should be loaded and executed. For example:

doh/runner.html?testModule=demo.tests.data
  &registerModulePath=demo,/documentation/tutorials/1.6/recipes/doh_testsuite/demo

This URL will cause the runner to load and run the demo.tests.data module. Similar to dojo.registerModulePath, registerModulePath can also be used in the URL to map a namespace to a particular path. This lets us fashion URLs to run individual test modules via the harness.

We’ve now successfully loaded a single set of unit tests through the DOH Runner. But what if we want to run them all?

Test Roll-up Modules

As with normal Dojo modules, when creating tests, we can define a module that is simply a list of other modules. Then, when specifying a modulePath for the test runner, we point to this “roll-up module”:

dojo.provide("demo.tests.module");
try{
	dojo.require("demo.tests.data");
	dojo.require("demo.tests.api");
	doh.registerUrl("in-page authorBar", dojo.moduleUrl("demo", "tests/test_authorBar.html"), 20000); // time out test after 20 seconds
}catch(e){
	doh.debug(e);
}

You can define as many different roll-ups as you need: one per directory, one per major component, or in any other fashion that makes sense for your codebase. It is conventional to create a module.js file which requires and runs all the tests for that collection or directory of tests.

A bad test can be worse than no test. Beware false negatives, and always confirm a test fails as expected in the appropriate conditions before finishing your test code.

To see it all together, let's look at the original demo:

View Testsuite Demo

Summary

DOH makes aggregation of tests and bulk test runs very simple. Because the browser runner is configured by its query string, it is also easy to use. A line of green lights can lure you into a false sense of security if test coverage and test implementation generate false negatives, but with review and iteration, a test suite can be an invaluable tool during development and maintenance of a project.

Colophon