
Привет! Меня зовут Элчин, я занимаюсь автоматизацией тестирования мобильных приложений в 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.
Далее создадим папку в нашем проекте, где будут лежать автотесты — жмем правой кнопкой мыши на папку main - 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. (а можно ещё проще: выделите пакет, внутри которого хотите создать другой пакет, нажмите на macOSCmd+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 {
//проверки и действия, например поиск title и открытие следующего экрана
}
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имеет встроенную защиту от флакований, обеспечивающую стабильность тестов.
Спасибо за прочтение, пишите в комментариях, с какими трудностями вы столкнулись при написании своих первых автотестов.
Увидимся в следующих выпусках!
Valet
Отличная статья, спасибо!
ElchinG Автор
Спасибо, приятного чтения.