Помогите себе и другим с чистым кодом
Я большой поклонник TDD. Но мой TDD заканчивался, как только я начинал писать код для ViewModel
. Раньше я страдал от беспорядка, которым стали мои модульные тесты. Чем больше логики я добавлял в ViewModel
, тем больше беспорядка я создавал в модульных тестах.
В конце концов, я заканчивал тем, что удалял тесты для ViewModel
, потому что их было слишком сложно поддерживать и писать новые. Но без тестов я страдал от ошибок, которые вносил я (или другой разработчик).
Так почему же сложно писать модульные тесты для ViewModel
?
ViewModel
сложного экрана может состоять из десяти и более зависимостей.- Каждый класс, от которого зависит
ViewModel
, имеет один или несколько общедоступных методов, которые используетViewModel
. - Некоторые общедоступные методы могут возвращать разные результаты, которые изменяют поведение
ViewModel
. - Иногда нам нужно проверить последовательность вызовов класса или классов. Например, это могут быть состояния видов, один вид показан, а другой скрыт. Кроме того, нам может потребоваться проверить, был ли вызван один метод класса, а другой нет.
- Было бы здорово повторно использовать код в модульных тестах.
Итак, позвольте мне сузить список всех трудностей до следующих вопросов:
- Как справиться со сложностью проверки состояния
ViewModel
? - Как справиться со сложностью создания экземпляра
ViewModel
? - Как справиться со сложностью взаимодействия с
ViewModel
s?
Но прежде чем я начну отвечать на эти вопросы, я хотел бы сказать пару вещей:
- Эта статья о том, как организовать тесты для
ViewModel
, чтобы упростить поддержку и написание новых. - Я буду максимально простыми примерами. Решения могут выглядеть как чрезмерная инженерия, но они сияют в реальном проекте. В конце статьи я покажу фрагменты тестов для проекта, над которым работаю.
Давай начнем.
Пример
Давайте рассмотрим простой случай load/content/error:
- Состояние загрузчика отображается во время выборки данных.
- Состояние содержимого отображается, если данные были успешно загружены.
- Состояние ошибки отображается, если данные были загружены с ошибкой.
- Данные загружаются и отображаются, когда пользователь нажимает кнопку повторной попытки.
Давайте также напишем модульные тесты, подобные показанному ниже:
Проблема 1: Как справиться со сложностью проверки состояния ViewModel?
Возможное решение: я считаю полезным использовать Verifier. Verifier — это служебный класс, содержащий логику проверки.
Итак, после рефакторинга SomeViewModelTest
это выглядит так:
Вот некоторые преимущества Verifier:
- Это повышает читабельность модульных тестов.
- Это уменьшает дублирование кода.
- Android Studio может подсказать, что можно проверить. Так сложнее что-то упустить, когда пишешь новый тест.
Проблема 2: Как справиться со сложностью создания ViewModel в модульных тестах?
Возможное решение: мне кажется полезным использовать ViewModelBuilder
. ViewModelBuilder
— это служебный класс, который отвечает за настройку ViewModel
для удовлетворения наших потребностей.
Насмешливая логика перемещена в файл ViewModelBuilder
. Важно дать описательные имена для каждого метода. Так вы сможете тратить меньше умственных усилий на чтение тела тестовой функции.
Давайте рефакторим SomeViewModelTest
:
Вот преимущества ViewModelBuilder
:
- Насмешливую логику можно повторно использовать в разных тестах.
- Повысить читабельность юнит-тестов. Создание экземпляра модели представления не приводит к беспорядку.
- Android Studio может подсказать, над чем можно издеваться.
Проблема 3: Как справиться со сложностью взаимодействия с ViewModel в модульных тестах?
Возможное решение: мне кажется полезным использовать класс Cases
. Класс Cases
— это служебный класс, который инкапсулирует логику взаимодействия с ViewModel
. Итак, он отвечает за:
- Имитация зависимостей после создания экземпляра
ViewModel
. - Инкапсуляция логики взаимодействия с
ViewModel
(например, вызов общедоступных методов для кликов или общедоступных методов, вызываемых Fragment или Activity и т. д.).
Обязательно ли иметь класс Case?
Я считаю, что это так. Взаимодействия с ViewModel
могут быть довольно сложными. Часто бывает необходимо снова и снова вызывать одни и те же общедоступные методы ViewModel
в определенном порядке для новых тестов. Также могут быть горячие наблюдаемые, которые генерируют события в случайное или определенное время, которые изменяют поведение ViewModel
.
Пример
Рассмотрим следующий пример:
- Данные
ViewModel
загружаются с ошибкой. - Пользователь нажимает кнопку «Повторить попытку»
- Данные
ViewModel
успешно загружены и показаны.
Напишем класс Cases
:
А тест выглядит так:
Преимущества наличия класса Cases
:
- Это позволяет повторно использовать логику взаимодействия между тестами.
- Явное наименование методов класса
Cases
дает явное представление о том, что происходит в тесте. Таким образом, повышается читабельность. - Android Studio дает подсказки о том, какой регистр можно использовать.
Примеры из реального проекта
Сравните следующие два модульных теста. Первый написан небрежно. Второй пишется по подходам.
Это только 2 из 42 написанных модульных тестов.
Если модульные тесты написаны с использованием первого подхода, сложно написать новый или изменить старый из-за плохой читабельности. Есть также много дублирования кода.
Второй подход устраняет все эти недостатки.
Спасибо за прочтение!