Перейти к содержанию

Тестирование Android UI

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

Объём теста и среда выполнения

JUnit предоставляет жизненный цикл теста, проверки и правила, но сам по себе не управляет Android UI.

  • Тесты в src/test выполняются локально на JVM. Они подходят для чистого Kotlin-кода и могут использовать Robolectric, когда достаточно имитации Android-среды.
  • Тесты в src/androidTest выполняются на эмуляторе или устройстве через инструментальный раннер. Обычно здесь находятся тесты Espresso и Compose, которым нужно устройство.

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

Espresso для View

Espresso находит объекты View, выполняет действия и проверяет утверждения:

onView(withId(R.id.emailInput))
    .perform(typeText("ada@example.com"), closeSoftKeyboard())

onView(withId(R.id.loginButton)).perform(click())
onView(withText("Welcome")).check(matches(isDisplayed()))

Espresso ждёт, пока освободятся главная очередь сообщений, задачи AsyncTask и зарегистрированные экземпляры IdlingResource. Он не знает автоматически о произвольных исполнителях, callback-функциях и внешней асинхронной работе. Предпочитайте контролируемые поддельные зависимости. Если в тесте нужна реальная асинхронная операция, зарегистрируйте idling resource до её запуска и удалите регистрацию после теста. Не используйте Thread.sleep(): он замедляет тест и зависит от времени выполнения.

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

UI-тесты Compose

Тесты Compose взаимодействуют с деревом семантики, а не с иерархией View:

@get:Rule
val composeRule = createComposeRule()

@Test
fun savingProfileShowsSuccess() {
    composeRule.setContent {
        ProfileScreen(
            state = ProfileUiState(userName = "Ada"),
            onSave = { }
        )
    }

    composeRule.onNodeWithText("Save").performClick()
    composeRule.onNodeWithText("Saved").assertIsDisplayed()
}

Используйте createComposeRule(), когда тест сам задаёт содержимое. Выбирайте createAndroidComposeRule<Activity>(), когда в сценарии участвуют Activity и интеграция с Android.

Предпочитайте семантику, которая также описывает интерфейс пользователю: текст, роль, описание содержимого, выбранное состояние и описание состояния. Используйте Modifier.testTag(), если устойчивого пользовательского селектора нет, но не считайте тег заменой доступности. Если узел не находится, изучите дерево семантики и проверьте, нужны ли объединённые дочерние элементы с useUnmergedTree = true; запросы к необъединённому дереву не должны быть выбором по умолчанию.

Compose ждёт известную ему работу и учитывает тестовые часы, но не видит каждый внешний асинхронный источник. По возможности внедряйте уже завершённые или управляемые поддельные зависимости. Для наблюдаемого условия, которое действительно завершается вне синхронизации Compose, используйте waitUntil с ограниченным тайм-аутом.

Для детерминированной проверки анимации управляйте виртуальным временем:

composeRule.mainClock.autoAdvance = false
composeRule.mainClock.advanceTimeBy(300)

Надёжные UI-тесты

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

Связанные темы: Стратегия тестирования, Тестирование ViewModel, Тестирование Compose.

Источники: основы тестирования Android, Espresso, idling resources Espresso, тестирование Compose UI, синхронизация тестов Compose.