Завершение всего класса для тестирования в силоне
Преамбула: я читал много сообщений SO и в блогах, но ничего не видел, чтобы ответить на этот конкретный вопрос. Может быть, я просто искал неправильную вещь...
Предположим, что я разрабатываю класс WidgetManager
, который будет работать с объектами Widget
.
Как использовать sinon для проверки того, что WidgetManager правильно использует API Widget
, не втягивая всю библиотеку Widget
?
Обоснование: тесты для WidgetManager должны быть отделены от класса Widget. Возможно, я еще не написал Widget, или, возможно, Widget - это внешняя библиотека. В любом случае, я должен проверить, что WidgetManager правильно использует Widget API без создания реальных виджетов.
Я знаю, что синонные издевки могут работать только на существующих классах, и, насколько я могу судить, синусные заглушки также нуждаются в существовании класса, прежде чем его можно будет опустить.
Чтобы сделать его конкретным, как бы я мог проверить, что Widget.create()
получает точно один раз с единственным аргументом "имя" в следующем коде?
проверенный код
// 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
для тестирования с помощью соответствующих методов, но мне больше интересно узнать, как правильно использовать функции sinon spy/stub/mock.
Ответы
Ответ 1
Ответ на вопрос об инъекции зависимостей.
Вы хотите проверить, что WidgetManager
взаимодействует с зависимостью (Widget
) ожидаемым образом - и вы хотите свободно манипулировать и опросить эту зависимость. Для этого вам нужно ввести версию заглушки Widget
во время тестирования.
В зависимости от того, как создается WidgetManager
, существует несколько вариантов инъекции зависимостей.
Простым методом является включение в конструкцию WidgetManager
зависимостей Widget
:
// 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');
});
Вы можете изменить поведение своего заглушки в зависимости от потребностей конкретного теста. Например, чтобы проверить, что длина списка виджетов увеличивается после создания виджета, вы можете просто вернуть объект из вашего обрезанного метода create()
:
var stubbedWidget = {
create: sinon.stub().returns({})
}
Это позволяет вам полностью контролировать зависимость, без необходимости издеваться или заглушать все методы, а также проверять взаимодействие с его API.
Есть также такие опции, как proxyquire или rewire, которые предоставляют более мощные опции для переопределения зависимостей во время тестирования. Наиболее подходящим вариантом является реализация и предпочтение - но во всех случаях вы просто пытаетесь заменить данную зависимость во время тестирования.
Ответ 2
Ваш метод addWidget
выполняет 2 действия:
- "преобразует" строку в экземпляр
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);