Как стать автором
Обновить
1302.6
OTUS
Цифровые навыки от ведущих экспертов

Полное руководство по тестовым дублерам в Android — Часть 2: Практика

Время на прочтение9 мин
Количество просмотров1.8K
Автор оригинала: Phellipe Silva

Моки, стабы, фейки, пустышки и шпионы на Android: от теории к (хорошей) практике

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

Как использовать тестовых дублеров в Android

Создавать тестовых дублеров на Android вручную — дело не хитрое. Вам просто нужно правильно реализовать их в соответствии с контрактом реальной зависимости (как в большинстве платформ или языков) и заменить реальный взаимодействующий объект фиктивным. Помочь нам в этом могут различные техники внедрения зависимостей.

Отличия от других технологий начинают появляться, когда речь заходит об инструментах. Для создания наших дублеров в среде Android мы можем использовать фреймворки, совместимые с языками Java и Kotlin. Среди самых известных можно отметить Mockito (для Java) и MockK (для Kotlin).

Ниже приведены несколько примеров использования MockK:

@Test
fun `Retrieve notes count from server when requested`() {
    val notesApiStub = mockk<NotesApi>() //Stub Double
    val noteRepository = NoteRepository(notesApiStub)
    val note = generateDummyNote() //Dummy Double
    every { notesApiStub.fetchAllNotes() } returns listOf(note, note)

    val allNotesCount = noteRepository.getNoteCount()

    assertEquals(expected = 2, actual = allNotesCount)
}

Создание стаба с помощью Mockk

@Test
fun `Track analytics event when creating new note`() {
    val analyticsWrapperMock = mockk<AnalyticsWrapper>() //Mock Double
    val noteAnalytics = NoteAnalytics(analyticsWrapperMock)

    noteAnalytics.trackNewNoteEvent(NoteType.Supermarket)

    //Observes that specific operation has happened
    verify(exactly = 1) { analyticsWrapperMock.logEvent("NewNote", "SuperMarket") }
}

Создание мока с помощью Mockk

Дублеры и поддержка тестов

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

Я думаю, это следует объяснить поподробнее. Теория гласит, что если мы тестируем какое-то поведение, то любая модификация, не меняющая этого поведения (скажем, рефакторинг), не должна ломать тестовый код.

@Test
fun `Retrieve notes count from server when requested`() {
    val notesApiStub = mockk<NotesApi>() //Stub Double
    val noteRepository = NoteRepository(notesApiStub)
    val note = generateDummyNote() //Dummy Double
    every { notesApiStub.fetchAllNotes() } returns listOf(note, note)

    val allNotesCount = noteRepository.getNoteCount()

    assertEquals(expected = 2, actual = allNotesCount)
}

Создание стаба с помощью Mockk

В приведенном выше примере рефакторинг NoteApi, а именно изменение метода fetchAllNotes() с целью получения нового параметра, не должно ломать NoteRepository… но, к сожалению, приведенный выше пример сломается, так как тест использует NoteApi как часть конфигурации MockK (строка 6). Если вы представите себе несколько тестов, использующих одну и ту же конфигурацию в нескольких частях кода, у нас потенциально может возникнуть несколько поломанных рефакторингом одного класса тестов, что неизменно приводит к лишним накладным расходам и затратам на обслуживание для каждого рефакторинга.

С другой стороны у нас есть дублеры, настроенные вручную, или дублеры, которые используют тот же контракт, что и реальный класс, для настройки. У них есть недостаток, заключающийся в потенциальном увеличении изначального размера кодовой базы, но также есть преимущество в том, что они не пропускают (leak) детали реализации в тесты, поскольку они не зависят от какой-либо инструментной магии. В долгосрочной перспективе это потенциально улучшает поддерживаемость.

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

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

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

Хочу отметить, что я выступаю не против инструментов, а против их неправильного использования, как сказано в твите (пост от Рафы Араужо, описывающее плохое использование MockK сообществом Android. Как тестировать на Android):

«Каждый { call(any(),any(),any(),any(),any(),any()) } просто запускает 

verify(exactly = 1) { call() }

Печально, что в сообществе Android укоренилась культура использования моков»

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

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

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

Использование тестовых дублеров в неинструментальных тестах

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

Чтобы продемонстрировать использование тестовых дублеров в среде Android, давайте посмотрим, как будет выглядеть приложение в MVVM-архитектуре:

Пример MVVM-архитектуры. Зеленым цветом окрашены компоненты, которые обычно зависят от платформы Android (View и локальный источник данных) для выполнения своих тестов. Синим цветом отмечены компоненты, которые обычно не зависят от платформы Android (ViewModel, репозиторий и удаленный источник данных) для выполнения своих тестов.
Пример MVVM-архитектуры. Зеленым цветом окрашены компоненты, которые обычно зависят от платформы Android (View и локальный источник данных) для выполнения своих тестов. Синим цветом отмечены компоненты, которые обычно не зависят от платформы Android (ViewModel, репозиторий и удаленный источник данных) для выполнения своих тестов.

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

Пример того, как можно применять тестовых дублеров в MVVM на неинструментальном уровне.
Пример того, как можно применять тестовых дублеров в MVVM на неинструментальном уровне.

В приведенной выше стратегии мы используем следующие рассуждения:

Тестирование View

