Kautomator: оболочка над UI Automator
Kautomator — Хороший и простой DSL для UI Automator в Kotlin, который позволяет ускорить работу самого UI Automator.
Вдохновлено Kakao и выступлением о UI Automator (спасибо Светлане Смельчаковой).
Введение
Тесты, написанные с помощью UI Automator, нечитаемые и сложные в обслуживании, особенно для тестировщиков. Взгляните на типичный фрагмент кода, написанный с помощью UI Automator:
val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
val uiDevice = UiDevice.getInstance(instrumentation)
val uiObject = uiDevice.wait(
Until.findObject(
By.res(
"com.kaspersky.kaspresso.sample_kautomator",
"editText"
)
),
2_000
)
uiObject.text = "Kaspresso"
assertEquals(uiObject.text, "Kaspresso")
mainScreen {
simpleEditText {
replaceText("Kaspresso")
hasText("Kaspresso")
}
}
Еще одним большим преимуществом Kautomator является возможность ускорения UI Automator.
Взгляните на видео ниже:
Левое видео — это улучшенный UI Automator, правое видео — это UI Automator по умолчанию.
Почему это возможно? Подробности доступны ниже.
Преимущества
- Читабельность
- Повторное использование
- Расширяемый DSL
- Удивительная скорость!
Как это использовать
Создать экран
Создайте свой объект UiScreen
, куда вы добавите UI-элементы, участвующие во взаимодействии тестов:
class FormScreen : UiScreen<FormScreen>()
UiScreen
может представлять весь пользовательский интерфейс или его часть.
Если вы используете шаблон Page Object, вы можете поместить взаимодействия с Kautomator внутри Page Objects.
Создание UiView
UiScreen
содержит различные UiView
. Внутри UiScreen описываются все UI-элементы, с которыми будет взаимодействовать тест:
class FormScreen: UiScreen<FormScreen>() {
val phone = UiView { withId(this@FormScreen.packageName, "phone") }
val email = UiEditText { withId(this@FormScreen.packageName, "email_edit") }
val submit = UiButton { withId(this@FormScreen.packageName, "submit_button") }
}
UiView
UiEditText
UiTextView
UiButton
UiCheckbox
UiChipGroup
UiSwitchView
UiScrollView
- и многое другое
Каждый UiView
содержит Matcher-ы для получения экземпляра ViewInteraction
-а. Некоторые примеры Matcher-ов из Kakao:
withId
withText
withPackage
withContentDescription
textStartsWith
- и многое другое
Как и в Ui Automator, вы можете комбинировать разные Matcher-ы:
val email = UiEditText {
withId(this@FormScreen.packageName, "email")
withText(this@FormScreen.packageName, "matsyuk@kaspresso.com")
}
Реализация взаимодействия
Синтаксис теста с Kautomator очень прост, как только вы определили UiScreen
и UiView
, вам нужно только выполнить action или assertion, как в UI Automator:
FormScreen {
phone {
hasText("971201771")
}
button {
click()
}
}
Отличие от Kakao-Espresso
В Espresso все взаимодействие с View
обрабатывается через ViewInteraction
, который имеет два основных метода: onCheck
и onPerform
, которые принимают ViewAction
и ViewAssertion
в качестве аргументов. Kakao был написан на основе этой архитектуры.
Итак, мы поставили перед собой цель написать Kautomator, максимально похожий на Kakao. Вот почему мы ввели дополнительный слой поверх UiObject2 и UiDevice, который так похож на ViewInteraction
. Этот уровень представлен UiObjectInteraction
и UiDeviceInteraction
, которые имеют два метода: onCheck
и onPerform
, принимающие UiObjectAssertion и UiObjectAction или UiDeviceAssertion и UiDeviceAction в качестве аргументов.
UiObjectInteraction
предназначен для работы с конкретным View
, таким как ViewInteraction
. UiDeviceInteraction
был создан, потому что UI Automator имеет функцию, позволяющую вам выполнять некоторые системные действия, такие как нажатие кнопки «Домой» или кнопки «Назад», открытие быстрых настроек, открытие уведомлений и так далее. Все подобные вещи скрыты классом UiSystem
.
Так что, наслаждайтесь =)
Кастомные UI-элементы
Если у вас есть нестандартные (кастомные) UI-элементы в ваших тестах и вы хотите создать свой собственный UiView
, у нас есть UiBaseView
. Просто унаследуйте этот класс и реализуйте столько дополнительных интерфейсов Action/Assertion, сколько хотите.
Вам также необходимо переопределить конструкторы, которые вам нужны.
class UiMyView : UiBaseView<KView>, UiMyActions, UiMyAssertions {
constructor(selector: UiViewSelector) : super(selector)
constructor(builder: UiViewBuilder.() -> Unit) : super(builder)
}
Работа interceptor-ов
Если вам нужно добавить свою логику во время цепочки вызовов Kautomator -> UI Automator
(например, ведение журнала) или если вам нужно полностью изменить UiAssertion
или UiAction
, которые отправляются в UI Automator во время выполнения, в некоторых случаях можно использовать механизм перехвата.
Перехватчики — это лямбда-выражения, которые вы передаете конфигурационному DSL. Они будут вызываться перед реальными вызовами внутри классов UiObject2
и UiDevice
в UI Automator.
У вас есть возможность предоставлять перехватчики на 3 разных уровнях: время выполнения Kautomator, на уровне ваших классов UiScreen
и на уровне отдельного экземпляра UiView
.
При каждом вызове функции UI Automator, которую можно перехватить, Kautomator агрегирует все доступные перехватчики для этого конкретного вызова и вызывает их в следующем порядке: UiView interceptor -> Active Screens interceptors -> Kautomator interceptor
.
Каждый из перехватчиков может разорвать вызов цепочки, установив isOverride
в true во время настройки.
В этом случае Kautomator не только перестанет вызывать оставшиеся перехватчики в цепочке, но и не будет выполнять вызовы UI Automator. Это означает, что в таком случае ответственность за фактический вызов KAutomator лежит на плечах разработчика.
Вот примеры конфигураций:
class SomeTest {
@Before
fun setup() {
KautomatorConfigurator { // Kautomator runtime
intercept {
onUiInteraction { // Перехват вызовов классов UiInteraction во время выполнения
onPerform { uiInteraction, uiAction -> // Перехватываем вызов execute()
testLogger.i("KautomatorIntercept", "interaction=$uiInteraction, action=$uiAction")
}
}
}
}
}
@Test
fun test() {
MyScreen {
intercept {
onUiInteraction { // Перехват вызовов классов UiInteraction в контексте MyScreen
onCheck { uiInteraction, uiAssert -> // Перехват вызова check()
testLogger.i("KautomatorIntercept", "interaction=$uiInteraction, assert=$uiAssert")
}
}
}
myView {
intercept { // Перехват вызовов ViewInteraction для этого отдельного UI-элемента
onPerform(true) { uiInteraction, uiAction -> // Перехватываем вызов execute() и переопределяем цепочку
// При выполнении действий над этим элементом не будет вызываться перехватчик уровня Kautomator
// и теперь нам нужно вручную вызывать UI Automator.
Log.d("KAUTOMATOR_VIEW", "$uiInteraction выполняет $uiAction")
uiInteraction.perform(uiAction)
}
}
}
}
}
}
Ускоряем UI Automator
Как вы помните, мы рассказывали о возможном ускорении UI Automator. Как это становится возможным?
UI Automator имеет внутренний механизм для предотвращения потенциальной нестабильности. Под капотом библиотека слушает и отдает команды через AccessibilityManagerService. AccessibilityManagerService — это единая точка для всех событий доступности в системе. В какой-то момент создатели UI Automator столкнулись с проблемой ненадёжности. Одной из самых популярных причин такого неопределенного поведения является большое количество обрабатываемых в системе событий в текущий момент. Но UI Automator имеет связь с AccessibilityManagerService. Такое подключение дает возможность прослушивать все события доступности в системе и ждать спокойного состояния, когда нет никаких действий. Спокойное состояние приводит к детерминированному поведению системы и снижает вероятность нестабильности.
Все это подтолкнуло авторов UI Automator к внедрению следующего алгоритма: UI Automator ожидает 500 мс (waitForIdleTimeout
и waitForSelectorTimeout
в окне androidx.test.uiautomator.Configurator
) в течение 10 секунд для каждого действия. КАЖДОЕ ДЕЙСТВИЕ.
Возможно, описанное решение сделало UI Automator более стабильным. Но скорость упала, спору нет.
Kautomator — это DSL поверх UI Automator, который предоставляет механизм перехватчиков. Kaspresso предлагает большой набор перехватчиков по умолчанию, что устраняет любые потенциальные нестабильные действия. Итак, Kaspresso + Kautomator помогает UI Automator бороться с ненадёжностью.
Через какое-то время мы подумали, зачем нам сохранять искусственные таймауты внутри UI Automator, в то время как Kaspresso + Kautomator делает ту же работу. Взгляните на пример измерения:
@RunWith(AndroidJUnit4::class)
class KautomatorMeasureTest : TestCase(
kaspressoBuilder = Kaspresso.Builder.simple {
kautomatorWaitForIdleSettings = KautomatorWaitForIdleSettings.boost()
}
) {
companion object {
private val RANGE = 0..20
}
@get:Rule
val runtimePermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
)
@get:Rule
val activityTestRule = ActivityTestRule(MainActivity::class.java, true, false)
@Test
fun test() =
before {
activityTestRule.launchActivity(null)
}.after { }.run {
======> UI Automator: 0 minutes, 1 seconds and 252 millis
======> UI Automator boost: 0 minutes, 0 seconds and 310 millis
step("MainScreen. Click on `measure fragment` button") {
UiMainScreen {
measureButton {
isDisplayed()
click()
}
}
}
======> UI Automator: 0 minutes, 11 seconds and 725 millis
======> UI Automator boost: 0 minutes, 1 seconds and 50 millis
step("Measure screen. Button_1 clicks comparing") {
UiMeasureScreen {
RANGE.forEach { _ ->
button1 {
click()
hasText(device.targetContext.getString(R.string.measure_fragment_text_button_1).toUpperCase())
}
}
}
}
======> UI Automator: 0 minutes, 11 seconds and 789 millis
======> UI Automator boost: 0 minutes, 1 seconds and 482 millis
step("Measure screen. Button_2 clicks and TextView changes comparing") {
UiMeasureScreen {
RANGE.forEach { index ->
button2 {
click()
hasText(device.targetContext.getString(R.string.measure_fragment_text_button_2).toUpperCase())
}
textView {
hasText(
"${device.targetContext.getString(R.string.measure_fragment_text_textview)}${index + 1}"
)
}
}
}
}
======> UI Automator: 0 minutes, 45 seconds and 903 millis
======> UI Automator boost: 0 minutes, 2 seconds and 967 millis
step("Measure fragment. EditText updates comparing") {
UiMeasureScreen {
edit {
isDisplayed()
hasText(device.targetContext.getString(R.string.measure_fragment_text_edittext))
RANGE.forEach { _ ->
clearText()
typeText("bla-bla-bla")
hasText("bla-bla-bla")
clearText()
typeText("mo-mo-mo")
hasText("mo-mo-mo")
clearText()
}
}
}
}
======> UI Automator: 0 minutes, 10 seconds and 901 millis
======> UI Automator boost: 0 minutes, 1 seconds and 23 millis
step("Measure fragment. Checkbox clicks comparing") {
UiMeasureScreen {
RANGE.forEach { index ->
checkBox {
if (index % 2 == 0) {
setChecked(true)
isChecked()
} else {
setChecked(false)
isNotChecked()
}
}
}
}
}
}
}
Также бывают случаи, когда UI Automator не может поймать окно 500 мс. Например, когда один элемент обновляется слишком быстро (одно обновление за 100 мс). Просто взгляните на этот тест. Только KautomatorWaitForIdleSettings.boost()
позволяет пройти тест.
Как видите, мы добавили в конфигуратор Kaspresso специальное свойство kautomatorWaitForIdleSettings
. По умолчанию это свойство не повышает производительность. Почему? Потому что:
1. У вас могут быть тесты, в которых вы напрямую используете UI Automator. Но указанные таймауты являются глобальными параметрами. Сброс этих тайм-аутов может привести к неопределенному состоянию.
2. Мы хотим потратить время на сбор данных со всего мира, а затем проанализировать потенциальные проблемы наших решений (но мы считаем, что это стабильное и блестящее решение).
Еще одно важное замечание касается конфигурации kaspressoBuilder = Kaspresso.Builder.simple
. Эта конфигурация быстрее, чем advanced
, из-за отсутствия перехватчика скриншотов на каждом шаге. При необходимости добавьте их вручную.
В любом случае, это небольшое изменение для разработчика, но большой шаг для всех нас =)