存根 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 中,我还添加了支持代码来演示下面提到的各种方法。
提出了三个问题:
- 控制
Controller
依赖的好方法是什么?
- 如何测试导出的单例?
- 你应该如何模拟测试?
您会在有关 #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" 像 proxyquire
或 rewire
这样的支持库来使用 link seams,所以它是您很自然地使用 setter 诉诸手动依赖注入。虽然使用 link 接缝可以让您避免更改生产代码,但它也有一个缺点,即假设您对模块依赖关系非常了解。 DI至少让你停留在界面层面。
您在自己的回答中提到的 "dirty feeling" 可能源于两件事:
- 您正在更改 模块(全局状态),而不是
Service
的任何创建的 实例
- 您正在公开有关模块直接依赖关系的知识,这些知识在测试中使用直接关系是不可能看到的(linter 可能会抱怨
Service
未被使用)。
依赖注入主要有两种形式:构造函数注入(我在这个定义中包括工厂方法)或setter注入。您可以同时看到 here.
构造函数注入在这里效果不佳,因为您正在使用在模块定义时创建的 Singleton 对象。使用 setter (export __setService(MyStubClass){ Service = MyStubClass; }
) 替换 Service.js
模块本身不会改变您正在更改 模块 的事实,这可能会影响测试 运行 如果您没有正确清理(例如添加 __reset
方法),请在此之后。这基本上就是您在自己提供的答案中所做的。
您可以在 link-seams
和 dependency-injection
分支的 mentioned repository 中找到两种方式的工作示例 运行。
就我个人而言,我不认为使用单元测试来测试 Controller
本身有多大价值,我尽量避免单例模块(在任何语言中)。我将测试 Service
实现并进行集成测试(在 HTTP 层上)并用假的服务替换正在使用的服务以控制响应。实际的 Service
似乎更有趣。
假设我有一个控制器来验证一些输入,并使用一个服务 (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 中,我还添加了支持代码来演示下面提到的各种方法。
提出了三个问题:
- 控制
Controller
依赖的好方法是什么? - 如何测试导出的单例?
- 你应该如何模拟测试?
您会在有关 #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" 像 proxyquire
或 rewire
这样的支持库来使用 link seams,所以它是您很自然地使用 setter 诉诸手动依赖注入。虽然使用 link 接缝可以让您避免更改生产代码,但它也有一个缺点,即假设您对模块依赖关系非常了解。 DI至少让你停留在界面层面。
您在自己的回答中提到的 "dirty feeling" 可能源于两件事:
- 您正在更改 模块(全局状态),而不是
Service
的任何创建的 实例
- 您正在公开有关模块直接依赖关系的知识,这些知识在测试中使用直接关系是不可能看到的(linter 可能会抱怨
Service
未被使用)。
依赖注入主要有两种形式:构造函数注入(我在这个定义中包括工厂方法)或setter注入。您可以同时看到 here.
构造函数注入在这里效果不佳,因为您正在使用在模块定义时创建的 Singleton 对象。使用 setter (export __setService(MyStubClass){ Service = MyStubClass; }
) 替换 Service.js
模块本身不会改变您正在更改 模块 的事实,这可能会影响测试 运行 如果您没有正确清理(例如添加 __reset
方法),请在此之后。这基本上就是您在自己提供的答案中所做的。
您可以在 link-seams
和 dependency-injection
分支的 mentioned repository 中找到两种方式的工作示例 运行。
就我个人而言,我不认为使用单元测试来测试 Controller
本身有多大价值,我尽量避免单例模块(在任何语言中)。我将测试 Service
实现并进行集成测试(在 HTTP 层上)并用假的服务替换正在使用的服务以控制响应。实际的 Service
似乎更有趣。