Настройка первого UI-теста на Android

Знакомство с Espresso и Robot Pattern

Если вам не терпится начать и завершить свой первый тест пользовательского интерфейса, эта статья предназначена для вас. Мы погрузимся в Espresso, мощный инструмент для написания UI-тестов в Android. Также мы познакомимся с “Robot Pattern” – шаблоном проектирования, который улучшает читаемость и сопровождаемость UI-тестов.

Скачать одну из самых популярных книг по тестированию "Как тестируют в Google"

Зачем нужно тестирование пользовательского интерфейса?

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

Разница между инструментальными и пользовательскими тестами

UI-тесты – это подмножество инструментальных тестов. Хотя все тесты пользовательского интерфейса являются инструментальными, не все инструментальные тесты являются UI-тестами.

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

Тесты пользовательского интерфейса

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

  • К распространенным инструментам тестирования пользовательского интерфейса Android относятся Espresso и UI Automator. Espresso больше подходит для тестирования в контексте приложения, в то время как UI Automator может взаимодействовать с несколькими приложениями или системой. Еще один вариант называется Robolectric, который полагается исключительно на JVM.
  • Тесты выполняются таким образом, что имитируют взаимодействие реального пользователя с приложением. Они могут нажимать на кнопки, вводить текст, прокручивать списки и т. д.
  • Эти тесты нацелены на пользовательский опыт, гарантируя, что приложение выглядит и ощущается так, как и ожидается, когда пользователь взаимодействует с ним.

Инструментальные тесты

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

  • Библиотека поддержки тестирования Android предоставляет набор API для написания инструментальных тестов. Сюда входят упомянутые выше Espresso и UI Automator, а также другие инструменты, такие как JUnit, для базовой функциональности тестирования.
  • Эти тесты запускаются на устройстве или в эмуляторе, поскольку могут требовать доступа к специфическим для Android API, аппаратным датчикам или пользовательским данным.
  • Сфера применения таких тестов шире, чем у тестов пользовательского интерфейса. Они могут включать тестирование взаимодействия с базами данных, операций с файловой системой, интеграции с другими приложениями или сервисами Android и многое другое.

Настройки проекта по умолчанию

Когда мы создаем новый проект в Android Studio, в него автоматически включаются зависимости по умолчанию и примеры тестовых классов. Такая настройка мгновенно подготавливает проект к запуску модульных и инструментальных тестов.

Зависимости по умолчанию

Предположим, мы используем Kotlin DSL вместо Groovy для наших скриптов сборки Gradle. В файле проекта build.gradle.kts (Модуль: приложение) Android Studio включает зависимости по умолчанию для запуска и тестирования нашего приложения. Для набора исходных текстов androidTestImplementation они обычно включают как минимум следующее:

androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")

? Если мы используем Jetpack Compose в нашем проекте, мы можем ожидать включения еще нескольких зависимостей по умолчанию.

Примеры тестовых классов

В каталоге app/src/androidTest/ мы можем обнаружить пример класса UI-теста под названием ExampleInstrumentedTest. Этот класс представляет собой базовый пример того, что может включать в себя инструментальный тест.

Предположим, что мы уже настроили тестовое устройство в Android Studio. В таком случае инициировать тест будет просто, достаточно нажать зеленую кнопку в форме треугольника рядом с объявлением класса.

? Как уже говорилось ранее, это инструментальный тест, а не тест пользовательского интерфейса. Ниже мы рассмотрим настройку нашего первого UI-теста.

Базовая настройка Espresso

Анимация пользовательского интерфейса может привести к нестабильным результатам UI-тестирования. Мы можем отключить ее на время проведения тестов.

Отключение анимации – обновить app/build.gradle.kts

android {
    ...
    testOptions {
        animationsDisabled = true
    }
    ...
}

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

Тесты Espresso обычно проходят по следующей схеме:

  1. Запуск активности: тесты Espresso нуждаются в активности, с которой они будут взаимодействовать. Вы можете запустить активность, которую хотите протестировать, используя ActivityScenario или ActivityTestRule(устаревший вариант).
  2. Поиск элементов представления:
    XML-представления: используйте средства сопоставления представлений Espresso для поиска элементов в вашем UI.
    Jetpack Compose: используйте AndroidComposeTestRule для нахождения нужного узла.
  3. Выполнение действий: имитируйте взаимодействие пользователя с элементами пользовательского интерфейса, например, щелчки или ввод текста (необязательный момент, если мы хотим только подтвердить видимость).
  4. Состояния представления утверждений: убедитесь, что пользовательский интерфейс находится в ожидаемом состоянии после взаимодействий.

Библиотеки Assertion(утверждений) и Matchers(матчеры)

При тестировании пользовательского интерфейса библиотеки Assertion и matchers играют решающую роль в проверке того, что приложение ведет себя так, как ожидается, после определенных взаимодействий. Хотя JUnit и Espresso предоставляют базовые утверждения (assertion) и matchers, использование специализированных библиотек может сделать ваши тесты более выразительными и читабельными.

