Оригинальная публикация Меня зовут Элчин, я занимаюсь автоматизацией тестирования мобильных приложений в hh.ru и расскажу вам о том, как написать первый тест на Android . В разработке автотестов мы используем Kotlin и нативный фреймворк Kaspresso , о котором я напишу подробней в этой статье. Эта статья рассчитана на начинающих тестировщиков, но, возможно, и более опытные коллеги найдут для себя что-то полезное.
В рассказе мы будем постепенно двигаться от основ к более сложным вещам: Установим среду разработки — Android Studio Скачаем и настроим проект Научимся обращаться к элементам и напишем первый тест Разберем, как писать page object и для чего они нужны Поработаем со списками Обсудим стабильность автотестов на Kaspresso
Как подготовиться? Android Studio - основной инструмент для разработки приложений под Android, который имеет множество встроенных фич, упрощающих написание тестов. Если у вас еще не установлена Android Studio , то ее можно скачать с официального сайта Далее, нам нужен код приложения, которое мы будем тестировать. Это может быть ваш рабочий проект. Если его нет, то вы можете скачать тестовый проект-репозиторий Kaspresso, примеры из которого я буду сегодня разбирать. Сделать это можно так: открываем репозиторий Kaspresso на Github, далее меню Code - Local - Https - копировать путь : После этого открываем Android Studio , жмем Get from VSC (или File > New > Project from Version Control, если у вас уже открыт какой то проект): После клонирования проекта у нас должен открыться README файл. Как запустить приложение?Воспользуемся заготовленным для нас приложением от Kaspresso - Tutorial . Тестировать мы будем на эмуляторе, который для начала нужно создать. Для этого идем в device manager и жмём Create device : У эмуляторов много разных настроек (версия Android, разрешение экрана, объём памяти и тд), но для первого автотеста нам все это не очень важно, поэтому можно создать абсолютно любой, не меняя никаких настроек, то есть просто прокликать next - next - finish . Когда эмулятор будет создан, он будет отображаться в списке девайсов, как у меня на скриншоте выше. Далее запускаем приложение. Для этого выбираем среди конфигураций приложения (на картинке цифра 1) tutorial , среди выпадающего списка доступных устройств (цифра 2) находим созданный нами эмулятор (цифра 3). После выбора нажимаем на кнопку Run (цифра 4): После того, как проект успешно соберется, у нас должно появиться окошко с эмулятором и запущенным тестовым приложением, которое выглядит примерно так (в зависимости от созданного эмулятора): Главный экран приложения состоит из кнопок, по нажатию на которые мы сможем проверять разные действия. Возьмем в качестве сценария первого автотеста простой клик на кнопку Simple Test и проверим, что произойдет. Как это сделать?Для того, чтобы уметь делать клики и проверки, воспользуемся возможностями Kaspresso . Для этого нам нужно добавить в проект зависимости в файле build.gradle.kts (tutorial) , после чего можно будет приступать к автоматизации тестирования: androidTestImplementation("com.kaspersky.android-components:kaspresso:1.5.2")
androidTestUtil("androidx.test:orchestrator:1.4.2")
Если на момент прочтения этой статьи выйдут новые версии зависимостей, Android Studio вам предложит их выставить. После правок файл будет выглядеть вот так: Как видно, мы добавили на 34 и 35 строки нужные зависимости. Чтобы эти зависимости “подтянулись” и вступили в силу, нажимаем кнопку Sync Now . Далее создадим папку в нашем проекте, где будут лежать автотесты — жмем правой кнопкой мыши на папку src - new - directory , и в появившемся окне выбираем androidTest/kotlin : Несмотря на то, что у нас будет всего один автотест, добавим подпапку для автотестов — на реальном проекте это будет очень удобно. Для этого кликаем правой кнопкой по папке Kotlin - new - Package , и вводим название - com.kaspersky.kaspresso.tutorial.test . Далее по аналогии добавим файл для теста: папка kotlin - new - Kotlin Class/File , назовем его SimpleTest . При именовании классов автотестов хорошей практикой является добавление в конце слова Test . Часто названия состоят из двух и более слов, чтобы детальнее донести суть проверок, например ProlongateVacancyIfMoneyExistsTest . Все автотесты должны наследоваться от класса TestCase , давайте выполним это требование: В kotlin можно унаследовать класс от другого с помощью символа двоеточия в формате Класс наследник : Класс родитель . Обратите внимание, что IDE предложит нам несколько вариантов импортов, нам нужен вариант — com.kaspersky.kaspresso.testcases.api.testcase.TestCase , выберем нужное нам из списка — делаем двойной клик на отмеченное предложение. После чего, студия автоматически добавит нам импорт нужного класса с помощью ключевого слова import : Добавим в нашем классе функцию, которая будет отвечать за запуск автотеста, и пометим ее аннотацией @Test(пакет junit.framework ). Наш тест готов: import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
import org.junit.Test
class SimpleTest : TestCase() {
@Test
fun test() {
}
}
Мы уже можем его запустить и убедиться, что тест проходит, так как он пока не имеет никакой логики. Следующим шагом нужно наполнить его действиями и проверками, чтобы наш автотест начал что то проверять. Как обращаться к элементам?В автоматизации тестирования, как веб, так и мобильных приложений, использование идентификаторов (ID) элементов - это один из самых надёжных способов поиска элементов интерфейса для дальнейшей работы. Нам нужно решить, к какому элементу мы хотим обращаться, например, button или title , и найти их id . Обычно у каждого элемента в приложении есть идентификатор, по которому можно обращаться и совершать действия над нужным конкретным элементом. Для простоты рассмотрим вариант, когда все id элементов описываются прямо внутри класса автотеста, то есть в SimpleTest . Находим идентификатор нужной нам кнопки:
Разберем по шагам, что происходит на видео: Для запуска Layout inspector — инструмента для просмотра иерархии элементов приложения — должно быть запущено приложение, на экране которого мы хотим искать наши id . Запускаем его через кнопку run . После запуска открываем Layout inspector — он находится во вкладке Tools - Layout inspector . Для экономии места на экране можно свернуть вкладку Project , так как она нам пока не понадобится. Layout inspector состоит из трех вкладок: Component tree , в котором мы видим структуру открытого экрана; область с самим экраном; Attributes , в котором можно найти интересующие нас id и другие свойства элементов. На записи видно, что при нажатии на разные элементы приложения в панели Component tree можно увидеть их id и другие атрибуты в панели Attributes .
Добавляем кнопку в автотест: Чтобы автотест мог обратиться к какому-либо элементу по его id , нужно сделать import пакета модуля, в котором находится этот id в коде приложения. Выбираем кнопку с названием Simple Test , на которую хотим тапнуть в тесте, в Layout Inspector . Раскрываем строку с id во вкладке Attributes , и нажимаем на ссылку activity_main.xml . Это файл, отвечающий за верстку экрана с кнопками, где описаны все элементы. Далее нужно найти файл AndroidManifest.xml из модуля, в котором находится наша кнопка. Сделать это можно и через поиск, но в больших проектах файлов с таким названием будет несколько, а нам нужен файл для модуля main , в котором содержится файл activity_main.xml . Для этого сворачиваем layout inspector , открываем вкладку Project , и нажимаем на кнопку “мишень”, которая показывает, где в структуре нашего проекта находится открытый файл. Далее мы видим, что файл activity_main.xml находится в папке main , и для этой папки определен нужный нам AndroidManifest.xml . Открываем его и копируем название пакета. Возвращаемся в класс с автотестом, добавляем import пакета и .R на конце. R — автоматически генерируемый класс, который содержит ссылки на такие ресурсы, как макеты, изображения, строки, цвета и другие ресурсы, используемые в приложении. Как будет видно дальше, с помощью R мы будем обращаться к id элемента. В Kaspresso для взаимодействия с элементами используется удобный Kotlin DSL над Espresso , который предоставляется библиотекой Kakao. Для стандартных UI-виджетов уже создано множество готовых обёрток. Воспользуемся одной из них для поиска кнопки - нам нужен класс KButton . Мы инициализируем его блоком, внутри которого вызываем функцию для поиска виджета с помощью id нужной кнопки - withId(R.id.simple_activity_btn) . После этого вызываем у simpleButton метод click() для клика по элементу. Осталась последняя деталь, не попавшая на видео: нужно добавить правило, которое обеспечивает управление activity MainActivity в автотесте. Это нужно для возможности взаимодействия с интерфейсом этой активити в приложении, в нашем случае это наш главный экран.
Получившийся автотест будет выглядеть так: import androidx.test.ext.junit.rules.activityScenarioRule
import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
import com.kaspersky.kaspresso.tutorial.MainActivity
import com.kaspersky.kaspresso.tutorial.R
import io.github.kakaocup.kakao.text.KButton
import org.junit.Test
import org.junit.Rule
class SimpleTest : TestCase() {
@get:Rule
val activityRule = activityScenarioRule<MainActivity>()
@Test
fun test() {
val simpleButton = KButton {
withId(R.id.simple_activity_btn)
}
simpleButton.click()
}
}
Запустив по нажатию на кнопку run напротив fun test() , увидим работу теста в эмуляторе, и, что тест passed : Готово! У нас появился первый рабочий тест. Обычно в автотесте намного больше элементов и, следовательно, обращений к ним по id . Это могло бы существенно перегрузить класс автотеста и сделать его практически нечитаемым и неподдерживаемым. К тому же, у нас может быть несколько автотестов, которые будут обращаться к одним и тем же элементам, и повторно описывать эти элементы для них было бы очень неудобно и трудозатратно. Все эти проблемы решает такой паттерн, как Page object . Что за Page object?Подход Page Object подразумевает, что моделируемый класс будет полностью описывать один экран тестируемого приложения — все элементы экрана и методы для взаимодействия с этими элементами. Таким образом, нам не придется каждый раз заново объявлять одни и те же элементы в своих автотестах. Давайте перепишем наш тест с использованием подхода Page object и добавим дополнительные проверки. Разберем по шагам, что происходит на видео: Хранить все page object удобно в одной папке — создадим для этого папку screen . Берем название пакета из файла MainActivity.kt , копируем путь пакета, и создаем в androidTest/kotlin package - вставляем скопированный путь и добавляем на конце .screen. (а можно ещё проще: выделите пакет, внутри которого хотите создать другой пакет, нажмите на macOS Cmd+Shift+N (на Windows/Linux обычно Ctrl+N), и появится окошко для создания нового пакета и начало пакета будет уже подставлено) Создаем класс в папке: new - kotlin/java class - выбираем тип object , и вводим название MainScreen . Хорошая практика нэйминга — добавлять в конце названия файла Screen , так все названия экранов будут выглядеть единообразно и их будет удобно искать, когда проект разрастется. Далее мы должны указать, что наш объект — это экран. Для этого наследуем наш MainScreen от класса Screen из библиотеки Kakao и параметризуем его только что созданным объектом.
Следующим шагом перенесем id кнопки и обращение к ней из файла автотеста в MainScreen . Разберем по шагам, что происходит на видео: Переносим объявление кнопки из файла SimpleTest в MainScreen . Необходимые импорты R и KButton будут добавлены IDE автоматически (а если нет, можно перенести их самостоятельно). Убираем ненужные импорты из SimpleTest . Прописываем нажатие на кнопку simpleButton . Для этого обращаемся к классу MainScreen , у класса обращаемся к нужному нам полю класса — simpleButton , и вызываем у simpleButton метод click .
Такая запись выглядит более компактной и удобной для чтения. Запустив тест, можно убедиться, что он все так же работает. Зачем добавлять в Page object методы?Один из принципов паттерна Page object подразумевает инкапсуляцию логики работы с элементами. Инкапсуляция — это один из принципов объектно ориентированного программирования, или ООП, который помогает организовать код так, чтобы скрыть детали его работы от внешнего мира. Другими словами, это создание "капсулы" вокруг данных и функций, чтобы предотвратить их случайное изменение или неправильное использование. Это можно сравнить с тем, как работает пульт от телевизора. У вас есть несколько кнопок, но вы не видите, как они работают внутри пульта. Вам и не нужно знать, как каждая кнопка взаимодействует с телевизором. Вы просто нажимаете на кнопку, и телевизор делает то, что нужно. Точно так же в программировании инкапсуляция позволяет скрыть сложные детали работы программы, предоставляя пользователю (или другим частям программы) только те функции, которые им нужны. Это делает код более безопасным и удобным в использовании, так как предотвращает ошибки и упрощает взаимодействие с программой. Скрытие реализации достигается с помощью модификаторов доступа, нам будут интересны модификаторы public и private . Об остальных модификаторах можно почитать в официальной документации Kotlin. Тут все интуитивно понятно — к членам класса с модификатором private можно обратиться только внутри самого класса, в котором они объявлены. А к членам класса с модификатором public — из любого класса приложения. Теперь добавим в классе MainScreen модификатор private к полю simpleButton : private val simpleButton = KButton { withId(R.id.simple_activity_btn) }
Вернемся обратно в класс теста — IDE сообщает, что мы не можем обратиться к приватному элементу внутри MainScreen : Вот теперь нам и пригодятся методы, которые будут нашим “пультом от телевизора”, и будут иметь модификатор доступа public . Добавим метод, который будет делать клик по кнопке simpleButton : import com.kaspersky.kaspresso.tutorial.R
import io.github.kakaocup.kakao.screen.Screen
import io.github.kakaocup.kakao.text.KButton
import io.github.kakaocup.kakao.text.KTextView
object MainScreen : Screen<MainScreen>() {
private val simpleButton = KButton { withId(R.id.simple_activity_btn) }
public fun clickSimpleButton() {
simpleButton.click()
}
}
Когда вы добавите этот код к IDE , модификатор public подсветится серым цветом, что означает что он не нужен, и метод по умолчанию public . Поэтому в данном случае модификатор можно опустить. Вернемся в класс SimpleTest и вызовем метод clickSimpleButton у класса page object MainScreen : import androidx.test.ext.junit.rules.activityScenarioRule
import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
import com.kaspersky.kaspresso.tutorial.MainActivity
import com.kaspersky.kaspresso.tutorial.screen.MainScreen
import org.junit.Test
import org.junit.Rule
class SimpleTest : TestCase() {
@get:Rule
val activityRule = activityScenarioRule<MainActivity>()
@Test
fun test() {
MainScreen.clickSimpleButton()
}
}
Готово! Мы научились инкапсулировать логику работы с приватными элементами экрана (телевизором) с помощью публичных методов (пульта от телевизора). Добавим еще методов?Все операции с элементами делятся на действия (actions ) и проверки (assertions ). Используемый нами click - это action . Обычно в работе требуется что то более сложное, чем просто нажатие на элемент — например, проверить, что после совершенного действия произошло то, что мы ожидали, или, что элемент с конкретным id имеет определенный текст и отображается на экране. Эти проверки удобно объединять в один метод. Добавим в наш page object такой метод для проверки title на главном экране приложения. Id для него находим точно так же, как и для кнопки, попутно удостоверившись, что лежит он в том же пакете и, следовательно, будет найден по тому же самому R , что и кнопка. Не забываем объявить screenTitle как private поле. Часть page object с методом проверки title будет выглядеть так: private val screenTitle = KTextView { withId(R.id.title) }
fun checkTitle(title: String) {
screenTitle {
isDisplayed()
hasText(title)
}
}
Вынесем клик по кнопке также в отдельный метод. Итого класс page object будет выглядеть следующим образом: package com.kaspersky.kaspresso.tutorial.screen
import com.kaspersky.kaspresso.tutorial.R
import io.github.kakaocup.kakao.screen.Screen
import io.github.kakaocup.kakao.text.KButton
import io.github.kakaocup.kakao.text.KTextView
object MainScreen : Screen<MainScreen>() {
private val simpleButton = KButton { withId(R.id.simple_activity_btn) }
private val screenTitle = KTextView { withId(R.id.title) }
fun clickSimpleButton() {
simpleButton.click()
}
fun checkTitle(title: String) {
screenTitle {
isDisplayed()
hasText(title)
}
}
}
import androidx.test.ext.junit.rules.activityScenarioRule
import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
import com.kaspersky.kaspresso.tutorial.MainActivity
import com.kaspersky.kaspresso.tutorial.screen.MainScreen
import org.junit.Test
import org.junit.Rule
class SimpleTest : TestCase() {
@get:Rule
val activityRule = activityScenarioRule<MainActivity>()
@Test
fun test() {
MainScreen {
checkTitle("Tutorial")
clickSimpleButton()
}
}
}
Таким образом, теперь наглядно видно, что page object — это набор приватных полей и публичных методов для работы с ними в рамках одного экрана. Обратите внимание, что в этом примере title вынесен во входной параметр метода — title: String , и может задаваться динамически из автотеста. Однако его можно “захардкодить”, задать статически внутри метода page object , как hasText("Tutorial”) . Работать будет и так, и так — выбор зависит от того, будет ли меняться текст на TextView . Класс автотеста тоже преобразился — с использованием page object и методов код стал еще более компактным и удобочитаемым. Типичная структура автотеста будет выглядеть так: @Test
fun test() {
firstScreen {
}
secondScreen {
}
firstScreen {
}
}
Как работать со списками?В Android разработке часто используется такой компонент пользовательского интерфейса, как RecyclerView - прокручиваемый список элементов. Этот компонент накладывает некоторые особенности при тестировании, давайте разберем их. Откроем приложение Tutorial и кликнем по кнопке List Activity . Откроется экран со списком дел пользователя. У каждого элемента списка есть порядковый номер, текст и цвет. Также имеется возможность удалять элементы списка при помощи свайпа. Напишем page object для этого экрана. Открыв layout inspector , можно увидеть, что все элементы списка лежат внутри RecyclerView , у которого id: rv_notes . Внутри него лежит три объекта, которые имеют одинаковые идентификаторы: note_container (id самой вьюшки), содержащий tv_note_id (id порядкового номера) и tv_note_text (id текста заметки): Соответственно, протестировать экран обычным способом у нас не получится, так как элементы повторяются и имеют один и тот же id . Вместо этого мы используем другой подход. В page object списка будут содержаться объявленная переменная recyclerView и класс айтема (ItemScreen) списка, внутри которого будут перечислены его элементы. То есть один Item — это одна заметка в нашем случае, список (RecyclerView) — набор таких айтемов(заметок). Создаем page object NoteListScreen , и добавим код для описания RecyclerView . object NoteListScreen : KScreen<NoteListScreen>() {
override val layoutId: Int? = null
override val viewClass: Class<*>? = null
val rvNotes = KRecyclerView(
builder = { withId(R.id.rv_notes) },
itemTypeBuilder = { itemType(::NoteItem) }
)
class NoteItem(matcher: Matcher<View>) : KRecyclerItem<NoteItem>(matcher) {
val noteContainer = KView(matcher) { withId(R.id.note_container) }
val tvNoteId = KTextView(matcher) { withId(R.id.tv_note_id) }
val tvNoteText = KTextView(matcher) { withId(R.id.tv_note_text) }
}
}
Разберем, что означает этот код: val rvNotes = KRecyclerView(...) : Здесь создается экземпляр класса KRecyclerView . Он потребуется нам для взаимодействия с элементами в RecyclerView , например, для прокрутки и выбора элементов.
builder = { withId(R.id.rv_notes) } : Эта часть определяет, как найти RecyclerView в пользовательском интерфейсе. Этот принцип поиска по id нам уже знаком.
itemTypeBuilder = { itemType(::NoteItemScreen) } : Это запись определяет, из каких элементов состоит наш RecyclerView . NoteItemScreen — это наш класс, который используется для описания элемента списка.
class NoteItemScreen(matcher: Matcher<View>) : KRecyclerItem<NoteItemScreen>(matcher) : Этот класс представляет элемент списка в RecyclerView . Он наследует от KRecyclerItem и принимает в качестве входного параметра matcher , который используется для определения того, как именно искать этот элемент в пользовательском интерфейсе.
val noteContainer = KView(matcher) { withId(R.id.note_container) } : Здесь определен элемент noteContainer , который представляет собой контейнер в элементе списка. Грубо говоря, это view элемента списка — заметки.
val tvNoteId = KTextView(matcher) { withId(R.id.tv_note_id) } : Это текстовое поле tvNoteId , которое содержит порядковый номер заметки.
val tvNoteText = KTextView(matcher) { withId(R.id.tv_note_text) } : Аналогично, это текстовое поле tvNoteText , содержащее текст заметки.
Обратите внимание на два важных момента: Первое: в конструктор View -элементов необходимо передать matcher , в котором будем произведен поиск необходимого объекта. Если этого не сделать, тест завершится неудачно - под критерий наличия нужного ID могут подойти несколько элементов, не получится отыскать виджет внутри конкретного элемента списка. Второе: если мы проверяем какое-то специфичное поведение элемента UI , то указываем конкретного наследника KView (KTextView, KEditText, KButton...) . Например, если мы хотим проверить наличие текста, то создаем KTextView , у которого есть возможность получить текст. Весь список доступных виджетов Kakao находится здесь. Если мы проверяем какие-то общие вещи, которые доступны во всех элементах интерфейса (цвет фона, размеры, видимость и т.д.), то можно использовать родительский класс KView . В нашем случае мы планируем проверять тексты у tvNoteId и tvNoteText , поэтому указали в качестве их типа KTextView . А контейнер, в котором лежат эти TextView , является экземпляром CardView , у него мы будем проверять только цвет фона, каких-то специфичных вещей проверять у него нет необходимости, поэтому в качестве типа мы указали родительский — KView . Добавляем по аналогии с другими кнопками кнопку перехода на экран со списком в MainScreen . А после добавляем проверку видимости всех элементов и того, что все они содержат какой-то текст: class SimpleTest : TestCase() {
@get:Rule
val activityRule = activityScenarioRule<MainActivity>()
@Test
fun test() = run {
MainScreen.clickListButton()
NoteListScreen {
rvNotes {
children<NoteListScreen.NoteItem> {
noteContainer.isVisible()
tvNoteId.isVisible()
tvNoteText.isVisible()
tvNoteId.hasAnyText()
tvNoteText.hasAnyText()
}
}
}
}
}
Также мы можем проверить каждый элемент в отдельности, например, что каждая заметка содержит правильные тексты и цвета фона: class SimpleTest : TestCase() {
@get:Rule
val activityRule = activityScenarioRule<MainActivity>()
@Test
fun test() = run {
MainScreen.clickListButton()
NoteListScreen {
rvNotes {
childAt<NoteListScreen.NoteItem>(0) {
noteContainer.hasBackgroundColor(android.R.color.holo_green_light)
tvNoteId.hasText("0")
tvNoteText.hasText("Note number 0")
}
childAt<NoteListScreen.NoteItem>(1) {
noteContainer.hasBackgroundColor(android.R.color.holo_orange_light)
tvNoteId.hasText("1")
tvNoteText.hasText("Note number 1")
}
childAt<NoteListScreen.NoteItem>(2) {
noteContainer.hasBackgroundColor(android.R.color.holo_red_light)
tvNoteId.hasText("2")
tvNoteText.hasText("Note number 2")
}
}
}
}
}
Как я описывал ранее, с помощью класса R мы можем не только обращаться к id элемента, но и к другим ресурсам, таким как цвета. Обратите внимание, что в первом примере мы использовали конструкцию children<'Нужный item'> , которая позволяет обращаться ко всем наследникам в списке, а во втором — конструкцию childAt<'Нужный item'>('Позиция') , которая позволяет обращаться к нужному айтему по позиции, нумерация идет с нуля. Это далеко не все функции для работы со списками, в Kakao их больше, например есть childWith<'Нужный item'> — обращение в нем происходит к определенному айтему, после чего с ним можно выполнять действия, например: someRecycler {
childWith<SomeItem> {
withDescendant {
withText("some text")
}
}.perform {
checkBox.click()
}
}
Прочитать этот код можно так: “Найди мне айтем SomeItem из ресайклера someRecycler , который имеет текст some text (часто выносится в параметр метода), и кликни на него”. Еще из полезных методов есть firstChild (обращение к первому элементу), lastChild (обращение к последнему элементу) и другие. Их использование зависит от конкретных целей и проверок, которые вам нужно будет реализовать. Если вы сомневаетесь, какая функция вам лучше подойдет, то нажмите прямо в студии на интересующую функцию с помощью Cmd + Click для macOS или Alt + Click для windows и Linux , и прочитайте открывшуюся документацию по функции. Итак, наш первый автотест готов. Теперь можно смело его запускать и радоваться, что в ручном регрессе стало меньше проверок. Но бывают ситуации, когда тест начинает вести себя нестабильно, что сильно омрачает нашу радость… А что со стабильностью?Флак или flakiness — это когда ваш тест успешно выполняется десять раз, а на одиннадцатый падает по непонятной причине. Одна из основных причин такого поведения — это когда автотест не дожидается искомого элемента на экране. Так вот Kaspresso имеет встроенную защиту от флаков в тестах. Разберем этот механизм на примере и напишем автотест на проверку текста, который появляется на экране с задержкой. Чтобы попасть на нужный экран, нам нужно нажать на кнопку Flaky Activity . Для этого по аналогии с прошлыми примерами добавим в page object основного экрана нужную нам кнопку: private val flakyButton = KButton { withId(R.id.flaky_activity_btn) }
После этого добавим в тест нажатие данной кнопки: @Test
fun test() = run {
MainScreen.clickFlakyButton()
}
Можно запустить автотест и убедиться, что он открывает нужный нам экран: Обратите внимание, что текст на TextView появляется с задержкой. Теперь нужно написать page object на этот экран. Делаем по аналогии с предыдущими примерами. Экран можно назвать FlakуScreen , должно получиться что-то наподобие: object FlakyScreen : Screen<FlakyScreen>() {
private val flakyText = KTextView { withId(R.id.text_1) }
fun checkFlakyText() {
flakyText {
hasText("TEXT1")
}
}
}
Теперь добавим проверку, что текст соответствует ожидаемому нами, то есть TEXT1 для первого textView : fun checkFlakyText() {
flakyText {
hasText("TEXT1")
}
}
Функция checkFlakyText содержит вызов функции hasText , которая проверяет, содержит ли выбранная TextView строго заданный нами текст. Добавим вызов функции checkFlakyText в автотест: @Test
fun test() = run {
MainScreen.clickFlakyButton()
FlakyScreen.checkFlakyText()
}
Запустим тест и убедимся, что фреймворк “дожидается” появления искомого текста и успешно проходит. Kaspresso под капотом содержит десятисекундное ожидание появления нужной нам view (элемента), что обеспечивает хорошую стабильность в большинстве случаев. Иногда могут возникнуть ситуации, когда стандартных десяти секунд может не хватать, например, когда в мобильном приложении начинается загрузка какого-либо содержимого с сервера. Чтобы автотест в этом месте не упал по таймауту, можно воспользоваться функцией flakySafely , с помощью которой можно выставить свое время ожидания. Для этого в автотесте должен присутствовать блок run . Это один из трех блоков, помогающий управлять состоянием приложения, в нем описываются основные действия и логика. В блоке before задаются настройки, состояние до запуска теста, а в блоке after можно вернуть настройки к первоначальному состоянию после прохождения теста, например, удалить созданные тестовые данные, так как это не входит в основную логику сценария. Итак, добавляем функцию flakySafely и блок run в наш код с параметром 15000 миллисекунд: @Test
fun test() = run {
MainScreen.clickFlakyButton()
flakySafely(15000) {
FlakyScreen.checkFlakyText()
}
}
Запускаем, и проверяем, что тест все так же проходит. Механизм встроенной защиты от флакований flakySafely неявно вызывается при каждой проверке со стандартным значением 10 секунд. Таким образом, явный вызов flakySafely нужно использовать только в тех случаях, когда стандартных десяти секунд не хватает для загрузки какого - либо содержимого. Иногда в интернете встречаются советы, что вместо flakySafely проще использовать метод sleep класса Thread из Java в формате Thread.sleep(15000) . Это плохой совет и им не нужно пользоваться. Да, вы добьетесь почти того же самого результата, что и с flakySafely , но с той разницей, что flakySafely “обрубит” ненужные секунды ожидания и приступит к следующему шагу сразу, как найдет нужный элемент на экране, в то время как sleep выждет отведенные секунды в любом случае. Чем больше в вашем проекте будет автотестов с использованием sleep , тем дольше они будут проходить. Чему мы научились?Это далеко не все возможности фреймворка, но, есть большая надежда, что этих знаний вам хватит для запуска первых тестов и дальнейшего их (как знаний, так и тестов) масштабирования. Итак, теперь мы можем писать автотесты на фреймворке Kaspresso используя следующие принципы: Обращаемся к элементам UI по id , в поиске которых нам помогает Layout inspector . Описываем экраны с помощью паттерна Page object , “один класс — один экран”, храним их в отдельной папке. При добавления элемента в page object нам нужно импортировать находящийся в нужном пакете иерархии класс R , который позволит обращаться к ресурсам приложения. При написании page object можно инкапсулировать логику работы с экраном с помощью приватных и публичных модификаторов доступа для элементов и методов соответственно. Со списками можно работать с помощью большого набора встроенных в виджет KRecyclerView функций, которые можно выбирать в зависимости от целей. Kaspresso имеет встроенную защиту от флакований, обеспечивающую стабильность тестов.
Спасибо за прочтение, пишите в комментариях, с какими трудностями вы столкнулись при написании своих первых автотестов. Обсудить в форуме |