Mocking data - msssk/intern GitHub Wiki
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:
File: 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
};
});
File: tests/intern.js
var dojoConfig = {
requestProvider: 'dojo/request/registry'
};
define({
// … Intern configuration
});
File: tests/unit/app/Controller.js
require([
'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:
File: 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 module after the mocked version has completed loading.
File: 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
};
});
File: 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.