Injecting Stubs/Mocks into Tests with Require.js

LMAX Exchange

I’ve recently embarked on a fairly complex new application, a large part of which is a webapp written in JavaScript. The application uses require.js to handle modules and loading of dependencies and we want to be able to unit test our JavaScript.

In order to test specific pieces of the application, we want to be able to inject stubs or mocks into the module being tested. For example, if we had a module:

define(['dataSource', 'utils'], function(dataSource, utils) {...});

We might want to load the normal ‘utils’ module but stub out the ‘dataSource’ module to hard code data instead of retrieving it from the server. You can do this across all tests using the require.js config:

require.config = {
paths: { 'dataSource': 'stubDataSource' }
}

Which causes require.js to always load stubDataSource.js when ‘dataSource’ is requested. This is simple to do, but means every test gets the stub data source and there’s no way to test dataSource itself.

Fortunately, require.js provides the ability to unload a module using require.undef and to define a module with an explicit name. We can combine these functions to override any module with a custom implementation temporarily. I define a test utils module that makes this simple for tests to use:

define(['require'], function(require) {
var stubbed = [];
return { stub: function(name, implementation) { stubbed.push(name); requirejs.undef(name); define(name, [], function() { return implementation; }); }, loadWithCurrentStubs: function(name, callback) { stubbed.push(name); requirejs.undef(name); require([name], callback); }, reset: function() { stubbed.forEach(function(name) { requirejs.undef(name); }); stubbed = []; } };
});

Which would be used like (typically utilising a test framework like mocha):

define(['testUtils'], function(testUtils) {
testUtils.stub('dataSource', {...});
testUtils.loadWithCurrentStubs('moduleToTest', function(moduleToTest) {
// Run tests
testUtils.reset();
});
});

For each stub, we first undef any real module that may have been loaded, then use the named version of define to inject our stub into require.js’ cache of loaded modules.

Then once all the required stubs are loaded, we force the module we want to test to be reloaded, otherwise require.js may have a cached version of it with its real dependencies already injected. Unfortunately that reload has to be asynchronous which adds a little complexity but any good test framework can handle that. For example with mocha the setup would be:

define(['testUtils'], function(testUtils) {
var moduleToTest
beforeEach(function(done) {
testUtils.stub('dataSource', {...});
testUtils.loadWithCurrentStubs('moduleToTest', function(loadedModule) {
moduleToTest = loadedModule;
done();
});
});

afterEach(function() {
testUtils.reset();
});

// Tests...
});

Once we’ve run our tests we simply undef any modules we’ve stubbed or that have been reloaded to use those stubs. Ideally we’d undef everything require.js has loaded to be completely sure our stubs haven’t leaked into any other modules, but I haven’t yet found a way to do that with require.js.

Any opinions, news, research, analyses, prices or other information ("information") contained on this Blog, constitutes marketing communication and it has not been prepared in accordance with legal requirements designed to promote the independence of investment research. Further, the information contained within this Blog does not contain (and should not be construed as containing) investment advice or an investment recommendation, or an offer of, or solicitation for, a transaction in any financial instrument. LMAX Group has not verified the accuracy or basis-in-fact of any claim or statement made by any third parties as comments for every Blog entry.

LMAX Group will not accept liability for any loss or damage, including without limitation to, any loss of profit, which may arise directly or indirectly from use of or reliance on such information. No representation or warranty is given as to the accuracy or completeness of the above information. While the produced information was obtained from sources deemed to be reliable, LMAX Group does not provide any guarantees about the reliability of such sources. Consequently any person acting on it does so entirely at his or her own risk. It is not a place to slander, use unacceptable language or to promote LMAX Group or any other FX and CFD provider and any such postings, excessive or unjust comments and attacks will not be allowed and will be removed from the site immediately.