Как стать автором
Обновить

Комментарии 16

Дополню.

Моки добавляют двойное описание внешнего (вторичного) модуля:
— во-первых, внешний модуль (его поведение) достаточно полно описываться своим набором юнит-тестов,
— во-вторых, его поведение описывается в setup'е соответствующего мока (и так для каждого теста/группы тестов).

В этом случае, с одной стороны улучшается покрытие, с другой — увеличивается хрупкость самого набора тестов.

Лично я по возможности использую классический вариант (и реальные, и фейк-объекты, и стабы), а к мокам склоняюсь на границах интеграции подсистем, когда четко представляю процесс взаимодействия и хочу проверить именно его или написание фейка/стаба оказывается слишком дорогим, а реальный класс неприменим.
А чем принципиально стабы отличаются от моков?
Если по Фаулеру, то стабы имеют внутреннее состояние, но не проверяют поведение (прямо, косвенно поведение может проверяться). Моки же предназначены для проверки поведения. Также в test doubles он выделяет fake (являющиеся упрощенными заменителями реальных, например, in-memory db) и dummy (не используемые в тесте, но необходимые, как зависимость тестового объекта) объекты.

Т. е. моку указывается, как он должен отреагировать на определенные вызовы (включая параметры вызовов). Если какой-либо вызов отличался, был сделан не описанный предварительно вызов или вызовы методов мока были сделаны не в том порядке — это fail теста.

В случае стаба не делается никаких предположений о порядке вызова, получаемых и возвращаемых в каждом из вызовов параметрах и т. п. О корректности работы вывод делается на базе финального состояния стаба (не всегда, конечно, но это Фаулер относит к fake и dummy реализация) и тестируемого модуля.

В посте есть ссылка на соответствующую статью: martinfowler.com/articles/mocksArentStubs.html.
А если в рамках одного теста для части вызовов мне нужно строго проверять порядок вызовов, а в другой — нет, то это мок или стаб?

Вопрос очень хороший, и мало кто на него может внятно ответить, на самом деле. (Не пойму, куда слились «лондонцы» — не вижу их следов в этой дискуссии). Похоже, Фаулер тоже «плавает» здесь. По крайней мере, его объяснения какие-то туманно-расплывчатые. Взять к примеру абзац про школы: только что речь шла про TestDoubles, как вдруг перескочились на моки. Не вызывает удивления?

Вообще, тема заслуживает не то, что отдельной статьи, а нескольких глав книге (если не цельной книги). Но попробую изложить основные мысли прямо здесь.

На мой взгляд, все непонятки идут от того, что до сих пор большинство не понимает разницу между TDD и тестированием. Сколько не повторяют умные (я не о себе) люди, что TDD — это не про проверку и надежность, что это — методология разработки, и потому имеет совершенно другие цели, что нельзя их смешивать… Как-то все без толку.

Так вот: моки нужны для TDD, а стабы — для тестирования.

Про стабы все хорошо расписано. Замените почти во всех статьях (включая упомянутого Дядю Боба), где встречается, слово мок на слово стаб — и все будет верно. Короче: стабы нужны чтобы в тесте не зависеть от того, от чего зависеть не надо.

Моки появились в результате попытки расширить рамки применимости TDD. Из известной мне литературы лучше всего это описано в Growing Object-Oriented Software, Guided by Tests (не знаю, существует ли ее перевод на русский).

Исходная проблема заключается в том, что с помощью классического TDD удобно строить систему из уже готовых кирпичиков, но невозможно строить из несуществующих. Наверное, надо пояснить…

Процесс разработки можно рассматрвать как два «попеременных» вида деятельности:
— анализ, целью которого является разбиение сложного комка исходных требований на отдельные простые подзадачки
— синтез, целью которого является соединить выделенные подзадачки в решение исходной проблемы.

В эпоху структурного программирования (еще до эры ООП) это еще называлось программированием сверху-вниз (анализ) и снизу-вверх (синтез). Верх и низ определяется так: целевая (сложная) система находится на верхнем уровне, ниже она расподается на более простые подсистемы, и так далее для каждой очередной подсистемы, пока не доберемся до некоторых примитивов.

Утверждается, что «классический» TDD прекрасно работает на этапе синтеза. (Разумеется, это утверждают люди, которые потратили определенные усилия на его освоение. Можно ли им верить — тема для отдельного разбирательства. Я лично верю, так как нескромно отношу себя к таковым.)

Но вот на этапе анализа TDD выглядит бесполезным. Действително, как можно тестировать работу чего-то, что не просто не существует, но и состоит из несуществующих частей (до уже готовых кирпичиков-примитивов еще добраться надо). Соответственно, до того как приступить к TDD для построения некой системы, человек должен крепко задуматься над тем, какие кирпичики для этого нужны (спроектировать), слепить (реализовать соответствующие модули) их, и только потом вернуться к любимому занятию. Многим это не нравилось. Часто не потому, что они такие твердолобые, упертые и ограниченные фанатики TDD, а потому что на практике такой процесс спекулятивного размышления часто приводит совсем не туда, куда хотелось бы… Обосновывать это здесь не буду — в нашем случае не так и важно почему, на самом деле. Не нравилось, и все тут!

И вот кто-то задумался: а почему, собственно? Почему TDD нельзя использовать для несуществующих объектов? Потому что в условии теста проверить мы можем только состояние некоторого объекта. Получить это состояние возможно только при условии, что все необходимые для работы этого некоторого объекта компоненты уже существуют. Именно на их реализацию нам и приходится отвлекаться… И тут возникает мысль: а зачем отвлекаться-то?! давайте напишем их прямо в тесте!

