Найти тему
Nuances of programming

Fake-объекты практичнее mock-объектов

Оглавление

Источник: Nuances of Programming

Стоит помнить  —  если вы имеете дело с неудачно разработанным API, предпочтительнее использовать mock-объекты.

Пример кода.

Что такое mock- и fake-объекты

Определения можно найти во множестве статей в сети. Я просто упомяну их здесь для полноты картины.

Mock  —  это объекты, обладающие тем же интерфейсом, что и реальные типы, но которые могут быть заданы сценарием для возврата заранее запрограммированных ответов. Они регистрируют полученные вызовы, которые можно проверить в рамках тестовых утверждений.

Fake  —  это объекты, обладающие полностью работоспособной реализацией реального типа, но оптимизированные и упрощённые для целей отладки.

Почему fake-объекты лучше?

Проверка состояния

Тестирование методом чёрного ящика: в чём же разница? Mock-объекты используются для тестирования поведения, а fake-объекты  —  для тестирования состояния.

Тестирование методом белого ящика: процесс mock-обработки вызовов, осуществляемых SUT (тестируемой системой) на её зависимостях с применением операторов when и последующей проверкой verify подразумевает, что тест знает и точно определяет, как тестируемая система должна себя вести. Это и называется тестированием методом белого ящика:

Проблемы тестирования методом белого ящика: любое изменение реализации, например оптимизация алгоритма, очистка кода или изменение порядка операторов, может привести к сбою теста. Эти изменения приводят к обновлению испытательного стенда, заставляя тест “следовать” реализации. Таким образом, тест становится ничем иным как дубликатом кода и требует такой же поддержки.

Тестирование методом чёрного ящика: тестирование состояния с помощью fake-объектов  —  это способ написания потребительских тестов. Тестируемая система рассматривается как “чёрный ящик”, и утверждения делаются только в сравнении с выводом тестируемой системы. Любые изменения в реализации действительны, только если подтверждены тестами. Такие тесты надёжны. Они указывают “что” и не указывают “как” (что в любом случае делает консьюмер API). Это и есть тестирование методом чёрного ящика. Имейте в виду, что тестируемая система не обязана быть одним классом:

Тестирование методом чёрного ящика заставляет нас думать об API как о консьюмере. Если для вас не так просто задавать состояние в тестах, консьюмеру API будет непросто его использовать.

Тесты как система поддержки

Тесты должны быть своего рода защитным ограждением для рефакторинга. Если алгоритмический рефакторинг заставляет вас обновлять тесты, фактически вы модифицируете и само это ограждение. Мало того, что в итоге вы меняете спецификации, как вам нравится, вы еще и выполняете больше работы. Это противоречит цели тестирования:

Тесты в качестве API документации

Пользовательские тесты ведут себя как документации. Имя теста  —  это краткое описание поведения тестируемой системы в заданных условиях, а сам тест объясняет возможности API, коэффициент использования и граничные сценарии. Это задаёт чёткое различие между тестовым и производственным кодом.

Для тестов проверки поведения, использующих mock-объекты, тест описывает “как” тестируемая система взаимодействует со своими зависимостями (а это не то, до чего есть дело консьюмеру). Это тоже документация, но избыточная  —  для этого мы можем просто прочесть производственный код.

Функциональные и интеграционные тесты

Fake-объекты, созданные для модульного тестирования, можно легко применять для написания функциональных или интеграционных тестов. С mock-объектами это сделать сложнее.

Поддержка использования реализации

Не нужно использовать fake-объекты для всего. Чистая функция или чистый функциональный тип полностью управляются своими входными данными и отслеживаются выходными данными, что можно непосредственно использовать в тестах. Mock-подход, напротив, приводит к тому, чтобы применять mock-объекты для всех зависимостей:

Другой пример: рассмотрим тестируемую систему, являющуюся не классом, а семейством классов. Например, если класс внедряется с абстрактными фабриками, вы можете использовать реальную фабрику, а fake применять только для её продуктов.

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

Fake снижает многословность

Mock-подход склонен игнорировать логику, непосредственно не проверенную тестом. Подобная логика, однако, будет реализована в производственном сценарии. Строгое применение mock помогает избежать этого, но делает тестовую установку многословнее.

Поскольку fake-объекты являются полноценными функциональными реализациями, вся логика проверяется постоянно как и в производственном коде. Тест становится менее многословным и более релевантным:

При mock многословность высока, читаемость теста низкая:

Fake богаче, чем mock

Fake-объекты имеют боле богатую функциональность. Например, они предоставляют возможность логирования. Таким образом, модульные тесты могут при необходимости генерировать логи, что упрощает отладку.

Fake поддерживает принцип персональной ответственности (SRP)

Оператор when при mock-подходе заставляет нас фокусироваться на конкретном методе, над которым реализуется mock в данный момент. Нам нет дела до того, как работает этот зависимый класс или как он устроен. Да и в целом всё равно, даже если этот метод принадлежит другому классу! Это может привести к тому, что класс API будет неудачно структурирован и нарушит SRP на уровне класса.

Даже на уровне метода использование такого ArgumentMatcher, как any(), как правило игнорирует сигнатуру метода (нам было бы всё равно, если бы у метода были дополнительные параметры, потому что мы бы просто использовали другой any()!). В тесте нас интересует только то, что возвращает оператор when.

Если метод выполняет две задачи, mock проще, а fake сложнее. Точно так же тип, выполняющий несколько действий, заставляет свой fake-объект имитировать несколько действий, что затрудняет создание и поддержку. В таком случае почему бы не заставить их делать что-то одно?

Тесты должны помогать в разработке API. Классы должно быть легко расцеплять и декомпозировать путём рефакторинга, когда тесты подтверждают, что новый дизайн работает как ожидалось:

Функциональные варианты использования  —  шаблон, помогающий следовать SRP.

Цена fake

Проверка API

Если вам нужны механизмы проверки, подобные mock, для отслеживания, вы можете добавить дополнительные методы к fake (например, FakeUserRepository.getUserCount()). Это дополнительные усилия, но они удерживают вас от излишнего использования библиотек, таких как mockito, которые легко некорректно использовать, что приводит к структурному тестированию.

Интерфейс с одной производственной реализацией

Использовать интерфейс для каждого тестируемого типа не очень удобно, потому что необходимо создавать fake-реализацию. Как по мне, это ничтожная цена в сравнении с предоставляемыми преимуществами.

Тестирование fake-реализации

Поскольку fake является полностью функциональной реализацией, она может быть достаточно сложна чтобы быть тестируемой. Обычно это отголосок неудачного дизайна API и намёк на необходимость сделать типы более компактными, чтобы fake-реализации оставались простыми.

Финальные замечания

Применение mock-объектов должно быть ограничено ситуациями, когда создание и поддержка fake-объектов сводит к нулю описанные выше преимущества. Такие ситуации возникают, как правило, при работе с неудачно разработанными API.

Существуют способы абстрагироваться от такого API, чтобы использование fake-объектов стало возможным, однако это выходит за рамки данной статьи. Типичные абстракции включают обёртывание или компоновку в более подходящий для fake-метода тип.

Код

Большинство фрагментов кода выше взяты отсюда.

Читайте также:

Читайте нас в Telegram, VK

Перевод статьи Pravin Sonawane: Mocking is not practical — Use fakes