Effective Unit Testing with AMD

AMD (short for Asynchronous Module Definition) is a JavaScript API specification for structuring modular code.Thewebaboundswith blog posts illustrating its use in front-end application development (and there’splentyofhealthydebate around its necessity, too). The topic of unit testing (despite being integral to the process of software development) does not receive much attention in these discussions.

As it happens, AMD has an impact on how one authors tests. More than just dictating some additional function calls, AMD (and theRequireJS library in particular) can actually enhance unit test suites.

Many thanks to James Burke for his tireless guidance in the development of these techniques and rapid response to issues with RequireJS. Thanks also to John Hann for review.

Why bother?

I didn’t take you for the nihilistic type, but that’s okay: there’s good reason to question the necessity of AMD in testing environments. In such contexts, network overhead is not a concern, so the prospect of automatically generating a single unified source fileloses its appeal. Furthermore, test file requirements are generally “flat”, so the implicit dependency resolution provided by AMD also seems less compelling.

But not all of AMD’s advantages are irrelevant in test environments. For example, AMD makes dependencies explicit, and unit tests benefit from this property just as much as any application code. And as we’ll see later on, AMD offers some little-known luxuries for unit tests specifically.

Finally, if you’re convinced AMD is right for your project, then the truth is: your hands may be tied. The structure of an application dictates the structure of its unit tests. This means your test environment is going to have to account for that sweet, sweet asynchronicity of your module definitions.

For example, imagine your application defines a module named identity:

// file: src/identity.js
define(function() {
  function identity(x) {
    return x;
  }
  return identity;
});

In order to test this module, you have to require it:

// file: test/tests/identity.js
requirejs(['../src/identity'], function(Identity) {
  // Unit tests for `Identity` go here
});

and then finally load this test file (and any others) via <script> tags:

<!-- file: test/index.html -->
<script src="tests/identity.js"></script>
<script src="tests/other-module.js"></script>

Herein lies the rub: many browser-based test runners require an explicit trigger to begin running your tests (e.g.mocha.run(),jasmineEnv.execute();). With the above approach to test loading, it’s not clear when to initiate the runner. Even test runners that execute automatically (e.g. QUnit andBuster.JS) do this only as a nicety–asychronously defining tests may lead to unexpected results (or in engineering-speak: “gum up the works”).

Running with AMD

The simplest way to detect when test files are done requireing their dependencies is to make those same test files AMD-compliant. (“In for a penny, in for a pound,” as the saying goes.) So if the identity module’s test file instead looked like this:

// file: test/tests/identity.js
define(['../src/identity'], function(Identity) {
  // Unit tests for `Identity` go here

  // No need to `return` anything; we're just using AMD for its dependency
  // resolution properties in this case.
});

…you would have a simple way to know when it is “safe” to begin test execution:

// Don't forget to prevent automatic test execution if your test runner of
// choice is one of the more excitable frameworks.
QUnit.config.autostart = false;

// file: test/main.js
requirejs([
  'tests/identity',
  'tests/other-module'
], function(/* remember: the test modules don't export anything */) {

  // All the test files have been loaded, and all the tests have been
  // defined--we're ready to start testing!
  QUnit.init();
});

RequireJS extensions

RequireJS is a popular implementation of the AMD API specification. It offers a number of additional features not included in the specification some of which are particularly useful in unit test environments. Keep in mind that if you have selected an alternative module loader such ascujoJS‘s curl*, then not all of this will apply. Or, in JavaScript:

if (require !== window.requirejs) {
  delete this.someStuff;
}
this.apply(you);

For the rest of this post, we’ll incrementally refine our usage of RequireJS and build these refinements into a testRequire function. We’ll verify that the function works as expected by viewing Network request data in the Firefox Web Console.

* curl offers a collection of unit test helper functionality; check out the source code on GitHub to learn more.

Re-using application configuration

The AMD specification contains a draft for configuration options, and many of these are already implemented by script loaders. They offer more control over the behavior of module loading, and if your application uses them, then individual module definitions may not function in their absence. This means you will want to re-use those same options when testing.

You could copy-and-paste them, but we’re all programmers here! Why not simply load your application’s main file from your unit test environment? Just don’t forget to use the baseUrloption so that module paths are interpreted from your application directory (not your test directory):

// file: test/require-config.js
requirejs.config({
  baseUrl: '../src'
});

requirejs(['../src/main.js'], function() {
  // Application configuration loaded.
  // Load your tests here
});

(By the way: baseUrl also comes in handy if you’re usingKarma to run your tests where–as of version 0.10–your application files are served from a virtual directory namedbase/.)