Вот несколько вариантов, которые вы можете рассмотреть после того, как освоите написание UI-тестов:

  1. Kotest – мощный и гибкий фреймворк тестирования для Kotlin. В нем реализовано множество стилей тестирования, богатый набор assertion и выразительные matchers. Kotest поддерживает разработку на основе поведения (BDD) и тестирование на основе свойств, а также другие парадигмы. Этот фреймворк хорошо интегрируется с Espresso для тестирования пользовательского интерфейса.
  2. Kakao – оболочка Kotlin DSL для Espresso, предлагающая более идиоматический Kotlin-способ написания UI-тестов с более простым и удобочитаемым синтаксисом.
  3. Hamcrest – библиотека, которая хорошо работает с JUnit, предлагая множество выразительных объектов matchers. Она основана на Java, но может быть использована в UI-тестах на Kotlin. В свое время она была популярной библиотекой, используемой в Android-Java проектах.

Наш первый UI-тест с Espresso

Теперь, когда Espresso настроен, давайте напишем наш первый тест пользовательского интерфейса.

? Здесь мы проводим тестирование пользовательского интерфейса на уровне активности. С помощью Jetpack Compose можно также тестировать отдельные компоненты.

Тестирование экрана пользовательского интерфейса, независимо от того, разработан ли он с помощью представлений XML или Jetpack Compose, может проходить по схожим схемам. Ключевое различие заключается в подходе к размещению элементов пользовательского интерфейса для тестирования:

  • В представлении XML, где элементам могут быть назначены идентификаторы, мы можем использовать withId(R.id.some_element) для определения конкретного элемента.
  • В Jetpack Compose существуют различные методы навигации по дереву узлов для поиска нужного элемента. Хотя некоторые могут выбрать свойства семантики для идентификации элемента, для простоты в нашем подходе мы используем testTag.

Допустим, у нас есть функция Composable для MainActivity, и мы хотим написать для нее наш первый UI-тест:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApplicationTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    SearchField("Android")
                }
            }
        }
    }
}

@Composable
fun SearchField(name: String, modifier: Modifier = Modifier) {
    val text = remember { mutableStateOf("") }

    TextField(
        value = text.value,
        onValueChange = { text.value = it },
        modifier = Modifier.testTag("searchField")
    )
}

? Для подхода с представлениями XML представим, что у нас есть макет, который включает EditText, с его идентификатором, установленным как R.id.search_field.

Создание тестового класса

Самый удобный способ создать тестовый класс — щелкнуть правой кнопкой мыши имя класса, для которого вы хотите написать тесты, выбрать Generate… Tests…, после чего заполнить необходимые данные.

создание тестового класса
  • Для UI-тестов, по причине постоянных проблем с совместимостью между Android Gradle Plugin и JUnit5, мы должны выбрать JUnit4 в качестве нашей библиотеки тестирования.
  • При появлении диалогового окна, сообщающего об отсутствии библиотеки JUnit4 в модуле, просто нажмите кнопку ‘Fix’. Это позволит IDE добавить необходимую зависимость и автоматически обновить проект.
  • Поскольку тесты пользовательского интерфейса не нацелены на конкретную функцию, в данном контексте нет необходимости выбирать какие-либо элементы.
создание тестового класса

Мы выбираем каталог AndroidTest в качестве места расположения нашего тестового класса UI. После этого для нас будет сгенерирован пустой тестовый класс.

выбор каталога

Написание базового теста

Давайте напишем тест, который:

  1. Запускает MainActivity.
  2. Находит поле поиска на экране по его тестовому тегу (или идентификатору для представлений XML).
  3. Вводит строку.
  4. Утверждает, что поле поиска содержит ожидаемый текст.

Версия Jetpack Compose:

@get:Rule
val composeTestRule = createAndroidComposeRule<MainActivity>()

@Test
fun userCanEnterTextInSearchField() {
    // Launch the MainActivity
    val scenario = ActivityScenario.launch(MainActivity::class.java)

    // Interact with the Compose element
    composeTestRule.onNodeWithTag("searchField")
        .performTextInput("Hello, Espresso!")
    composeTestRule.onNodeWithTag("searchField")
        .assertTextEquals("Hello, Espresso!")
}

Версия представлений XML:

@Test
fun userCanEnterTextInSearchField() {
    // Launch the MainActivity
    ActivityScenario.launch(MainActivity::class.java)
    // Find the search field and type in a query
    onView(withId(R.id.search_field)).perform(typeText("Hello, Espresso!"))
    // Assert that the query is indeed in the search field
    onView(withId(R.id.search_field)).check(matches(withText("Hello, Espresso!")))
}

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

Реализация Robot Pattern

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

Robot Pattern – это шаблон проектирования в UI-тестировании, который помогает абстрагировать взаимодействие с элементами пользовательского интерфейса, делая тесты более читаемыми и удобными для сопровождения.

? При написании тестов такая абстракция помогает нам сосредоточиться на том, что тестируется, а не на том, как это тестируется.

