我有一个我想要测试的AMD模块,但我想模拟它的依赖项而不是加载实际的依赖项。我正在使用requirejs,我的模块的代码看起来像这样:
define(['hurp', 'durp'], function(Hurp, Durp) {
return {
foo: function () {
console.log(Hurp.beans)
},
bar: function () {
console.log(Durp.beans)
}
}
}
如何模拟hurp
和durp
以便我可以有效地进行单元测试?
答案 0 :(得分:64)
所以在阅读this post之后,我想出了一个解决方案,它使用requirejs配置函数为你的测试创建一个新的上下文,你可以简单地模拟你的依赖:
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/'
});
}
因此它创建了一个新的上下文,其中Hurp
和Durp
的定义将由您传递给函数的对象设置。这个名字的Math.random可能有点脏,但它有效。因为如果你有一堆测试,你需要为每个套件创建新的上下文,以防止重用你的模拟,或者当你想要真正的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');
})
});
});
})();
所以我在生产中使用这种方法已经有一段时间了,而且非常强大。
答案 1 :(得分:44)
答案 2 :(得分:16)
我找到了解决这个问题的三种不同方法,但没有一种方法令人愉快。
define('hurp', [], function () {
return {
beans: 'Beans'
};
});
define('durp', [], function () {
return {
beans: 'durp beans'
};
});
require('hurpdhurp', function () {
// test hurpdurp in here
});
的fugly。你必须使用大量的AMD样板来破坏你的测试。
这涉及使用单独的config.js文件来定义指向模拟而不是原始依赖项的每个依赖项的路径。这也很难看,需要创建大量的测试文件和配置文件。
这是我目前的解决方案,但仍然是一个糟糕的解决方案。
您可以创建自己的define
函数,为模块提供自己的模拟,并将测试放入回调中。然后你eval
模块运行测试,如下所示:
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);
这是我的首选解决方案。它看起来有点神奇,但它有一些好处。
eval
,想象一下Crockford会愤怒地爆炸。显然它仍有一些缺点。
define
,因为这是您的测试实际运行的地方。我正在为一个测试运行器工作,为这种东西提供更好的语法,但我仍然没有解决问题1的好方法。
在requirejs中嘲笑deps很难。我发现了一种有效的方式,但我仍然不满意。如果您有任何更好的想法,请告诉我。
答案 3 :(得分:15)
有一个config.map
选项http://requirejs.org/docs/api.html#config-map。
关于如何使用它:
expicitely配置RequireJS;
requirejs.config({
map: {
'source/js': {
'foo': 'normalModule'
},
'source/test': {
'foo': 'stubModule'
}
}
});
在这种情况下,对于普通代码和测试代码,您可以使用foo
模块,它将是真正的模块引用和存根。
答案 4 :(得分:9)
您可以使用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/
答案 5 :(得分:2)
此答案基于Andreas Köberle's answer 对我来说实现和理解他的解决方案并不容易,因此我将更详细地解释它是如何工作的,以及一些需要避免的陷阱,希望它能够帮助未来的访问者。
所以,首先是设置:
我使用 Karma 作为测试运行者,使用 MochaJs 作为测试框架。
使用像 Squire 这样的东西对我来说不起作用,出于某种原因,当我使用它时,测试框架会出错:
TypeError:无法读取未定义的属性“call”
RequireJs有可能map模块id到其他模块ID。它还允许创建使用require
function而不是全局require
的{{3}}
这些功能对于此解决方案的工作至关重要。
这是我的模拟代码版本,包括(很多)评论(我希望它可以理解)。我将它包装在一个模块中,以便测试可以轻松地要求它。
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;
});
我遇到的最大的陷阱,实际上花了我几个小时,就是在创建RequireJs配置。我试图(深度)复制它,并且只覆盖必要的属性(如上下文或映射)。这不起作用!只复制baseUrl
,这样可以正常工作。
要使用它,请在测试中使用它,创建模拟,然后将其传递给createMockRequire
。例如:
var ModuleMock = function () {
this.method = function () {
methodCalled += 1;
};
};
var mocks = {
"ModuleIdOrPath": ModuleMock
}
var requireMocks = mocker.createMockRequire(mocks);
这是完整测试文件的示例:
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);
}
});
});
});
});
});
});
答案 6 :(得分:0)
如果你想做一些孤立一个单位的普通js测试,那么你可以简单地使用这个片段:
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;