如何在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) } } }
我怎样才能模拟出hurp
和durp
这样我就可以有效地进行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一个新的上下文,其中Hurp
和Durp
的定义将由您传递给函数的对象设置。 该名称的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。 它看起来有点神奇,但它有一些好处。
- 在节点中运行testing,所以不要搞乱浏览器自动化。
- 在您的testing中不需要凌乱的AMD样板。
- 你会愤怒地使用
eval
,并想象Crockford愤怒的爆炸。
它显然还有一些缺点。
- 由于您在节点中进行testing,因此无法对浏览器事件或DOM操作进行任何操作。 只适用于testing逻辑。
- 还有点笨重的设置。 您需要在每个testing中剔除
define
,因为这是您的testing实际运行的地方。
我正在testing跑步者给这种东西更好的语法,但我仍然没有问题1的好scheme。
结论
在requirejs中嘲笑decks很难。 我发现了一个这样的工作方式,但我还是不太满意。 请让我知道,如果你有更好的想法。
有一个config.map
选项http://requirejs.org/docs/api.html#config-map 。
关于如何使用它:
- 定义正常模块;
- 定义存根模块;
-
明确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;