“But wait! The file that has my configuration also bootstraps my application! If I load it, then my application will attempt to initialize in the test environment 🙁 🙁 :(“

We can get around that, so cheer up! Move the call to require.config to a separate file (require-config.js seems like a good name):

// file: src/require-config.js
requirejs.config({
  // fancy configuration options go here
});

This file can be required from your main application file and test runner alike–you just need to “wrap” it around your existing require call to make sure the configuration loads first:

// file: src/main.js
requirejs(['require-config'], function() {
  // RequireJS has been configured. It's now safe to load application code.

  requirejs(['app-dependency-1', 'app-dependency-2', function(ad1, ad2) {
    // "main" logic
  });
});

Finally, when the time comes to optimize this build, be sure to set thefindNestedDependenciesoption.

“But wait! Again! All those paths are relative to my application source directly, so requiring test (and test helper) modules is going to be a real drag.”

// file: test/require-config.js
requirejs.config({
  baseUrl: '../src'
});

define([
  '../test/helpers/a-helper',
  '../test/tests/identity'
], function(helper) {
  // tests go here
});

Luckily, your test environment can use thewidelysupported pathsconfiguration option to make these a little more concise. Using RequireJS, you might write:

// file: test/require-config.js
requirejs.config({
  baseUrl: '../src',
  paths: {
    tests: '../test/tests',
    helpers: '../test/helpers'
  }
});

Instead of overwriting important pathing information that your application expects, these paths will be merged in. This means that the modules under test will still load correctly, and your test modules can be much more concise:

define([
  'helpers/a-helper',
  'tests/identity'
], function(helper) {
  // tests go here
});

Now (finally), we’re loading our application source and our test files:

Correct resource loading

The Latest Versions, Every Time

Browser caching has likely saved untold millions of kilowatt-hours of energy and made the web operate faster for users around the world. It has also been the source of many frustrated bug fixing attempts where “I swear just added a debugging statement…”

In unit test environments, the benefits of caching are minimal. Files likely reside on a local network (where latency is negligible), and only the latest version of each file is relevant. Fortunately, RequireJS implements aurlArgs option that lets you define a query string to be appended to module requests. By setting this to the current time, you can be sure that every time you load the page, the browser will fetch the very latest version of each module:

// file: test/require-config.js
requirejs.config({
  baseUrl: '../src',
  urlArgs: 'now=' + Date.now(),
  paths: [
    tests: '../test/tests',
    helpers: '../test/helpers'
  ]
});

Check out the requests now:

Cache-busting URLs

Fresh modules for each test suite

Strong, maintainable test environments generally isolate each test case’s state. This means that failures in one test do not effect any other, and it also allows tests to be added, removed, and re-ordered arbitrarily.

Sometimes, AMD modules maintain some internal state. We could discuss at length whether this is generally an acceptable practice, but for the purposes of this post, let’s just assume that a few of your modules do this and that you want to test them cleanly.

It can be tricky to write isolated tests against such modules because the modules’ state becomes part of the tests’ state. Sometimes you may see modules that define an init method (or similar) for the express purpose of facilitating testing. RequireJS has our backs once again; with the contextoption, we can prevent testing concerns from creeping into the application logic.

Before we can really understand what this option does, we should discuss some of RequireJS’s internals (don’t worry: this will be quick). Each time yourequire a JavaScript module, the library first references an internal store of all the modules it has loaded (called the “loading context”). If it finds the module there, it simply returns the previously-defined value. If not, it loads the module (updating the loading context for future use).

When configured with a unique context option, RequireJS will use a completely different loading context. This is useful in application code when different modules need different versions of the same library. It’s also very handy for unit testing: if every group of tests uses a unique loading context, then modules with internal state will not be re-used across groups.

Here’s how you might define a testRequire function that has the same API as the AMD require but uses a “fresh” loading context for each and every call:

// file: test/require-config.js
(function(window) {
  var contextId = 0;

  window.testRequire = function() {
    var context = requirejs.config({
      baseUrl: '../src',
      urlArgs: 'now=' + Date.now(),
      context: 'test-context' + contextId++,
      paths: {
        tests: '../test/tests',
        helpers: '../test/helpers'
      }
    });

    return context.apply(this, arguments);
  };
}(typeof global === 'undefined' ? this : global));

We can use testRequire to re-load the same module, and we should expect RequireJS to fetch the module each time:

requirejs(['main'], function() {
  testRequire(['identity'], function() {});
  testRequire(['identity'], function() {});
});

Loading contexts at work

Concise mocks

Unit tests ought to be fast and deterministic. Sometimes though, modules implement slow and/or unpredictable functionality (classic examples being heavy data processing and network operations). For example, imagine a utility module named pick that returns a random element from the input array:

// file: src/pick.js
define(function() {
  function pick(array) {
    return array[Math.floor(Math.random() * array.length)];
  }

  return pick;
});

Writing tests for such code is a challenge unto itself (thankfully outside the scope of this blog post), but modules like this also make trouble when testing modules that depend on them. Consider a Banner module that, among other things, displays a random tip in a “Did you know?” section:

// file: src/banner.js
define(['pick'], function(pick) {
  function Banner() {
    // ...
  }

  // ...
  Banner.prototype.render = function() {

    // rendering logic

    this.$dom.didjaknow.text(pick(this.tipsText));

    // more rendering logic
  };

  return Banner;
});

Testing Banner#render is mostly straightforward, except for the part that uses pick. Making sure the “Did you know?” section gets updated properly is equivalent to ensuring the behavior of the pick module itself.

In cases like this, it often makes sense to use an alternate, “fake” implementation of the module that exposes an identical API but behaves in a faster/more predictable way. Such modules are sometimes referred to as “mocks”. Here’s a mock implementation of pick that simply returns the first element of the input array, every time:

// file: test/mocks/pick.js
define(function() {
  function mockPick(array) {
    return array[0];
  }

  return mockPick;
};

Obviously this module isn’t suitable for production (the Banner would never render most of our awesome tips), but it would make testing a whole lot easier. If only there was some way to “inject” this module into the banner module in the test environment…

What’s that, you say? There is?! Well, out with it: we don’t have much time!

Oh, the map configuration parameter. That’s right, I remember now:

For the given module prefix, instead of loading the module with the given ID, substitute a different module ID.

One basic approach would be to simply never load those troublesome modules anywhere in your test environment. We could even incorporate that logic into the testRequire function from above:

// file: test/require-config.js
(function(window) {
  var contextId = 0;

  window.testRequire = function() {
    var context = requirejs.config({
      baseUrl: '../src',
      urlArgs: 'now=' + Date.now(),
      context: 'test-context' + contextId++,
      paths: {
        tests: '../test/tests',
        helpers: '../test/helpers'
      },
      map: {
       // The asterisk character will apply to *all* modules
       '*': {
         pick: '../test/mocks/pick'
       }
      }
    });

    return context.apply(this, arguments);
  };
}(typeof global === 'undefined' ? this : global));

Now any module we require with testRequire will receive mockPick when it requests the pick module. Pretty tricky, huh?

This behavior is a form of “dependency injection”. Sam Breed spoke about using this technique at this year’sBackbone Conf, but what’s different here is that we’re operating at a much lower level. We’re able to author the application code however we like: notice how Banner doesn’t have to treat pickdifferently than any other dependency, but we’re still able to slip in our mock.

We can take this one step further though. Since each loading context has its own configuration, we can specify different mocks for each call totestRequire:

// file: test/require-config.js
(function(window) {
  var contextId = 0;

  window.testRequire = function(moduleIds, options, callback) {
    var toMock = options && options.mocks;
    var config = {
      baseUrl: '../src',
      urlArgs: 'now=' + Date.now(),
      context: 'test-context' + contextId++,
      paths: {
        tests: '../test/tests',
        helpers: '../test/helpers'
      }
    };
    var map, context;

    if (toMock) {
      map = {
        '*': {}
      };
      toMock.forEach(function(id) {
        // **Important:** This assumes you have organized all your mocks in a
        // directory named `mocks`, and that their filename is identical to
        // that of the module they emulate. If you want a simple API for the
        // `testRequire` function, you will have to organize your files
        // rigorously.
        var mockId = '../test/mocks/' + id;

        // Any module that requires the module in this context should receive
        // the mock instead.
        map['*'][id] = mockId;

        // If the mock itself requires the module, then it should get the real
        // thing.
        map[mockId] = {};
        map[mockId][id] = id;
      });

      config.map = map;
    }

    context = requirejs.config(config);

    return context.call(this, moduleIds, callback);
  };
}(typeof global === 'undefined' ? this : global));

Whew! With this in place, we can explicitly mock some dependencies for specific test suites only:

// file: test/tests/banner.js
testRequire(['banner'], { mocks: ['pick'] }, function(Banner) {
  // Banner tests go here.
});

And here’s a listing of the network requests:

Mocking out tricky modules

Be careful, though! Having this facility close at hand may tempt you to mock out dependencies excessively. There are two good reasons to avoid this approach:

  • The more you mock, the less your tests reflect the reality of your application.
  • Each mock is another piece of code you have to maintain but not ship.

For more thoughts on this topic, check out Ian Cooper’s presentation, “TDD, where did it all go wrong”

Review

The testRequire function is now pretty sophisticated!

  • Source code will never be loaded from an outdated cache version (thanks to urlArgs)
  • Module instances will be completely unique (and isolated) for each test suite (thanks to context)
  • Dependencies will be seamlessly injected with any mocks we specify (thanks tomap)

Go Forth and Write Tests

Maintaining all this tooling inside your project might be a little intimidating. If that’s the case, you may be interested in the JavaScript testing service Intern (which uses AMD by default) orMerrick Christensen‘sSquire.js project (which implements dependency injection for Require.js).

But this stuff isn’t just hypothetical! As you may know, Bocoup has been working with Mozilla on Firefox OS for almost a year now, and we’ve put this functionality to good use in the platform’s “Clock” application. If you’d like to see a real-world example, check out that app’s unit tests on GitHub.com.

In any case, I hope this post has got you excited about improving your test environment!

Comments

Contact Us

We'd love to hear from you. Get in touch!

Boston

201 South Street, Boston, MA 02111

New York

315 Church St, New York, NY 10013

Phone & Email

(617)379-2752 hello@bocoup.com