Require-with

Require-with is a project I've recently been working on. The main purpose of the module is to enable simple dependency injection in Node.js. Dependency injection can be great for a number of purposes. The two that come most immediately to mind are testing, and experimentation.

Dependency injection

For just a little bit of background, dependency injection is a design pattern where a module expects its dependencies to be provided to it externally. The alternative pattern is for the module to know and retrieve its dependencies itself. A DI pattern can encourage looser coupling by enforcing a higher degree of isolation between modules. The problem though, is that normally using a dependency injection pattern requires a lot of set up work up front at the start of a project. DI is usually accomplished by relying on complex frameworks which can examine each module and locate installed libraries to fulfill its dependencies.

Without getting too much into the relative merits of using a DI pattern or not, it has a fairly high barrier to entry. You have to install and configure a DI framework, integrate it into your build process, and you have to do it pretty early in the development process. Trying to refit it into a mature project will be a massive undertaking.

Require-with provides a drop-in solution for targeted dependency injection that is stylistically compatible with the standard Node require function.

How it works

At a high level, Require-with works by allowing you to provide a mocks object when requiring modules. Require-with will then intercept that module's internal require calls, and if the required module is provided by the mocks object, then that mock dependency is returned instead. Otherwise require will continue to work as normal.

function context(required, mocks) {
    mocks = mocks || {};
    for (var i in mocks){
        if(mocks.hasOwnProperty(i) && (i !== getModulePath(i))){
            mocks[getModulePath(i)] = mocks[i];
            delete mocks[i];
        }
    }

    var exports = {};
    var context = {
        require: function(name) {
            name = getModulePath(name);
            var parsed = path.parse(name);
            if(mocks[name]){
                return mocks[name];
            }
            else if(mocks[parsed.name]){
                return mocks[parsed.name];
            }
            else if(mocks[parsed.base]){
                return mocks[parsed.base];
            }
            return require(name);
        },
        console: console,
        exports: exports,
        module: {
            exports: exports
        }
    };

    vm.runInNewContext(fs.readFileSync(getModulePath(required)), context);
    return context;
}

The way that works is actually a little complicated. Require-with uses the Node vm module to create a sandboxed execution space for the required module. Then within this sandbox, calls to require are first compared to the mock objects, and if a matching mock is found, that is returned instead. If not, then the call is handed off to the global require function.

Beyond that, there is some work done to resolve paths to local modules, so that, for instance, requireWith('./myModule', mocks) will work in the expected way. And if myModule internally calls require('./myOtherModule'), that will also continue to work as before. That logic is all provided by the getModulePath() function referenced above. It's less interesting than the injection mechanism, but if you want to see it, the source is on github, here.

But wait, there's more!

Since we're already creating a vm sandbox, we can also just expose that execution context. This is probably not what you want for production code, but it's amazing for testing. With the module's execution context, you can inspect the state of private internal data.

For instance, imagine you have a singleton config manager.

var defaults = { 
  url: 'http://example.com',
  path: '/service'
}

var current = {};

module.exports = function config(conf){
  current = object.assign(defaults, current, conf);
  return current;
}

Normally the defaults and current objects would be hidden to this module's consumers. But instead, we can use requireWith.context(). Consider this snippet from a test function:

var requireWith = require('require-with');
var configvm = requireWith.context('./config');

configvm.module.exports({ "environment": "test" });
expect(configvm.current.environment).toBe('test');

Require-with exposes those internal variables for testing.

Speaking of testing

Getting back to the topic of dependency injection, one of the biggest benefits is that it makes mocking your dependencies for doing unit tests extremely easy. And that's where Require-with excels. Dependencies can be mocked on the fly without having to use a big complicated mocking framework. If you're testing a service controller that relies on jquery.ajax, you can provide a mocked object like { "$": { "ajax": function(){} }}.

And experimenting

Likewise, you can experiment with replacement dependencies. For example, if you depend on left-pad you might have recently found yourself in an awkward position. Using DI, you could quickly run your unit tests with a replacement library such as padleft. Require-with would enable that just as well as a large DI framework, but without all of the work of setting up that framework.

Using it

If this sounds good to you, then you're in luck. In addition to being developed on github, I've also published the module on NPM. You can get it via

npm install --save-dev require-with

You could use --save instead, but I expect that it would mostly be used for testing and development, without becoming a regular dependency for regular apps.