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.

Comments

  • Pingback: Dojo FAQ: How to use JSFiddle to provide a functioning test case | Blog | SitePen()

  • Jerry

    It appears that dojo/_base/config prefixes it’s has additions with “config” yet dojo/request/default.js is looking for has(“config-requestProvider”)…so I had to modify my dojoConfig in my intern.js with the hash key “-requestProvider” instead which looks a little fishy…

  • line 25 of tests/support/requestMocker.js should not have a semicolon. You also have an extra open parenthesis on line 38.

  • Thanks Gavin, I’ve fixed line 38. That said, line 25 doesn’t have a semicolon. If you meant line 26, it actually should have one.

  • Great. Thanks!

  • Actually, I take it back, Colin already fixed the semicolon, whereas the extra pair of () was intentional, to show that it is supposed to be an assignment and not a conditional operation where someone typoed. Anyway, thanks for the feedback and for trying out the code!

  • Allen

    How do I mock a complex JSON response?

  • Allen Zhang
  • Pingback: From DOH to Intern: Updating Dojo core's tests | Blog | SitePen()

  • Tory G

    Hi,

    I’m intrigued by the pattern of the request mocker, but in attempting to at least experiment with it for Intern, I got stuck because I placed `var dojoConfig = { … };` at the top of my `intern.js` file, but upon executing my functional tests, it seemed not to be recognized at all. (I put a dummy string value, something that definitely doesn’t exist, in for the `requestProvider` to make sure – didn’t get errors or anything.)

    I realize this article is a bit old now, so maybe something has changed? I’m using Intern version 3.0.6.

  • Hi Tory, It works pretty well for us with Intern 2.x for Dojo, see https://github.com/dojo/dojo/blob/master/tests/dojo.intern.js and this should be unchanged and still works for us with Intern 3. Have you tried using something node-debug to step through things?

  • Tina

    Hi guys,

    I used your AMD mocking and it worked very well! Unfortunately, I came across some difficulties I cannot solve myself. Maybe you have some ideas?

    Mocking twice. When I want to mock the module more than once, I get an error. Basically this line: // originalModule = require(moduleId); // fails. Is there a workaround available?

    The second issue is that I fail to mock inheritance: When I have a child that inherits from a parent (var child = declare(parent, {…}) ) – how can I exchange the parent’s dependencies while creating a child object?

    I have posted/will post these question also on stackoverflow, with the intern-tag. I would be sooo happy if someone has some ideas to share!

  • Hi @Tina, sorry we’ve been so slow to respond. We haven’t had time to put together an answer yet, and we think it’s a big enough question to probably warrant a follow-up blog post. We’ll let you know when we have that written.