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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/5747035/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-23 18:40:30  来源:igfitidea点击:

How to unit test a Node.js module that requires other modules and how to mock the global require function?

javascriptnode.jsmocking

提问by Matthew Taylor

This is a trivial example that illustrates the crux of my problem:

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

var innerLib = require('./path/to/innerLib');

function underTest() {
    return innerLib.doComplexStuff();
}

module.exports = underTest;

I am trying to write a unit test for this code. How can I mock out the requirement for the innerLibwithout mocking out the requirefunction entirely?

我正在尝试为此代码编写单元测试。如何在innerLibrequire完全模拟功能的情况下模拟出对 的要求?

So this is me trying to mock out the global requireand finding out that it won't work even to do that:

所以这是我试图模拟全局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;
    }
};

The problem is that the requirefunction inside the underTest.jsfile has actually not been mocked out. It still points to the global requirefunction. So it seems that I can only mock out the requirefunction within the same file I'm doing the mocking in. If I use the global requireto include anything, even after I've overridden the local copy, the files being required will still have the global requirereference.

问题是文件中的require函数underTest.js实际上并没有被模拟出来。它仍然指向全局require函数。因此,似乎我只能在我进行模拟require的同一个文件中模拟该函数。如果我使用全局require来包含任何内容,即使在我覆盖本地副本之后,所需的文件仍将具有全球require参考。

回答by Thorsten Lorenz

You can now!

您现在可以!

I published proxyquirewhich will take care of overriding the global require inside your module while you are testing it.

我发布了proxyquire,它将在您测试模块时负责覆盖模块内部的全局需求。

This means you need no changes to your codein order to inject mocks for required modules.

这意味着您无需更改代码即可为所需模块注入模拟。

Proxyquire has a very simple api which allows resolving the module you are trying to test and pass along mocks/stubs for its required modules in one simple step.

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

@Raynos is right that traditionally you had to resort to not very ideal solutions in order to achieve that or do bottom-up development instead

@Raynos 是对的,传统上您必须求助于不太理想的解决方案才能实现这一目标或进行自下而上的开发

Which is the main reason why I created proxyquire - to allow top-down test driven development without any hassle.

这是我创建 proxyquire 的主要原因 - 允许自上而下的测试驱动开发没有任何麻烦。

Have a look at the documentation and the examples in order to gauge if it will fit your needs.

查看文档和示例,以判断它是否适合您的需求。

回答by Elliot Foster

A better option in this case is to mock methods of the module that gets returned.

在这种情况下,更好的选择是模拟返回的模块的方法。

For better or worse, most node.js modules are singletons; two pieces of code that require() the same module get the same reference to that module.

无论好坏,大多数 node.js 模块都是单例的;require() 相同模块的两段代码获得对该模块的相同引用。

You can leverage this and use something like sinonto mock out items that are required. mochatest follows:

您可以利用它并使用类似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 has good integration with chaifor making assertions, and I wrote a module to integrate sinon with mochato allow for easier spy/stub cleanup (to avoid test pollution.)

Sinon与 chai进行了很好的集成以进行断言,我编写了一个模块将 sinon 与 mocha 集成在一起,以便更轻松地清理 spy/stub(以避免测试污染)。

Note that underTest cannot be mocked in the same way, as underTest returns only a function.

请注意,underTest 不能以相同的方式模拟,因为 underTest 仅返回一个函数。

Another option is to use Jest mocks. Follow up on their page

另一种选择是使用 Jest 模拟。在他们的页面上跟进

回答by Kunal

I use mock-require. Make sure you define your mocks before you requirethe module to be tested.

我使用mock-require。确保在require要测试的模块之前定义模拟。

回答by AlexM

Mocking requirefeels like a nasty hack to me. I would personally try to avoid it and refactor the code to make it more testable. There are various approaches to handle dependencies.

嘲笑require对我来说就像一个讨厌的黑客。我个人会尽量避免它并重构代码以使其更易于测试。有多种方法来处理依赖关系。

1) pass dependencies as arguments

1) 将依赖项作为参数传递

function underTest(innerLib) {
    return innerLib.doComplexStuff();
}

This will make the code universally testable. The downside is that you need to pass dependencies around, which can make the code look more complicated.

这将使代码普遍可测试。缺点是需要传递依赖关系,这会使代码看起来更复杂。

2) implement the module as a class, then use class methods/ properties to obtain dependencies

2) 将模块实现为类,然后使用类的方法/属性来获取依赖

(This is a contrived example, where class usage is not reasonable, but it conveys the idea) (ES6 example)

