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:
In order to test this module, you have to require it:
and then finally load this test file (and any others) via <script> tags:
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 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:
…you would have a simple way to know when it is “safe” to begin test
execution:
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:
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):
(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):
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:
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.”
Luckily, your test environment can use the
widely-
supported paths
configuration
option to make
these a little more concise. Using RequireJS, you might write:
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:
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:
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:
We can use testRequire to re-load the same module, and we should expect
RequireJS to fetch the module each time:
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:
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:
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:
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:
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:
Whew! With this in place, we can explicitly mock some dependencies for specific
test suites only:
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!