Это, на мой взгляд, и есть основное отличие мока от других тестовых двойников: мок лепится непосредственно в теле теста что называется на лету, по мере необходимости. Практика сразу же показала, что не надо лепить полноценный объект, достаточно сказать ему как реагировать на нужные сообщения. И более того, оказалось (хотя это и так вроде бы «очевидно»?), что на этапе анализа нас мало интересуют всяческие детали, нам нужно только понять, какие средства нужны для разрабатываемой в данный момент системы, чтобы она могла выполнить требуемую функциональность. То есть проверять надо сам факт взаимодействия нашей системы с другими компонентами… Ну, и понеслось… В результате сформировалась целая школа TDD на этой базе (кто ее называет лондонской, кто «мокистской»). Возникла даже мысль переименовать TDD — появилась Behavior-Driven Development… Ну и так далее… 
Мой личный опыт показывает, что использование моков должно ограничиваться именно описанным выше процессом анализа. Попытки использовать их на этапе синтеза благополучно провалились. На этапе синтеза обычно нужно сформировать двойника и затем использовать его в разных тестах снова и снова. Мок сформирован прямо в тексте теста, и вытащить его в другой для повторного использования проблемно. Можно, но очень скоро преимущества этого инструмента становятся недостатками. Здесь удобнее создать отдельный объект — вне кода теста — и подсовывать его куда надо. Это может быть стаб или фейк или что-то еще, но уже не мок.

P.S.: Прошу прощения у нелюбителей многобукв. Получилось длинновато для комментария, но сейчас не имею возможности переоформлять в виде статьи. Может быть как-нибудь позже.
Мне кажется, что это:
мок лепится непосредственно в теле теста что называется на лету, по мере необходимости

Не является основной фишкой и отличительной чертой мока. Более того, это ведь просто сахар и удобство технологий. То, что можно писать мок в теле теста, заслуга не мока, как паттерна, а конкретной реализации.

И вот это:
Но вот на этапе анализа TDD выглядит бесполезным.

Тоже не совсем верно. Точнее я не понял, что вы подразумеваете под TDD на этапе анализа. Вы имеете ввиду, что TDD не описывает принципов как проводить анализ, аля RUP. Или то, что на этапе анализа тесты бесполезны?
Но, конечно, я могу ошибаться во всем. Рад услышать другую точку зрения.
Я вообще не говорил про TDD/BDD, т. к. test doubles используются в тестах вне зависимости от методологии. Т. е. разрабатывая не по BDD/TDD я вполне могу писать тесты, как модульные, так и интеграционные. То, что моки используются преимущественно в BDD, согласен.

Например, зная контракт внешнего сервиса (например, это какой-то хитрый FSM, но мы знаем все условия переходов) мы можем написать стаб его реализации, а можем описать его в виде набора моков в тестах. В первом случае стаб (или набор стабов) может оказаться довольно сложным и требовать отдельного тестирования, в другом (при использовании моков) — при изменении контракта возникнет необходимость править все моки (хотя правка стаба тоже необходима, знание о его поведении всего в 2 местах: его тестах, если они есть, и в нем самом).
Знаю что везде по разному. Но насколько я понимаю:
— Стаб — тупо заглушка, нужен просто для реализации зависимостей
— Мок — умная заглушка. Нужен для реализации зависимостей, но может еще и контролировать то, как его вызывали и что в результате вызова возвращать

Т.е. если Вам надо проверить, был ли вызван метод с определенными параметрами, и вернуть что то определенное — то мок. Если вам нужно просто сделать так, что бы вызов метода не упал — то стаб.

Это мое понимание, оно, конечно, может отличаться от любого другого.
Главное назначение мока, имхо, контроль того, что был произведён его вызов с определенными параметрами. И контроль внешний, из тела теста в виде утверждения, а не просто какое-нибудь исключение при недопустимых параметрах.
Мне кажется, или наши мнения совпали? :)
Ключевое отличие: у вас «но может еще и », а у меня «Главное назначение».
Мне кажется Вы придираетесь. :) Это ведь просто оборот речи. Обратите внимание, что без фразы «может еще и», мок это тот же самый стаб. :)
В стабах может использоваться как верификация поведения (например, ожидание вызова методов), так и верификация состояния (вызвали, посмотрели на изменившуюся переменную), причем второе используется чаще. В моках — только верификация поведения. Предназначение у них одинаковое.
У нас не возникало сложностей понять реальную причину сбоя, даже если при этом падали соседние тесты.


+100500

Со временем пришёл к тому, что изоляция от других модулей (считая модулем класс обычно) не должно быть самоцелью. Если один класс тянет за собой зависимости от других классов, то вовсе не обязательно делать моки и стабы на все его зависимости и инжектить их, вполне достаточно использовать уже протестированные классы. И вполне нормально, что для прохождения теста класса Contract мы правим код класса Customer, не покрывая его тестами отдельно (по крайней мере при использовании нормального редактора, для которого Find usages не является проблемой).

Но это касается только так называемых «старых плоских объектов», но вот внешние сервисы (включая ФС и СУБД) я так стараюсь не использовать — они выделяются в отдельные модули/сервисы и тестируются отдельно, а зависимости от них других модулей исключительно стабятся. Иначе юнит-тесты постепенно переходят в интеграционные, их скорость падает, количество кейсов увеличивается и в итоге на тесты забивают.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Изменить настройки темы

Истории