Давайте подумаем насчет UI-тестирования
UI-тесты реально помогают, когда нет ресурса тестировщиков. А если тестировщика нет совсем, то вручную перетестировать приложение каждый раз практически невозможно. Но дело не только в этом. После длительной разработки пользоваться своим приложением становится тяжело - просто не хочется. Есть похожая мысль в геймдеве: когда ты любишь игры, но начинаешь их делать, ты перестаешь их любить. Тут то же самое. Обычно в корпоративной разработке выбирают инструменты вроде Kaspresso: в коде выставляешь test tag, по нему находишь элемент и взаимодействуешь с ним.
Button(
modifier = Modifier.testTag("login_button"),
onClick = { }
) {
Text("Войти")
}
Тесты обычно пишутся в папке androidTest, то есть они максимально близки к кодовой базе. Для запуска используют Marathon - дальше остается только настроить окружение, и все готово. Но есть нюанс: такие тесты хоть и хороши, стабильны и близки к коду, время на поддержку уходит существенное.
Что же в этом случае делать?
Допустим, у нас небольшое приложение: тестировщиков нет (или их очень мало), и тратить время на тестирование не хочется - все уходит в разработку. Kaspresso хоть и работает отлично, но с ним есть определенные сложности, и для написания таких тестов нужен соответствующий уровень. После попытки внедрить Kaspresso в небольшой проект я на себе ощутил, насколько тяжело писать и поддерживать такие тесты. Нужно что-то полегче. Погуглив и посмотрев варианты, найден Maestro инструмент для UI-тестов с очень низким порогом входа.
Он работает так: у тебя есть отдельное приложение (Maestro Studio), ты подключаешься к эмулятору и прямо в реальном времени выбираешь компоненты - кнопки, текст и так далее. Ты пользуешься приложением как обычно, действия записываются в шаги, и тест готов. Вот так выглядит пример написанных тестов:
Пример Maestro Studio - запись UI-тестов и шагов сценария
В целом он работает замечательно, нюансов почти нет. Но возникает вопрос: как этот инструмент поведет себя в больших масштабах? Мне показалось, что он не особо расширяем. Например, в Kaspresso ты можешь из кодовой базы через DI менять какие-то состояния и управлять окружением теста. А здесь у меня так сделать не получилось и это как раз существенный минус. Но для небольшого проекта этот инструмент работает на 100%. Но проект придется немного подготовить: в тестах приложение должно работать на мок-источниках (фейки/стабы), а не на реальных данных.
Подходы к мокам и тестовому окружению
Давайте возьмем самые популярные подходы:
- Отдельный mock-модуль / mock-flavor
- Mock Web Server (OkHttp MockWebServer)
Примеры статей по подходам:
- Espresso testing with Hilt and MockWebServer (PulseLive, Medium)
- Best practices of UI testing for Android (dev.to)
Без адаптации приложения и кода под тесты делать UI-тесты, кажется, будет ошибкой. Сейчас не будем подробно на этом останавливаться - тема уже много раз обсуждалась в сообществе. Выбери подход, который тебе ближе, и наверное легче внедрить, лучше сделать после изучения материала. Я выбрал mock-модуль и реализую репозитории под мок-объекты. Состояния менять не собираюсь - мне нужно проверить базовую работоспособность: запуск экранов и первые состояния, которые замоканы в mock repository. Для небольшого проекта этого достаточно.
Пример структуры проекта с mock-модулем
Визуально это выглядит так: data-модуль заменяется и подставляется через build variant. Если запустить приложение, оно не будет ходить в интернет - все будет работать локально. Не будем останавливаться с этим все понятно: проект подготовлен под UI-тесты.
Пример файлов тестов Maestro
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-report/<AVD_NAME>-
Результат
Когда вы работаете в одиночку, тяжелые инструменты начинают тянуть вниз. Поддержка Kaspresso, стабильных id - на это уходит время, за которое вы могли бы писать фичи. Maestro в моем случае - легкая подстраховка. Это страховка на “экран не открылся”, “кнопка не нажимается” - именно такие риски он закрывает. Настройка и использование Kaspresso в одиночном проекте займут много времени и не всегда окупятся. Maestro уже при первых пушах изменений дал мне то спокойствие: меняешь UI, а базовые сценарии все равно остаются рабочими. Для небольшого проекта этого достаточно поверхностная проверка, быстрый прогон, минимум поддержки. Но если вам нужны сложные сценарии, глубокая проверка логики, контроль и состояний приложения - тяжелые инструменты уже оправданы. Главное выбирать инструмент под задачу. Не стоит заранее городить огромную тестовую инфраструктуру, если проект этого не требует.