在节点中模拟构造函数

Mocking constructor functions in node

其他使用 sinon 的节点开发人员如何在他们的单元测试中模拟构造函数调用?例如,假设我有一些函数 foo

function foo() {
  var dependency = new Dependency(args);
  // do stuff with dependency
}
exports.module.foo = foo;

并且在一个单独的测试文件中,我有一些测试,我想在其中验证调用依赖构造函数的内容(args),我需要控制它 returns

it('should call Dependency constructor with bar', function() {
  var foo = require('myModule').foo
  var DependencyMock; //code to make the mock

  foo();
  expect(DependencyMock.calledWith(bar)).to.equal(true);
});

问题在于 sinon 只能模拟附加到对象的函数,因此我们必须先将构造函数附加到对象才能模拟它。

我一直在做的只是在调用构造函数的模块中创建一个对象来附加构造函数,调用构造函数作为该对象的方法,然后导出对象以在测试中使用它:

var Dependency = require('path/to/dependency');

var namespace = {
  Dependency: Dependency
}

function foo() {
  var dependency = new namespace.Dependency(args);
  // do stuff with dependency
}
exports.moduole.foo = foo;
exports.module.namespace = namespace;

测试文件:

it('should call Dependency constructor with bar', function() {
  var foo = require('myModule').foo;
  var namespace = require('myModule').namespace;

  var DependencyMock = sinon.mock(namespace, 'Dependency').returns(0);
  foo();
  expect(DependencyMock.calledWith(bar)).to.equal(true);
});

这行得通,但仅仅为了测试而在我的模块上公开一个对象感觉真的很笨拙。

有什么建议吗?

没试过,但这可行:存根 Dependency 的构造函数并让它 return 模拟。

var constructor = sinon.stub(Dependency.prototype, "constructor");
constructor.returns(sinon.mock(Dependency));

我认为值得一问的是为什么要模拟依赖项的构造函数而不是注入该依赖项

考虑您的示例代码:

// in "foo.js"
function foo() {
  var dependency = new Dependency(args);
  // do stuff with dependency
}
exports.module.foo = foo;

如果 foo 需要 Dependency 才能工作,您可以将其作为 foo:

的参数注入
// in "foo.js"
function foo(dependency) {
  // do stuff with dependency
}
exports.module.foo = foo;

// in "bar.js"
var foo = require('./foo.js')(new Dependency(args));

有了这个改变,在你的测试中注入任何测试替身现在变得微不足道了(要了解更多关于 JavaScript 测试替身的信息,请查看我的 article on the subject)。

这种方法使您的 function/module 的依赖关系显式化,但需要您在某些时候将它们连接起来(此处:require('./foo.js')(new Dependency(args));)。


如果您不想手动连接,还有另一种方法可以使用 rewire and replacing constructor with factory method:

// in "dependency.js"
module.exports= function(args) {
  return new Dependency(args);
}

// in "foo.js"
var dependency = require('./dependency');

function foo() {
  var dep = dependency(args);
  // do stuff with dependency
}
exports.module.foo = foo;

在你的测试中:

var rewire = require("rewire"),
    foo    = rewire("../lib/foo.js");

it('should call dependency... ', function() {
   foo.__set__("dependency", /* some spy */ );

   foo();
});

希望对您有所帮助!

一月

我在公开函数而不是对象的模块中遇到了类似的问题 - this was my solution。只是把它放在这里作为解决问题的另一种方法,但我必须说,正如 Jan Molak 所建议的那样,通过更好的设计来解决问题似乎是一种更好的方法。

我为此使用了一个解决方法:

// Keep a reference to your dependancy class.
const dependencyClass = Dependency;
let item = new Dependency();
// Replace class with a stub.(optionally return an item.
Dependency = sinon.stub(Dependency, 'constructor').returns(item);

// Call your code that you expect the class constructor to be called.
foo();

assert.isTrue(Dependency.calledWithNew());
assert.equal(1, Dependency.callCount);
// Restore class reference.
Dependency = dependencyClass;

此外,在上述情况下,返回了一个项目,因此用户可以访问依赖项以进行进一步测试。 例如

assert.equal(item.someValue, 10);

使用它可能会遇到其他问题,例如定义的属性将不再可用于 class。 我同意 Jan Molek 的观点,如果您无法更改代码,请使用它。