I'm currently working with a legacy requireJS browser module, in which several 3rd party dependencies are being selectively loaded at runtime. This has been achieved using an explicit require()
call in the module's main body of code at which point, (given they are hosted externally and cannot not be compiled into all.js), the dependencies are asynchronously pulled in.
The following is a simplified example:
define(['module/videoPlayer'], function(player) {
return {
loadPlugins: function(config) {
if (config.advertsEnabled) {
require('plugins/adverts', function(advertsPlugin) {
player.loadPlugin(advertsPlugin);
});
}
}
}
});
Even though the majority of the module's behaviour is covered by Jasmine specs, I noticed this particular logic was not. So, I set about writing some simple specs to prevent any regression bugs slipping through the net. The suite looked something like this:
describe('Plugin loader', function() {
var subject, player, plugin;
beforeEach(function(done) {
player = { loadPlugin: sinon.spy() };
plugin = sinon.stub();
var injector = new Squire();
injector
.mock('module/videoPlayer', player)
.require(['module/pluginLoader'], function(loader) {
subject = loader;
done();
});
});
// new specs added to the suite
it('should not load adverts when showingAdverts is false', function() {
subject.loadPlugins({
advertsEnabled: false
});
expect(player.loadPlugin).not.toHaveBeenCalled();
});
it('should load the adverts when showingAdverts is true', function() {
subject.loadPlugins({
advertsEnabled: true
});
expect(player.loadPlugin).toHaveBeenCalledWith(plugin);
});
});
You'll see in the beforeEach
block, that Squire is being used to mock the module's main dependencies. This works pretty well and the module is neatly exposed as the variable subject
for each spec to use.
The two specs I added to the suite look to ensure loadPlugin()
is only called on the player when adverts are enabled. I also wanted to ensure that the correct plugin is loaded into the player.
The first spec ran without any problems. The second spec wasn't so kind and presented me with a couple of challenges:
-
RequireJS kicks in and attempts to load the actual adverts plugin, at which point things blow up
-
I needed some way to inject
plugin
into the execution context of the require callback, otherwise my assertions againstplayer.loadPlugin()
would never be successful
So I set about trying to do this, eventually having some success.
Using Squire
My first thought was to continue using Squire, since it seemed to be doing a pretty good job. By overriding the global require
function with Squire.require
, I would be able to, I thought, inject my mock plugin at the appropriate point. So I reconfigured the injector slightly:
injector
.mock('module/videoPlayer', player)
.mock('plugins/adverts', plugin)
.require(['module/pluginLoader'], function(loader) {
subject = loader;
// overriding require
actualRequire = require;
require = injector.require;
done();
});
I also added an afterEach
to put require back, ready for the next spec:
afterEach(function(done) {
// putting require back
require = actualRequire;
});
When I ran this I got an error cannot find forEach of undefined, which was occurring in the bowls of squire.js
somewhere. After a bit of digging, it became apparent there was a scoping issue, which I was able to fix by changing the line:
require = injector.require
to
require = function() {
return injector.require.apply(injector, arguments);
}
This would ensure that when require was eventually called, it would be scoped to injector
, and not window
.
It this point, plugin
was being injected correctly and as a result I had fixed issue #1.
However, I now had an async problem. The expectations were being evaluated in my spec before the require callback inside the module.
I couldn’t find a way to cleanly resolve this with Squire, so I went back to the drawing board.
Using Sinon
To the best of my knowledge Sinon mocks are called synchronously. So in theory, replacing Squire.require
with a one such mock might just do the trick.
Turns out it did. The beforeEach
block now looked like this:
var requireMock = sinon.mock();
// mocking the require call
requireMock.callsArgWith(1, plugin);
injector
.mock('module/videoPlayer', player)
.require(['module/pluginLoader'], function(loader) {
subject = loader;
// overriding require
actualRequire = require;
require = requireMock;
done();
});
The line requireMock.callsArgWith(1, plugin)
tells sinon to call the 2nd argument with a single parameter, the mocked adverts plugin. Since this is called immediately by sinon, when the expectations are evaluated the module is then in the correct state and everything behaves as it should.
As a brucie bonus, this technique allows you make assertions on the require
call to ensure it's not being called when it shouldn't, in my case when adverts are disabled.
expect(requireMock).notToHaveBeenCalled();