Практическое руководство по тестированию пар с Sinon, Mocha и Chai
Как написать тест, в котором вы не можете (или решили не использовать) реальные зависимости того, что вы тестируете? Какие у вас есть возможности в JavaScript? А что дает вам библиотека тестовых двойников такого, чего не может дать простой старый js? Хотите узнать? Читать дальше!
Тестируемая система
Предположим, у нас есть ситуация, аналогичная описанной дядей Бобом Мартином в Маленьком насмешнике [1].
У нас есть `Система`:
‘use strict’; /** * @constructor * @param {Authoriser} authoriser */ function System(authoriser) { /** @private */ var login_count = 0; /** @public */ this.login = function(username, password) { if (authoriser.authorise(username, password)) { ++login_count; } }; /** @public */ this.loginCount: function() { return login_count; }; }
который делегирует процесс входа пользователей в систему `Авторизатору`, реализация которого может выглядеть примерно так:
‘use strict’; /** @constructor */ function Authoriser() { } /** * @public * * @param {String} username * @param {String} password * * @returns {Boolean} */ Authoriser.prototype.authorise = function(username, password) { // connects to LDAP or some database, verifies user credentials // returns true if the username/password pair is valid, // false otherwise // ... };
Манекен
Как мы можем доказать, что вновь созданная `Система` не имеет зарегистрированных пользователей? И нужно ли нам вообще для этого создавать настоящий объект `Authoriser`?
Поскольку мы знаем, что при вызове system.loginCount даже не затрагивается объект authoriser, мы можем заменить authoriser на пустышка:
describe(‘A newly created System’, function() { var dummy_authoriser = { }; it(‘has no logged in users’, function() { var system = new System(dummy_authoriser); expect(system.loginCount()).to.equal(0); }); });
И это все, что нужно про манекен! Вы передаете его во что-то, когда вам нужно предоставить аргумент, но вы знаете, что он никогда не будет использоваться.
Заглушка
Теперь предположим, что вы хотите протестировать часть своей «Системы», которая требует, чтобы вы вошли в систему.
Конечно, вы можете сказать, что можете просто войти, но вы уже проверили, что вход работает, и, поскольку процесс входа в систему требует времени, зачем делать это дважды? Кроме того, если есть ошибка при входе в систему, ваш тест тоже не пройдет! Это ненужное связывание, которого можно легко избежать с помощью заглушки:
describe(‘A System with a logged in user’, function() { var accepting_authoriser = { authorise: function() { return true; } }; it(‘has a loginCount of 1’, function() { var system = new System(accepting_authoriser); system.login(‘bob’, ‘SecretPassword’); expect(system.loginCount()).to.equal(1); }); });
Так что же делает этот заглушенный `accept_authoriser`? Он возвращает "true", поэтому принимает любые пары имя пользователя / пароль.
Если вы теперь хотите протестировать другую часть вашей системы, которая обрабатывает неавторизованных пользователей, вы можете создать rejecting_authoriser следующим образом:
var rejecting_authoriser = { authorise: function() { return false; } };
Так что это заглушка, она просто возвращает значение и не имеет никакой другой логики.
Заглушки предоставляют стандартные ответы на звонки, сделанные во время теста, обычно не отвечая ни на что, кроме того, что запрограммировано для теста - Мартин Фаулер, Test Double
Красивый! Так красиво и просто, не правда ли? Всего одна строчка кода! Разве утка печатает не блестяще? Представьте, сколько еще кода вам пришлось бы написать, если бы вы использовали Java или C #! Вам, вероятно, придется сначала создать интерфейс `Authoriser`, затем убедиться, что и реальная реализация, и ваша заглушка` реализуют` этот интерфейс, а затем ...
Ах да, `interface` ... У нас нет этой конструкции в JavaScript, не так ли?
Проблема с использованием plain-old-js для ручной прокрутки ваших тестовых заглушек заключается в том, что вы не получите значимой ошибки, когда интерфейс заглушенного объекта изменится, а ваши тестовые заглушки больше не будут правильными. В худшем случае вы можете вообще не получить ошибку!
Каковы могут быть последствия этого, спросите вы? Что ж, я видел, как проходили огромные наборы тестов, хотя приложение, которое они должны были тестировать, было в корне сломано, так как половина кода вообще больше не существовала ... Поверьте мне, вы не хотите быть там [2]: -)
Да, это не круто, но можем ли мы сделать что-то лучше? Да мы можем! Войдите в sinon.js! [3]
Как тогда будет выглядеть `acceptpting_authoriser` с помощью sinon?
describe(‘A System with a logged in user’, function() { var accepting_authoriser = sinon.createStubInstance(Authoriser); accepting_authoriser.authorise.returns(true); it(‘has a loginCount of 1’, function() { var system = new System(accepting_authoriser); system.login(‘bob’, ‘SecretPassword’); expect(system.loginCount()).to.equal(1); }); });
А что отличает его? Вызов sinon.createStubInstance (Authoriser) создает объект-заглушку, который позволяет исключить только те методы, которые существуют в прототипе исходного Authoriser.
Это важно, потому что это означает, что если метод `# authorise` с радостью решит изменить свое имя на #isAuthorised` однажды, вы получите TypeError в вашем тесте, именно там, где вы пытаетесь заглушить уже не существующий `# authorise`. Круто, правда?
Шпион
Вы помните, как я сказал в начале, что Система делегирует процесс входа пользователей в систему Авторизатору? Как мы можем проверить, действительно ли имеет место это взаимодействие? Что нам нужно сделать, так это шпионить за вызывающим, чтобы увидеть изнутри работу алгоритма, который мы тестируем - и именно для этого нужны шпионы [4 ]:
describe(‘A System’, function() { var accepting_authoriser_spy = { authorise_was_called: false, authorise: function() { this.authorise_was_called = true; return true; } } afterEach(function() { accepting_authoriser_spy.authorise_was_called = false }; it(‘delegates logging users in to the authoriser’, function() { var system = new System(accepting_authoriser_spy); system.login(‘bob’, ‘SecretPassword’); expect(accepting_authoriser_spy.authorise_was_called). to.equal(true); }); });
Вы вводите шпионский объект точно так же, как вводили заглушку, но затем в конце теста вы проверяете, соответствуют ли взаимодействия, записанные вашим шпионом, тем, что вы ожидали от него.
Шпионы - это заглушки, которые также записывают некоторую информацию в зависимости от того, как их называли. - Мартин Фаулер, Тестовый дубль »
Также следует отметить, что поскольку шпион поддерживает состояние (в данном примере флаг «authorise_was_called`), его необходимо сбрасывать между тестами, чтобы один тестовый пример не влиял на другой - это делается в теге afterEach block.
Конечно, наша ручная реализация страдает от проблем, которые я описал ранее, когда мы говорили о ручных заглушках. Давайте улучшим нашу предыдущую реализацию с помощью sinon:
describe(‘A System’, function() { var accepting_authoriser = sinon.createStubInstance(Authoriser); accepting_authoriser.authorise.returns(true); afterEach(function() { accepting_authoriser.authorise.restore(); }); it(‘delegates logging users in to the authoriser’, function() { var system = new System(accepting_authoriser); system.login(‘bob’, ‘SecretPassword’); expect(accepting_authoriser.authorise).to.have.been.called; }); });
Как и в примере с заглушкой, здесь я использую sinon.createStubInstance.
Однако есть одно существенное различие между нашей ручной реализацией шпиона и приведенной выше: sinon spy сам по себе не является основным объектом, который вы вводите, это оболочка вокруг метода объекта. Вот почему я ввожу `accept_authoriser`, но выполняю утверждение для шпиона ` accept_authoriser.authorise` и использую `accept_authoriser.authorise.restore ()` сбросить его.
Кроме того, тот факт, что sinon spy является оболочкой, означает, что если вы хотите шпионить за настоящим методом авторизации, вы можете сделать это следующим образом:
var authoriser = new Authoriser(), system = new System(authoriser), authorise_spy = sinon.spy(authoriser, ‘authorise’); system.login(‘bob’, ‘SecretPassword’); expect(authorise_spy).to.have.been.called;
Издевательство
Мок не так заинтересован в возвращаемых значениях функций.
Его больше интересует, какая функция была вызвана, с какими аргументами, когда и как часто. - Дядя Боб Роберт 'Мартин, Маленький пересмешник »
Еще одна вещь, которая отличает mock [5] от заглушки, - это метод `verify`, который группирует все утверждения и выполняет их в конце теста, проверяя поведение нашей Системы:
describe(‘A System’, function() { var accepting_authoriser_mock = { authorise_was_called: false, authorise: function() { this.authorise_was_called = true; return true; }, verify: function() { expect(this.authorise_was_called).to.equal(true); } }; it(‘delegates logging users in to the authoriser’, function() { var system = new System(accepting_authoriser_mock); system.login(‘bob’, ‘SecretPassword’); // note: assertions moved to #verify accepting_authoriser_mock.verify(); }); });
Теперь, когда мы понимаем, как работают mocks, и знаем, как их создавать сами, давайте посмотрим, как sinon может упростить задачу. Однако есть важное различие, которое нам нужно сделать, прежде чем погрузиться в моки sinon:
«В Sinon основная единица подделки - это функции, всегда функции. Таким образом, «объект-заглушка» в литературе по Java-типу переводится как «функция / метод-заглушка» в мире Sinon / JavaScript ».
- Кристиан Йохансен, «Test Spies, Stubs and Mocks, Part 1.5»
… И то же самое касается издевательств sinon. Хорошо, но что это значит для вас? Давайте сначала посмотрим на различия в реализации:
describe(‘A System’, function() { var authoriser = new Authoriser(), mock = sinon.mock(authoriser); afterEach(function() { mock.restore(); }); it(‘delegates logging users in to the authoriser’, function() { // prepare var system = new System(authoriser); // expect mock.expects(‘authorise’).once().returns(true); // act system.login(‘bob’, ‘SecretPassword’); // assert mock.verify(); }); });
Здесь важно отметить, что mock sinon создается как оболочка вокруг экземпляра реального Authoriser, но вместо макета мы внедряем уже упомянутый реальный экземпляр в System.
Эта стратегия может быть проблематичной, если создание вашего зависимого объекта (Authoriser в нашем примере) стоит дорого; В этом случае вы можете не вызывать конструктор напрямую, используя Object.create [6]:
var authoriser = Object.create(Authoriser.prototype), mock = sinon.mock(authoriser);
Примечание. На момент написания sinon.js (версия 1.10.3) не предоставляет никакого эквивалента `createStubInstance` для моков.
Подделка
У фейка есть деловое поведение. Вы можете заставить подделку вести себя по-разному, передавая ей разные данные. - Роберт Дядя Боб Мартин, Маленький пересмешник »
Предположим, мы хотим определить несколько персонажей, на которых наша «Система» по-разному реагирует. Допустим, «боб» знает свой пароль, а «алиса» его забыла; Я лично рекомендую использовать две отдельные заглушки, но давайте поговорим о подделках, так как некоторые могут предпочесть их использовать:
describe(‘A System’, function() { var fake_authoriser = { authorise: function(username, password) { return (username === ‘bob’ && password === ‘SecretPassword’); } }, system = new System(fake_authoriser); it(‘allows Bob in’, function() { system.login(‘bob’, ‘SecretPassword’); expect(system.loginCount()).to.equal(1); }); it(‘does not allow Alice in’, function() { system.login(‘alice’, ‘password’); expect(system.loginCount()).to.equal(0); }); });
… И вы также можете добиться того же результата с помощью sinon:
describe(‘A System’, function () { var fake_authoriser = sinon.createStubInstance(Authoriser); fake_authoriser.authorise .returns(false) .withArgs(‘bob’, ‘SecretPassword’).returns(true), var system = new System(fake_authoriser); it(‘allows Bob in’, function () { system.login(‘bob’, ‘SecretPassword’); expect(system.loginCount()).to.equal(1); }); it(‘does not allow Alice in’, function () { system.login(‘alice’, ‘password’); expect(system.loginCount()).to.equal(0); }); });
Будьте очень осторожны с подделками, так как они могут быстро стать очень сложными. Если вы можете использовать вместо них заглушки - пожалуйста. Если вы не можете, то, возможно, это просто подчеркнуло, что вещь, которую вы пытаетесь протестировать, слишком сложна и требует разрушения?
Инструменты
Методы, описанные в этой статье, могут применяться как к интерфейсной, так и к внутренней (node.js) разработке, то же самое относится к инструментам, которые я использовал в приведенных выше примерах, а именно:
- Mocha - популярный фреймворк для тестирования JavaScript.
- Sinon.js - библиотека тестовых пар
- Chai.js и его библиотека утверждений bdd-style
- Sinon-chai - утверждения sinon.js для chai
Заключительные мысли и предупреждение
Имейте в виду, что чем больше вы шпионите и высмеиваете, тем теснее вы связываете свои тесты с реализацией вашей системы. Это приводит к хрупким тестам, которые могут выйти из строя по причинам, которые не должны нарушать их.
Тестовые двойники следует использовать с осторожностью и применять только тогда, когда они являются лучшим инструментом для выполнения поставленной задачи. Как и с любым другим инструментом в вашем наборе инструментов для разработки: не используйте отвертку, чтобы забить гвоздь только потому, что у вас есть новая отвертка и вы хотите ее опробовать ;–)
Сноски
- Маленький пересмешник - это сообщение в блоге дяди Боба Мартина. Как вы уже могли заметить, сообщение дяди Боба очень вдохновило на создание этой статьи! ↩
- С другой стороны, если вы уже находитесь в этой ситуации выходите на связь, я могу помочь вам выйти из нее! Хорошо, больше никакого дерзкого маркетинга. Вернуться к статье, быстро, быстро: ↩
- Sinon.js, написанный Кристианом Йохансеном, автором этой прекрасной книги ↩
- Чтобы лучше понять, что такое шпионы и другие тестовые двойники и как ими пользоваться, настоятельно рекомендую прочитать эту книгу ↩
- Концепция фиктивных объектов была первоначально представлена Тимом Маккинноном, Стивом Фриманом и Филипом Крейгом в их статье Эндотестирование: модульное тестирование с использованием имитирующих объектов. Стив Фриман - соавтор книги GOOSE. Опять же, очень рекомендуется. ↩
- Object.create хорошо документирован на MDN ↩
Ян Молак выступает на конференциях, проводит учебные курсы и семинары, а также помогает организациям часто и надежно предоставлять ценное высококачественное программное обеспечение, внедряя эффективные инженерные практики. Свяжитесь с нами чтобы узнать больше!
Понравилась статья? Возможно, вам понравятся мои другие статьи и руководства!
Также нажмите ниже, чтобы другие люди увидели эту статью здесь, на Medium.