Skip to content

Instantly share code, notes, and snippets.

@keithamus
Created November 22, 2012 12:20
Show Gist options
  • Save keithamus/4130885 to your computer and use it in GitHub Desktop.
Save keithamus/4130885 to your computer and use it in GitHub Desktop.
How to do Dependency Injection in RequireJS?
define([], function () {
return function HypotheticalHelperMethod() {
doSomeOtherStuff();
}
});
define(["helperMethod"], function (helperMethod) {
MyClass = function () {
this.init()
}
MyClass.prototype.init = function () {
helperMethod(1, 2, 3);
}
});
require(["helperMethod", "myClass"], function (helperMethod, myClass) {
describe("MyClass tests", function () {
it("calls `helperMethod` upon init", function () {
// here i need to spyOn helperMethod, but if I set helperMethod
// to a jasmime spy:
helperMethod = jasmine.createSpy("helperMethod");
// I've altered my reference, and MyClass still has the original
// HypotheticalHelperMethod function.
var instance = new MyClass();
expect(helperMethod).toHaveBeenCalledWith(1, 2, 3);
});
});
});
@keithamus
Copy link
Author

Btw I am aware that I could turn helperMethod into an object and spy on a property of that, and I'm also aware I could add helperMethod to MyClass as a property to expose it to the tests there, but neither of these solutions are satisfactory to me.

@lennym
Copy link

lennym commented Nov 22, 2012

I don't think this is a problem with Jasmine so much as a limitation of Javascript itself since functions are passed by value rather than by reference.

@lennym
Copy link

lennym commented Nov 22, 2012

Or indeed a problem with require, which I realise seems to be where you're actually pointing the finger.

I can't see any way you could expect a modifying a reference to a function in one place to modify the "same*" function in another, unless that function is referenced as a property of an object.

[* not the same]

@jrburke
Copy link

jrburke commented Nov 22, 2012

Something to try:

Use themap config to point all consumers of 'helperMethod' to an adapter:

requirejs.config({
  map: {
    '*': {
      helperMethod: 'helperMethodSpy'
    },
    'helperMethodSpy': {
      helperMethod: 'helperMethod'
    }
  }
});

Then, helperMethodSpy.js can do the method interception:

define(['helperMethod'], function (hm) {
    //Wrap hm however you want
    return wrappedHmFn;
});

If you only need do to this for one test/if you want to mock different things, then you can use the multiversion/context support to create different buckets of modules for each test.

@keithamus
Copy link
Author

Thanks for the replies.

helperMethod still needs to be tested, and so altering the config means the test that requires helperMethod also has the spied version, correct? This method seems like a large, sweeping change to the require config given this problem happens tens of times across a large codebase.

@jrburke
Copy link

jrburke commented Nov 23, 2012

Not sure I follow -- for tests that do not need the altered helperMethod, create new multiversion contexts for those tests that either do or do not have that map config applied.

Put another way, how would you do the testing of a non-modified helperMethod and a spied version of it, if AMD modules were not involved? It would seem to me that you would have to have two different test runs of the code, which is what is provided by the multiversion contexts. But I could very well not understand the test methodology involved.

@keithamus
Copy link
Author

Maybe I haven't explained my self best, so here goes:

I think the real problem I have is because of the declarative nature of require, and the fact that a module is loaded when its dependencies are met, rather than when it itself is called as a dependency. Because of these issues it becomes really difficult to get code before dependency resolution, which is the typical pattern I'd use to spy on something.

If AMD was not involved I would have to either expose the module via name spacing (which makes it easy to spy on) or if I was using some Node Style module loader which requires dependencies imperatively I could stub over the require function to reroute it (aka behaviour like Syringe or Rewire). Both of these means I can mock or spy a module imperatively -- and inline to the tests -- and restore old functionality when I'm done.

Meanwhile AMDs solution seems to be to provide an additional layer of complexity - in that if I want to spy on a module I need to alter the config and set up spies independent of tests. I was hoping for something which could be managed purely inside the test environment, rather than having additional config files to manage tests.

@jrburke
Copy link

jrburke commented Nov 23, 2012

Ah, OK. If you want to just cram in a modification after a module has been created but before listeners are given a reference to it, you can use the semi-private onResourceLoad API:

https://github.com/jrburke/requirejs/wiki/Internal-API:-onResourceLoad

Note this API is always subject to change. It has been stable for a while, but no guarantees for the future, even though I have no immediate plans to change it.

To modify the value passed to modules dependent on the current module, this would work:

requirejs.onResourceLoad = function (context, map, depArray) {
  var id = map.id,
    internalModule = context.registry[id],
    existingExport = context.defined[id];

    //modify existing export here

    //If the modification is a modification to the base export (like
    //if the export was a function) and not just a property
    //modification on the export, hard set the new module value:
    internalModule.exports = context.defined[id] = newExportValue;
};

This onResourceLoad definition should be done after require.js loads, but before any module loading is done, at least for modules that you want to intercept.

@hisapy
Copy link

hisapy commented Nov 19, 2014

@keithamus did you find a way to test that your helperMethod is called?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment