为sinon测试而中断整个类

时间:2022-09-18 17:09:39

Preamble: I've read lots of of SO and blog posts, but haven't seen anything that answers this particular question. Maybe I'm just looking for the wrong thing...

序言:我读过很多SO和博客文章,但是没有看到任何能回答这个问题的东西。也许我只是在寻找错误的东西……

Suppose I'm developing a WidgetManager class that will operate on Widget objects.

假设我正在开发一个WidgetManager类,它将对小部件对象进行操作。

How do I use sinon to test that WidgetManager is using the Widget API correctly without pulling in the whole Widget library?

我如何使用sinon来测试WidgetManager正确地使用小部件API,而不引入整个小部件库?

Rationale: The tests for a WidgetManager should be decoupled from the Widget class. Perhaps I haven't written Widget yet, or perhaps Widget is an external library. Either way, I should be able to test that WidgetManager is using Widget's API correctly without creating real Widgets.

基本原理:WidgetManager的测试应该与小部件类分离。也许我还没有写小部件,或者小部件是一个外部库。无论哪种方式,我都应该能够测试WidgetManager正确地使用小部件的API,而无需创建真正的小部件。

I know that sinon mocks can only work on existing classes, and as far as I can tell, sinon stubs also need the class to exist before it can be stubbed.

我知道sinon mock只能在现有的类上工作,而且据我所知,sinon stub也需要类存在才能被存根化。

To make it concrete, how would I test that Widget.create() is getting called exactly once with a single argument 'name' in the following code?

为了使它具体化,我要如何测试这个Widget.create()在下面的代码中只调用了一个参数“name”?

code under test

// file: widget-manager.js

function WidgetManager() {
   this.widgets = []
}

WidgetManager.prototype.addWidget = function(name) {
    this.widgets.push(Widget.create(name));
}

testing code

// 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);
  });
});

Aside: Of course, I could define a MockWidget class for testing with the appropriate methods, but I'm more interested in really learning how to use sinon's spy / stub / mock facilities correctly.

旁白:当然,我可以定义一个MockWidget类来使用适当的方法进行测试,但是我更感兴趣的是如何正确地使用sinon的spy / stub / mock工具。

2 个解决方案

#1


9  

The answer is really about dependency injection.

答案就是依赖注入。

You want to test that WidgetManager is interacting with a dependency (Widget) in the expected way - and you want freedom to manipulate and interrogate that dependency. To do this, you need to inject a stub version of Widget at testing time.

您希望测试WidgetManager以期望的方式与依赖项(小部件)交互,并且您希望能够*地操作和查询依赖关系。为此,您需要在测试时注入小部件的存根版本。

Depending on how WidgetManager is created, there are several options for dependency injection.

依赖于WidgetManager的创建方式,依赖注入有几个选项。

A simple method is to allow the Widget dependency to be injected into the WidgetManager constructor:

一个简单的方法是将小部件依赖项注入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));
}

And then in your test you simply pass a stubbed Widget to the WidgetManager under test:

然后在测试中,您只需将存根化的小部件传递给被测试的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');
});

You can modify the behaviour of your stub depending on the needs of a particular test. For example, to test that the widget list length increments after widget creation, you can simply return an object from your stubbed create() method:

您可以根据特定测试的需要修改存根的行为。例如,要测试小部件创建后小部件列表长度的增量,只需从存根创建()方法返回一个对象:

  var stubbedWidget = {
      create: sinon.stub().returns({})
  }

This allows you to have full control over the dependency, without having to mock or stub all methods, and lets you test the interaction with its API.

这允许您完全控制依赖项,而不必模拟或存根所有方法,并允许您测试与它的API的交互。

There are also options like proxyquire or rewire which give more powerful options for overriding dependencies at test time. The most suitable option is down to implementation and preference - but in all cases you are simply aiming to replace a given dependency at testing time.

还有一些选项,如proxyquire或rewire,可以在测试时为重写依赖项提供更强大的选项。最合适的选项是实现和首选项——但在所有情况下,您只是想在测试时替换给定的依赖项。

#2


3  

Your addWidget method does 2 things:

你的addWidget方法有两件事:

  • "converts" a string to a Widget instance;
  • 将字符串“转换”为小部件实例;
  • adds that instance to internal storage.
  • 将该实例添加到内部存储。

I suggest you change addWidget signature to accept instance directly, instead of a name, and move out creation some other place. Will make testing easier:

我建议您更改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]);

After that, you'll need a way of creating widgets by name, which should be pretty straight-forward to test as well:

在此之后,您将需要一种按名称创建小部件的方法,这也应该非常直接地进行测试:

// Maybe this belongs to other place, not necessarily Manager class…
Manager.createWidget = function (name) {
  return new Widget(name);
}

assert(Manager.createWidget('calendar') instanceof Widget);

#1


9  

The answer is really about dependency injection.

答案就是依赖注入。

You want to test that WidgetManager is interacting with a dependency (Widget) in the expected way - and you want freedom to manipulate and interrogate that dependency. To do this, you need to inject a stub version of Widget at testing time.

您希望测试WidgetManager以期望的方式与依赖项(小部件)交互,并且您希望能够*地操作和查询依赖关系。为此,您需要在测试时注入小部件的存根版本。

Depending on how WidgetManager is created, there are several options for dependency injection.

依赖于WidgetManager的创建方式,依赖注入有几个选项。

A simple method is to allow the Widget dependency to be injected into the WidgetManager constructor:

一个简单的方法是将小部件依赖项注入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));
}

And then in your test you simply pass a stubbed Widget to the WidgetManager under test:

然后在测试中,您只需将存根化的小部件传递给被测试的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');
});

You can modify the behaviour of your stub depending on the needs of a particular test. For example, to test that the widget list length increments after widget creation, you can simply return an object from your stubbed create() method:

您可以根据特定测试的需要修改存根的行为。例如,要测试小部件创建后小部件列表长度的增量,只需从存根创建()方法返回一个对象:

  var stubbedWidget = {
      create: sinon.stub().returns({})
  }

This allows you to have full control over the dependency, without having to mock or stub all methods, and lets you test the interaction with its API.

这允许您完全控制依赖项,而不必模拟或存根所有方法,并允许您测试与它的API的交互。

There are also options like proxyquire or rewire which give more powerful options for overriding dependencies at test time. The most suitable option is down to implementation and preference - but in all cases you are simply aiming to replace a given dependency at testing time.

还有一些选项,如proxyquire或rewire,可以在测试时为重写依赖项提供更强大的选项。最合适的选项是实现和首选项——但在所有情况下,您只是想在测试时替换给定的依赖项。

#2


3  

Your addWidget method does 2 things:

你的addWidget方法有两件事:

  • "converts" a string to a Widget instance;
  • 将字符串“转换”为小部件实例;
  • adds that instance to internal storage.
  • 将该实例添加到内部存储。

I suggest you change addWidget signature to accept instance directly, instead of a name, and move out creation some other place. Will make testing easier:

我建议您更改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]);

After that, you'll need a way of creating widgets by name, which should be pretty straight-forward to test as well:

在此之后,您将需要一种按名称创建小部件的方法,这也应该非常直接地进行测试:

// Maybe this belongs to other place, not necessarily Manager class…
Manager.createWidget = function (name) {
  return new Widget(name);
}

assert(Manager.createWidget('calendar') instanceof Widget);