(这是一个人为的例子,其中类的使用不合理,但它传达了这个想法)(ES6 示例)

const innerLib = require('./path/to/innerLib')

class underTestClass {
    getInnerLib () {
        return innerLib
    }

    underTestMethod () {
        return this.getInnerLib().doComplexStuff()
    }
}

Now you can easily stub getInnerLibmethod to test your code. The code becomes more verbose, but also easier to test.

现在您可以轻松地存根getInnerLib方法来测试您的代码。代码变得更加冗长,但也更容易测试。

回答by allesklarbeidir

If you've ever used jest, then you're probably familiar with jest's mock feature.

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

Using "jest.mock(...)" you can simply specify the string that would occur in a require-statement in your code somewhere and whenever a module is required using that string a mock-object would be returned instead.

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

For example

例如

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

would completely replace all imports/requires of "firebase-admin" with the object you returned from that "factory"-function.

将使用您从“工厂”功能返回的对象完全替换“firebase-admin”的所有导入/要求。

Well, you can do that when using jest because jest creates a runtime around every module it runs and injects a "hooked" version of require into the module, but you wouldn't be able to do this without jest.

嗯,您可以在使用 jest 时做到这一点,因为 jest 会围绕它运行的每个模块创建一个运行时,并将 require 的“挂钩”版本注入模块,但是如果没有 jest,您将无法做到这一点。

I have tried to achieve this with mock-requirebut for me it didn't work for nested levels in my source. Have a look at the following issue on github: mock-require not always called with Mocha.

我试图用mock-require来实现这一点,但对我来说它不适用于我的源代码中的嵌套级别。在 github 上查看以下问题:mock-require 并不总是用 Mocha 调用

To address this I have created two npm-modules you can use to achieve what you want.

为了解决这个问题,我创建了两个 npm-modules,你可以用它来实现你想要的。

You need one babel-plugin and a module mocker.

你需要一个 babel-plugin 和一个模块 mocker。

In your .babelrc use the babel-plugin-mock-require plugin with following options:

在你的 .babelrc 中使用 babel-plugin-mock-require 插件和以下选项:

...
"plugins": [
        ["babel-plugin-mock-require", { "moduleMocker": "jestlike-mock" }],
        ...
]
...

and in your test file use the jestlike-mock module like so:

并在您的测试文件中使用 jestlike-mock 模块,如下所示:

import {jestMocker} from "jestlike-mock";
...
jestMocker.mock("firebase-admin", () => {
            const firebase = new (require("firebase-mock").MockFirebaseSdk)();
            ...
            return firebase;
});
...

The jestlike-mockmodule is still very rudimental and does not have a lot of documentation but there's not much code either. I appreciate any PRs for a more complete feature set. The goal would be to recreate the whole "jest.mock" feature.

jestlike-mock模块仍然非常简陋,没有很多文档,但也没有太多代码。我感谢任何 PR 提供更完整的功能集。目标是重新创建整个“jest.mock”功能。

In order to see how jest implements that one can look up the code in the "jest-runtime" package. See https://github.com/facebook/jest/blob/master/packages/jest-runtime/src/index.js#L734for example, here they generate an "automock" of a module.

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

Hope that helps ;)

希望有帮助;)

回答by Jason Sebring

Simple code to mock modules for the curious

为好奇的人模拟模块的简单代码

Notice the parts where you manipulate the require.cacheand note require.resolvemethod as this is the secret sauce.

注意您操作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 = {}
  }
}

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

BUT... proxyquire is pretty awesome and you should use that. It keeps your require overrides localized to tests only and I highly recommend it.

但是...... proxyquire 非常棒,你应该使用它。它使您的需求覆盖仅本地化为测试,我强烈推荐它。

回答by Raynos

You can't. You have to build up your unit test suite so that the lowest modules are tested first and that the higher level modules that require modules are tested afterwards.

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

You also have to assume that any 3rd party code and node.js itself is well tested.

您还必须假设任何 3rd 方代码和 node.js 本身都经过良好测试。

I presume you'll see mocking frameworks arrive in the near future that overwrite global.require

我想你会看到在不久的将来会出现覆盖覆盖的模拟框架 global.require

If you really must inject a mock you can change your code to expose modular scope.

如果您真的必须注入一个模拟,您可以更改您的代码以公开模块化范围。

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

Be warned this exposes .__moduleinto your API and any code can access modular scope at their own danger.

请注意,这会暴露.__module在您的 API 中,任何代码都可以自行承担访问模块化范围的风险。

回答by Hirurg103

You can use mockerylibrary:

您可以使用嘲讽库:

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'