如何在 RequreJS 中模拟单元测试的依赖关系?

我想测试一个 AMD 模块,但是我想模拟出它的依赖关系,而不是加载实际的依赖关系。我使用的是必要条件,我的模块的代码如下所示:

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

我如何模拟出 hurpdurp,以便能够有效地进行单元测试?

33214 次浏览

我找到了解决这个问题的三种不同的方法,但没有一种是令人愉快的。

内联定义依赖项

define('hurp', [], function () {
return {
beans: 'Beans'
};
});


define('durp', [], function () {
return {
beans: 'durp beans'
};
});


require('hurpdhurp', function () {
// test hurpdurp in here
});

丑陋。你必须用大量的 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);

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

  1. 在节点中运行测试,所以不要干扰浏览器自动化。
  2. 在您的测试中不需要凌乱的 AMD 样板。
  3. 你可以在愤怒时使用 eval,想象一下克罗克福德因愤怒而爆炸。

显然,它还是有一些缺点。

  1. 由于是在节点中进行测试,因此不能对浏览器事件或 DOM 操作进行任何操作。只适合测试逻辑。
  2. 还是有点笨重。您需要在每个测试中模拟出 define,因为那是您的测试实际运行的地方。

我正在开发一个测试运行程序来为这类东西提供更好的语法,但是我仍然没有解决问题1的好办法。

结论

需求中的嘲笑深度很难。我找到了一种可行的方法,但还是不太满意。如果你有更好的主意,请告诉我。

因此,在阅读了 这篇文章之后,我想出了一个解决方案,它使用 requjs config 函数为您的测试创建一个新的上下文,在这个上下文中,您可以简单地模拟您的依赖项:

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/'
});
}

因此,它创建了一个新的上下文,其中 HurpDurp的定义将由传递到函数中的对象设置。这个名字的 Math.Random 可能有点脏,但是很有用。因为如果您有大量的测试,那么您需要为每个套件创建新的上下文,以防止重用您的模拟,或者在您需要真正的必需模块时加载模拟。

你的情况是这样的:

(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');
})
});
});
})();

因此,我在生产中使用这种方法一段时间,它非常健壮。

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

关于如何使用:

  1. 定义标准模块;
  2. 定义存根模块;
  3. 快速配置 RequreJS;

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

In this case for normal and test code you could use the foo module which will be real module reference and stub accordingly.

可以使用 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/

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

来自文件:

Js 是一个针对 Requre.js 用户的依赖注入器,可以让模拟依赖变得简单!

如果你想做一些简单的 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;

这个答案是基于 Andreas Köberle 的回答的。
实现和理解他的解决方案对我来说并不容易,所以我将更详细地解释它是如何工作的,以及一些需要避免的陷阱,希望它能帮助未来的访问者。

所以,首先,设置:
我使用 因果报应作为测试运行器,使用 MochaJ作为测试框架。

使用像 乡绅这样的东西对我来说不起作用,出于某种原因,当我使用它时,测试框架抛出了错误:

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

RequreJs 具有 地图模块 ID 到其他模块 ID 的可能性。它还允许创建使用 不同的配置而不是全局 requirerequire功能
这些特性对于这个解决方案的工作至关重要。

下面是我的模拟代码版本,包括(很多)注释(我希望它是可以理解的)。我将它包装在一个模块中,这样测试就可以很容易地要求它。

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;
});

我遇到的 最大的陷阱花费了我好几个小时,它正在创建 RequreJs 配置。我尝试(深度)复制它,只覆盖必要的属性(如上下文或映射)。这不管用!只复制 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);
}
});
});
});
});
});
});