Sinon позволяет легко заглушать отдельные экземпляры методов объектов. Конечно, так как b
является одиночным, вам нужно будет откатывать его после каждого теста вместе с любыми другими изменениями, которые вы можете внести в b
. Если вы этого не сделаете, счетчики вызовов и другие состояния будут переходить из одного теста в другой. Если этот тип глобального состояния обрабатывается плохо, ваш набор может превратиться в адский клубок тестов в зависимости от других тестов.
Изменить порядок некоторых тестов? Что-то не так, как раньше.
Добавить, изменить или удалить тест? Куча других тестов сейчас не пройдена.
Попробуйте запустить один тест или подмножество тестов? Они могут потерпеть неудачу сейчас. Или, что еще хуже, они проходят изолированно, когда вы пишете или редактируете их, но не работают, когда запускается весь пакет.
Поверь мне, это отстой.
Итак, следуя этому совету, ваши тесты могут выглядеть примерно так:
const sinon = require('sinon');
const { expect } = require('chai');
const A = require('./a');
const b = require('./b');
describe('A', function() {
describe('#doSomething', function() {
beforeEach(function() {
sinon.stub(b, 'doSomething').resolves();
});
afterEach(function() {
b.doSomething.restore();
});
it('does something', function() {
let a = new A(b);
return a.doSomething()
.then(() => {
sinon.assert.calledOnce(b.doSomething);
// Whatever other assertions you might want...
});
});
});
});
Однако это не совсем то, что я бы порекомендовал.
Я обычно стараюсь избегать догматических советов, но это будет одним из немногих исключений. Если вы проводите модульное тестирование, TDD или BDD, вам следует избегать синглетонов. Они плохо сочетаются с этими методами, потому что они значительно затрудняют очистку после испытаний. В приведенном выше примере это довольно тривиально, но поскольку к классу B
добавляется все больше и больше функций, очистка становится все более обременительной и подверженной ошибкам.
Так что вы делаете вместо этого? Пусть ваш B
модуль экспортирует класс B
. Если вы хотите сохранить свой шаблон DI и избежать использования модуля B
в модуле A
, вам просто нужно будет создавать новый экземпляр B
каждый раз, когда вы создаете экземпляр A
.
Следуя этому совету, ваши тесты могут выглядеть примерно так:
const sinon = require('sinon');
const { expect } = require('chai');
const A = require('./a');
const B = require('./b');
describe('A', function() {
describe('#doSomething', function() {
it('does something', function() {
let b = new B();
let a = new A(b);
sinon.stub(b, 'doSomething').resolves();
return a.doSomething()
.then(() => {
sinon.assert.calledOnce(b.doSomething);
// Whatever other assertions you might want...
});
});
});
});
Вы заметите, что, поскольку экземпляр B
воссоздается каждый раз, больше нет необходимости восстанавливать заглушенный метод doSomething
.
Sinon также имеет полезную служебную функцию под названием createStubInstance , которая позволяет избежать полного вызова конструктора B
во время ваших тестов. По сути, он просто создает пустой объект с заглушками для любых методов-прототипов:
const sinon = require('sinon');
const { expect } = require('chai');
const A = require('./a');
const B = require('./b');
describe('A', function() {
describe('#doSomething', function() {
it('does something', function() {
let b = sinon.createStubInstance(B);
let a = new A(b);
b.doSomething.resolves();
return a.doSomething()
.then(() => {
sinon.assert.calledOnce(b.doSomething);
// Whatever other assertions you might want...
});
});
});
});
Наконец, последний совет, который не имеет прямого отношения к вопросу - конструктор Promise
никогда не должен использоваться для переноса обещаний. Это избыточно и вводит в заблуждение и отрицает цель обещаний, заключающихся в том, чтобы сделать асинхронный код проще для написания.
Метод Promise.prototype.then имеет встроенное полезное поведение, поэтому вам никогда не придется выполнять эту избыточную упаковку. Вызов then
всегда возвращает обещание (которое я в дальнейшем буду называть «цепочечным обещанием»), состояние которого будет зависеть от обработчиков:
- Обработчик
then
, который возвращает значение без обещания, заставит связанное обещание разрешиться с этим значением.
- Обработчик
then
, который выбрасывает, приведет к отклонению связанного обещания с брошенным значением.
then
, который возвращает обещание, приведет к тому, что связанное обещание будет соответствовать состоянию этого возвращенного обещания. Поэтому, если оно разрешается или отклоняется со значением, связанное обещание будет разрешаться или отклоняться с тем же значением.
Таким образом, ваш A
класс может быть значительно упрощен следующим образом:
class A {
constructor(b) {
this.b = b;
}
doSomething(id) {
return this.b.doOther()
.then(() =>{
// various things that will return or throw
});
}
}
module.exports = A;