这是一个简单的示例,它说明了我的问题的症结所在:
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;
}
};
问题在于文件require
内部的功能underTest.js
实际上尚未被模拟。它仍然指向全局require
功能。因此,看来我只能require
在进行模拟的同一个文件中模拟出该函数。如果我使用全局require
来包含任何内容,即使在覆盖本地副本之后,所需的文件仍将具有全球require
参考。
您现在可以!
我发布了proxyquire,它将在测试模块时覆盖模块内部的全局需求。
这意味着您无需更改代码即可为所需模块注入模拟。
Proxyquire有一个非常简单的API,它允许您解析您要测试的模块,并通过一个简单的步骤传递其所需模块的模拟/存根。
@Raynos是正确的,传统上,您必须诉诸不太理想的解决方案才能实现该目标或进行自下而上的开发
这就是我创建proxyquire的主要原因-允许自上而下的测试驱动开发而无任何麻烦。
请查看文档和示例,以判断它是否适合您的需求。
在这种情况下,更好的选择是模拟要返回的模块的方法。
不管好坏,大多数node.js模块都是单例的。需要同一模块的两段代码获得对该模块的相同引用。
您可以利用此功能,并使用诸如sinon之类的东西来模拟所需的项目。 摩卡咖啡测试如下:
// 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与chai具有良好的集成,可以进行断言,并且我编写了一个模块将sinon与摩卡集成在一起,以简化间谍/存根的清理(避免测试污染)。
请注意,underTest不能以相同的方式进行模拟,因为underTest仅返回一个函数。
另一种选择是使用Jest模拟。在他们的页面上跟进
我使用模拟需求。确保require
在测试模块之前定义了模拟。
require
对我来说,嘲笑感觉就像是一个讨厌的骇客。我个人将尝试避免它,并重构代码以使其更具可测试性。有多种处理依赖关系的方法。
1)将依赖项作为参数传递
function underTest(innerLib) {
return innerLib.doComplexStuff();
}
这将使代码可以普遍测试。缺点是您需要传递依赖关系,这会使代码看起来更加复杂。
2)将模块实现为类,然后使用类的方法/属性获取依赖项
(这是一个人为的示例,其中类的使用不合理,但传达了这一思想)(ES6示例)
const innerLib = require('./path/to/innerLib')
class underTestClass {
getInnerLib () {
return innerLib
}
underTestMethod () {
return this.getInnerLib().doComplexStuff()
}
}
现在,您可以轻松地对getInnerLib
方法进行存根测试来测试您的代码。代码变得更冗长,但也更易于测试。
如果您曾经使用过jest,那么您可能对jest的模拟功能很熟悉。
使用“ jest.mock(...)”,您可以简单地在代码中的某处的需求语句中指定将出现的字符串,并且每当需要使用该字符串的模块时,都会返回一个模拟对象。
例如
jest.mock("firebase-admin", () => {
const a = require("mocked-version-of-firebase-admin");
a.someAdditionalMockedMethod = () => {}
return a;
})
会用您从该“工厂”功能返回的对象完全替换“ firebase-admin”的所有导入/要求。
很好,您可以在使用jest时执行此操作,因为jest会在它运行的每个模块周围创建一个运行时,并将“挂钩”版本的require注入到模块中,但是如果没有jest,您将无法执行此操作。
我试图通过模拟需求来实现这一点,但对我来说,它不适用于源代码中的嵌套级别。看看github上的以下问题:并非总是用Mocha调用mock-require。
为了解决这个问题,我创建了两个npm模块,您可以使用它们来实现所需的功能。
您需要一个babel插件和一个模块模拟程序。
在您的.babelrc中,使用带有以下选项的babel-plugin-mock-require插件:
...
"plugins": [
["babel-plugin-mock-require", { "moduleMocker": "jestlike-mock" }],
...
]
...
并在测试文件中使用jestlike-mock模块,如下所示:
import {jestMocker} from "jestlike-mock";
...
jestMocker.mock("firebase-admin", () => {
const firebase = new (require("firebase-mock").MockFirebaseSdk)();
...
return firebase;
});
...
该jestlike-mock
模块仍然非常初级,没有很多文档,但是也没有太多代码。我感谢任何PR提供了更完整的功能集。目标是重新创建整个“ jest.mock”功能。
为了了解jest如何实现,可以在“ jest-runtime”包中查找代码。例如,请参见https://github.com/facebook/jest/blob/master/packages/jest-runtime/src/index.js#L734,此处它们生成模块的“自动模拟”。
希望能有所帮助;)
你不能 您必须构建单元测试套件,以便首先测试最低的模块,然后再测试需要模块的更高级别的模块。
您还必须假设任何第三方代码和node.js本身都经过了良好的测试。
我想您会在不久的将来看到模拟框架被覆盖 global.require
如果确实必须注入模拟,则可以更改代码以公开模块化作用域。
// 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());
}
请注意,这会暴露.__module
在您的API中,任何代码都可能冒着危险访问模块范围。
您可以使用嘲笑库:
describe 'UnderTest', ->
before ->
mockery.enable( warnOnUnregistered: false )
mockery.registerMock('./path/to/innerLib', { doComplexStuff: -> 'Complex result' })
@underTest = require('./path/to/underTest')
it 'should compute complex value', ->
expect(@underTest()).to.eq 'Complex result'
简单的代码为好奇者模拟模块
请注意您在其中操作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 = {}
}
}
像这样使用:
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非常棒,您应该使用它。它使您的需求覆盖仅局限于测试,我强烈建议您这样做。
文章标签:javascript , mocking , node.js
版权声明:本文为原创文章,版权归 javascript 所有,欢迎分享本文,转载请保留出处!
评论已关闭!