存根整个 class 用于在 sinon 中进行测试
stubbing an entire class for testing in sinon
序言:我已经阅读了很多 SO 和博客文章,但还没有看到任何可以回答这个特定问题的内容。也许我只是在寻找错误的东西...
假设我正在开发一个将在 Widget
对象上运行的 WidgetManager
class。
如何使用 sinon 测试 WidgetManager 是否正确使用 Widget
API 而无需引入整个 Widget
库?
基本原理:WidgetManager 的测试应该与 Widget 分离 class。可能我还没有写Widget,也可能Widget是一个外部库。无论哪种方式,我都应该能够测试 WidgetManager 是否正确使用 Widget 的 API 而无需创建真正的 Widgets。
我知道 sinon 模拟只能在现有的 classes 上工作,据我所知,sinon 存根也需要 class 存在才能被存根。
为了使其具体化,我将如何测试 Widget.create()
在以下代码中使用单个参数 'name' 被恰好调用一次?
正在测试的代码
// file: widget-manager.js
function WidgetManager() {
this.widgets = []
}
WidgetManager.prototype.addWidget = function(name) {
this.widgets.push(Widget.create(name));
}
测试代码
// file: widget-manager-test.js
var WidgetManager = require('../lib/widget-manager.js')
var sinon = require('sinon');
describe('WidgetManager', function() {
describe('#addWidget', function() {
it('should call Widget.create with the correct name', function() {
var widget_manager = new WidgetManager();
// what goes here?
});
it('should push one widget onto the widgets list', function() {
var widget_manager = new WidgetManager();
// what setup goes here?
widget_manager.addWidget('fred');
expect(widget_manager.widgets.length).to.equal(1);
});
});
旁白:当然,我可以定义一个MockWidget
class来用适当的方法进行测试,但我更感兴趣的是真正学习如何使用sinon的spy / stub / mock 设施正确。
答案实际上是关于依赖注入的。
您想测试 WidgetManager
是否以预期的方式与 依赖关系 (Widget
) 交互 - 并且您希望自由地操纵和询问那种依赖。为此,您需要在测试时注入 Widget
的存根版本。
根据 WidgetManager
的创建方式,依赖注入有多种选择。
一个简单的方法是允许将 Widget
依赖项注入到 WidgetManager
构造函数中:
// file: widget-manager.js
function WidgetManager(Widget) {
this.Widget = Widget;
this.widgets = [];
}
WidgetManager.prototype.addWidget = function(name) {
this.widgets.push(this.Widget.create(name));
}
然后在您的测试中,您只需将存根 Widget
传递给被测 WidgetManager
:
it('should call Widget.create with the correct name', function() {
var stubbedWidget = {
create: sinon.stub()
}
var widget_manager = new WidgetManager(stubbedWidget);
widget_manager.addWidget('fred');
expect(stubbedWidget.create.calledOnce);
expect(stubbedWidget.create.args[0] === 'fred');
});
您可以根据特定测试的需要修改存根的行为。例如,要测试小部件列表长度在小部件创建后增加,您可以简单地 return 来自存根 create()
方法的对象:
var stubbedWidget = {
create: sinon.stub().returns({})
}
这使您可以完全控制依赖关系,而不必模拟或存根所有方法,并允许您测试与其 API 的交互。
还有像 proxyquire or rewire 这样的选项,它们提供了更强大的选项来在测试时覆盖依赖项。最合适的选项取决于实现和偏好 - 但在所有情况下,您的目标只是在测试时替换给定的依赖项。
你的 addWidget
方法做了两件事:
- "converts" 字符串到
Widget
实例;
- 将该实例添加到内部存储。
我建议您更改 addWidget
签名以直接接受实例,而不是名称,然后将创建移到其他地方。将使测试更容易:
Manager.prototype.addWidget = function (widget) {
this.widgets.push(widget);
}
// no stubs needed for testing:
const manager = new Manager();
const widget = {};
manager.addWidget(widget);
assert.deepStrictEquals(manager.widgets, [widget]);
之后,您将需要一种按名称创建小部件的方法,这也应该非常容易测试:
// Maybe this belongs to other place, not necessarily Manager class…
Manager.createWidget = function (name) {
return new Widget(name);
}
assert(Manager.createWidget('calendar') instanceof Widget);
序言:我已经阅读了很多 SO 和博客文章,但还没有看到任何可以回答这个特定问题的内容。也许我只是在寻找错误的东西...
假设我正在开发一个将在 Widget
对象上运行的 WidgetManager
class。
如何使用 sinon 测试 WidgetManager 是否正确使用 Widget
API 而无需引入整个 Widget
库?
基本原理:WidgetManager 的测试应该与 Widget 分离 class。可能我还没有写Widget,也可能Widget是一个外部库。无论哪种方式,我都应该能够测试 WidgetManager 是否正确使用 Widget 的 API 而无需创建真正的 Widgets。
我知道 sinon 模拟只能在现有的 classes 上工作,据我所知,sinon 存根也需要 class 存在才能被存根。
为了使其具体化,我将如何测试 Widget.create()
在以下代码中使用单个参数 'name' 被恰好调用一次?
正在测试的代码
// file: widget-manager.js
function WidgetManager() {
this.widgets = []
}
WidgetManager.prototype.addWidget = function(name) {
this.widgets.push(Widget.create(name));
}
测试代码
// file: widget-manager-test.js
var WidgetManager = require('../lib/widget-manager.js')
var sinon = require('sinon');
describe('WidgetManager', function() {
describe('#addWidget', function() {
it('should call Widget.create with the correct name', function() {
var widget_manager = new WidgetManager();
// what goes here?
});
it('should push one widget onto the widgets list', function() {
var widget_manager = new WidgetManager();
// what setup goes here?
widget_manager.addWidget('fred');
expect(widget_manager.widgets.length).to.equal(1);
});
});
旁白:当然,我可以定义一个MockWidget
class来用适当的方法进行测试,但我更感兴趣的是真正学习如何使用sinon的spy / stub / mock 设施正确。
答案实际上是关于依赖注入的。
您想测试 WidgetManager
是否以预期的方式与 依赖关系 (Widget
) 交互 - 并且您希望自由地操纵和询问那种依赖。为此,您需要在测试时注入 Widget
的存根版本。
根据 WidgetManager
的创建方式,依赖注入有多种选择。
一个简单的方法是允许将 Widget
依赖项注入到 WidgetManager
构造函数中:
// file: widget-manager.js
function WidgetManager(Widget) {
this.Widget = Widget;
this.widgets = [];
}
WidgetManager.prototype.addWidget = function(name) {
this.widgets.push(this.Widget.create(name));
}
然后在您的测试中,您只需将存根 Widget
传递给被测 WidgetManager
:
it('should call Widget.create with the correct name', function() {
var stubbedWidget = {
create: sinon.stub()
}
var widget_manager = new WidgetManager(stubbedWidget);
widget_manager.addWidget('fred');
expect(stubbedWidget.create.calledOnce);
expect(stubbedWidget.create.args[0] === 'fred');
});
您可以根据特定测试的需要修改存根的行为。例如,要测试小部件列表长度在小部件创建后增加,您可以简单地 return 来自存根 create()
方法的对象:
var stubbedWidget = {
create: sinon.stub().returns({})
}
这使您可以完全控制依赖关系,而不必模拟或存根所有方法,并允许您测试与其 API 的交互。
还有像 proxyquire or rewire 这样的选项,它们提供了更强大的选项来在测试时覆盖依赖项。最合适的选项取决于实现和偏好 - 但在所有情况下,您的目标只是在测试时替换给定的依赖项。
你的 addWidget
方法做了两件事:
- "converts" 字符串到
Widget
实例; - 将该实例添加到内部存储。
我建议您更改 addWidget
签名以直接接受实例,而不是名称,然后将创建移到其他地方。将使测试更容易:
Manager.prototype.addWidget = function (widget) {
this.widgets.push(widget);
}
// no stubs needed for testing:
const manager = new Manager();
const widget = {};
manager.addWidget(widget);
assert.deepStrictEquals(manager.widgets, [widget]);
之后,您将需要一种按名称创建小部件的方法,这也应该非常容易测试:
// Maybe this belongs to other place, not necessarily Manager class…
Manager.createWidget = function (name) {
return new Widget(name);
}
assert(Manager.createWidget('calendar') instanceof Widget);