Помогите себе и другим с чистым кодом

Я большой поклонник TDD. Но мой TDD заканчивался, как только я начинал писать код для ViewModel. Раньше я страдал от беспорядка, которым стали мои модульные тесты. Чем больше логики я добавлял в ViewModel, тем больше беспорядка я создавал в модульных тестах.

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

Так почему же сложно писать модульные тесты для ViewModel?

  • ViewModel сложного экрана может состоять из десяти и более зависимостей.
  • Каждый класс, от которого зависит ViewModel, имеет один или несколько общедоступных методов, которые использует ViewModel.
  • Некоторые общедоступные методы могут возвращать разные результаты, которые изменяют поведение ViewModel.
  • Иногда нам нужно проверить последовательность вызовов класса или классов. Например, это могут быть состояния видов, один вид показан, а другой скрыт. Кроме того, нам может потребоваться проверить, был ли вызван один метод класса, а другой нет.
  • Было бы здорово повторно использовать код в модульных тестах.

Итак, позвольте мне сузить список всех трудностей до следующих вопросов:

  • Как справиться со сложностью проверки состояния ViewModel?
  • Как справиться со сложностью создания экземпляра ViewModel?
  • Как справиться со сложностью взаимодействия с ViewModels?

Но прежде чем я начну отвечать на эти вопросы, я хотел бы сказать пару вещей:

  • Эта статья о том, как организовать тесты для 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.

Пример

Рассмотрим следующий пример:

  1. Данные ViewModel загружаются с ошибкой.
  2. Пользователь нажимает кнопку «Повторить попытку»
  3. Данные ViewModel успешно загружены и показаны.

Напишем класс Cases:

А тест выглядит так:

Преимущества наличия класса Cases:

  • Это позволяет повторно использовать логику взаимодействия между тестами.
  • Явное наименование методов класса Cases дает явное представление о том, что происходит в тесте. Таким образом, повышается читабельность.
  • Android Studio дает подсказки о том, какой регистр можно использовать.

Примеры из реального проекта

Сравните следующие два модульных теста. Первый написан небрежно. Второй пишется по подходам.

Это только 2 из 42 написанных модульных тестов.

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

Второй подход устраняет все эти недостатки.

Спасибо за прочтение!