存根 es6 class 函数依赖

Stubbing an es6 class function dependancy

假设我有一个控制器来验证一些输入,并使用一个服务 (es6 class) 来执行部分业务逻辑。

Controller.js(对象函数)

const Service = require('./Service');

module.exports = {
  get: id => {
    //validates id
    return new Service().get(id);
  }
}

Service.js(ES6class函数)

class Service {
  constructor() {...}
  get(id) { return 'actual function'; }
}

module.exports = Service;

我希望在服务中存入 get 函数来测试 Controller。

Controller_test.js

const Controller = require('../src/Controller.js);
const Service = require('../src/Service.js);

describe('Controller', () => {
  let sandbox;

  beforeEach(() => {
    sandbox = sinon.sandbox.create();
  });
  afterEach(() => {
    sandbox.restore();
  });

  it('should use fake get function', done => {
    sandbox.stub(Service.protoype, 'get').callsFake(() => {
      return 'Fake has been called');
    });

    const result = Controller.get(id);
    expect(reuslt).to.equal('Fake has been called'); //returns 'actual function'
  });
});

所以,重申一下。我无法用 sinon 存根对象函数中使用的 class 函数。如果不需要,我不想提出额外的论点。

我发现的可能解决方案:从控制器导出服务并通过控制器存根。

控制器

const Service = require('./Service');

const Controller = {
  get: id => {
    //validates id
    return new Service().get(id);
  }
}

module.exports.default = Controller;
module.exports.Service = Service;

测试

const Controller = require('../src/Controller.js');

describe('Controller', () => {
  let sandbox;

  beforeEach(() => {
    sandbox = sinon.sandbox.create();
  });
  afterEach(() => {
    sandbox.restore();
  });

  it('should use fake get function', done => {
    sandbox.stub(Controller.Service.protoype, 'get').callsFake(() => {
      return 'Fake has been called');
    });

    const result = Controller.get(id);
    expect(reuslt).to.equal('Fake has been called'); //returns 'actual function'
  });
});

这感觉很脏!如果有更好的解决方案,我们将不胜感激。

你的测试代码是正确的

首先:你的测试代码没有问题。有效!

我看了看,认为它看起来是正确的,所以我重新创建了它(删除了拼写和语法错误)here。测试顺利通过。这意味着您正在做的事情与您假设的不同。

在那个 repo 中,我还添加了支持代码来演示下面提到的各种方法。

提出了三个问题:

  1. 控制Controller依赖的好方法是什么?
  2. 如何测试导出的单例?
  3. 你应该如何模拟测试?

您会在有关 #1 的 Sinon 问题跟踪器上找到一些很好的讨论,其中 this 可能会引起您的兴趣。

如您所知Java,您可能知道这两个阵营是控制反转(依赖注入(传递依赖)和 IOC Containers(给我一些类型 'foo')——在 JS 中用得不多,除了 Angular)。如果您熟悉 Michael Feathers,您还有 link seams,它在较低级别工作以替换模块加载程序提供的代码(require 调用)。

后者可以像这样替换 Service 实现:

// myServiceStub is at this point set up by you using Sinon
// use `sandbox.resetHistory()` to reset the stub between tests
const Controller = proxyquire('../src/Controller', { './Service' : myServiceStub });
// test that the stub is invoked as expected

在 JS 中,你 "need" 像 proxyquirerewire 这样的支持库来使用 link seams,所以它是您很自然地使用 setter 诉诸手动依赖注入。虽然使用 link 接缝可以让您避免更改生产代码,但它也有一个缺点,即假设您对模块依赖关系非常了解。 DI至少让你停留在界面层面。

您在自己的回答中提到的 "dirty feeling" 可能源于两件事:

  • 您正在更改 模块(全局状态),而不是 Service
  • 的任何创建的 实例
  • 您正在公开有关模块直接依赖关系的知识,这些知识在测试中使用直接关系是不可能看到的(linter 可能会抱怨 Service 未被使用)。

依赖注入主要有两种形式:构造函数注入(我在这个定义中包括工厂方法)或setter注入。您可以同时看到 here.

构造函数注入在这里效果不佳,因为您正在使用在模块定义时创建的 Singleton 对象。使用 setter (export __setService(MyStubClass){ Service = MyStubClass; }) 替换 Service.js 模块本身不会改变您正在更改 模块 的事实,这可能会影响测试 运行 如果您没有正确清理(例如添加 __reset 方法),请在此之后。这基本上就是您在自己提供的答案中所做的。

您可以在 link-seamsdependency-injection 分支的 mentioned repository 中找到两种方式的工作示例 运行。

就我个人而言,我不认为使用单元测试来测试 Controller 本身有多大价值,我尽量避免单例模块(在任何语言中)。我将测试 Service 实现并进行集成测试(在 HTTP 层上)并用假的服务替换正在使用的服务以控制响应。实际的 Service 似乎更有趣。