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

IT技术 javascript node.js mocking
2021-03-14 17:02:26

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

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参考。

6个回答

您现在可以!

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

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

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

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

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

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

这只是一个了不起的module!让我想起了 .NET 的 Moq 库。
2021-04-26 17:02:26
我使用proxyquire,但我说的不够好。它救了我!我的任务是为在 appcelerator Titanium 中开发的应用程序编写 jasmine 节点测试,该应用程序强制某些module成为绝对路径和许多循环依赖项。proxyquire 让我停止间隙,并模拟出每次测试不需要的杂物。(解释here)。非常感谢!
2021-05-04 17:02:26
非常好的@ThorstenLorenz,我会确定。正在使用proxyquire
2021-05-07 17:02:26
对于那些使用 Webpack 的人,不要花时间研究 proxyquire。它不支持 Webpack。我正在研究注入加载器(github.com/plasticine/inject-loader)。
2021-05-15 17:02:26

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

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

您可以利用它并使用类似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进行了很好的集成以进行断言,我编写了一个module将 sinon 与 mocha 集成在一起,以便更轻松地清理 spy/stub(以避免测试污染)。

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

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

嗯。除非我们中的一个人真的挖掘出证明某一点或另一点的代码,否则我会采用你的依赖注入解决方案,或者只是简单地传递对象,它更安全,更有未来证明。
2021-04-26 17:02:26
我不确定你要证明什么。节点module的单例(缓存)特性是众所周知的。依赖注入虽然是一个很好的方法,但可以是更多样板和更多代码。DI 在静态类型语言中更为常见,在这种语言中,很难动态地将间谍/存根/模拟插入代码中。我在过去三年中完成的多个项目都使用我上面回答中描述的方法。这是所有方法中最简单的方法,尽管我很少使用它。
2021-04-29 17:02:26
我建议你阅读 sinon.js。如果您正在使用 sinon(如上例所示),您可以使用innerLib.toCrazyCrap.restore()并重新存根,或调用 sinon 通过sinon.stub(innerLib, 'toCrazyCrap')它允许您更改存根的行为方式: innerLib.toCrazyCrap.returns(false). 此外,重新布线似乎与proxyquire上面扩展非常相似
2021-05-04 17:02:26
不幸的是,node.js module不能保证是单例的,正如这里所解释的:justjs.com/posts/...
2021-05-09 17:02:26
@FrontierPsycho 有几点:首先,就测试而言,这篇文章无关紧要。只要您正在测试您的依赖项(而不是依赖项的依赖项),您的所有代码都会在您返回相同的对象时返回相同的对象require('some_module'),因为您的所有代码都共享相同的 node_modules 目录。其次,这篇文章将命名空间与单例混为一谈,这有点正交。第三,那篇文章已经很老了(就 node.js 而言),所以当时可能有效的内容现在可能无效。
2021-05-09 17:02:26

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

立即执行 stop(<file>) 或 stopAll() 也很好,这样您就不会在不需要模拟的测试中获得缓存文件。
2021-04-28 17:02:26

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

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

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

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

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

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

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

class underTestClass {
    getInnerLib () {
        return innerLib
    }

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

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

方法 1 的问题在于您将内部实现细节向上传递。有了多个层,成为module的使用者就会变得更加复杂。它可以与类似 IOC 容器的方法一起使用,以便为您自动注入依赖项,但是感觉就像因为我们已经通过导入语句在节点module中注入了依赖项,因此能够在该级别模拟它们是有意义的.
2021-04-29 17:02:26
我不认为它像你认为的那样 hacky ......这是嘲弄的本质。模拟所需的依赖项使事情变得如此简单,它可以在不更改代码结构的情况下为开发人员提供控制权。您的方法过于冗长,因此难以推理。我选择了 proxyrequire 或 mock-require ;我在这里没有看到任何问题。代码干净且易于推理,并记住大多数阅读本文的人已经编写了您希望他们复杂化的代码。如果这些库是骇人听闻的,那么根据您的定义,嘲笑和存根也是骇人听闻的,应该停止。
2021-05-18 17:02:26
1) 这只是将问题移到另一个文件 2) 仍然加载另一个module,从而增加了性能开销,并可能导致副作用(比如流行的colorsmodule,它会混淆String.prototype
2021-05-19 17:02:26

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

注意您操作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 非常棒,你应该使用它。它使您的需求覆盖仅本地化为测试,我强烈推荐它。