【问题标题】:Spying on a function used as a constructutor监视用作构造函数的函数
【发布时间】:2020-05-07 11:15:03
【问题描述】:

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

...sinon.spy(object, "method") 创建一个封装现有函数 object.method 的 spy。间谍的行为将与原始方法完全相同(包括用作构造函数时)... 但是到目前为止,即使尝试监视在测试函数中调用的构造函数,我也未能使其工作,更不用说由另一个函数调用了。

单元测试:

   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)

提前感谢您的帮助!

【问题讨论】:

  • 我认为你做不到。需要/导入的模块与您正在测试的代码处于不同的级别。当您的测试代码运行时,这些模块已经加载。您对module.exports 的访问权限与对当前执行上下文中引用的对象(例如Foof 的实例)的访问权限不同。

标签: javascript node.js unit-testing sinon


【解决方案1】:

您的问题不在于 sinon.spy API,而在于 Node 模块如何导入函数。在调用sinon.spy 时,除非我们正在测试回调函数,否则我们通常需要一个对象作为我们想要监视特定方法的上下文。这就是您的示例尝试访问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 依赖项。

这让我们回到了我们最初的问题,即当导入的(构造函数)函数不是上下文对象上的方法时,我们如何将其转换为间谍,因此我们不能用sinon.spy(context, 'method') 覆盖它。

幸运的是,我们不是第一个遇到这个问题的人。存在允许覆盖所需模块中的依赖关系的 NPM 模块。 Sinon.js 提供了一个 How-To 来做这类事情,他们使用一个名为 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);
    });
});

【讨论】:

  • 非常感谢! proxyquire 正是我所需要的。
猜你喜欢
  • 2012-03-09
  • 1970-01-01
  • 1970-01-01
  • 2012-01-09
  • 2013-01-25
  • 1970-01-01
  • 1970-01-01
  • 2018-06-21
  • 1970-01-01
相关资源
最近更新 更多