Как правильно издеваться над классами ES6 с помощью sinon - PullRequest
0 голосов
/ 17 мая 2018

Я хочу иметь возможность правильно протестировать мой класс ES6, для его конструктора требуется другой класс, и все это выглядит так:

Класс A

class A {
  constructor(b) {
    this.b = b;
  }

  doSomething(id) {
    return new Promise( (resolve, reject) => {
      this.b.doOther()
        .then( () => {
          // various things that will resolve or reject
        });
    });
  }
}
module.exports = A;

Класс B

class B {
  constructor() {}

  doOther() {
    return new Promise( (resolve, reject) => {
      // various things that will resolve or reject
    });
}
module.exports = new B();

index

const A = require('A');
const b = require('b');

const a = new A(b);
a.doSomething(123)
  .then(() => {
    // things
  });

Поскольку я пытаюсь внедрить зависимости, а не требовать вверхуклассы, я не уверен, что делать с насмешкой над классом B и его функциями для тестирования класса A.

Ответы [ 3 ]

0 голосов
/ 18 мая 2018

Кажется довольно простым, так как sinon осуществляет макет объекта, заменяя один из его методов поведением (как описано здесь ):

(я добавил resolve() -s к обоим обещаниям в ваших функциях, чтобы иметь возможность проверять)

const sinon = require('sinon');

const A = require('./A');
const b = require('./b');

describe('Test A using B', () => {
  it('should verify B.doOther', async () => {
    const mockB = sinon.mock(b);
    mockB.expects("doOther").once().returns(Promise.resolve());

    const a = new A(b);
    return a.doSomething(123)
      .then(() => {
        // things
        mockB.verify();
      });
  });
});

Пожалуйста, дайте мне знать, если я что-то неправильно понял или дополнительную информацию о том, что вы хотите проверить ...

0 голосов
/ 29 мая 2018

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;
0 голосов
/ 17 мая 2018

Я думаю, что вы ищете библиотеку proxyquire .

Чтобы продемонстрировать это, я немного отредактировал ваши файлы, чтобы напрямую включить b в (я сделал этоиз-за вашего синглтона new B) , но вы можете сохранить свой код, с этим проще понять прокси-запрос.

b.js

class B {
  constructor() {}
  doOther(number) {
    return new Promise(resolve => resolve(`B${number}`));
  }
}

module.exports = new B();

a.js

const b = require('./b');

class A {
  testThis(number) {
    return b.doOther(number)
      .then(result => `res for ${number} is ${result}`);
  }
}

module.exports = A;

Теперь я хочу проверить a.js, посмеиваясь над поведением b.Здесь вы можете сделать это:

const proxyquire = require('proxyquire');
const expect = require('chai').expect;

describe('Test A', () => {
  it('should resolve with B', async() => { // Use `chai-as-promised` for Promise like tests
    const bMock = {
      doOther: (num) => {
        expect(num).to.equal(123);
        return Promise.resolve('__PROXYQUIRE_HEY__')
      }
    };
    const A = proxyquire('./a', { './b': bMock });

    const instance = new A();
    const output = await instance.testThis(123);
    expect(output).to.equal('res for 123 is __PROXYQUIRE_HEY__');
  });
});

Используя proxyquire, вы можете легко смоделировать зависимость и рассчитывать на ложную библиотеку. sinon используется для непосредственного шпионажа / окурка объекта, обычно вы должны использовать оба из них.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...