Chapter 16. Build Tools, Testing, and Production Considerations

After all your hard work developing with Dojo, there comes a point when your application is ready for prime time. Util provides terrific build tools and a testing framework that can get you ready for production before you know it. The build tools provided by Util are the same ones that are used to produce each official Dojo release, and the Dojo Objective Harness (DOH) is a unit-testing framework that facilitates achieving some automated quality assurance before your app ever gets out the door.

Building

For any production setting, minimizing the overall footprint of your JavaScript files and the number of synchronous requests to the server is absolutely essential. The difference in downloading scores of individual resource files via synchronous requests incurred by dojo.require versus one or two calls back to the server makes all the difference in the world in terms of a snappy page load.

Dojo's build tools makes accomplishing what may initially seem like such an arduous task quite easy. In a nutshell, the build tools automate the following tasks:

  • Consolidates multiple modules into a single JavaScript file called a layer

  • Interns template strings into JavaScript files, including layers, so that a standalone template is no longer needed

  • Applies ShrinkSafe, a JavaScript compressor based on Rhino, to minify the size of the layers by removing whitespace, linebreaks, comments, and shortening variable names

  • Copies all of the "built" files into a standalone directory that can be copied and deployed to a web server

One reason you may not have been aware of the build tools is that they aren't included in the util directory of an official release. To get them, you have to download a source release (a source release will have the -src suffix on the file base part of the filename) or just grab the source from the Subversion trunk. Chapter 1 provides an overview of getting the Dojo from Subversion, but basically, all that is necessary is to point your client at the Dojo repository and wait for it to download everything, whether it is the trunk or a specific tag.

In either case, you'll find that the util directory now holds some additional directories; one of these directories is buildscripts, which contains the goods we're looking for.

http://svnbook.red-bean.com/ contains the unofficial Subversion book, which is available in a variety of formats. Taking a moment to bookmark this valuable resource now will save you time later.

To run the build tools, you'll have to have Java 1.4.2 or later installed, available from http://java.sun.com (because ShrinkSafe is based on Rhino, which is written in Java). But don't worry about having to be a Java programmer to use ShrinkSafe; ShrinkSafe comes packaged as a single jar file (an executable Java archive), so you can treat it like any other executable.

Running a Build

The primary entry point for kicking off a build is via the buildscripts/build.sh (or build.bat for Windows users), and is really just a call through to the custom Rhino jar that does all of the work based on a custom profile that is provided (more on that in just a moment). As an ordinary executable, however, build tools such as Make or ant can easily include the jar file as an ordinary part of the production build process. This ability is especially convenient when server-side components are based on languages that must be compiled.

Executing the corresponding build script or executing the jar without any command-line options provides an impressive list of options. Table 16-1 is adapted directly from the standard option list that is displayed.

Table 16-1. Build script parameters

Option

Description

xdScopeArgs

If the loader=xdomain build option is used, then the value of this option will be used as the arguments to the function that defines the modules in the .xd.js files. This allows for more than one version of the same module to be in a page. See documentation on djConfig.scopeMap for more information.

cssOptimize

Specifies how to optimize CSS files. If comments is specified, then code comments and line returns are stripped. If comments.keepLines is specified, then code comments are stripped, but line returns are preserved. In either case, @import statements are inlined.

releaseName

The name of the release. A directory inside releaseDir will be created with this name. By default, this value is dojo.

localeList

The set of locales to use when flattening i18n bundles. By default this value is cs,de-de, en-gb,en-us,es-es,fr-fr,hu,it-it,ja-jp,ko-kr,pl,pt-br,ru, zh-tw,zh-cn.

releaseDir

The top-level release directory where builds end up. The releaseName directories will be placed inside this directory. By default, this value is ../../release/.

copyTests

Turn on or off copying of test files. This value is true by default.

symbol

Inserts function symbols as global references so that anonymous functions will show up in all debuggers (especially in IE, which does not attempt to infer function names from the context of their definition). Valid values are long and short. If short is used, then a symboltables.txt file will be generated in each module prefix's release directory that maps the short symbol names to more descriptive names.

action

The build action(s) to run. Can be a comma-separated list, like action=clean,release. The possible build actions are: clean and release. This value is help by default.

internStrings

Turn on or off for widget template file interning. This value is true by default.

scopeMap

