Привет! Меня зовут Элчин, я занимаюсь автоматизацией тестирования мобильных приложений в hh.ru и расскажу вам о том, как написать первый тест на Android. В разработке автотестов мы используем Kotlin и нативный фреймворк Kaspresso, о котором я напишу подробней в этой статье.

Эта статья рассчитана на начинающих тестировщиков, но, возможно, и более опытные коллеги найдут для себя что-то полезное.

В рассказе мы будем постепенно двигаться от основ к более сложным вещам:

  1. Установим среду разработки — Android Studio

  2. Скачаем и настроим проект

  3. Научимся обращаться к элементам и напишем первый тест

  4. Разберем, как писать page object и для чего они нужны

  5. Поработаем со списками

  6. Обсудим стабильность автотестов на 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. Находим идентификатор нужной нам кнопки:

Разберем по шагам, что происходит на видео:

  1. Для запуска Layout inspector — инструмента для просмотра иерархии элементов приложения — должно быть запущено приложение, на экране которого мы хотим искать наши id. Запускаем его через кнопку run.

  2. После запуска открываем Layout inspector — он находится во вкладке Tools - Layout inspector.

  3. Для экономии места на экране можно свернуть вкладку Project, так как она нам пока не понадобится. Layout inspector состоит из трех вкладок: Component tree, в котором мы видим структуру открытого экрана; область с самим экраном; Attributes, в котором можно найти интересующие нас id и другие свойства элементов. На записи видно, что при нажатии на разные элементы приложения в панели Component tree можно увидеть их id и другие атрибуты в панели Attributes.

Добавляем кнопку в автотест:

  1. Чтобы автотест мог обратиться к какому-либо элементу по его 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. Открываем его и копируем название пакета.

  2. Возвращаемся в класс с автотестом, добавляем import пакета и .R на конце. R — автоматически генерируемый класс, который содержит ссылки на такие ресурсы, как макеты, изображения, строки, цвета и другие ресурсы, используемые в приложении. Как будет видно дальше, с помощью R мы будем обращаться к id элемента.

  3. В Kaspresso для взаимодействия с элементами используется удобный Kotlin DSL над Espresso, который предоставляется библиотекой Kakao. Для стандартных UI-виджетов уже создано множество готовых обёрток. Воспользуемся одной из них для поиска кнопки - нам нужен класс KButton. Мы инициализируем его блоком, внутри которого вызываем функцию для поиска виджета с помощью id нужной кнопки - withId(R.id.simple_activity_btn).

  4. После этого вызываем у simpleButton метод click() для клика по элементу.

  5. Осталась последняя деталь, не попавшая на видео: нужно добавить правило, которое обеспечивает управление 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 и добавим дополнительные проверки. 

Разберем по шагам, что происходит на видео:

  1. Хранить все page object удобно в одной папке — создадим для этого папку screen. Берем название пакета из файла MainActivity.kt, копируем путь пакета, и создаем в androidTest/kotlin package - вставляем скопированный путь и добавляем на конце .screen. (а можно ещё проще: выделите пакет, внутри которого хотите создать другой пакет, нажмите на macOS Cmd+Shift+N (на Windows/Linux обычно Ctrl+N), и появится окошко для создания нового пакета и начало пакета будет уже подставлено)

  2. Создаем класс в папке: new - kotlin/java class - выбираем тип object, и вводим название MainScreen. Хорошая практика нэйминга — добавлять в конце названия файла Screen, так все названия экранов будут выглядеть единообразно и их будет удобно искать, когда проект разрастется.

  3. Далее мы должны указать, что наш объект — это экран. Для этого наследуем наш MainScreen от класса Screen из библиотеки Kakao и параметризуем его только что созданным объектом.

Следующим шагом перенесем id кнопки и обращение к ней из файла автотеста в MainScreen.

Разберем по шагам, что происходит на видео:

  1. Переносим объявление кнопки из файла SimpleTest в MainScreen. Необходимые импорты R и KButton будут добавлены IDE автоматически (а если нет, можно перенести их самостоятельно).

  2. Убираем ненужные импорты из SimpleTest.

  3. Прописываем нажатие на кнопку 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) }
    }
}

Разберем, что означает этот код:

  1. val rvNotes = KRecyclerView(...): Здесь создается экземпляр класса KRecyclerView. Он потребуется нам для взаимодействия с элементами в RecyclerView, например, для прокрутки и выбора элементов.

  2. builder = { withId(R.id.rv_notes) }: Эта часть определяет, как найти RecyclerView в пользовательском интерфейсе. Этот принцип поиска по id нам уже знаком.

  3. itemTypeBuilder = { itemType(::NoteItemScreen) }: Это запись определяет, из каких элементов состоит наш RecyclerView. NoteItemScreen —  это наш класс, который используется для описания элемента списка. 

  4. class NoteItemScreen(matcher: Matcher<View>) : KRecyclerItem<NoteItemScreen>(matcher): Этот класс представляет элемент списка в RecyclerView. Он наследует от KRecyclerItem и принимает в качестве входного параметра matcher, который используется для определения того, как именно искать этот элемент в пользовательском интерфейсе.

  5. val noteContainer = KView(matcher) { withId(R.id.note_container) }: Здесь определен элемент noteContainer, который представляет собой контейнер в элементе списка. Грубо говоря, это view элемента списка — заметки.

  6. val tvNoteId = KTextView(matcher) { withId(R.id.tv_note_id) }: Это текстовое поле tvNoteId, которое содержит порядковый номер заметки.

  7. 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 используя следующие принципы:

  1. Обращаемся к элементам UI по id, в поиске которых нам помогает Layout inspector.

  2. Описываем экраны с помощью паттерна Page object, “один класс — один экран”, храним их в отдельной папке.

  3. При добавления элемента в page object нам нужно импортировать находящийся в нужном пакете иерархии класс R, который позволит обращаться к ресурсам приложения.

  4. При написании page object можно инкапсулировать логику работы с экраном с помощью приватных и публичных модификаторов доступа для элементов и методов соответственно.

  5. Со списками можно работать с помощью большого набора встроенных в виджет KRecyclerView функций, которые можно выбирать в зависимости от целей.

  6. Kaspresso имеет встроенную защиту от флакований, обеспечивающую стабильность тестов.

Спасибо за прочтение, пишите в комментариях, с какими трудностями вы столкнулись при написании своих первых автотестов.

Увидимся в следующих выпусках!

Комментарии (6)


  1. Valet
    15.11.2023 09:03
    +3

    Отличная статья, спасибо!


    1. ElchinG Автор
      15.11.2023 09:03

      Спасибо, приятного чтения.


  1. Vacxe
    15.11.2023 09:03

    Ну хотя бы слово про Kakao, пожалуйста


    1. Vacxe
      15.11.2023 09:03
      +1

      Спасибо за редактирование, теперь намного понятнее, что откуда растет


  1. bogdanburkov
    15.11.2023 09:03
    +1

    Большое спасибо! Нашел себе занятие на выходные)


    1. ElchinG Автор
      15.11.2023 09:03

      Рад помочь. Если будут трудности, пишите сюда вопросы.