Javascript 如何对需要其他模块的Node.js模块进行单元测试,以及如何模拟global require函数?
这是一个简单的例子,说明了问题的症结:Javascript 如何对需要其他模块的Node.js模块进行单元测试,以及如何模拟global require函数?,javascript,node.js,mocking,Javascript,Node.js,Mocking,这是一个简单的例子,说明了问题的症结: var innerLib = require('./path/to/innerLib'); function underTest() { return innerLib.doComplexStuff(); } module.exports = underTest; 我正试图为这段代码编写一个单元测试。如何模拟innerLib的需求,而不完全模拟require函数 因此,这是我试图模拟全局require,发现这样做根本不起作用: var pat
var innerLib = require('./path/to/innerLib');
function underTest() {
return innerLib.doComplexStuff();
}
module.exports = underTest;
我正试图为这段代码编写一个单元测试。如何模拟innerLib
的需求,而不完全模拟require
函数
因此,这是我试图模拟全局require
,发现这样做根本不起作用:
var path = require('path'),
vm = require('vm'),
fs = require('fs'),
indexPath = path.join(__dirname, './underTest');
var globalRequire = require;
require = function(name) {
console.log('require: ' + name);
switch(name) {
case 'connect':
case indexPath:
return globalRequire(name);
break;
}
};
问题是
underTest.js
文件中的require
函数实际上没有被模拟出来。它仍然指向全局require
功能。因此,我似乎只能在同一个文件中模拟出require
函数。如果我使用全局require
包含任何内容,即使我覆盖了本地副本,所需的文件仍将具有全局require
引用。您不能。您必须构建单元测试套件,以便首先测试最低级别的模块,然后测试需要模块的较高级别的模块
您还必须假设任何第三方代码和node.js本身都经过良好测试
我想在不久的将来,您会看到覆盖global.require的模拟框架出现
如果您真的必须注入一个mock,那么您可以更改代码以公开模块范围
// underTest.js
var innerLib = require('./path/to/innerLib');
function underTest() {
return innerLib.toCrazyCrap();
}
module.exports = underTest;
module.exports.__module = module;
// test.js
function test() {
var underTest = require("underTest");
underTest.__module.innerLib = {
toCrazyCrap: function() { return true; }
};
assert.ok(underTest());
}
请注意,这会将\u模块
暴露到您的API中,任何代码都可以访问模块作用域,这会带来危险。您现在可以
我发表了一篇文章,它将在您测试模块时覆盖模块内部的全局需求
这意味着您不需要对代码进行任何更改,以便为所需的模块注入模拟
Proxyquire有一个非常简单的api,它允许在一个简单的步骤中解析您试图测试的模块,并传递所需模块的mock/stub
@Raynos是对的,传统上,为了实现这一点,您必须求助于不太理想的解决方案,或者进行自下而上的开发
这就是我创建proxyquire的主要原因——允许自上而下的测试驱动开发,而不需要任何麻烦
查看文档和示例,以确定它是否适合您的需要。在这种情况下,更好的选择是模拟返回的模块的方法
不管是好是坏,大多数node.js模块都是单例的;需要()相同模块的两段代码获得对该模块的相同引用
您可以利用这一点,并使用类似的方法模拟所需的项目。测试如下:
// in your testfile
var innerLib = require('./path/to/innerLib');
var underTest = require('./path/to/underTest');
var sinon = require('sinon');
describe("underTest", function() {
it("does something", function() {
sinon.stub(innerLib, 'toCrazyCrap').callsFake(function() {
// whatever you would like innerLib.toCrazyCrap to do under test
});
underTest();
sinon.assert.calledOnce(innerLib.toCrazyCrap); // sinon assertion
innerLib.toCrazyCrap.restore(); // restore original functionality
});
});
Sinon很擅长断言,我编写了一个模块来简化间谍/存根清理(以避免测试污染)
请注意,不能以相同的方式模拟欠测试,因为欠测试只返回一个函数
另一种选择是使用Jest Mock。跟进我使用的。确保在要求测试模块之前定义模拟。
您可以使用库:
嘲笑
require
对我来说就像是一种讨厌的攻击。我个人会尽量避免它,并重构代码,使其更易于测试。
处理依赖关系有多种方法
1) 将依赖项作为参数传递
function underTest(innerLib) {
return innerLib.doComplexStuff();
}
这将使代码具有普遍可测试性。缺点是需要传递依赖项,这会使代码看起来更复杂
2) 将模块作为类实现,然后使用类方法/属性获取依赖项
(这是一个人为的例子,其中类的使用是不合理的,但它传达了这个想法)
(ES6示例)
现在,您可以轻松地使用stubgetInnerLib
方法测试代码。
代码变得更加冗长,但也更易于测试。为好奇的用户模拟模块的简单代码
注意操作require.cache
的部分,并注意require.resolve
方法,因为这是秘密
class MockModules {
constructor() {
this._resolvedPaths = {}
}
add({ path, mock }) {
const resolvedPath = require.resolve(path)
this._resolvedPaths[resolvedPath] = true
require.cache[resolvedPath] = {
id: resolvedPath,
file: resolvedPath,
loaded: true,
exports: mock
}
}
clear(path) {
const resolvedPath = require.resolve(path)
delete this._resolvedPaths[resolvedPath]
delete require.cache[resolvedPath]
}
clearAll() {
Object.keys(this._resolvedPaths).forEach(resolvedPath =>
delete require.cache[resolvedPath]
)
this._resolvedPaths = {}
}
}
使用like:
describe('#someModuleUsingTheThing', () => {
const mockModules = new MockModules()
beforeAll(() => {
mockModules.add({
// use the same require path as you normally would
path: '../theThing',
// mock return an object with "theThingMethod"
mock: {
theThingMethod: () => true
}
})
})
afterAll(() => {
mockModules.clearAll()
})
it('should do the thing', async () => {
const someModuleUsingTheThing = require('./someModuleUsingTheThing')
expect(someModuleUsingTheThing.theThingMethod()).to.equal(true)
})
})
但是。。。proxyquire非常棒,你应该使用它。它使您的require覆盖仅局限于测试,我强烈建议您这样做。如果您曾经使用过jest,那么您可能熟悉jest的mock特性 使用“jest.mock(…)”可以简单地指定代码中某个地方require语句中出现的字符串,并且每当需要使用该字符串的模块时,将返回一个mock对象 比如说
jest.mock("firebase-admin", () => {
const a = require("mocked-version-of-firebase-admin");
a.someAdditionalMockedMethod = () => {}
return a;
})
将使用从“工厂”函数返回的对象完全替换“firebase admin”的所有导入/要求
在使用jest时,您可以这样做,因为jest围绕它运行的每个模块创建一个运行时,并将“钩住”版本的require注入到模块中,但是如果没有jest,您将无法做到这一点
我曾试图用实现这一点,但对我来说,它不适用于源代码中的嵌套级别。请查看github上的以下问题:
为了解决这个问题,我创建了两个npm模块,您可以使用它们来实现您想要的
您需要一个babel插件和一个模块模拟程序
...
"plugins": [
["babel-plugin-mock-require", { "moduleMocker": "jestlike-mock" }],
...
]
...
在测试文件中使用jestlike模拟模块,如下所示:
import {jestMocker} from "jestlike-mock";
...
jestMocker.mock("firebase-admin", () => {
const firebase = new (require("firebase-mock").MockFirebaseSdk)();
...
return firebase;
});
...
jestlikemock
模块仍然非常初级,没有太多文档,但也没有太多代码。我感谢任何PRs提供更完整的功能集。目标是重新创建整个“jest.mock”功能
为了了解jest是如何实现的,可以在“jest运行时”包中查找代码。例如,请参见此处,它们生成模块的“自动模拟”
希望有帮助;) 您必须覆盖
全局。需要。变量写入模块
...
"plugins": [
["babel-plugin-mock-require", { "moduleMocker": "jestlike-mock" }],
...
]
...
import {jestMocker} from "jestlike-mock";
...
jestMocker.mock("firebase-admin", () => {
const firebase = new (require("firebase-mock").MockFirebaseSdk)();
...
return firebase;
});
...