Mocking data with Intern

By on July 14, 2014 11:43 am

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.