This tutorial is for Dojo 1.7 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 not 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.
A suite of tests in the DOH browser test runner
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 authorBar
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.
Defining Tests
The authorBar
has a simple API. Each of the steps in its setup is represented by a method. The normal order of operation looks like this:
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 theauthorBar
at an API level. It checks if the methods get called in the correct sequence, and if they return 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.php
is an HTML page that loads Dojo, theauthorBar
module (the test module), and thedoh.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:
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. Likedojo/on
, it returns a handle object which can be passed into its partnerutil.unMockMethod
to restore the original behavior.util.Fixture
A helper class for defining individual test fixtures. Instances have the samesetUp
,runTest
andtearDown
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 data
test module gives an example of using a fixture class to ensure the 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 = declare(util.Fixture, { setUp: function() { // make sure the configured authorName is a // known value at the start of every test config.authorName = author.name; } }); // usage: doh.register("group name", [ new TF("test name", function() { // runTest - the test function doh.is( author.name, 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 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 // test to ensure update calls loadData new TF("update calls loadData", function() { var hdl = mockMethod(authorBar, "loadData", function() { return "pass"; }); doh.is("pass", authorBar.update(), "call to update() should result in call to loadData"); unMockMethod(hdl); }),
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:
/js/dojo/1.7/util/doh/runner.html?testModule=demo/tests/data®isterModulePath=demo,/documentation/tutorials/1.7/recipes/doh_testsuite/demo
This URL will cause the runner to load and run the data
module. registerModulePath
is 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”:
// there are three test sets to run: api, data and page require(["doh/runner", "require", "demo/tests/api", "demo/tests/data"], function(doh, require){ try { // register the test page, which initiates the doh doh.registerUrl("in-page authorBar", require.toUrl("demo/tests/test_authorBar.php"), 5000); // timeout test after 5 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 DemoSummary
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.