Dojo (from 0.9 onward) features a from-scratch unit testing harness, D.O.H., that's both flexible, easy to use, and portable. D.O.H. is designed to run in both browser and command line environments and unlike it's predecessor doesn't need to be run from the build system. D.O.H. has no Dojo dependencies, but can use the Dojo package system if it's available, thereby allowing tests to be run without need of the build tools at all. D.O.H. also includes a browser-based test runner which can execute command-line tests as well as browser-specific tests. When run from the command line, only pure-JS test are run.
D.O.H. defines one global object (doh.*) and code building tests can use APIs defined on it to pass in a test, a group of tests, or a URL to pull a test group from:
/* GeSHi (C) 2004 - 2007 Nigel McNie (http://qbnz.com/highlighter) */ .geshifilter {font-family: monospace;} .geshifilter .imp {font-weight: bold; color: red;} .geshifilter .kw1 {color: #000066; font-weight: bold;} .geshifilter .kw2 {color: #003366; font-weight: bold;} .geshifilter .kw3 {color: #000066;} .geshifilter .co1 {color: #009900; font-style: italic;} .geshifilter .coMULTI {color: #009900; font-style: italic;} .geshifilter .es0 {color: #000099; font-weight: bold;} .geshifilter .br0 {color: #66cc66;} .geshifilter .st0 {color: #3366CC;} .geshifilter .nu0 {color: #CC0000;} .geshifilter .me1 {color: #006600;} .geshifilter .re0 {color: #0066FF;}The tests.register() method accepts the function signatures of any of the other registration functions and determines the correct underlying function to dispatch registration to.
The contents of a typical, command-line-only, test file might look something like: /* GeSHi (C) 2004 - 2007 Nigel McNie (http://qbnz.com/highlighter) */ .geshifilter {font-family: monospace;} .geshifilter .imp {font-weight: bold; color: red;} .geshifilter .kw1 {color: #000066; font-weight: bold;} .geshifilter .kw2 {color: #003366; font-weight: bold;} .geshifilter .kw3 {color: #000066;} .geshifilter .co1 {color: #009900; font-style: italic;} .geshifilter .coMULTI {color: #009900; font-style: italic;} .geshifilter .es0 {color: #000099; font-weight: bold;} .geshifilter .br0 {color: #66cc66;} .geshifilter .st0 {color: #3366CC;} .geshifilter .nu0 {color: #CC0000;} .geshifilter .me1 {color: #006600;} .geshifilter .re0 {color: #0066FF;}In this example, we see a variant of the test system that uses the doh.addTests() style of add() and registers both independent tests and fixture-driven tests. Note that we give the functions that are registered names even though we could easily provide anonymous functions. This allows the system to more correctly report on what went wrong and where, allowing you to talk more intelligently about your tests passing or failing. The fixture-based example uses the "name" property to provide the same information.
D.O.H. exposes a small but adequate number of assertion APIs:
/* GeSHi (C) 2004 - 2007 Nigel McNie (http://qbnz.com/highlighter) */ .geshifilter {font-family: monospace;} .geshifilter .imp {font-weight: bold; color: red;} .geshifilter .kw1 {color: #000066; font-weight: bold;} .geshifilter .kw2 {color: #003366; font-weight: bold;} .geshifilter .kw3 {color: #000066;} .geshifilter .co1 {color: #009900; font-style: italic;} .geshifilter .coMULTI {color: #009900; font-style: italic;} .geshifilter .es0 {color: #000099; font-weight: bold;} .geshifilter .br0 {color: #66cc66;} .geshifilter .st0 {color: #3366CC;} .geshifilter .nu0 {color: #CC0000;} .geshifilter .me1 {color: #006600;} .geshifilter .re0 {color: #0066FF;}D.O.H. provides direct support for asynchronous test cases. Writing asynchronous tests depends on a script context that "knows about" asynchronous execution (aka, a browser but not Rhino) and a slightly modified test authoring syntax:
/* GeSHi (C) 2004 - 2007 Nigel McNie (http://qbnz.com/highlighter) */ .geshifilter {font-family: monospace;} .geshifilter .imp {font-weight: bold; color: red;} .geshifilter .kw1 {color: #000066; font-weight: bold;} .geshifilter .kw2 {color: #003366; font-weight: bold;} .geshifilter .kw3 {color: #000066;} .geshifilter .co1 {color: #009900; font-style: italic;} .geshifilter .coMULTI {color: #009900; font-style: italic;} .geshifilter .es0 {color: #000099; font-weight: bold;} .geshifilter .br0 {color: #66cc66;} .geshifilter .st0 {color: #3366CC;} .geshifilter .nu0 {color: #CC0000;} .geshifilter .me1 {color: #006600;} .geshifilter .re0 {color: #0066FF;}
Note that in the above example, the runTest function explicitly returns a doh.Deferred object. This is how the system knows that you are going to be testing potentially asynchronous conditions. Also, in our delayed call (see the setTimeout), we explicitly catch errors and pass them to the Deferred's errback() function. It is expected that your code will do this if you are testing asynchronous conditions. Lastly, you may specify a timeout in milliseconds as part of the fixture object.
As with the previous examples, you can specify an anonymous or single function in place of a the full fixture used here to handle asynchronous cases. The only caveat is that it must return a Deferred object in order to be treated as an async test. We can also simplify the above by using the tests.Deferred classes getTestCallback() method. Here's a simplified async test case:
/* GeSHi (C) 2004 - 2007 Nigel McNie (http://qbnz.com/highlighter) */ .geshifilter {font-family: monospace;} .geshifilter .imp {font-weight: bold; color: red;} .geshifilter .kw1 {color: #000066; font-weight: bold;} .geshifilter .kw2 {color: #003366; font-weight: bold;} .geshifilter .kw3 {color: #000066;} .geshifilter .co1 {color: #009900; font-style: italic;} .geshifilter .coMULTI {color: #009900; font-style: italic;} .geshifilter .es0 {color: #000099; font-weight: bold;} .geshifilter .br0 {color: #66cc66;} .geshifilter .st0 {color: #3366CC;} .geshifilter .nu0 {color: #CC0000;} .geshifilter .me1 {color: #006600;} .geshifilter .re0 {color: #0066FF;}While the test case still needs to pass back a Deferred object, the use of the getTestCallback() to wrap the success or failure test function allows us to stop manually handling exceptions that might be thrown in the callback function, specifically from assertTrue(), assertEqual(), or assertFalse().
Many times, it's advantageous to register an entire group of tests at once. D.O.H. provides a method for doing this as well as for registering group-level setUp and tearDown methods. The tests.registerGroup(name, tests, setUp, tearDown) method lets you handle this in a single call:
Note that when using registerGroup, setUp and tearDown replace existing group-level handlers, but the registered tests are additive to any pre-existing tests registered for the group.
The above example also introduces yet another shorthand for writing tests, the string-only test. This style of test authoring is particularly terse. Tests written this way do not provide explicit fixture names and so the test code itself is used as the test name in reporting. In these tests, there is also always a variable t which is an alias to the global tests variable. This allows for very compact tests to be written in the form:
With group registration, this style of test authoring requires very little typing. Just mind your string quotes!
Being developed explicitly to test JavaScript applications, D.O.H. includes features for browser-based test harnesses to load sub-documents which may run a set of tests explicitly on a browser-provided DOM. This lets you automate UI testing and isolate browser-specific bugs by writing tests once and quickly running them through the unified test harness UI. To support this, browser runtimes for D.O.H. provide an implementation for tests.registerUrl(groupName, url). On other environments, this may be a no-op.
A real example from Dojo Core:
/* GeSHi (C) 2004 - 2007 Nigel McNie (http://qbnz.com/highlighter) */ .geshifilter {font-family: monospace;} .geshifilter .imp {font-weight: bold; color: red;} .geshifilter .kw1 {color: #000066; font-weight: bold;} .geshifilter .kw2 {color: #003366; font-weight: bold;} .geshifilter .kw3 {color: #000066;} .geshifilter .co1 {color: #009900; font-style: italic;} .geshifilter .coMULTI {color: #009900; font-style: italic;} .geshifilter .es0 {color: #000099; font-weight: bold;} .geshifilter .br0 {color: #66cc66;} .geshifilter .st0 {color: #3366CC;} .geshifilter .nu0 {color: #CC0000;} .geshifilter .me1 {color: #006600;} .geshifilter .re0 {color: #0066FF;}This example uses Dojo to normalize the tested URL with relationship to the loading code, but you can just as easily specify a full URL manually. Just be aware that in order for D.O.H. to be able to record the results of tests from this page, it must be hosted on the same domain as the hosting test harness.
But what does the page itself look like? Here's a snapshot of the page referenced above:
/* GeSHi (C) 2004 - 2007 Nigel McNie (http://qbnz.com/highlighter) */ .geshifilter {font-family: monospace;} .geshifilter .imp {font-weight: bold; color: red;} .geshifilter .kw1 {color: #b1b100;} .geshifilter .kw2 {color: #000000; font-weight: bold;} .geshifilter .kw3 {color: #000066;} .geshifilter .coMULTI {color: #808080; font-style: italic;} .geshifilter .es0 {color: #000099; font-weight: bold;} .geshifilter .br0 {color: #66cc66;} .geshifilter .st0 {color: #ff0000;} .geshifilter .nu0 {color: #cc66cc;} .geshifilter .sc0 {color: #00bbdd;} .geshifilter .sc1 {color: #ddbb00;} .geshifilter .sc2 {color: #009900;}The above code has several important features (in bold). First, we ensure that the test system itself is loaded into the tested page. Without this, the tests won't run. D.O.H. is smart enough to know if it's being loaded into a child frame or as a parent document. If you load this file into a normal browser window, the tests will still run, but you won't get the pretty D.O.H. chrome or audio feedback. Instead, the results of only the tests from this page will be sent to whatever console facility is available.
The second important feature of our tested URL is that it manually calls tests.run(), in this case after the page has been loaded and tests have been registered (a good time to do it). There are Dojo-isms in the test page, but they don't affect the important bits of the system. You can still load the test system with <script> tags and hard-wired URLs and this file would participate in the larger test group correctly.
Since testing on loaded pages may take a long time (relatively), a default timeout of 10 seconds per URL is provided. If your tested page requires more (or less) time, you can pass an explicit timeout parameter to the tests.registerUrl method:
Here are some instructions for running the tests in Rhino. Ideally they should run in any Rhino version 1.6R4 or later, however it has only be verified to work with the custom_rhino.jar in Dojo's util repository (it is in util/buildscripts/lib). Steps:
> svn svn co http://svn.dojotoolkit.org/dojo/view/anon/all/trunk dojo_0.9 > cd dojo_0.9 > cd util/doh > java -jar ../buildscripts/lib/custom_rhino.jar runner.js
NOTE:Do not use a built version of dojo when trying to run DOH through the Rhino command line. The built versions of Dojo that are available for download are optimized for use in the browser, and they do not have the auto-detection logic to load the right environment code for Rhino. So be sure to use a source distribution when running the DOH tests in Rhino.