如何对需要其他模块的node.js模块进行单元测试

时间:2011-04-21 16:30:56

标签: javascript node.js mocking

这是一个简单的例子,说明了我的问题的关键:

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

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

module.exports = underTest;

我正在尝试为此代码编写单元测试。如何在不完全嘲笑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;
    }
};

问题是underTest.js文件中的require函数实际上没有被模拟掉。它仍然指向全局require函数。所以我似乎只能在我正在进行模拟的同一个文件中模拟require函数。如果我使用全局require来包含任何内容,即使在我重写了本地副本之后,所需的文件仍将具有全局require引用。

8 个答案:

答案 0 :(得分:167)

你现在可以!

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

这意味着您需要不对代码进行任何更改,以便为所需模块注入模拟。

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

@Raynos是对的,传统上你不得不诉诸不是非常理想的解决方案来实现这一目标或者做自下而上的开发

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

查看文档和示例,以评估它是否符合您的需求。

答案 1 :(得分:108)

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

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

您可以利用此功能并使用类似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', 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对于断言有很好的integration with chai,我给integrate sinon with mocha写了一个模块,以便更容易地进行间谍/存根清理(以避免测试污染。)

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

答案 2 :(得分:10)

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

答案 3 :(得分: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方法来测试您的代码。 代码变得更加冗长,但也更容易测试。

答案 4 :(得分: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,并且任何代码都可以自行访问模块化范围。

答案 5 :(得分:1)

您可以使用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'

答案 6 :(得分: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非常棒,您应该使用它。它使您的要求覆盖仅局限于测试,我强烈建议您这样做。

答案 7 :(得分:0)

如果您曾经玩过玩笑,那么您可能对玩笑的模拟功能很熟悉。

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

例如

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 not always called with Mocha

为解决这个问题,我创建了两个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,此处它们生成模块的“自动模拟”。

希望有帮助;)