Тесты слоя View не очень распространены в неинструментальной части. Для их создания нам, вероятно, потребуется создать Robolectric, который, как известно, медленнее, чем чистые Java/Kotlin тесты.

Если вы тестируете свои View, поскольку мы имеем дело с дорогостоящим тестом, я рекомендую не использовать дублеров для ViewModel, а использовать для репозитория. Причина в том, что ViewModels (по своей структуре) являются компонентами, тесно связанными с View, и поэтому поведение View было бы лучше представлено, если бы мы использовали реальные ViewModel. Кроме того, мы не получим большого выигрыша в скорости, если мы будем использовать дублеры для ViewModel, этот тест все равно был бы медленным. В этом конкретном примере можно было бы заменить стабом репозиторий.

Тестирование ViewModel

Чтобы протестировать слой ViewModel, мы вполне можем выполнить модульный тест вне Android и заменить репозиторий стабом. Другие зависимости ViewModel также могут быть заменены тестовыми дублерами.

Тестирование репозитория (Repository)

Чтобы протестировать уровень репозитория, мы также можем выполнить модульный тест вне Android. Здесь мы заменяем локальный и удаленный источники данных на стабы или фейки.

Тестирование удаленного источника данных (Remote Data Source)

Чтобы протестировать слой удаленного источника данных, нам, вероятно, придется заменить каким-нибудь дублером серверную часть. Самый простой способ — создать фейк с помощью MockWebServer, который будет имитировать HTTP-сервер, отвечающий за возврат предварительно настроенных ответов. Тесты на этом уровне полезны для подтверждения того, что сериализация ответов бэкенда работает правильно.

Тестирование локального источника данных (Local Data Source)

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

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

Использование дублеров в инструментальных тестах

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

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

Также следует обратить внимание на то, что большинство инструментов (например, MockK или Mockito) манипулируют байт-кодом для создания моков и стабов. Поскольку Android использует собственную виртуальную машину и генерирует байт-код в определенном формате (Dalvik), некоторые из этих инструментов имеют свои ограничения на создание дублеров в этом инструментальном слое. По этим причинам я не советую использовать дублеры в инструментальных тестах. За исключением двух ситуаций:

  • Когда нужно заменить зависимости от границ ввода-вывода (I/O boundaries), таких как бэкенды и базы данных, более быстрыми и более детерминированными дублерами. Например, используя такие инструменты, как MockWebServer.

  • Или заменить сторонние инструменты, которые сложно конфигурировать в инструментальных тестах, например классы Firebase.

Стратегия использования тестовых двойников в инструментальных тестах
Стратегия использования тестовых двойников в инструментальных тестах

Инструменты внедрения зависимостей, такие как Dagger, Hilt или Koin, и Product Flavors могут быть отличными союзниками при использовании тестовых дублеров на инструментальном уровне.

Сообщество Android и тестовые дублеры

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

Взаимосвязь между хрупкостью тестов и рефакторингом — одна из самых распространенных тем, которые вам предстоит увидеть. Вот всего несколько примеров:

1) Твит Райана Хартера, рекламирующий его (отличную) статью о замене моков на фейки. Возможно, термин «шпион» (вместо «фейк») был бы более уместен в его статье:

После хорошего обсуждения с @HandstandSam и другими, я написал небольшой пост о своем подходе к замене моков на фейки.

Отвечая @HandstandSam: Мне любопытно, почему вы вообще решили использовать mockito в этой ситуации. Это выглядит как довольно стандартный фейк, который можно реализовать с 3 свойствами, чтобы вообще не использовать мок-инстанс и просто проверять состояние.

2) Твит Зака ​​Свирса, явно неудовлетворенного тем, как Mockito создает тестовых дублеров, что, по его мнению, способствует хрупкости тестов:

«Помогите, я попал в ад»:

3) Пост Джейка Уортона, выступающего против использования моков, вероятно, в соответствии с классическим подходом к тестированию:

Отказ от моков сэкономило мне огромное количество времени при рефакторинге. Вместо обновления 100 скриптов при изменении деталей реализации я обновляю один тест изменения наблюдаемого поведения.

Заключение

Теперь вы должны быть в состоянии ответить или, по крайней мере, понять вопросы, заданные во введении к первой статье:

  • «Нам просто нужно мокнуть эту зависимость, и все будет работать нормально»

  • «Избегайте моков!»

  • «Моки или стабы?»

  • «Предпочитаю мокам фейки”

Тестовые дублеры (также зачастую называемые моками) очень важны в рамках ваших стратегий тестирования. Хорошо изучите их концепции, разберитесь в связанных с ними компромиссах и, таким образом, вы сможете сделать тестирование ваших Android-приложений масштабируемым и в то же время простым.

Ссылки


Уже завтра вечером в OTUS состоится открытое занятие «UI Profiling. Обзор возможностей тестирования производительности приложений и инструментов оптимизации». На нем разберёмся в том, что такое «тормозящее приложение», рассмотрим основные причины такого поведения, и инструменты, призванные найти и исправить эту проблему. Но мало понять, какова производительность приложения на вашем устройстве. Поэтому мы рассмотрим несколько сервисов, позволяющих измерить производительность в бою — на телефонах ваших пользователей. Присоединяйтесь, perf matters! Регистрация по ссылке.

Теги:
Хабы:
Всего голосов 10: ↑6 и ↓4+2
Комментарии0

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS