Давайте подумаем насчет UI-тестирования

UI-тесты реально помогают, когда нет ресурса тестировщиков. А если тестировщика нет совсем, то вручную перетестировать приложение каждый раз практически невозможно. Но дело не только в этом. После длительной разработки пользоваться своим приложением становится тяжело - просто не хочется. Есть похожая мысль в геймдеве: когда ты любишь игры, но начинаешь их делать, ты перестаешь их любить. Тут то же самое. Обычно в корпоративной разработке выбирают инструменты вроде Kaspresso: в коде выставляешь test tag, по нему находишь элемент и взаимодействуешь с ним.

Button(
    modifier = Modifier.testTag("login_button"),
    onClick = { }
) { 
    Text("Войти")
}

Тесты обычно пишутся в папке androidTest, то есть они максимально близки к кодовой базе. Для запуска используют Marathon - дальше остается только настроить окружение, и все готово. Но есть нюанс: такие тесты хоть и хороши, стабильны и близки к коду, время на поддержку уходит существенное.

Что же в этом случае делать?

Допустим, у нас небольшое приложение: тестировщиков нет (или их очень мало), и тратить время на тестирование не хочется - все уходит в разработку. Kaspresso хоть и работает отлично, но с ним есть определенные сложности, и для написания таких тестов нужен соответствующий уровень. После попытки внедрить Kaspresso в небольшой проект я на себе ощутил, насколько тяжело писать и поддерживать такие тесты. Нужно что-то полегче. Погуглив и посмотрев варианты, найден Maestro инструмент для UI-тестов с очень низким порогом входа.

Он работает так: у тебя есть отдельное приложение (Maestro Studio), ты подключаешься к эмулятору и прямо в реальном времени выбираешь компоненты - кнопки, текст и так далее. Ты пользуешься приложением как обычно, действия записываются в шаги, и тест готов. Вот так выглядит пример написанных тестов:

Пример Maestro Studio - запись UI-тестов и шагов сценария

Пример Maestro Studio - запись UI-тестов и шагов сценария

В целом он работает замечательно, нюансов почти нет. Но возникает вопрос: как этот инструмент поведет себя в больших масштабах? Мне показалось, что он не особо расширяем. Например, в Kaspresso ты можешь из кодовой базы через DI менять какие-то состояния и управлять окружением теста. А здесь у меня так сделать не получилось и это как раз существенный минус. Но для небольшого проекта этот инструмент работает на 100%. Но проект придется немного подготовить: в тестах приложение должно работать на мок-источниках (фейки/стабы), а не на реальных данных.

Подходы к мокам и тестовому окружению

Давайте возьмем самые популярные подходы:

  • Отдельный mock-модуль / mock-flavor
  • Mock Web Server (OkHttp MockWebServer)

Примеры статей по подходам:

Без адаптации приложения и кода под тесты делать UI-тесты, кажется, будет ошибкой. Сейчас не будем подробно на этом останавливаться - тема уже много раз обсуждалась в сообществе. Выбери подход, который тебе ближе, и наверное легче внедрить, лучше сделать после изучения материала. Я выбрал mock-модуль и реализую репозитории под мок-объекты. Состояния менять не собираюсь - мне нужно проверить базовую работоспособность: запуск экранов и первые состояния, которые замоканы в mock repository. Для небольшого проекта этого достаточно.

Пример структуры проекта с mock-модулем

Пример структуры проекта с mock-модулем

Визуально это выглядит так: data-модуль заменяется и подставляется через build variant. Если запустить приложение, оно не будет ходить в интернет - все будет работать локально. Не будем останавливаться с этим все понятно: проект подготовлен под UI-тесты.

Пример файлов тестов Maestro

Пример файлов тестов Maestro

Допустим, у нас есть файлы тестов, которые можно запускать в Maestro UI. Если гонять их вручную все будет работать отлично. Но есть нюанс: со временем ты начнешь забывать их запускать или просто не так частно как нужно. Уверяю, забудешь. Или станет лень. Проверено. Решение очевидное - CI/CD, который при вливании кода прогоняет тесты. Maestro спокойно умеет это через CLI. Но если сейчас я скажу: настройте CI/CD, настройте пайплайн - это уже не про легкий вход, а будет тот еще пранк. Но нет. Выбираем решение проще.

Pre-push хук для UI-тестов

