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

发布于 2024-11-02 17:59:27 字数 976 浏览 1 评论 0原文

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

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

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

module.exports = underTest;

我正在尝试为此代码编写单元测试。如何在不完全模拟 require 函数的情况下模拟对 innerLib 的需求?

所以这是我试图模拟全局 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 引用。

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 innerLib without mocking out the require function entirely?

So this is me trying to mock out the global require and finding out that it won’t work even to do that:

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 require function inside the underTest.js file has actually not been mocked out. It still points to the global require function. So it seems that I can only mock out the require function within the same file I’m doing the mocking in. If I use the global require to include anything, even after I’ve overridden the local copy, the files being required will still have the global require reference.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(10

屋顶上的小猫咪 2024-11-09 17:59:28

使用 NodeJS 很简单。这是我在这个名为 mock 的库中使用的整个模拟函数:

var Module = require('module')
var _require = Module.prototype.require

var mocks = {}

Module.prototype.require = function () {
  var name = arguments[0]

  if (typeof mocks[name] == 'function') {
    return mocks[name]()
  }

  return _require.apply(this, arguments)
}

function start(name, callback) {
  mocks[name] = callback
}

function stop(name) {
  if (name) {
    delete mocks[name]
  } else {
    mocks = {}
  }
}

module.exports = { start, stop }

像这样使用它:


var mokk = require('mokk')

// Start mocking 'waveorb'
mokk.start('waveorb', function() {
  var companies = [{ name: 'Firmalisten' }]
  return companies
})

// Stop mocking 'waveorb'
mokk.stop('waveorb')

// Stop all mocking
mokk.stop()

It's simple using NodeJS. Here's the entire mocking function I'm using in this library called mock:

var Module = require('module')
var _require = Module.prototype.require

var mocks = {}

Module.prototype.require = function () {
  var name = arguments[0]

  if (typeof mocks[name] == 'function') {
    return mocks[name]()
  }

  return _require.apply(this, arguments)
}

function start(name, callback) {
  mocks[name] = callback
}

function stop(name) {
  if (name) {
    delete mocks[name]
  } else {
    mocks = {}
  }
}

module.exports = { start, stop }

Use it like this:


var mokk = require('mokk')

// Start mocking 'waveorb'
mokk.start('waveorb', function() {
  var companies = [{ name: 'Firmalisten' }]
  return companies
})

// Stop mocking 'waveorb'
mokk.stop('waveorb')

// Stop all mocking
mokk.stop()
我还不会笑 2024-11-09 17:59:28

该函数调用一个具有其所有依赖项的函数:

/**
 * 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!");
});

我使用一个简单的工厂,它返回一个函数, sstatic.net/ZZNmY.png" rel="nofollow noreferrer">查看测试结果

在生产代码中,我将手动注入被调用者的依赖项,如下所示:

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

"use strict";

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

underTest(innerLib, 10);

我倾向于限制我编写的大多数模块的范围到一件事,这减少了测试并将其集成到项目中时必须考虑的依赖项数量。

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

I use a simple factory the returns a function that calls a function with all of its dependencies:

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

"use strict";

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

module.exports = fnFactory;

Wanting to test the following function:

/*
 * underTest
*/

"use strict";

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

module.exports = underTest;

I would setup my test (I use Jest) as follows:

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

See results of test

In production code I would manually inject the callee's dependencies as below:

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

"use strict";

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

underTest(innerLib, 10);

I tend to limit the scope of most of the modules that I write to one thing, which reduces the number of dependencies that have to be accounted for when testing and integrating them into the project.

And that's my approach to dealing with dependencies.

烧了回忆取暖 2024-11-09 17:59:27

现在你可以了!

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

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

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

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

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

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

You can now!

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

This means you need no changes to your code in 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.

@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

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

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

你的心境我的脸 2024-11-09 17:59:27

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

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

您可以利用这一点并使用诸如 sinon 之类的东西来模拟所需的项目。 mocha 测试如下:

// 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 与 mocha 集成,以便更轻松地进行间谍/存根清理(以避免测试污染。)

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

另一种选择是使用 Jest 模拟。关注他们的页面

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.

You can leverage this and use something like sinon to mock out items that are required. mocha test follows:

// 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 chai for making assertions, and I wrote a module to integrate sinon with mocha to allow for easier spy/stub cleanup (to avoid test pollution.)

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

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

汹涌人海 2024-11-09 17:59:27

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

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

吻安 2024-11-09 17:59:27

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

请注意操作 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 内置了此功能,我建议您使用测试框架来进行测试。

Simple code to mock modules for the curious

Notice the parts where you manipulate the require.cache and note require.resolve method as this is the secret sauce.

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... jest has this functionality built in and I recommend that testing framework over rolling your own for testing purposes.

z祗昰~ 2024-11-09 17:59:27

对我来说,嘲笑 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 方法来测试您的代码。
代码变得更加冗长,但也更容易测试。

Mocking require feels 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.

1) pass dependencies as arguments

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

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

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

class underTestClass {
    getInnerLib () {
        return innerLib
    }

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

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

他是夢罘是命 2024-11-09 17:59:27

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

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

例如,

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

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

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

我尝试使用 mock-require 来实现此目的,但对我来说它不起作用对于我的源代码中的嵌套级别。查看 github 上的以下问题: mock-require 并不总是用 Mocha 调用。

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

你需要一个 babel-plugin 和一个 module mocker。

在你的 .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,这里它们生成模块的“automock”。

希望有帮助;)

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

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.

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.

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.

I have tried to achieve this with mock-require but 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.

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

You need one babel-plugin and a module mocker.

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

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

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

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

The jestlike-mock module 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.

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#L734 for example, here they generate an "automock" of a module.

Hope that helps ;)

赢得她心 2024-11-09 17:59:27

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

您还必须假设任何第 3 方代码和 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 中,任何代码都可以访问模块化范围,但会带来危险。

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.

I presume you'll see mocking frameworks arrive in the near future that overwrite 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 .__module into your API and any code can access modular scope at their own danger.

深者入戏 2024-11-09 17:59:27

您可以使用 mockery 库:

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'

You can use mockery library:

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'
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文