Как писать хорошие тесты?

3 minute read

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

Тестовый код должен быть компактным и читаемым

Чтобы это реализовать применяйте безжалостный рефакторинг, только такой годится для production-кода. Иначе это все будет похоже на создание древнего устаревшего кода который постепенно разлагается.

Если тесты невозможно легко зарефакторить тогда production-код тоже будет сложно отрефакторить, что приведет к legacy(устаревшему) production коду. Всегда выбирай путь храброго рефакторера

Избегайте тавтологии в коде

Например, когда тестовый код генерирует вещи используя очень похожий regexp который используется в обработчике. Проще говоря не надо переносить логику которую мы тестируем в тесты. Таким образом копирование regexp или чего то подобного в тест не вариант. В этом случае думать о тестовых входных параметрах и выходных результатах помогает(f(input) -> output). Например если код обрабатывает какой то шаблон, не добавляйте параметры. Вместо этого тестируйте на вычисленных результатах.

// используйте
Assertions.assertThat(processTemplate("param1", "param2")).isEqualTo("this is 'param1', and this is 'param2'"));
 
// вместо
Assertions.assertThat(processTemplate("param1", "param2")).isEqualTo("this is '%s', and this is '%s'", param1, param2));

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

Обычно лучше всего это достигается с помощью практики TDD. С TDD вы сможете определить на этапе дизайна что может сломаться. Не стесняйтесь создавать простые тесты для неважных вещей. Вы никогда не узнаете как ваш код будет использоваться в будущем. Можно также исследовать валидность тестов используя mutation testing используя такой инструментарий как PIT.

Не создавайте моков для типов которые не принадлежат вашей codebase

Это не такое уж жесткое предписание, однако если вы это делаете это может иметь последствия(скорее всего так и будет). TDD затрагивает не меньше чем тестирование также и архитектуру вашего приложения. Когда вы мокаете внешний API тесты не могут использоваться чтобы затрагивать архитектуру так как API принадлежит кому то другому. Эта внешняя зависимость может и будет изменять поведение и API.

Представьте себе код который мокирует внешнюю библиотеку. После обновления этой библиотеки, логика может немного поменяться, но наши тесты по прежнему будут отрабатывать правильно, потому что мы используем mock. И только позднее уже думая что все хорошо(билд же прошел нормально и тесты прошли) наше приложение падает в runtime.

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

Вместо этого наиболее общий подход это создавать обертки вокруг внешних библиотек/систем. В этом случае мы должны понимать что есть риск “утечки абстракций” - когда слишком много низкоуровнего API выходит за рамки границ нашей обертки.

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

Не создавайте моков на все, это anti-pattern

Если все замокано, то что на самом деле мы тестируем? Не стесняйтесь не мокать.

Не создавайте моков на value objects

Зачем кто то захочет сделать это? Потомучто создание объекта слишком болезненно!? => не достаточная причина. Если это слишком сложно создать новые fixtures(объекты для тестирования) это может быть знаком что код нуждается в серьезном рефакторинге.

Альтернативой может быть создание билдеров для ваших value-объектов – есть даже специальные тулы для этого, включая IDE плагины, Lombok и другие. Также можно создать factory methods в тестовом пакете.

abstract class CustomerCreations {
   public static Customer customer_with_a_single_item_in_the_basket() {
       // long init sequence
   }
}

Сутью Mockito является концентрирование на взаимодействие между объектами, что является важной частью Объектно Ориентированного Программирования (или messaging).

Читайте Growing Object Oriented Software Guided by Tests

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

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

По мотивам: How-to-write-good-tests

Leave a Comment