最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - stubbing an entire class for testing in sinon - Stack Overflow

programmeradmin0浏览0评论

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...

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

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

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.

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.

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?

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.

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...

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

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

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.

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.

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?

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.

Share Improve this question edited Aug 17, 2016 at 9:52 duncanhall 11.4k6 gold badges61 silver badges91 bronze badges asked Mar 2, 2016 at 2:03 fearless_foolfearless_fool 35.2k25 gold badges145 silver badges230 bronze badges 2
  • Looks like the case where addWidget(widget: WidgetInstance) is preferred over addWidget(name: String). Is there anything that prevents you from doing this? – Aleksei Zabrodskii Commented Aug 20, 2016 at 10:40
  • This seems a duplicate of stackoverflow.com/questions/12819242/… – Ricardo Stuven Commented Nov 28, 2016 at 13:59
Add a comment  | 

2 Answers 2

Reset to default 14 +50

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.

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

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

// 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:

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.

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.

Your addWidget method does 2 things:

  • "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:

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);
发布评论

评论列表(0)

  1. 暂无评论