如何对需要其他模块的 Node.js 模块进行单元测试,以及如何模拟全局需求函数?

这是一个简单的例子,说明了我的问题的症结所在:

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引用。

96862 次浏览

你不能。您必须构建您的单元测试套件,以便首先测试最低级的模块,然后测试需要模块的较高级模块。

您还必须假设任何第三方代码和 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());
}

请注意,这将暴露 .__module到您的 API 和任何代码都可以访问模块化范围在他们自己的危险。

现在可以了!

我发布的 Proxyquire将负责覆盖您的模块内的全局要求,而您正在测试它。

这意味着您需要 代码没有变化来为所需的模块注入模拟。

Proxyquire 有一个非常简单的 api,它允许解析您试图测试的模块,并在一个简单的步骤中传递所需模块的模拟/存根。

@ 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 有很好的 与印度茶的结合来进行断言,我为 用摩卡把罪孽与摩卡结合起来编写了一个模块,以便更容易地清除间谍/存根(以避免测试污染)

请注意,underTest 不能以同样的方式进行模拟,因为 underTest 只返回一个函数。

另一种选择是使用 Jest 模拟

我使用 模拟要求。请确保您定义您的模拟之前,您 require的模块进行测试。

你可以使用 嘲笑库:

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就像是一次肮脏的黑客行为。我个人会尽量避免这种情况,并重构代码,使其更具可测试性。 有各种各样的方法来处理依赖关系。

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方法来测试代码。 代码变得更加冗长,但也更容易测试。

模拟模块的简单代码

请注意操作 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)
})
})

但是... jest 已经内置了这个功能,我建议为了测试的目的,在测试框架之上滚动您自己的框架。

如果您曾经使用过 jest,那么您可能熟悉 jest 的模拟特性。

使用“ jest.mock (...)”,您可以简单地在代码中的某个地方指定将出现在请求语句中的字符串,并且每当需要使用该字符串的模块时,就会返回一个模拟对象。

比如说

jest.mock("firebase-admin", () => {
const a = require("mocked-version-of-firebase-admin");
a.someAdditionalMockedMethod = () => {}
return a;
})

将完全替换“ firebase-admin”的所有导入/需求,并使用从该“ Factory”-函数返回的对象。

你可以在使用 jest 的时候这样做,因为 jest 在它运行的每个模块周围创建了一个运行时,并且在模块中注入了一个“钩住”版本的 need,但是如果没有 jest 你就不能做到这一点。

我已经尝试实现这与 模拟要求,但对我来说,它不工作的嵌套层次在我的源。看看 github 上的以下问题: 模拟要求并不总是与摩卡调用

为了解决这个问题,我创建了两个 npm 模块,您可以使用它们来实现您想要的目标。

您需要一个 babel-plugin 和一个模块模拟器。

在.babelrc 中,使用 babel-plugin-mock-demand 插件,可以选择以下选项:

...
"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模块仍然是非常基础的,没有很多文档,但也没有太多的代码。我感谢任何公关为更完整的功能集。我们的目标是重新创建整个“ jest.mock”特性。

为了了解 jest 是如何实现的,我们可以在“ jest-running”包中查找代码。例如,参见 https://github.com/facebook/jest/blob/master/packages/jest-runtime/src/index.js#L734,这里他们生成了一个模块的“自动模块”。

希望这有所帮助;)

我使用一个简单的工厂,返回一个函数,它调用一个函数及其所有依赖项:

/**
* fnFactory
* Returns a function that calls a function with all of its dependencies.
*/


"use strict";


const fnFactory = ({ target, dependencies }) => () => target(...dependencies);


module.exports = fnFactory;

要测试以下函数:

/*
* underTest
*/


"use strict";


const underTest = ( innerLib, millions ) => innerLib.doComplexStuff(millions);


module.exports = underTest;

我会设置我的测试(我使用 Jest)如下:

"use strict";


const fnFactory = require("./fnFactory");
const _underTest = require("./underTest");


test("fnFactory can mock a function by returng a function that calls a function with all its dependencies", () => {
const fake = millions => `Didn't do anything with ${millions} million dollars!`;
const underTest = fnFactory({ target: _underTest, dependencies: [{ doComplexStuff: fake  }, 10] });
expect(underTest()).toBe("Didn't do anything with 10 million dollars!");
});

请参阅测试结果

在产品代码中,我会手动注入被调用方的依赖项,如下所示:

/**
* main
* Entry point for the real application.
*/


"use strict";


const underTest = require("./underTest");
const innerLib = require("./innerLib");


underTest(innerLib, 10);

我倾向于将我编写的大多数模块的范围限制在一件事情上,这样可以减少在测试和将它们集成到项目中时必须考虑的依赖项的数量。

这就是我处理依赖关系的方法。