如何在RequireJS中模拟unit testing的依赖关系?

我有一个AMD模块我想testing,但我想嘲笑其依赖关系,而不是加载实际的依赖关系。 我正在使用requirejs,而我的模块代码如下所示:

define(['hurp', 'durp'], function(Hurp, Durp) { return { foo: function () { console.log(Hurp.beans) }, bar: function () { console.log(Durp.beans) } } } 

我怎样才能模拟出hurpdurp这样我就可以有效地进行unit testing了?

所以在阅读这篇文章后,我想出了一个解决scheme,它使用requirejsconfiguration函数为您的testing创build一个新的上下文,您可以简单地模拟您的依赖关系:

 var cnt = 0; function createContext(stubs) { cnt++; var map = {}; var i18n = stubs.i18n; stubs.i18n = { load: sinon.spy(function(name, req, onLoad) { onLoad(i18n); }) }; _.each(stubs, function(value, key) { var stubName = 'stub' + key + cnt; map[key] = stubName; define(stubName, function() { return value; }); }); return require.config({ context: "context_" + cnt, map: { "*": map }, baseUrl: 'js/cfe/app/' }); } 

因此它会创build一个新的上下文,其中HurpDurp的定义将由您传递给函数的对象设置。 该名称的Math.random可能有点脏,但它的工作原理。 因为如果你有一堆testing,你需要为每个套件创build新的上下文,以防止重复使用你的模拟,或者当你想要真正的requirejs模块时加载模拟。

在你的情况下,它会看起来像这样:

 (function () { var stubs = { hurp: 'hurp', durp: 'durp' }; var context = createContext(stubs); context(['yourModuleName'], function (yourModule) { //your normal jasmine test starts here describe("yourModuleName", function () { it('should log', function(){ spyOn(console, 'log'); yourModule.foo(); expect(console.log).toHasBeenCalledWith('hurp'); }) }); }); })(); 

所以我在生产中使用这种方法一段时间,它非常强大。

你可能想看看新的Squire.js库

从文档:

Squire.js是Require.js用户的一个dependency injection器,使得模拟依赖变得容易!

我已经find了三个不同的解决scheme来解决这个问题,没有一个是愉快

定义内联依赖关系

 define('hurp', [], function () { return { beans: 'Beans' }; }); define('durp', [], function () { return { beans: 'durp beans' }; }); require('hurpdhurp', function () { // test hurpdurp in here }); 

的fugly。 你必须用许多AMD样板来混淆你的testing。

从不同的path加载模拟依赖关系

这涉及到使用一个单独的config.js文件来定义指向mock而不是原始依赖关系的每个依赖关系的path。 这也是丑陋的,需要创build大量的testing文件和configuration文件。

在节点中伪造

这是我目前的解决scheme,但仍然是一个可怕的。

你创build你自己的define函数来为模块提供你自己的模拟,并把你的testing放在callback中。 然后你eval模块来运行你的testing,如下所示:

 var fs = require('fs') , hurp = { beans: 'BEANS' } , durp = { beans: 'durp beans' } , hurpDurp = fs.readFileSync('path/to/hurpDurp', 'utf8'); ; function define(deps, cb) { var TestableHurpDurp = cb(hurp, durp); // now run tests below on TestableHurpDurp, which is using your // passed-in mocks as dependencies. } // evaluate the AMD module, running your mocked define function and your tests. eval(hurpDurp); 

这是我首选的解决scheme。 它看起来有点神奇,但它有一些好处。

  1. 在节点中运行testing,所以不要搞乱浏览器自动化。
  2. 在您的testing中不需要凌乱的AMD样板。
  3. 你会愤怒地使用eval ,并想象Crockford愤怒的爆炸。

它显然还有一些缺点。

  1. 由于您在节点中进行testing,因此无法对浏览器事件或DOM操作进行任何操作。 只适用于testing逻辑。
  2. 还有点笨重的设置。 您需要在每个testing中剔除define ,因为这是您的testing实际运行的地方。

我正在testing跑步者给这种东西更好的语法,但我仍然没有问题1的好scheme。

结论

在requirejs中嘲笑decks很难。 我发现了一个这样的工作方式,但我还是不太满意。 请让我知道,如果你有更好的想法。

有一个config.map选项http://requirejs.org/docs/api.html#config-map

关于如何使用它:

  1. 定义正常模块;
  2. 定义存根模块;
  3. 明确configurationRequireJS;

     requirejs.config({ map: { 'source/js': { 'foo': 'normalModule' }, 'source/test': { 'foo': 'stubModule' } } }); 

在这种情况下,对于正常的和testing代码,你可以使用foo模块,这将是真正的模块引用和相应的存根。

您可以使用testr.js来模拟依赖关系。 您可以设置testr来加载模拟依赖关系而不是原来的依赖关系。 以下是一个示例用法:

 var fakeDep = function(){ this.getText = function(){ return 'Fake Dependancy'; }; }; var Module1 = testr('module1', { 'dependancies/dependancy1':fakeDep }); 

看看这个以及: http : //cyberasylum.janithw.com/mocking-requirejs-dependencies-for-unit-testing/

这个答案是基于AndreasKöberle的回答 。
要实现和理解他的解决scheme并不是那么容易,所以我会更详细地解释它的工作方式,以及一些避免的陷阱,希望能够帮助未来的访问者。

所以,首先设置:
我使用Karma作为testing运行者和MochaJs作为testing框架。

使用像Squire这样的东西不适合我,因为某些原因,当我使用它时,testing框架抛出了错误:

TypeError:无法读取未定义的属性“调用”

RequireJs可以将模块ID 映射到其他模块ID。 它还允许创build一个使用与全局require 不同的configuration的require函数 。
这些function对于此解决scheme的工作至关重要。

这里是我的模拟代码的版本,包括(很多)评论(我希望它是可以理解的)。 我将它包装在一个模块中,以便testing可以很容易地要求它。

 define([], function () { var count = 0; var requireJsMock= Object.create(null); requireJsMock.createMockRequire = function (mocks) { //mocks is an object with the module ids/paths as keys, and the module as value count++; var map = {}; //register the mocks with unique names, and create a mapping from the mocked module id to the mock module id //this will cause RequireJs to load the mock module instead of the real one for (property in mocks) { if (mocks.hasOwnProperty(property)) { var moduleId = property; //the object property is the module id var module = mocks[property]; //the value is the mock var stubId = 'stub' + moduleId + count; //create a unique name to register the module map[moduleId] = stubId; //add to the mapping //register the mock with the unique id, so that RequireJs can actually call it define(stubId, function () { return module; }); } } var defaultContext = requirejs.s.contexts._.config; var requireMockContext = { baseUrl: defaultContext.baseUrl }; //use the baseUrl of the global RequireJs config, so that it doesn't have to be repeated here requireMockContext.context = "context_" + count; //use a unique context name, so that the configs dont overlap //use the mapping for all modules requireMockContext.map = { "*": map }; return require.config(requireMockContext); //create a require function that uses the new config }; return requireJsMock; }); 

我遇到的最大的陷阱 ,几乎花了我几个小时,创buildRequireJsconfiguration。 我试图(深)复制它,只覆盖必要的属性(如上下文或地图)。 这不行! 只复制baseUrl ,这工作正常。

用法

要使用它,请在您的testing中,创build模拟,然后将其传递给createMockRequire 。 例如:

 var ModuleMock = function () { this.method = function () { methodCalled += 1; }; }; var mocks = { "ModuleIdOrPath": ModuleMock } var requireMocks = mocker.createMockRequire(mocks); 

这里是一个完整的testing文件例子

 define(["chai", "requireJsMock"], function (chai, requireJsMock) { var expect = chai.expect; describe("Module", function () { describe("Method", function () { it("should work", function () { return new Promise(function (resolve, reject) { var handler = { handle: function () { } }; var called = 0; var moduleBMock = function () { this.method = function () { methodCalled += 1; }; }; var mocks = { "ModuleBIdOrPath": moduleBMock } var requireMocks = requireJsMock.createMockRequire(mocks); requireMocks(["js/ModuleA"], function (moduleA) { try { moduleA.method(); //moduleA should call method of moduleBMock expect(called).to.equal(1); resolve(); } catch (e) { reject(e); } }); }); }); }); }); }); 

如果你想做一些简单的jstesting来隔离一个单元,那么你可以简单地使用这个代码片段:

 function define(args, func){ if(!args.length){ throw new Error("please stick to the require.js api which wants a: define(['mydependency'], function(){})"); } var fileName = document.scripts[document.scripts.length-1].src; // get rid of the url and path elements fileName = fileName.split("/"); fileName = fileName[fileName.length-1]; // get rid of the file ending fileName = fileName.split("."); fileName = fileName[0]; window[fileName] = func; return func; } window.define = define;