Знакомство с 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 обычно проходят по следующей схеме:
- Запуск активности: тесты Espresso нуждаются в активности, с которой они будут взаимодействовать. Вы можете запустить активность, которую хотите протестировать, используя
ActivityScenario
илиActivityTestRule
(устаревший вариант). - Поиск элементов представления:
XML-представления: используйте средства сопоставления представлений Espresso для поиска элементов в вашем UI.
Jetpack Compose: используйтеAndroidComposeTestRule
для нахождения нужного узла. - Выполнение действий: имитируйте взаимодействие пользователя с элементами пользовательского интерфейса, например, щелчки или ввод текста (необязательный момент, если мы хотим только подтвердить видимость).
- Состояния представления утверждений: убедитесь, что пользовательский интерфейс находится в ожидаемом состоянии после взаимодействий.
Библиотеки Assertion(утверждений) и Matchers(матчеры)
При тестировании пользовательского интерфейса библиотеки Assertion и matchers играют решающую роль в проверке того, что приложение ведет себя так, как ожидается, после определенных взаимодействий. Хотя JUnit и Espresso предоставляют базовые утверждения (assertion) и matchers, использование специализированных библиотек может сделать ваши тесты более выразительными и читабельными.
Вот несколько вариантов, которые вы можете рассмотреть после того, как освоите написание UI-тестов:
- Kotest – мощный и гибкий фреймворк тестирования для Kotlin. В нем реализовано множество стилей тестирования, богатый набор assertion и выразительные matchers. Kotest поддерживает разработку на основе поведения (BDD) и тестирование на основе свойств, а также другие парадигмы. Этот фреймворк хорошо интегрируется с Espresso для тестирования пользовательского интерфейса.
- Kakao – оболочка Kotlin DSL для Espresso, предлагающая более идиоматический Kotlin-способ написания UI-тестов с более простым и удобочитаемым синтаксисом.
- 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. После этого для нас будет сгенерирован пустой тестовый класс.
Написание базового теста
Давайте напишем тест, который:
- Запускает
MainActivity
. - Находит поле поиска на экране по его тестовому тегу (или идентификатору для представлений XML).
- Вводит строку.
- Утверждает, что поле поиска содержит ожидаемый текст.
Версия 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-тесты часто предполагают тестирование нескольких элементов на одном экране (что также повышает эффективность тестирования за счет сокращения времени инициализации), длинная, повторяющаяся цепочка из onNodeWithTag
, onView
и различных вызовов функций утверждения может сделать наши тесты менее читабельными.
Robot Pattern – это шаблон проектирования в UI-тестировании, который помогает абстрагировать взаимодействие с элементами пользовательского интерфейса, делая тесты более читаемыми и удобными для сопровождения.
? При написании тестов такая абстракция помогает нам сосредоточиться на том, что тестируется, а не на том, как это тестируется.
Этот шаблон предполагает создание отдельного класса “robot” для каждой основной части пользовательского интерфейса.
Преимущества Robot Pattern
- Читабельность – тесты становятся более читабельными и напоминают пользовательские истории, что облегчает понимание того, что делает тест.
- Возможность повторного использования – общие взаимодействия и утверждения (assertions) можно повторно использовать в разных тестах.
- Удобство обслуживания – изменения пользовательского интерфейса требуют обновления только в одном месте, что сокращает усилия, необходимые для обслуживания.
Создание базового класса robot
- Начните с определения компонентов или экранов в приложении, которое мы будем тестировать.
- Для каждого компонента или экрана создайте соответствующий класс 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-тестов.
Определение общих задач/функций
- Анализ взаимодействия с пользователями – рассмотрите общие задачи, которые пользователи выполняют в нашем приложении, и ожидаемые результаты.
- Для каждой задачи создайте метод в соответствующем классе 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».