В этой статье мы увидим, как просто создавать чистые, выразительные и надежные модульные тесты.

Обзор

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

Чистый код прост и прямолинеен. Чистый код читается как хорошо написанная проза.
 – Грэйди Буч

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

Примеры кода

Для примеров кода в этой статье мы предположим, что хотим протестировать службу рекомендаций статей Medium.

У нас будет ArticleController, где мы будем обрабатывать входящие запросы, и ArticleService —, где мы реализуем варианты использования в бизнесе (в нашем случае — логику рекомендации статьи.

Фрагменты кода написаны на Java, но основные концепции легко применимы к любому другому языку.

1. Тестируйте вариант использования, а не его реализацию

Часто мы находим модульные тесты, которые проверяют объект, имитируя все компоненты, с которыми он взаимодействует.

В нашем случае мы можем представить, что служба заменяется тестовым двойником или фиктивным объектом и получает указание возвращать жестко запрограммированное значение, когда называется:

Давайте будем честными… что мы на самом деле тестируем здесь?

Неужели мы действительно собираемся написать 10 строк стандартного тестового кода только для того, чтобы проверить вызов метода?

Решением этого будет тестирование фактического варианта использования. А именно — для читателя, интересующегося технологиями, мы хотим порекомендовать ровно одну техническую статью, если это единственная статья, сохраненная в базе данных.

Объем теста будет расти, но количество фиктивных объектов будет уменьшаться. В идеале мы должны использовать макеты только для замены внешних зависимостей, таких как база данных и внешние вызовы HTTP.

В зависимости от того, насколько быстрыми должны быть ваши тесты, вы можете использовать test-double для уровня данных, базу данных в памяти или тестовые контейнеры.

Пойдем с первым вариантом. Мы вручную создадим тестовые двойники для слоя данных (в нашем случае ArticleRepository и ReaderRepository), которые сохранят данные в HashMap в памяти.

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

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

2. Принцип единой ответственности

Несмотря на то, что увеличение масштаба теста может помочь нам свести к минимуму использование макетов, принцип единой ответственности применим и к модульным тестам.

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

Другими словами, когда дело доходит до этих более крупных тестов, мы должны быть осторожны и соблюдать следующие правила:

  • Каждый тест имеет один вызов производственного кода.
  • Каждый тест имеет одно логическое утверждение.
  • Между тестами не сохраняется состояние.
  • Любая ошибка в коде должна нарушить наименьшее количество тестов.

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

3. Вспомогательные методы для настройки теста

Мы уже видим некоторые улучшения — сейчас мы тестируем поведение нашего приложения! Это позволяет нам рефакторить производственный код, не касаясь тестов.

Тем не менее, методы тестирования по-прежнему содержат много шаблонного кода, что затрудняет их понимание.

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

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

4. Пользовательские утверждения

Как мы обсуждали ранее, каждый тест должен иметь одно логическое утверждение. Это означает, что мы можем легко выделить его в отдельный метод с осмысленным именем:

В результате мы еще больше упрощаем наш тест. Подводя итог, наш тест теперь состоит из:

  • часть «данные», состоящая из построителей тестов и вспомогательных методов.
  • часть «когда» — однострочник, который вызывает производственный код.
  • часть «then» — пользовательское утверждение, которое инкапсулирует одно логическое утверждение.

5. Создание небольшой среды тестирования

Наконец, давайте добавим тесты для других сценариев.

В демонстрационных целях мы добавим тесты для читателей с премиум-членством и без него на Medium и проверим их доступ к статьям «только для участников»:

Мы можем заметить, что все тесты очень легко читаются и понимаются.

Кроме того, было очень легко добавить эти тесты, потому что мы смогли использовать все помощники тестов и пользовательские утверждения.

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

Повышение уровня кодирования

Спасибо, что являетесь частью нашего сообщества! Перед тем, как ты уйдешь:

  • 👏 Хлопайте за историю и подписывайтесь на автора 👉
  • 📰 Смотрите больше контента в публикации Level Up Coding
  • 🔔 Подписывайтесь на нас: Twitter | ЛинкедИн | "Новостная рассылка"

🚀👉 Присоединяйтесь к коллективу талантов Level Up и найдите прекрасную работу