Change the default dojo, dijit, and dojox scope names to something else. Useful if you want to use Dojo as part of a JS library, but want to make a self-contained library with no external dojo/dijit/dojox references. Format is a string that contains no spaces, and is similar to the djConfig.scopeMap value (note that the backslashes below are required to avoid shell escaping):

scopeMap: [[\"dojo\",\"mydojo\"],[\"dijit\",\"mydijit\"],
  [\"dojox\",\"mydojox\"]]

optimize

Specifies how to optimize module files. If comments is specified, code comments are stripped. If shrinksafe is specified, the Dojo compressor is used on the files, and line returns is removed. If shrinksafe.keepLines is specified, the Dojo compressor is used on the files, and line returns are preserved. If packer is specified, Dean Edwards's Packer is used (see http://dean.edwards.name/packer/).

loader

The type of dojo loader to use. default (the default value) or xdomain are acceptable values.

log

Sets the logging verbosity. See util/buildtools/jslib/logger.js for possible integer values. The default value is 0.

profileFile

A file path to the profile file. Use this if your profile is outside of the profiles directory. Do not specify the profile build option if you use profileFile.

xdDojoPath

If the loader=xdomain build option is used, then the value of this option will be used to call dojo.registerModulePath( ) for dojo, dijit., and dojox. The xdDojoPath should be the directory that contains the dojo, dijit, and dojox directories, and it should not end in a slash. For instance: http://www.example.com/path/to/dojo.

version

The build will be stamped with this version string. The default value is 0.0.0.dev.

profile

The name of the profile to use for the build. It must be the first part of the profile file name in the profiles/ directory. For instance, to use base.profile.js, specify profile=base (the default).

layerOptimize

Specifies how to optimize the layer files. If comments is specified, code comments are stripped. If shrinksafe is specified, the Dojo compressor is used on the files, and line returns are removed. If shrinksafe.keepLines is specified, the Dojo compressor is used on the layer files, and line returns are preserved. If packer is specified, the Dean Edwards's Packer is used. shrinksafe is the default.

xdDojoScopeName

If the loader=xdomain build option is employed, the value of this option is used instead of dojo (the default) for the dojo._xdResourceLoaded( ) calls that are done in the .xd.js files. This allows for dojo to be under a different scope name, but still allows XDomain loading with that scope name.

cssImportIgnore

You can use cssOptimize=comments to force the @import inlining process to ignore a set of files. The value of this option should be a comma-separated list of CSS filenames to ignore. The filenames should match the string values that are used for the @import calls.

buildLayers

A comma-separated list of layer names to build. Using this option means that only those layers will be built. This helps if you are doing quick development and test cycles with layers. If you have problems with this option, try removing it and doing a full build with action=clean,release. This build option assumes you have done at least one full build first.

symbol

Inserts function symbols as global references so that anonymous functions will show up in all debuggers (especially IE, which does not attempt to infer function names from the context of their definition). Valid values are long and short. If short is used, then a symboltables.txt file will be generated in each module prefix's release directory, mapping the short symbol names to more descriptive names.

scopeDjConfig

Burns a djConfig object into the built dojo.js file, which is useful if you are making your own scoped build and you want a djConfig object local to your version that will not be affected by any globally declared djConfig object in the page. This value must be a string that will look like a JavaScript object literal once it is placed in the built source. Can also be useful for situations where you want to use Dojo as part of a JavaScript library that is self-contained and has no external dojo, dijit, or dojox. Example:

scopeDjConfig={isDebug:true,scopeMap:[[\"dojo\",\"mydojo\"],
  [\"dijit\",\"mydijit\"], [\"dojox\",\"mydojox\"]]}

Note that the backslashes are required to avoid shell escaping if you type this on the command line.

While all of those options may seem like a lot to manage, the routine builds are really quite simple and involve only a handful of options. But first, we need a profile.

Build Profiles

A profile is the configuration for your build as provided via the profile or profileFile option. The most basic function of a profile is to specify the exact Dojo resources that should consolidated into a standalone JavaScript file, also known as a layer; a typical rule of thumb is that each page of your application should have its own layer. The beauty of a layer is that it is an ordinary JavaScript file, and can be included directly into the head of a page, loading everything you've crammed into it via a single synchronous request to the server—well, sort of. By convention, Base is so heavily used that it generally stays in its own individual dojo.js file, so you normally have two synchronous calls, one for Base, and one for your own layer.

Setting up a build profile

Assuming your application has three distinct pages, you might have three layer files and one copy of Base.

If you really want to bundle up your own modules inside of the dojo.js file that normally only contains Base, you can name your layer dojo.js. However, it's often a good idea to keep Base separated because it would be used in every page of you application and is cacheable by your web browser.

Physically speaking, a profile is simply a file containing a JSON object. Example 16-1 shows a profile that consolidates several of the form dijits that are explicitly dojo.required into a page. All internal dependencies are tracked down automatically. Just like with dojo.require, you state what you need to use directly, and dependency tracking is automated behind the scenes for you.

Example 16-1. A simple build profile
dependencies ={
    layers:  [
        {
            name: "form.js",
            dependencies: [
                "dijit.form.Button",
                "dijit.form.Form",
                "dijit.form.ValidationTextBox"
            ]
        }
    ],
    prefixes: [
        [ "dijit", "../dijit" ]
    ]
};

Assuming the previous profile is located at util/buildscripts/profiles/form.profile.js and you're working in a Bash shell, the following command from within the util/buildscripts directory would kick off a build. Note that the profile option expects profiles to be of the form <profile name>.profile.js and only expects the <profile name> as an option:

bash build.sh profile=form action=release

If you don't want to save the file in util/buildscripts/profiles/form.profile.js, you can use the profileFile option instead of the profile option.

After executing the command, you should see a bunch of output indicating that the build is taking place and that of strings are being interned from template files into JavaScript files. The artifact of the build is a release directory containing dojo, dijit, and util. Inside of the dojo directory, you'll find the usual suspects, but there are four especially important artifacts to note:

  • The compressed and uncompressed version of Base, dojo.js and dojo.js.uncompressed.js

  • The compressed and uncompressed version of your form layer in form.js and form.js.uncompressed.js (go ahead and take a peek inside to see for yourself)

But what if you need resources that are not included in your custom layer file? No problem—if resources aren't included in a profile, they are fetched from the server whenever the dojo.require statement that specifies them is encountered. Assuming you take the entire release directory and drop it somewhere out on your server, the dojo.require statements requesting nonlayered resources will behave normally, though you will incur a small roundtrip cost for the request to the server.

Requests for Base functions and resources in your layer do not incur server-side requests when they are encountered in a dojo.require statement because they're already available locally. Resources not in your layer, however, incur the routine overhead of synchronous HTTP requests (Figure 16-1).

Conceptual server request illustrating various JavaScript files loading
Figure 16-1. Conceptual server request illustrating various JavaScript files loading

While you may generally want to include every possible resource that is needed in a build, there may be some situations where you want to lazy load. The tradeoff is always between a "small enough" initial payload size over the wire versus the cost of synchronous loading via dojo.require later.

If you accidentally misspell or otherwise provide a dependency that does not exist, ShrinkSafe may still complete your build even though it could not find all of the dependencies. For example, if you accidentally specify dijit.Button (instead of dijit.form.Button), you'll most likely still get a successful build, and you may not ever notice that dijit.form.Button wasn't bundled because a call to dojo.require("dijit.form.Button") would fetch it from the server and your application would behave as normal.

It's always a good idea to double-check your build by taking a look at the Net tab in Firebug to ensure that everything you expect to be bundled up is indeed bundled up.

Setting up a (more clever) build profile

A slightly more clever way to set up the build profile just discussed is to create a custom module that does nothing more than require in all of the resources that were previously placed in the layer via the profile file. Then, in the profile file, simply include the custom module as your sole dependency for the layer.

First, Example 16-2 shows how your custom module would look. Let's assume the module is dtdg.page1 and is located at called dtdg/page1.js.

Example 16-2. A custom module for a more clever build profile
dojo.provide("dtdg.page1");

dojo.require("dijit.form.Form");
dojo.require("dijit.form.Button");
dojo.require("dijit.form.ValidationTextBox");

Now, your profile need only point to the custom module, as the other dependencies are specified inside of it and will be tracked down automatically. Example 16-3 demonstrates an updated profile, which assumes your custom module directory is a sibling directory of util.

Example 16-3. Updated build profile
dependencies ={
    layers:  [
        {
            name: "form.js",
            dependencies: [
                "custom.page1"
            ]
        }
    ],
    prefixes: [
        [ "custom", "../custom" ]
    ]
};

Finally, your page might contain the following SCRIPT tag to pull in the module along with Base:

<script type="text/javascript"
djConfig="baseUrl: './',modulePaths: {custom:'path/to/custom/page1.js'},
    require: ['custom.page1']"
src="scripts/dojo.js"></script>

Standard build profile

Notice that the util/buildscripts/profiles directory contains a number of example build profiles as well as the standard.profile.js file that contains the layers for a standard build of Dojo. The standard profile builds Base as well as a baseline Dijit layer that contains common machinery that is used in virtually any circumstance involving dijits, as well as a couple of other useful layers. Note that any profile in the standard.profile.js file should be available over AOL's CDN. For example, to retrieve the baseline Dijit profile, you could simply execute the following statement:

dojo.require("dijit.dijit");

Remember, however, that the first SCRIPT tag should always be the one for Base (dojo.xd.js), so you'd include any additional SCRIPT tags for layers after the one for Base.

ShrinkSafe optimization and other common options

In virtually any production setting, you'll want to apply ShrinkSafe to minify all of your code. While the previous build example build did optimize the build in the sense that it minified dojo.js and form.js as well as interned template strings, ShrinkSafe can minify every file in the release.

Recall that the size "over the wire" is what really matters when you're talking about performance from a payload perspective. While files may be a set size as they exist on the server, most servers are able to apply gzip compression to them if the web browser is capable of handling it. While ShrinkSafe minifies JavaScript files by removing artifacts like whitespace, comments, and so on, the further compression is possible because the repetitive use of public symbols such as dojo, dijit, and your own custom tokens allows for actual compression to occur.

Minification is the reduction of a file's size by removing artifacts such as commas, whitespace, linebreaks, etc. Compression is an algorithmic manipulation that reduces a file's size by using by finding multiple instances of the same tokens and encoding an equivalent file by using shorter placeholders for the repetitive tokens. To learn more, see http://en.wikipedia.org/wiki/Gzip for an overview of gzip compression.

An especially notable feature of ShrinkSafe is that it never mangles a public API; this is a direct contrast to some JavaScript tools that attempt to encrypt JavaScript by applying regular expressions or convoluted logic to "protect" the script. In general, attempting to protect your JavaScript is mostly pointless. As an interpreted language that runs in the browser, the user of your application will almost certainly have access to your source code, and it's not terribly difficult to use a debugger to unroll the protected script into something that's fairly intelligible.

ShrinkSafe itself is not a Dojo-specific tool; you can apply it to any JavaScript file to gain the benefits of compression using the online demonstration at http://shrinksafe.dojotoolkit.org/. OS X users can download a version at http://dojotoolkit.org/downloads, and users of other platforms can grab the standalone custom Rhino jar from http://svn.dojotoolkit.org/dojo/trunk/buildscripts/lib/custom_rhino.jar.

In other words, ShrinkSafe shrinks your files without changing public symbol names. In fact, if you look at the form.js file that is an artifact of the previous build examples, you can see for yourself that ShrinkSafe strips comments, collapses and/or eliminates frivolous whitespace, including newline characters, and replaces nonpublic symbols with shorter names. Note that replacing all symbols with shorter, meaningless names qualifies as a lame attempt at encryption—not particularly useful for debugging purposes either.

Let's update our existing profile:

  • Minify all files in the release with the optimize="shrinksafe" option

  • Designate a custom notice that should appear at the top of every minified JavaScript file in an additional (mythical) foo module provided by CUSTOM_FILE_NOTICE.txt

  • Designate a custom notice that should appear at the top of the final form.js provided by the same CUSTOM_LAYER_NOTICE.txt

  • Provide a custom name for the release directory via the releaseName="form" option

  • Provide a custom version number for the build via the version="0.1.0." option

Here's the modified form.profile.js file from Example 16-1. Note that the information in the custom notices must be wrapped in JavaScript comments; the path for the custom notices should be relative to the util/buildscripts directory or an absolute path:

dependencies ={
    layers:  [
        {
            copyrightFile : "CUSTOM_LAYER_NOTICE.txt",
            name: "form.js",
            dependencies: [
                "dijit.form.Button",
                "dijit.form.Form",
                "dijit.form.ValidationTextBox"
            ]
        }
    ],
    prefixes: [
        [ "dijit", "../dijit" ],
        [ "foo", "../foo", "CUSTOM_FILE_NOTICE.txt" ]

    ]
};

The augmented command to kick off this build is straightforward enough, and creates the artifacts in the release/form directory that exist alongside the dojo source directories:

bash build.sh profile=form action=release optimize=shrinksafe releaseName=form
version=0.1.0

To actually use your custom release, simply include the paths to the compressed dojo.js and form.js files in script tags in the head of your page, like so. The dojo.js layer must be included first, because form.js depends on it:

<html>
  <head><title>Fun With Forms!</title>
    <!--  include stylesheets, etc. -->
    <script type="text/javascript" path="relative/path/to/form/dojo.js"></script>
    <script type="text/javascript" path="relative/path/to/form/form.js"></script>
  </head>
  <!--  rest of your page -->

And that's it. It takes only two synchronous requests to load the JavaScript (which now have interned templates) into the page; other resources included in your build via the prefixes list are at your disposal via the standard dojo.require statements.

If you are completely sure you'll never need any additional JavaScript resources beyond dojo.js and your layer files, it is possible to pluck out just the individual resources you need from the release directory structure. However, you'll have to go through a little extra work to track down dependencies with built-in CSS themes such as tundra because some of the stylesheets may use relative paths and relative URLs in import statements.

Inspecting the Net tab of Firebug is very useful in tracking down the dependencies you need to pluck out of the release directory, but be advised that Firebug may not display 404 (Not Found) errors for import statements that are used in stylesheets.

Dojo Objective Harness (DOH)

Automated testing practices for web applications are becoming increasingly common because of the sheer amount of coding and complexity involved in many of today's rich Internet applications. DOH uses Dojo internally but is not a Dojo-specific tool; like ShrinkSafe, you could use it to create unit tests for any JavaScript scripts, although no DOM manipulation or browser-specific functions will be available.

DOH provides three simple assertion constructs that go a long way toward automating your tests. Each of these assertions is provided via the global object, doh, exposed by the framework:

  • doh.assertEqual(expected, actual)

  • doh.assertTrue(condition)

  • doh.assertFalse(condition)

Before diving into some of the more complex things that you can do with DOH, take a look at trivial test harness that you can run from the command line via Rhino to get a better idea of exactly the kinds of things you could be doing with DOH. The harness below demonstrates the ability for DOH to run standalone tests via regular Function objects as well as via test fixtures. Test fixtures are little more than a way of surrounding a test with initialization and clean up.

Rhino Test Harness Without Dojo

Without further ado, here's that test harness. Note that the harness doesn't involve any Dojo specifics; it merely uses the doh object. In particular, the doh.register function is used in this example, where the first parameter specifies a module name (a JavaScript file located as a sibling of the util directory), and the second parameter provides a list of test functions and fixtures:

doh.register("testMe", [

    //test fixture that passes
    {
        name : "fooTest",
        setUp : function(  ) {},
        runTest : function(t) { t.assertTrue(1); },
        tearDown : function(  ) {}
    },
    //test fixture that fails
    {
        name : "barTest",
        setUp : function(  ) { this.bar="bar"},
        runTest : function(t) { t.assertEqual(this.bar, "b"+"a"+"rr"
); },
        tearDown : function(  ) {delete this.bar;}
    },
    //standalone function that passes
    function baz(  ) {doh.assertFalse(0)}

]);

Assuming this test harness were saved in a testMe.js file and placed alongside the util directory, you could run it by executing the following command from within util/doh. (Note that although the custom Rhino jar included with the build tools is used, any recent Rhino jar should work just fine):

java -jar ../shrinksafe/custom_rhino.jar runner.js dojoUrl="../../dojo/dojo.js"
testModule=testMe

The command simply tells the Rhino jar to run the testMe module via the runner.js JavaScript file (the substance of DOH) using the copy of Base specified. Although no Dojo was involved in the test harness itself, DOH does use Base internally, so you do have to provide a path to it.

Now that you've seen DOH in action, you're ready for Table 16-2, which summarizes the additional functions exposed by the doh object.

Table 16-2. doh module functions

Function

Comment

registerTest(/*String*/group,

/* Function || Object */ test)

Adds the test or fixture object to the specified test group.

registerTests(/*String*/group,

/*Array*/ tests)

Automates registering a group of tests provided in the tests Array.

registerTestNs(/*String*/group,

/*Object*/ns)

Adds the functions included in the ns object to the collection that should be test group. Functions beginning with an underscore are not included since the underscore normally denotes the notion of private.

register(/* ...*/)

Applies the proper register function by inspecting the arguments and determining which one to use.

assertEqual(expected, actual)

Used to assert that two values should be equal.

assertTrue(/*Boolean*/condition)

Used to assert that a value should evaluate to true.

assertFalse(/*Boolean*/ condition)

Used to assert that a value should evaluate to false.

is(expected, actual)

Shorthand for assertEqual.

t(/*Boolean*/condition)

Shorthand for assertTrue.

f(/*Boolean*/condition)

Shorthand for assertFalse.

registerGroup(/*String*/ group,

/*Array||Function||Object*/tests, /*Function*/ setUp,

/*Function*/tearDown)

Adds an entire group of tests provided in tests to the group at one time. Uses a custom setUp and tearDown function, if provided.

run( )

Used to programmatically run tests.

runGroup(/*String*/groupName)

Used to programmatically run a group of tests.

pause

Can be used to programmatically pause tests that are running; they may be resumed with run( ).

togglePaused

May be applied sequentially to pause and run the tests.

Additionally, note that the runner.js file accepts any of the options shown in Table 16-3.

Table 16-3. Options for runner.js

Function

Comment

dojoUrl

The path to dojo.js.

testUrl

The path to a test file.

testModule

A comma-separated list of test modules that should be executed, such as foo.bar, foo.baz.

Rhino Test Harness with Dojo

Although it is possible to use DOH without Dojo, chances are that you will want to use Dojo with Rhino. Core contains some great examples that you can run by executing runner.js without any additional arguments. The default values will point to the tests located in dojo/tests and use the version of Base located at dojo/dojo.js.

If you peek inside any of Core's test files, you'll see the usage is straightforward enough. Each file begins with a dojo.provide that specifies the name of the test module, requires the resources that are being tested, and then uses a series of register functions to create fixtures for the tests.

Assume you have a custom foo.bar module located at /tmp/foo/bar.js and that you have a testBar.js test harness located at /tmp/testBar.js. The contents of each JavaScript file follows.

First, there's testBar.js:

/* dojo.provide the test module just like any other module */
dojo.provide("testBar");

/* You may need to register your module paths when using
   custom modules outside of the dojo root directory */
dojo.registerModulePath("foo.bar", "/tmp/foo/bar");

/* dojo.require anything you might need */
dojo.require("foo.bar");

/* register the module */
doh.register("testBar", [

    function(  ) { doh.t(alwaysReturnsTrue(  )); },
    function(  ) { doh.f(alwaysReturnsFalse(  )); },
    function(  ) { doh.is(alwaysReturnsOdd(  )%2, 1); },
    function(  ) { doh.is(alwaysReturnsOdd(  )%2, 1); },
    function(  ) { doh.is(alwaysReturnsOdd(  )%2, 1); },
    {
        name : "BazFixture",
        setUp : function(  ) {this.baz = new Baz;},
        runTest : function(  ) {doh.is(this.baz.talk(  ), "hello");},
        tearDown : function(  ) {delete this.baz;}
    }
]);

And now, for your foo.bar module residing in foo/bar.js:

/* A collection of not-so-useful functions */
dojo.provide("foo.bar");

function alwaysReturnsTrue(  ) {
    return true;
}

function alwaysReturnsFalse(  ) {
    return false;
}

function alwaysReturnsOdd(  ) {
    return Math.floor(Math.random(  )*10)*2-1;
}

// Look, there's even a "class"
dojo.declare("Baz", null, {
    talk : function(  ) {
        return "hello";
    }
});

The following command from within util/buildscripts kicks off the tests:

java -jar ../shrinksafe/custom_rhino.jar runner.js dojoUrl=../../dojo/dojo.js
testUrl=/tmp/testBar.js

Especially note that the test harness explicitly registered the module path for foo.bar before requiring it. For resources outside of the dojo root directory, this extra step is necessary for locating your custom module.

If all goes as planned, you'd see a test summary message indicating that all tests passed or failed. Registering a group of tests sharing some common setup and tear down criteria entails the very same approach, except you would use the doh.registerGroup function instead of the doh.register function (or a more specific variation thereof).

If you want more finely grained control over the execution of your tests so you can pause and restart them programmatically, you apply the following updates to testBar.js:

/* load up dojo.js and runner.js */
load("/usr/local/dojo/dojo.js");
load("/usr/local/dojo/util/doh/runner.js");

/* dojo.provide the test module just like any other module */
dojo.provide("testBar");
/* You may need to register your module paths when using
   custom modules outside of the dojo root directory */
dojo.registerModulePath("foo.bar", "/tmp/foo/bar");

/* dojo.require anything you might need */
dojo.require("foo.bar");

/* register the module */
doh.register("testBar", [

    function(  ) { doh.t(alwaysReturnsTrue(  )); },
    function(  ) { doh.f(alwaysReturnsFalse(  )); },
    function(  ) { doh.is(alwaysReturnsOdd(  )%2, 1); },
    function(  ) { doh.is(alwaysReturnsOdd(  )%2, 1); },
    function(  ) { doh.is(alwaysReturnsOdd(  )%2, 1); },
    {
        name : "BazFixture",
        setUp : function(  ) {this.baz = new Baz;},
        runTest : function(  ) {doh.is(this.baz.talk(  ), "hello");},
        tearDown : function(  ) {delete this.baz;}
    }
]);

doh.run(  );
/* pause and restart at will... */

Although we didn't make use of the fact that testBar is a module that dojo.provides itself, you can very easily aggregate collections of tests together via dojo.require, just like you would for any module that provides itself.

Although you could run asynchronous tests using Rhino as well, the next section introduces asynchronous tests because they are particularly useful for browser-based tests involving network input/output and events such as animations.

Browser-Based Test Harness

Although running tests from Rhino is tremendously useful, DOH also provides a harness that allows you to automate running tests from within a browser window. Basically, you just define a test as an ordinary HTML page and then load the test page into the DOH test runner using query string parameters in the test runner's URL; internally, JavaScript in the test runner examines the query string, pulls out configuration values such as testUrl and uses them to inject your test page into a frame.

Of course, you can still run your browser-based test without the DOH test runner, but you won't get a nice visual display with optional Homer Simpson sound effects if you're willing to read the test results as console output.

Browser Test Example

The following is an example test defined as an ordinary HTML page. Notice that the example uses a local installation of Dojo because as of version 1.1, DOH is not delivered via AOL's CDN:

<html>
    <head><title>Fun with DOH!</title>

        <script
        type="text/javascript"
        src="local/path/to/dojo/dojo.js">
        </script>

        <script type="text/javascript">
            dojo.require("doh.runner");

            dojo.addOnLoad(function(  ) {
                doh.register("fooTest", [
                    function foo(  ) {
                        var bar = [];
                        bar.push(1);
                        bar.push(2);
                        bar.push(3);

                        doh.is(bar.indexOf(1), 0); //not portable!
                    }
                ]);

                doh.run(  );
            });
        </script>

    </head>
    <body></body>
</html>

Asynchronous Browser Test Example

Almost any web application test suite worth its salt is going to involve a significant number of tests that depend upon asynchronous conditions such as waiting for an animation to happen, a server side callback to occur, and so on. Example 16-4 introduces how you can create asynchronous test with DOH. The key concept is that a doh.Deferred (pretty much an ordinary dojo.Deferred with some tweaks) except that it is internal to DOH and, as such, doesn't have external dependencies. Chapter 4 included an extensive discussion of Deferreds if you need a quick refresher.

Before the relevant code sample, here's the basic pattern at play for asynchronous testing with DOH:

  • Create a doh.Deferred that will be used to verify the results from asynchronous function (that returns back a dojo.Deferred)

  • Call whatever asynchronous function returns back the dojo.Deferred and save a reference to it

  • Add callbacks and errbacks to the dojo.Deferred that will simply pass the asynchronous function's results through to the doh.Deferred's own callbacks and errbacks

Example 16-4. Skeleton for an asynchronous test
doh.register("foo", [

    function(  ) {
        var dohDfd = new doh.Deferred();
        var expectedResult = "baz";

        var  dojoDfd = asynchronousBarFunction();
        dojoDfd.addBoth(function(response, io) {

            //reference the dohDfd as needed...
            if (response == expectedResult) {
                  dohDfd.callback(true);
            }
            else {
                  dohDfd.errback(new Error( /* ... */));
            }
        });

        //...and return back the dohDfd
        return dohDfd;
     }
]);

Depending on your specific test constraints, you might provide explicit timeout values to ensure that the asynchronous operations involved timeout according to your specific testing criteria. At any rate, the key takeaway is that asynchronous testing doesn't need to be terribly complicated; the Deferred abstraction simplifies most of that complexity, so you're left to focus on the task at hand.

Performance Considerations

This section touches on some of the low-hanging fruit that you can strive to achieve in your frontend engineering. For a fabulous reference on ways to improve performance, be sure to check out High Performance Web Sites: Essential Knowledge for Front-End Engineers by Steve Souders (O'Reilly). It's a quick read and really does live up to the "essential" part of the title. Much of the content is available at http://developer.yahoo.com/performance/rules.html.

While writing good JavaScript goes a long way toward having a snappy web application, there are a few considerations to be particularly cognizant of when it comes time for production. The topic of optimizing a web application's performance could be the subject of an entire book on its own, but the following list captures some of the most obvious low-hanging fruit that you can go after:

Dojo's build tools

The build tools accomplish a number of essential tasks for you and the effort required on your behalf is trivial. The build process minifies your source, reducing the overall size of the payload, and significantly reduces the HTTP latency by consolidating multiple JavaScript files into layers and interning template strings where applicable.

Lazy loading

While much has been said in this chapter on the virtues of using the build tools to create a minimal number of layer files for your application, there will certainly be times when it just makes more sense to do some lazy loading. For example, if you determine that users very infrequently make use of a particular feature that adds a nontrivial amount of script to your layer, you may just opt to dojo.require it on the fly instead of packaging it up.

Another consideration with respect to lazy loading is to intelligently use the layout widgets to load content on the fly. For example, you may choose to only initially load the visible tab of a TabContainer, and either load the other content when it is requested, or wait long enough that you are certain the rest of the page has been loaded before fetching the other tabs. The ContentPane dijit is a common vehicle for lazy-loading content.

Web server configuration

Explore options to have web browsers aggressively cache JavaScript files and other static content by configuring your server to issue a far future Expires header; configure your server to take full advantage of common configuration options such as gzip compression.

Maximize static content

Because static content can be served so quickly, the more of it you can serve, the less time your web server will spend per request. Maximize the use of static HTML files that are nearly identical by filling in the user-specific portions via cookies or XHR requests where possible. For example, if the only difference on a login page is a few hundred bytes of text containing some user-specific information, serve the page statically, and use script to asynchronously fetch the small bits that need to get filled in instead of dynamically generating the entire page.

Profiling

If a page seems particularly slow or performance is choppy once it has loaded, use the built-in Firebug profiler to get a better idea of where time is being spent in your JavaScript logic and consider optimizing the execution of the culprit functions.

Benefits of XDomain builds

Although it may not be initially obvious, if you opt to create and use an XDomain build for your application, you potentially gain a number of benefits:

  • You'll be able to host Dojo on a dedicated machine and share it amongst various applications—whether or not they are on the same domain in your network.

  • The dojo.require statements that happen when the page loads are satisfied asynchronously instead of synchronously (the case for a default build), which can improve page load times since the requests are nonblocking.

  • Some browsers, such as IE, limit you to two open connections per subdomain by default, so using an XDomain build essentially doubles the number of potential connections for your application—two for Dojo and two for everything else in the local domain.

  • If you serve multiple applications that all use the XDomain build, the overall HTTP latency your clients endure is likely decreased, as the overall amount of content that their browsers can cache locally is increased.

Don't optimize prematurely

As a final word of caution, don't prematurely optimize your application; when you do optimize it, never do so blindly based on guessing games. Always use demonstrable information such as profiling information or server logs to your advantage. Particularly with respect to optimization, our instincts can often be deceived. And remember: Firebug is your friend.

Summary

After reading this chapter, you should:

  • Be able to use Dojo's build tools to create consolidated, compressed layers for your web application

  • Be familiar with some of the most common options for creating a custom build

  • Be aware that dojo.js generally remains in its own separate JavaScript file; it is not rolled up into a custom layer

  • Be able to use DOH to write unit tests for JavaScript functions

  • Be more familiar with Rhino and understand the role it plays in the build tools and with DOH

  • Be aware that while ShrinkSafe and DOH are important parts of the toolkit, they aren't Dojo-specific, and you may be able to use them in other venues

  • Be aware of some of the low-hanging fruit you can go after when it comes time to maximize performance for your web application