Put mock data into Node require cache

Avoid loading source files - put the code directly into the require cache.

I love playing around with Node require, see a couple of previous blog posts and existing projects:

Recently I had another epiphany: if I need to mock a module, why bother reading its source file from disk at all? Or how about loading fake or non-existing modules? Turns out this is very simple to do and can be used during unit testing and a few other fun things (stay tuned for the future updates).

Simple module mocking

Let us solve the first problem: imagine we really want to load something else instead of the actual file foo.js. We can insert the mock data first before the actual file had a chance to load. Node has a cache of all loaded and evaluated code. Each entry is stored under the fully resolved filename. The loaded source file foo.js has something like this

foo.js
1
module.exports = 'this is foo';
see the cached value for foo.js
1
2
3
4
5
6
7
8
9
10
11
12
13
require('./foo.js') // loads "foo.js" and sticks it into the cache
// see the entire loaded info for foo.js
require.cache[require('path').resolve('./foo.js')]
{ id: '/Users/gleb/git/grunty/test/foo.js',
exports: 'this is foo',
parent:
{ id: 'repl', ... } // info about the REPL module I used
filename: '/test/foo.js',
loaded: true,
children: [],
paths:
[ ... ] // not interesting
}

The only interesting properties that we need to insert ourselves are:

  • id (same as the full resolved filename)
  • exports - the actual compiled module.exports value
  • loaded - might not matter, but better to be set to true

Thus we can insert the mock data directly into the require.cache at desired key to use completely fake module.

insert-fake-foo.js
1
2
3
4
5
6
7
var resolved = require('path').resolve('./foo.js');
require.cache[resolved] = {
id: resolved,
filename: resolved,
loaded: true,
exports: function () { console.log('hi there!'); }
};

Done. Any require('./foo.js') after that will return the fake exports value from the cache.

foo.js
1
module.exports = 'this is foo';
test-foo.js
1
2
3
4
// test-foo.js loads after insert-fake-foo.js
var foo = require('./foo');
foo();
// prints "hi there!"

Loading a non-existing module

Let us try loading a file called bar.js that actually does not exist. Typically this would be impossible: there would be an exception

Error: Cannot find module './bar.js'
    at Function.Module._resolveFilename (module.js:331:15)

Even if we insert the mock data into the require.cache, the next call require('./bar.js') still check if the file exists in order to properly resolve it. Thus we need to fake the filename resolution. Luckily this is simple to do.

1
2
3
4
5
6
7
8
9
var resolvedFakeName = require('path').resolve('./bar.js');
var Module = require('module');
var realResolve = Module._resolveFilename;
Module._resolveFilename = function fakeResolve(request, parent) {
if (request === resolvedFakeName) {
return true; // pretend 'bar.js' really exists
}
return realResolve(request, parent);
};

We only return the fake yes answer for our non-existent module, and let the real method do the actual work for every other module. Combined with inserting mock data into the cache, this work around allows to load non-existent modules in the entire app.

Update 1

I hate to write an entire file Gruntfile.js just to run a single grunt plugin, so I wrote grunty - loads and runs a given plugin without global grunt CLI or a gruntfile. For example to run the excellent grunt-contrib-concat plugin, just add this to the package.json (and install plugin and grunty as dependencies).

package.json
1
2
3
"scripts": {
"concat": "grunty grunt-contrib-concat concat --src=a.js,b.js --dest=dist/out.js"
}

In order to make this work, I avoided writing even a temporary Gruntfile.js, instead putting the configuration function directly into the module cache.

grunty writes the fake config function into require cache
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// this returns initialized config function
function fakeGruntfileInit(options) {
function fakeGruntfile(grunt) {
var pkg = grunt.file.readJSON('package.json');
var config = {
pkg: pkg
};
if (isMultiTask(options)) {
config[options.target] = {
default: options
};
}
grunt.initConfig(config);
grunt.task.loadNpmTasks(options.plugin);
grunt.registerTask('default', []);
}
return fakeGruntfile;
}
// get, update and set the fake grunt config function into cache
var fakeGruntfileFunc = fakeGruntfileInit({
plugin: plugin,
target: target,
src: grunt.cli.options.src,
dest: grunt.cli.options.dest
});
require.cache[fullFakeGruntfileFilename] = {
id: fullFakeGruntfileFilename,
exports: fakeGruntfileFunc,
parent: null,
filename: fullFakeGruntfileFilename,
loaded: true
};

Pretty useful.