Привет! Меня зовут Элчин, я занимаюсь автоматизацией тестирования мобильных приложений в 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. (а можно ещё проще: выделите пакет, внутри которого хотите создать другой пакет, нажмите на 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
имеет встроенную защиту от флакований, обеспечивающую стабильность тестов.
Спасибо за прочтение, пишите в комментариях, с какими трудностями вы столкнулись при написании своих первых автотестов.
Увидимся в следующих выпусках!