Этот шаблон предполагает создание отдельного класса “robot” для каждой основной части пользовательского интерфейса.

Преимущества Robot Pattern

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

Создание базового класса robot

  1. Начните с определения компонентов или экранов в приложении, которое мы будем тестировать.
  2. Для каждого компонента или экрана создайте соответствующий класс robot. Этот класс будет содержать методы для взаимодействия и утверждения (assertions), характерные для данного компонента.

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

class LoginRobotCompose(
    private val composeTestRule: ComposeTestRule,
) {
    fun enterUsername(username: String) {
        composeTestRule.onNodeWithTag(testTag = "username")
            .performTextInput(text = username)
    }

    fun enterPassword(password: String) {
        composeTestRule.onNodeWithTag(testTag = "password")
            .performTextInput(text = password)
    }

    fun clickLogin() {
        composeTestRule.onNodeWithTag(testTag = "loginButton")
            .performClick()
    }

    fun checkLoginSuccess() {
        composeTestRule.onNodeWithTag(testTag = "loginStatus")
            .assertTextEquals("Success")
    }
}
fun login(composeTestRule: ComposeTestRule, func: LoginRobotCompose.() -> Unit) {
    LoginRobotCompose(composeTestRule).apply { func() }
}

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

class LoginRobotXML {
    fun enterUsername(username: String) {
        onView(withId(R.id.username)).perform(typeText(username))
    }

    fun enterPassword(password: String) {
        onView(withId(R.id.password)).perform(typeText(password))
    }

    fun clickLogin() {
        onView(withId(R.id.login_button)).perform(click())
    }

    fun checkLoginSuccess() {
        onView(withId(R.id.login_status)).check(matches(withText("Success")))
    }
}
fun login(func: LoginRobotXML.() -> Unit) = LoginRobotXML().apply { func() }

Использование Robot Pattern в тестах

При использовании Robot Pattern наши тестовые методы становятся последовательностями вызовов классов robot:

@Test
fun successfulLoginXML() {
    ActivityScenario.launch(LoginActivity::class.java)
    login {
        enterUsername(username = "user")
        enterPassword(password = "12345")
        clickLogin()
        checkLoginSuccess()
    }
}

@Test
fun successfulLoginCompose() {
    login(composeTestRule) {
        enterUsername(username = "user")
        enterPassword(password = "12345")
        clickLogin()
        checkLoginSuccess()
    }
}

После сокрытия реализации теста тестовая функция становится простой, читаемой и понятной.

Создание тестов пользовательского интерфейса с помощью Robot Pattern

Теперь у нас есть базовое понимание реализации этого шаблона. Давайте применим его для создания более полных UI-тестов.

Определение общих задач/функций

  1. Анализ взаимодействия с пользователями – рассмотрите общие задачи, которые пользователи выполняют в нашем приложении, и ожидаемые результаты.
  2. Для каждой задачи создайте метод в соответствующем классе Robot. Эти методы могут связывать действия и утверждения, чтобы имитировать взаимодействие с пользователем.

Написание теста с несколькими взаимодействиями

Этот сценарий особенно распространен в Journey Tests, где в рамках одной тестовой функции проверяется серия экранов. Используя несколько классов Robot, мы можем четко понять, что именно тестируется, не вникая во все aasertions и matchers.

Вот пример более сложного теста с использованием Robot Pattern:

@Test
fun userCompletesProfileAfterLogin() {
    ActivityScenario.launch(MainActivity::class.java)
    login {
        enterUsername(username = "some-user")
        enterPassword(password = "some-password")
        clickLogin()
    }

    profile {
        enterBio(text = "Hello, this is my profile")
        submitProfile()
        checkProfileUpdated()
    }
}

Этот тест имитирует вход пользователя в систему, а затем заполнение его профиля, используя двух разных классов Robot для обработки взаимодействия на экране входа в систему и на экране профиля.

Лучшие практики и советы

При написании тестов пользовательского интерфейса с помощью Espresso, Robot Pattern и таких инструментов, как Hilt, важно следовать хорошим практикам, чтобы наши тесты были надежными, поддерживаемыми и эффективными.

1. Используйте описательные названия тестов. Выбирайте названия, которые четко описывают, что делает тест. Так легче понять назначение теста с первого взгляда.

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

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

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

5. Регулярно проводить рефакторинг тестов. По мере развития приложения должны развиваться и тесты. Регулярно пересматривайте и рефакторите тесты, чтобы сохранить их актуальность и эффективность.

Заключение

Мы только что сделали важный шаг в освоении UI-тестирования в Android. Теперь мы должны понимать, как настроить Espresso и использовать Robot Pattern в тестах пользовательского интерфейса. Далее мы можем углубиться в создание различных тестовых сценариев.

Учитывая тот факт, что UI-тесты обычно требуют больше времени для написания и выполнения, важно последовательно оценивать, можно ли эффективно протестировать определенные функции с помощью модульных или интеграционных тестов.

Перевод статьи «Setting Up the First Android UI Test».

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *