AMD (short for Asynchronous Module Definition) is a JavaScript API specification for structuring modular code. The web abounds with blog posts illustrating its use in front-end application development (and there’s plenty of healthy debate 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 the RequireJS 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 file loses 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 and
Buster.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 require
ing 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 as cujoJS‘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 baseUrl
option 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 using
Karma to run your tests where–as of version
0.10–your application files are served from a virtual directory named
base/
.)
“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 the
findNestedDependencies
option.
“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 the
widely–
supported paths
configuration
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:
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 a
urlArgs
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:
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 context
option, 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 you
require
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() {});
});
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’s
Backbone 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 pick
differently 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 to
testRequire
:
// 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:
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 to
map
)
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) or Merrick Christensen‘s Squire.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!