监视用作构造函数的函数

Spying on a function used as a constructutor

在我的一个单元测试中,我需要监视一个函数,该函数被另一个具有 Sinon 库的函数用作构造函数。根据他们的文档

...sinon.spy(object, "method") creates a spy that wraps the existing function object.method. The spy will behave exactly like the original method (including when used as a constructor)... But so far I have failed to make it work even when trying to spy on a constructor called within the test function let alone called by another function.

单元测试:

   it('constructor was called.', () => {
        const Foo = require('../app/foo');
        const fooModule = module.children.find(m => m.id.includes('foo.js'));
        const fooSpy = sinon.spy(fooModule, 'exports');
        const f = new Foo(5);
        expect(fooSpy).calledOnce;
    }); 

要实例化的函数:

const Foo = function(param) {
    console.log('Foo called with: ' + param);
};

Foo.prototype.bar = function(x) {
    console.log(`Foo.prototype.bar() called with x: ` + x);
};

module.exports = Foo;

当我使用调试器时,我可以看到 const fooSpy = sinon.spy(fooModule, 'exports'); 处的函数被间谍替换(添加了所有 sinon 属性,如 calledOnce 等等...)然而,当 new Foo(5); 时,Foo 似乎不是间谍对象。

我认为这可能是范围界定或引用错误,但我似乎无法找到 module.children 之外的其他地方定义 Foo。它不在 global 上,也不在 window 上,因为它在 node 上 运行。

目前测试当然失败了:

Foo called with: 5

AssertionError: expected exports to have been called exactly once, but it was called 0 times
    at Context.it (test/fooTest.js:18:23)

在此先感谢您的帮助!

您的问题不在于 sinon.spy API,而在于 Node 模块导入函数的方式。在调用 sinon.spy 时,除非我们正在测试回调函数,否则我们通常需要一个 Object 作为我们想要监视特定方法的上下文。这就是您的示例尝试访问 foo.js 模块的 exports 对象的原因。我不知道 Node 允许我们访问这样的对象。

但是,为了使您的示例正常工作,我们不需要访问 Foo 模块的 exports,我们可以简单地创建一个我们自己的上下文。例如:

const chai = require("chai");
const sinon = require("sinon");
const sinonChai = require("sinon-chai");

const expect = chai.expect;

chai.use(sinonChai);

describe("foo", function () {
    it('constructor was called.', function () {
        const context = {
            Foo: require("../app/foo"),
        };

        const fooSpy = sinon.spy(context, "Foo");

        new context.Foo(5);

        expect(fooSpy).to.be.calledOnceWith(5);
    });
});

当然,上面的测试是针对您的示例中提供的问题的有效解决方案,但是,作为测试,它不是很有用,因为断言只是验证它上面的行。

间谍当它们是被测系统 (SUT) 的 依赖项 时更有用。换句话说,如果我们有一些应该构造 Foo 的模块,我们希望使 Foo 构造函数成为 Spy,以便它可以向我们的测试报告该模块确实调用了它。

例如,假设我们有 fooFactory.js 个模块:

const Foo = require("./foo");

module.exports = {
  createFoo(num) {
    return new Foo(num);
  },
};

现在我们要创建一个单元测试,确认调用 fooFactory.js 模块的 createFoo 函数会调用带有指定参数的 Foo 构造函数。我们需要用 Spy 覆盖 fooFactory.jsFoo 依赖。

这 returns 我们解决了我们最初的问题,即当导入的(构造函数)函数不是上下文对象上的方法时,我们如何将其变成间谍,因此我们不能用 [=26 覆盖它=].

幸运的是,我们不是第一个遇到这个问题的人。 NPM 模块允许覆盖所需模块中的依赖项。 Sinon.js 提供了一个 How-To on doing this sort of thing and they use a module called proxyquire.

proxyquire 将允许我们将 fooFactory.js 模块导入到我们的单元测试中,而且(更重要的是)覆盖它所依赖的 Foo。这将允许我们的单元测试使 fooFactory.js 使用 sinon.spy 代替 Foo 构造函数。

测试文件变为:

const chai = require("chai");
const proxyquire = require("proxyquire");
const sinon = require("sinon");
const sinonChai = require("sinon-chai");

const expect = chai.expect;

chai.use(sinonChai);

describe("fooFactory", function () {
    it("calls Foo constructor", function () {
        const fooSpy = sinon.spy();
        const { createFoo } = proxyquire("../app/fooFactory", {
            "./foo": fooSpy,
        });

        createFoo(5);

        expect(fooSpy).to.be.calledOnceWith(5);
    });
});