When writing tests for an application, it’s prudent to add mock or stub data in order to allow code to be properly tested in isolation from other parts of the system. Within a normal Dojo application, there are typically three places where mocking will occur: I/O requests, stores, and module dependencies.
Mocking I/O requests
Implementing mock services makes it easy to decouple the testing of client-side application logic from server-side data sources. Most bugs that are reported in Web application development are initially reported against the client-side application. By having clearly-established tests against mock objects, it is easier to isolate the source of a bug, and determine if the error is the result of an unexpected change to an API, or a failing data service. This reduces the frequency of reporting bugs against the wrong component, and streamlines the process for identifying, resolving, and testing fixes to application source code.
Mocking services client-side can be accomplished fairly simply by creating a custom dojo/request
provider using dojo/request/registry
. The following simple example creates a simple mock for a /info
service endpoint which is simply expected to yield a hard-coded object:
// in tests/support/requestMocker.js
define([
'dojo/request/registry',
'dojo/when'
], function (registry, when) {
var mocking = false,
handles = [];
function start() {
if (mocking) {
return;
}
mocking = true;
// Set up a handler for requests to '/info' that mocks a
// response without requesting from the server at all
handles.push(
registry.register('/info', function (url, options) {
// Wrap using `when` to return a promise;
// you could also delay the response
return when({
hello: 'world'
});
})
);
}
function stop() {
if (!mocking) {
return;
}
mocking = false;
var handle;
while ((handle = handles.pop())) {
handle.remove();
}
}
return {
start: start,
stop: stop
};
});
Once you have a mock service, dojo/request
will need to be configured to use the request registry so that the mock provider can be loaded:
// in tests/intern.js
var dojoConfig = {
requestProvider: 'dojo/request/registry'
};
define({
// … Intern configuration
});
Finally, the unit test itself will load the mock service and enable it during the test suite’s execution:
// in tests/unit/app/Controller.js
define([
'intern!tdd',
'intern/chai!assert',
'./support/requestMocker',
'app/Controller'
], function (tdd, assert, requestMocker, Controller) {
tdd.suite('app/Controller', function () {
// start the data mocker when the test suite starts,
// and stop it after the suite suite has finished
tdd.before(function () {
requestMocker.start();
});
tdd.after(function () {
requestMocker.stop();
});
tdd.test('GET /info', function () {
// this code assumes Controller uses dojo/request
Controller.get({
url: '/info'
}).then(function (data) {
assert.deepEqual(data, {
hello: 'world'
});
});
});
});
});
This data mocking mechanism provides the lowest-level cross-platform I/O abstraction possible. As an added benefit, creating a mock request provider also enables client-side development to proceed independently from any back-end development or maintenance that might normally prevent client-side developers from being able to continue working.
Mocking stores
The dojo/store
API provides a standard, high-level data access API that abstracts away any underlying I/O transport layer and allows data to be requested and provided from a wide range of compatible stores. While a networked store like dojo/store/JsonRest
could be used in conjunction with a dojo/request
mock provider to mock store data, it is often simpler to mock the store itself using dojo/store/Memory
. This is because, unlike a dojo/request
mock, a mock dojo/store
implementation does not need to know anything about how the back-end server might behave in production—or if there is even a back-end server in production at all.
By convention, and following the recommended principle of dependency injection, stores are typically passed to components that use a data store through the constructor:
// in tests/unit/util/Grid.js
define([
'intern!tdd',
'intern/chai!assert',
'dojo/store/Memory',
'app/Grid'
], function (tdd, assert, Memory, Grid) {
var mockStore = new Memory({
data: [
{ id: 1, name: 'Foo' },
{ id: 2, name: 'Bar' }
]
});
tdd.suite('app/Grid', function () {
var grid;
tdd.before(function () {
grid = new Grid({
store: mockStore
});
grid.placeAt(document.body);
grid.startup();
});
tdd.after(function () {
grid.destroyRecursive();
grid = null;
});
// …
});
});
Mocking AMD dependencies
Rewriting code to use dependency injection is strongly recommended over attempting to mock AMD modules, as doing so simplifies testing and improves code reusability. However, it is still possible to mock AMD dependencies by undefining the module under test and its mocked dependencies, modifying one of its dependencies using the loader’s module remapping functionality, then restoring the original modules after the mocked version has completed loading.
// in tests/support/amdMocker.js
define([
'dojo/Deferred'
], function (Deferred) {
function mock(moduleId, dependencyMap) {
var dfd = new Deferred();
// retrieve the original module values so they can be
// restored after the mocked copy has loaded
var originalModule;
var originalDependencies = {};
var NOT_LOADED = {};
try {
originalModule = require(moduleId);
require.undef(moduleId);
} catch (error) {
originalModule = NOT_LOADED;
}
for (var dependencyId in dependencyMap) {
try {
originalDependencies[dependencyId] = require(dependencyId);
require.undef(dependencyId);
} catch (error) {
originalDependencies[dependencyId] = NOT_LOADED;
}
}
// remap the module's dependencies with the provided map
var map = {};
map[moduleId] = dependencyMap;
require({
map: map
});
// reload the module using the mocked dependencies
require([moduleId], function (mockedModule) {
// restore the original condition of the loader by
// replacing all the modules that were unloaded
require.undef(moduleId);
if (originalModule !== NOT_LOADED) {
define(moduleId, [], function () {
return originalModule;
});
}
for (var dependencyId in dependencyMap) {
map[moduleId][dependencyId] = dependencyId;
require.undef(dependencyId);
(function (originalDependency) {
if (originalDependency !== NOT_LOADED) {
define(dependencyId, [], function () {
return originalDependency;
});
}
})(originalDependencies[dependencyId]);
}
require({
map: map
});
// provide the mocked copy to the caller
dfd.resolve(mockedModule);
});
return dfd.promise;
}
return {
mock: mock
};
});
With this AMD mocker, you simply call it from within your test suite to remap the dependencies of the module you’re trying to test, and load the newly mocked module:
// in tests/unit/app/Controller.js
define([
'intern!tdd',
'intern/chai!assert',
'tests/support/amdMocker'
], function (tdd, assert, amdMocker) {
tdd.suite('app/Controller', function () {
var Controller;
tdd.before(function () {
return amdMocker.mock('app/Controller', {
'util/ErrorDialog': 'tests/mocks/util/ErrorDialog',
'util/StatusDialog': 'tests/mocks/util/StatusDialog'
}).then(function (mocked) {
Controller = mocked;
});
});
tdd.test('basic tests', function () {
// use mocked `Controller`
});
});
});
More information on avoiding this pattern by loosely coupling components and performing dependency injection is discussed in Testable code best practices.
In the future, we hope to include several of these mocking systems directly within Intern in order to make mocking data even easier. For now, by following these simple patterns in your own tests, it becomes much easier to isolate sections of code for proper unit testing. Happy testing!
If you’re still not sure where to start, or would like extra assistance making your code more testable and reliable, we’re here to help! Get in touch today for a free 30-minute consultation.