Saturday, May 11, 2024
 Popular · Latest · Hot · Upcoming
103
rated 0 times [  109] [ 6]  / answers: 1 / hits: 5439  / 8 Years ago, wed, march 2, 2016, 12:00:00

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.


More From » node.js

 Answers
19

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.


[#30433] Monday, February 29, 2016, 8 Years  [reply] [flag answer]
Only authorized users can answer the question. Please sign in first, or register a free account.
kurtisl

Total Points: 559
Total Questions: 110
Total Answers: 97

Location: Tokelau
Member since Sun, May 7, 2023
1 Year ago
;