Что если я хочу запускать на pre-push мои тесты? Есть аргумент против: тесты будут долго запускаться и это будет мешать разработке. И это правда. Но если прогонять только основной тест-кейс, для небольшого проекта этого будет достаточно. Это реально дождаться около 5 минут. Давайте обсудим это подробнее. Что нужно для запуска тестов на pre-push? Для этого напишем скрипт и положим его в pre-push.

Требования к скрипту:

  • проверяет наличие Android SDK, adb, emulator, Maestro, scrcpy
  • запускает эмулятор, если он не запущен
  • ждет, пока система полностью загрузится
  • билдит новый APK
  • находит сгенерированный APK по указанному пути
  • устанавливает APK на эмулятор с помощью adb install
  • запускает тестовые фалы Maestro из указанного каталога
  • логирование результата и запись видео

Это позволит, по сути, не зависеть от CI/CD и сделать локальную реализацию по смыслу результат будет тот же. Если у вас командная разработка, CI/CD имеет смысл настраивать полноценно. Но если вы работаете один, это часто трудозатратно. Поэтому напишем такой скрипт. Я выбираю sh для простоты: под Android и управление эмулятором есть много готовых примеров.

Схема работы скрипта:

Схема работы скрипта

Схема работы скрипта

Pre-push hook и используемый в нем скрипт:

Показать pre-push и скрипт

Параметры позволяют гибко настраивать окружение: путь к Android SDK, Maestro, AVD, каталог с тестами, Gradle-таску сборки и путь до APK.

& "C:\Program Files\Git\bin\bash.exe" scripts/maestro_emulator_check.sh `
    --sdk "C:\Users\codin\AppData\Local\Android\Sdk" `
    --maestro "C:\Users\codin\.maestro\bin\maestro" `
    --avd Pixel_6_Pro `
    --flows testMaestro/mock `
    --gradle-task assembleRustoreMockDebug `
    --apk app/build/outputs/apk/rustoreMock/debug/app-rustore-mock-debug.apk

Пример выше показан для Windows, но сам скрипт должен запускается и на macOS и Linux.

Параметры скрипта

--sdk SDK_PATH
Путь к Android SDK. Если не указан, скрипт ищет SDK по умолчанию:
Windows - ~/AppData/Local/Android/Sdk.

--maestro MAESTRO_PATH
Путь к исполняемому файлу Maestro (maestro или maestro.bat).
Если не задан - используется версия из PATH.

--avd AVD_NAME
Имя AVD. По умолчанию - Pixel_6_Pro.
Используется для запуска или проверки эмулятора, а также для маркировки отчетов. Создайте AVD заранее через Android Studio.

--flows DIR (алиас --maestro-dir)
Директория с Maestro flow-тестами.
По умолчанию - testMaestro/mock.

--gradle-task TASK
Gradle-задача для сборки APK.
По умолчанию - assembleRustoreMockDebug.

--apk APK_PATH
Путь к собранному APK.
По умолчанию - app/build/outputs/apk/rustoreMock/debug/app-rustore-mock-debug.apk.

--help или -h
Выводит встроенную справку по параметрам скрипта.

Пример отчета Maestro

Пример отчета Maestro

Все результаты прогона сохраняются в директорию maestro-report/<AVD_NAME>-/, где лежат логи запуска, HTML-отчет Maestro, debug-артефакты, видео выполнения тестов.

Результат

Когда вы работаете в одиночку, тяжелые инструменты начинают тянуть вниз. Поддержка Kaspresso, стабильных id - на это уходит время, за которое вы могли бы писать фичи. Maestro в моем случае - легкая подстраховка. Это страховка на “экран не открылся”, “кнопка не нажимается” - именно такие риски он закрывает. Настройка и использование Kaspresso в одиночном проекте займут много времени и не всегда окупятся. Maestro уже при первых пушах изменений дал мне то спокойствие: меняешь UI, а базовые сценарии все равно остаются рабочими. Для небольшого проекта этого достаточно поверхностная проверка, быстрый прогон, минимум поддержки. Но если вам нужны сложные сценарии, глубокая проверка логики, контроль и состояний приложения - тяжелые инструменты уже оправданы. Главное выбирать инструмент под задачу. Не стоит заранее городить огромную тестовую инфраструктуру, если проект этого не требует.