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


156

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

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

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

module.exports = underTest;

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

因此,这是我试图模拟全局变量,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参考。


您必须覆盖global.requiremodule默认情况下,变量是在模块作用域内写入的。
雷诺斯2011年

@Raynos我该怎么办?global.require是否未定义?即使我将其替换为自己的函数,其他函数也将永远不会使用它们?
HMR 2014年

Answers:


174

您现在可以!

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

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

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

@Raynos是正确的,传统上,您必须诉诸不太理想的解决方案才能实现该目标或进行自下而上的开发

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

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


5
我使用proxyquire,我不能说足够多的好话。它救了我!我的任务是为在appcelerator Titanium中开发的应用程序编写茉莉花节点测试,该测试迫使某些模块成为绝对路径和许多循环依赖项。proxyquire让我制止那些空白并嘲笑我不需要每次测试的过程。(在这里解释)。非常感谢!
Sukima

很高兴听到proxyquire可以帮助您正确测试代码:)
Thorsten Lorenz

1
非常好@ThorstenLorenz,我会的。正在使用proxyquire
bevacqua

太棒了!当我看到公认的答案:“你不能”时,我想到了“哦,上帝,认真吗?!” 但这确实保存了它。
Chadwick

3
对于那些使用Webpack的人,不要花时间研究proxyquire。它不支持Webpack。我正在研究inject-loader(github.com/plasticine/inject-loader)。
Artif3x

116

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

不管好坏,大多数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模拟。在他们的页面上跟进


1
不幸的是,不保证将node.js模块设置为单例,如下所述:justjs.com/posts/…–
FrontierPsycho

4
@FrontierPsycho几件事:首先,就测试而言,本文无关紧要。只要您正在测试自己的依赖项(而不是依赖项的依赖项),当您使用时require('some_module'),所有代码都将返回相同的对象,因为所有代码共享相同的node_modules目录。其次,本文将名称空间与单例(singleton)混合,这是正交的。第三,该文章已经很老了(就node.js而言),因此,当日可能有效的内容现在可能无效。
艾略特·福斯特

2
嗯 除非我们中的一个人真正地挖掘证明了一点或另一点的代码,否则我会采用您的依赖项注入解决方案,或者只是简单地传递对象,这将是更安全和未来的证明。
FrontierPsycho

1
我不确定您要证明什么。节点模块的单例(缓存)性质是众所周知的。依赖注入虽然是一条不错的路线,但可以增加很多样板代码和更多代码。DI在静态类型的语言中更为常见,在静态类型的语言中,将间谍/存根/模拟程序动态地插入到代码中比较困难。我在过去三年中完成的多个项目都使用上面我的答案中描述的方法。尽管我很少使用它,但它是所有方法中最简单的方法。
艾略特·福斯特

1
我建议您阅读sinon.js。如果您使用的是sinon(如上例所示),则可以使用innerLib.toCrazyCrap.restore()restub或调用sinon,通过sinon.stub(innerLib, 'toCrazyCrap')它可以更改存根的行为: innerLib.toCrazyCrap.returns(false)。另外,重新布线似乎与proxyquire上面的扩展非常相似。
艾略特·福斯特


2

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方法进行存根测试来测试您的代码。代码变得更冗长,但也更易于测试。


1
我认为您认为这并不容易...这是嘲笑的本质。模拟所需的依赖关系使事情变得如此简单,以至于无需更改代码结构就可以将控制权交给开发人员。您的方法过于冗长,因此难以推理。我为此选择了proxyrequire或模拟需求;我在这里看不到任何问题。代码干净整洁,易于推理,并且记住大多数阅读此代码的人已经编写了想要他们复杂化的代码。如果这些库是骇客的,那么根据您的定义,嘲笑和存根也是骇客的,应该停止。
伊曼纽尔·马哈尼

1
方法1的问题在于,您正在内部实现细节传递给堆栈。如果使用多层,那么成为模块的使用者就变得更加复杂。它可以与类似IOC容器的方法一起使用,以便为您自动注入依赖关系,但是由于我们已经通过imports语句将依赖关系注入到节点模块中,因此可以在该级别上对它们进行模拟是有意义的。
magritte

1)这只是将问题移到了另一个文件2)仍然加载了另一个模块,从而增加了性能开销,并可能导致副作用(例如流行的colors模块弄乱了String.prototype
ThomasR

2

如果您曾经使用过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,此处它们生成模块的“自动模拟”。

希望能有所帮助;)


1

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

您还必须假设任何第三方代码和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中,任何代码都可能冒着危险访问模块范围。


2
假定第三方代码已经过良好测试,这不是工作IMO的好方法。
henry.oswald 2012年

5
@beck是一种很好的工作方式。它迫使您只能使用高质量的第三方代码或编写代码的所有部分,以便对每个依赖项都进行良好的测试
Raynos

好的,我认为您指的是不在代码与第三方代码之间进行集成测试。同意
henry.oswald 2012年

1
“单元测试套件”只是单元测试的集合,但是单元测试应该彼此独立,因此单元测试中的单元。为了能够使用,单元测试应该是快速且独立的,以便您可以清楚地看到单元测试失败时代码在何处中断。
Andreas Berheim Brudin 2015年

这对我不起作用。模块对象不会公开“ var innerLib ...”等
。– AnitKryst 2015年

1

您可以使用嘲笑库:

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'

1

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

注意操作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非常棒,您应该使用它。它使您的需求覆盖仅局限于测试,我强烈建议您这样做。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.