Recently, I turned to using Dependency Injection to help understand an easy way to separate code and aid in testing. However, modules in Node.js rely on the system API provided by Node, which makes it difficult to judge whether private dependencies are used appropriately. General dependency injection is difficult to use in this case, but don't give up hope just yet.
requireCauses problem
Node.js can easily import dependencies as needed. It works very well and is simpler than AMD schema loaders such as RequireJS. The problem comes when we mock those dependencies. If the loading of models in Node.js is controlled, how can we control the pseudo objects being used during testing? We can use Node's vm mode, through vm we can load the model in a new context. Running in a new context, we can control how requirements are reflected into the model's methods.
Solution
Thanks for this article, I can now provide you with a pretty good solution. The code is below:
var vm = require('vm'); var fs = require('fs'); var path = require('path'); /** * Helper for unit testing: * – load module with mocked dependencies * – allow accessing private state of the module * * @param {string} filePath Absolute path to module (file to load) * @param {Object=} mocks Hash of mocked dependencies */ exports.loadModule = function(filePath, mocks) { mocks = mocks || {}; // this is necessary to allow relative path modules within loaded file // i.e. requiring ./some inside file /a/b.js needs to be resolved to /a/some var resolveModule = function(module) { if (module.charAt(0) !== '.') return module; return path.resolve(path.dirname(filePath), module); }; var exports = {}; var context = { require: function(name) { return mocks[name] || require(resolveModule(name)); }, console: console, exports: exports, module: { exports: exports } }; vm.runInNewContext(fs.readFileSync(filePath), context); return context; };
You can also download the code snippet here. Although the code is not posted in the article the most, it could still use some explanation. When we test, we want to load this module into the test, using theloadModulefunction Load module test instead ofrequire.
The first parameter, filePath, specifies the search location where we want to test the model. The second parameter, mocks, contains an object whose property names match the names of the model we are trying to require. The values specified by those attributes are pseudo-objects, which are used to replace the generally required models.
Essentially, it uses vm to load and run the model in another "context". In other words, we recreate global variables (such as require and exports) so that we can control them. Note that we have written a new require function that is available. All it does is check to see if there is a mock dependency for the execution name, and if so, I delegate it to the usual require function.
Example of using module loader
If you are still a little confused, you can look at the code example below to see how it is used in context, which may help make it clearer for you. First, we create a simple module.
var fs = require('fs'); module.exports = { // Do something with `fs` } 想象一下这个很酷,对吗?不管怎样,现在我们测试那个模块,但是我们要模拟fs来看看它是怎么在内部使用的。 // Jasmine's syntax http://pivotal.github.com/jasmine/ describe('someModule', function() { var loadModule = require('module-loader').loadModule; var module, fsMock; beforeEach(function() { fsMock = { // a mock for `fs` }; // load the module with mock fs instead of real fs module = loadModule('./web-server.js', {fs: fsMock}); }); it('should work', function() { // a test that utilizes the fact that we can now control `fs` }); });
The main thing to note is that on lines 7 to 12, we create a dummy object for fs and use our new loadModule function to tie this used object into the little module above (I mean awesome! Remember , which is awesome, right?).