Как стать автором
Обновить

Путь к автотестированию Android нативными инструментами: испробовали всё, что есть на рынке и сделали свои выводы

Время на прочтение 13 мин
Количество просмотров 7.8K

Введение

Давайте сначала представимся. Мы - команда управления тестирования и контроля качества в БКС Мир Инвестиций.

Наш продукт - это приложения и сайты, созданные для удобства всех желающих окунуться в мир ценных бумаг и инвестиций. Конкретно в нашем “ведомстве” приложения на IOS  и Android, сайт личного кабинета, инвест стратегии Fintarget, новостной портал BCS-Express, сайт ФГ БКС и прочее.

В прошлом году мы начали трансформацию и преображение всех ключевых процессов в разрезе тестирования и контроля качества. 

Конечно же, автоматизация наших процессов важна, ведь без этого невозможно построение действительно сильного QA отдела. Безусловно, мы встретились с множеством разнообразных проблем.

Мы решили сделать серию статей на тему того, что у нас получилось, что не получилось и как мы этого добивались. Часть статей будет чисто лирической, на размышления о подходах и способах работы. Другая часть будет сугубо технической, с рассказами, примерами и всем-всем, что нам интересно рассказать, и что интересно обсудить.

Начать эти рассказы мы решили с повести о становлении мобильного автотестирования. Первым “попался под руку” Android, про него и начнем.


Проблематика

Как было?

Красивых слов и речей писать не будем. Был Appium.

Какую проблему решали?

Как обычно и бывает, проблема не была единой и сформулировать ее одним предложением сложно.

Так что выделим ряд нюансов, с которыми столкнулись:

  • Команды не пользовались тестами.

  • Тесты были “красными”.

  • Тесты практически не писались в командах.

  • Команды не доверяли имевшимся автотестам при проведении регресса.

  • Практика регулярного запуска тестов умирала.

  • Практически никто не поддерживал тесты, они жили своей жизнью.

В связи с тем, что тестов было не так много и они были не слишком актуальными, встал вопрос выбора технологии - взвешивали плюсы и минусы нативных технологий и Appium. 

После ресерча, плюсы выделили такие:

  • Нативные инструменты близки к разработке и являются ее неотъемлемой частью.

  • Более стабильные тесты.

  • Обширный функционал взаимодействия с приложением - можно делать множество действий, жестов и настроек ОС и самого приложения, которые недоступны в Appium.

  • QA глубоко погружены в продукт - видят MR, код приложения, умеют его собирать самостоятельно, с нужными параметрами.

  • Модно-молодежно.

Минусы:

  • Более высокий порог вхождения в тесты.

  • Проект придется поднимать с нуля, переписать часть тестов.

  • Нет части элементарных методов, например, получение текста из элемента.


Пути поиска решения

Первое решение

Первое и самое очевидное решение было Espresso

Спойлер - отказались довольно быстро.

Из особенностей/нюансов можно выделить настройку самого Espresso и работу с приложением. 

Проблема раз

Запуск самих тестов. 

Казалось бы, что по законам здравого смысла, да и судя по официальной документации, должно быть всё элементарно, но нет. Через аннотацию ActivityTestRule, а так же различные правила запуска, рекомендуемые Google, тесты вообще не запускались. 

Довольно много времени ушло на то чтобы разобраться с банальным запуском. В итоге решение нашлось в результате длительных брейнштормов: пришлось работать через полноценный запуск MainActivity при помощи намерений (Intent).

fun setup() {
    val intent = Intent(ApplicationProvider.getApplicationContext(), MainActivity::class.java)
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
    val activity = InstrumentationRegistry.getInstrumentation()
            .startActivitySync(intent)
}

Проблема два

Espresso не имеет возможности разнести локаторы по PageObject, так как вся основная работа с элементами строится через прямой вызов onView внутри самого теста, в котором прописываются локаторы через обращение к R.

Такая реализация является одной из важных причин итогового отказа от использования данного инструмента в “голом” виде. Написав пару-тройку тестов, оказалась что это действительно проблема. 

Проблема три

Полное отсутствие работы с RecyclerView. 

Немного расскажем про RecyclerView, и почему мы так на него ориентируемся.

Начнем с вопроса почему? 

Ответ - Значительная часть нашего приложения реализована на компонентах RecyclerView.

Данный компонент интерфейса представляет собой хранилище типизированных элементов внутри него, то есть, проще говоря, список. И  внутри Espresso нет никаких инструментов для работы с элементами такого списка.

Естественно, на просторах сети существуют костыльные решения, но в таком случае работа с локаторами была бы крайне неудобной, не удалось бы полноценно придерживаться паттерну PageObject, в том числе многократно усложнялась работа с любыми элементарными вещами вроде кликов свайпов.

Тесты выглядели примерно следующим образом:

@Test
    fun checkElementsOnShareScreen() {
        login()
        onView(withContentDescription("Рынки")).perform(click())
        waitUntilViewIsDisplayed(withText("Российский"))
        onView(withText("Российский")).perform(click())
        onView(withText("Российский")).check(matches(isDisplayed()))
        onView(withContentDescription("Акции")).check(matches(isSelected()))
        onView(allOf(withId(R.string.markets_details_quotes), isDisplayed())).check(matches(isDisplayed()))
        onView(allOf(withId(R.string.markets_details_news), isDisplayed())).check(matches(isDisplayed()))
        onView(allOf(withId(R.string.markets_details_ideas), isDisplayed())).check(matches(isDisplayed()))
        onView(allOf(withId(R.string.markets_details_quotes), isDisplayed())).check((matches(isChecked())))
        onView(allOf(withId(R.id.market_instrument_pager), isDisplayed())).check(matches(isDisplayed()))
    }

В связи с проблемами описанными выше,  мы поняли, что это не лучший выбор и пошли ресерчить дальше.

Второе решение

Второе решение было найдено на просторах интернета - это обвязка Espresso под названием espresso-page-object от пользователя alex-tiurin.

После изучения кодовой базы и просмотра презентации, показалось, что данная библиотека нам может подойти.

Уже просвечивался паттерн разработки автотестов PageObject, а также была более менее удобная работа с RecyclerView. Решили попробовать данную библиотеку, и работа что с тестами что с локаторами стала намного проще, они лежали в отдельных объектах, и локаторы использовались только там. 

Что же пошло не так, ведь на бумаге выглядит неплохо?

Проблема раз

Данный фреймворк перестал активно поддерживаться, что в будущем может привести к проблемам: очередной переход на другой фреймворк, рефакторинг тестов и объектов, в которых хранятся сами локаторы.

Проблема два

Это всё тот же наш любимчик - RecyclerView. 

Работа с ним стала проще, но появились другие проблемы. Работа с индексами элементов упростилась, но не стала идеальной.

Самым сложным в реализации моментом стала история про скролл до нужного элемента внутри RecyclerView.

Да, есть метод scrollTo, но он попросту не работает внутри RecyclerView. Работал скролл по координатам, но этот подход оказался слишком трудозатратным на постоянной основе.

А тем более, если элемент не виден на экране, как это зачастую бывает, то приходится использовать метод swipeUp, который в свою очередь скроллит очень сильно и оказаться в нужном месте экрана становится слишком сложно. 

Тесты выглядели вот так:

@Test
fun checkAuthorizationFlow() {
    StartScreen {
        loginButton.isDisplayed().click()
        loginInputField.isDisplayed()
        allOf(supportsInputMethods(), isDescendantOfA(loginInputField))
                .typeText("login").closeSoftKeyboard()
        allOf(supportsInputMethods(), isDescendantOfA(passwordInputField))
                .typeText("password")
        enterButton.isEnabled().click()
        waitUntilViewIsDisplayed(pincodeTitle)
        repeat(2) {
            PinCodeScreen {
                buttonOne.click()
                buttonFour.click()
                buttonTwo.click()
                buttonThree.click()
            }
        }
    }
    PortfolioScreen.portfolioTitle.isDisplayed()
}

Третье решение

В конечном итоге пришли к решению с Kaspresso. Имеет много внутренних плюшек, от неявного ожидания до корректной работы с RecyclerView. Их можно перечислять до бесконечности, но основные фичи которыми мы пользуемся - это работа с RecyclerView, использование элементов по их назначению, KButton, KTextInputLayout и другие, работа с adb, использование скриншотов, запись видео на тесты и прочие прелести. 


Финальное техническое решение

Описание Kaspresso

Если коротко, то Kaspresso - это фреймворк для автотестирования, сделанный на базе Espresso и UIAutomator. Из основных особенностей и плюсов можно отметить высокую стабильность, удобный для чтения DSL, намного более быстрое выполнение команд UIAutomator, возможность работы с adb из коробки и довольно гибкое логирование. Ознакомиться подробнее можно на странице проекта на github.

Технические нюансы

Рассказывать подробно про то, как именно развернуть Kaspresso мы не будем, эта информация уже есть в сети, там неожиданностей и больших проблем не встретили. Мы же расскажем о ряде интересных технических нюансов, с которыми мы столкнулись в процессе реализации данного проекта, а также покажем примеры тестов, которые мы пишем и регулярно используем.

Уровень знаний специалистов

По сравнению с многими фреймворками мобильного автотестирования (Cucumber, Gherkin, Appium) нужно уметь хотя бы минимально кодить. Совсем вслепую копипастить тесты или писать их на русском языке не получится.

Allure делал скриншоты на каждый шаг

Здесь стоит уточнить, что мы используем декораторы, в том числе, Step. Делается это для разделения логических шагов внутри каждого теста, для разделения его важных частей/экранов. По умолчанию Allure делал скриншоты на каждый шаг (step), что, мягко говоря, засоряло отчеты, не имело смысла, съедало место на раннерах.

Правила, по которым реализована штатная работа со скриншотами и шагами

Эти правила очень неудобны, так как мы не используем внутри тестов так называемые секции, то данный способ нам не подходит.

Было принято решение написать кастомные интерсепторы (рассказывать что такое интерсептор подробно не будем, гугл ответит лучше нас) для работы с тестами, которые не будут ссылаться на шаги внутри тестов, а также не будут работать с секциями, а будут просто использоваться для тестовых методов. 

Что же было написано для работы с нашими тестовыми методами?

Первое, нам нужно объявить билдер для работы с логами:

TestCase(

        kaspressoBuilder = Kaspresso.Builder.advanced()
)

Данный фрагмент кода помогает прикладывать скриншоты, видео, а так же дампы logcat и иерархии приложения.  

Было реализовано 2 интерсептора:

Работа с видео

Так как VideoRecordingInterceptor работал на все тесты, и успешные и упавшие, то в итоге раннер очень быстро наполнялся ненужными видео. Мы реализовали свой интерсептор, который в случае успешного прохождения теста, не прикладывает к нему видео, так как это не имеет никакого смысла, видео теперь записываются только на упавшие тесты. 

Реализация довольно проста: 

class VideoRecordingInterceptorForDelete(
        private val videos: Videos
) : TestRunWatcherInterceptor {

    override fun onTestStarted(testInfo: TestInfo) {
        videos.record("Video_${testInfo.testName}")
    }

    override fun onTestFinished(testInfo: TestInfo, success: Boolean) {
        videos.saveAndApply { if (success) delete() else attachVideoToAllureReport() }
    }
}

Работа со скриншотами

На каждый успешный или сломанный тест прикладывается скриншот, весят они не очень много, поэтому раннер не сильно засоряется. Естественно это работает на каждый тестовый метод. 

Также ниже показываем класс, в котором реализованы скриншоты на все тестовые методы и кастомный билдер, который позволяет работать внутри TestCase.

class AttachScreenshotToAllureReportInterceptor(
        private val screenshots: Screenshots
) : TestRunWatcherInterceptor {

    override fun onTestFinished(testInfo: TestInfo, result: Boolean) {
        attachScreenshot(makeTag())
    }

    private fun attachScreenshot(tag: String) {
        screenshots.takeAndApply(tag) { attachScreenshotToAllureReport() }
    }

    private fun makeTag(): String =
            "Screenshot_${CustomLocalDateTime.dateTimeFormatter()}"
}

fun Companion.withCustomAllureSupport(
        customize: Builder.() -> Unit = {}
): Builder = simple(customize).addAllureSupport()

fun Builder.addAllureSupport(): Builder = apply {
    if (isAndroidRuntime) {
        testRunWatcherInterceptors.addAll(
                listOf (
                        AttachScreenshotToAllureReportInterceptor(screenshots),
                        VideoRecordingInterceptorForDelete(videos),
                        DumpLogcatTestInterceptor(logcatDumper)
                )
        )
    }
}

Примеры тестов

Как это выглядит и что вообще получилось в формате самих тестов и отчетов?

Примерно следующим образом у нас выглядят все файлы, в которых описываются локаторы. В целом, используем классический PageObject.

В Kaspresso есть более богатые возможности работы со стандартными элементами AndroidX. Например в Kaspresso есть KTextInputLayout, которого нам не хватало в библиотеке espresso-page-object и нативном Espresso.

object StartScreen : Screen<StartScreen>() {

    val loginButton = KButton { withId(R.id.btnLogin) }
    val loginInputField = KTextInputLayout { withId(R.id.loginInputTextEdit) }
    val passwordInputField = KTextInputLayout { withId(R.id.passwordInputTextEdit) }
    val enterButton = KButton { withId(R.id.bottomButtonView) }
    val pincodeTitle = KTextView { withId(R.id.pincodeConfirmTitle) }
    val forgetPasswordButton = KButton { withId(R.id.forgetPasswordButton) }
    val loginTitle = KTextView { withId(R.id.login_title) }
}

Далее хотим показать вам, как выглядит простейший тест на авторизацию.

Да, в сценарии нет ничего сложного: по сути, в этом тесте всего несколько действий и сравнение ожидаемого и фактического результатов. 

Ниже код теста с пояснениями:

 @Test
    @Link(name = "Тест-кейс", url = "https://ссылка_на_тестрейл.ру/")
    @DisplayName("Проверка успешной авторизации")
    fun checkAuthorizationFlow() = run {
        StartScreen {
          // Дожидаемся появления (загрузки) кнопки авторизации
            loginButton { isDisplayed() }
          // Нажимаем на нее 
            loginButton { click() }
            loginInputField { isDisplayed() }
          // Вводим логин
            loginInputField.edit { user }
          // Закрываем клавиатуру, так как она перекрывает поле пароля
            closeSoftKeyboard()
          // Вводим пароль  
            passwordInputField.edit { password }
          // Проверяем, что кнопка входа стала активной и нажимаем на нее
            enterButton { isEnabled() }
            enterButton { click() }
          // Задаем пин-код
            repeat(2) {
                PinCodeScreen {
                    buttonOne { click() }
                    buttonFour { click() }
                    buttonTwo { click() }
                    buttonThree { click() }
                }
            }
        }
      // Проверяем то, что открылась авторизованная зона
        PortfolioScreen.portfolioTitle { isDisplayed() }
    }

Ниже можно увидеть пример хранения локаторов внутри нашего любимого RecyclerView. В целом довольно тривиально, но поначалу были некоторые сложности с самим билдером, но после ресерча данного вопроса,  стало очень приятно работать с данной вьюшкой:

val connectionBodyRecycler = KRecyclerView({ withId(R.id.rvConnection) },
                                       itemTypeBuilder = {
                                           itemType(::ConnectionBody)
                                       })

    class ConnectionBody(parent: Matcher<View>) : KRecyclerItem<ConnectionBody>(parent) {
        val title = KTextView(parent) { withId(R.id.tvTitle) }
        val subTitle = KTextView(parent) { withId(R.id.tvSubTitle) }
        val componentTitle = KTextView(parent) { withId(R.id.tvComponentTitle) }
        val componentSubtitle = KTextView(parent) { withId(R.id.tvComponentSubtitle) }
        val instagramIcon = KButton(parent) { withId(R.id.ivInstagram) }
        val vkIcon = KButton(parent) { withId(R.id.ivVk) }
        val facebookIcon = KButton(parent) { withId(R.id.ivFacebook) }
        val youtubeIcon = KButton(parent) { withId(R.id.ivYoutube) }
        val twitterIcon = KButton(parent) { withId(R.id.ivTwitter) }
    }

Вот так выглядит тест, в котором мы по второму индексу обращаемся к типизированной кнопке sendButton, которая, в свою очередь, находится на второй позиции в списке и проверяем что она выключена:

 @Test
    @Link(name = "Тест-кейс", url = "https://ссылка_на_тестрейл.ру/")
    @DisplayName("Перевыбрать значение в поле 'Тема отзыва'")
    fun checkResetTopicTheme() = run {
        openDeeplink(FEEDBACK)
        feedbackScreen {
            recycler.childAt<FeedbackBody>(2) {
                selectorInput { click() }
            }
            topicThemeRecycler.childAt<FeedbackScreen.FeedbackTopicTheme>(5) { click() }

            sendButton { isDisabled() }

            recycler.childAt<FeedbackBody>(2) {
                selectorInput { click() }
            }
            topicThemeRecycler.childAt<FeedbackScreen.FeedbackTopicTheme>(2) { click() }

            sendButton { isDisabled() }
        }
    }

Помимо прочего, у нас есть работа с диплинками через adb-desktop-server. Это очень крутая и полезная фича, предназначенная сокращать время работы с нашим приложением. Особенно, если путь довольно тернистый и непростой. Отдельно мы проверяем переходы на экраны. 

Мы реализуем отдельный автотест для того, чтобы убедиться, что функционал действительно рабочий и в нем нет дефектов, а потом уже начинаем работать с конкретными ячейками, перейдя по диплинку. При этом экономия достигает по несколько минут на каждую фичу.

Инфраструктура

Про инфраструктуру будет кратко, потому что по-хорошему это тема для отдельной статьи, и мы ее обязательно сделаем.

Итак, тесты у нас живут в репозитории проекта в Gitlab и там же соответственно запускаются через Gitlab-ci. Запускаются тесты в контейнерах с эмуляторами на раннерах в несколько потоков (в зависимости от мощности раннера - от 4 до 8 параллельных сессий).

Сами тесты запускаются через marathon runner. Это очень гибкий инструмент, который позволяет параллелить тесты, записывать видео (но мы этим функционалом не пользуемся, так как пишем видео через кастомный интерсептор, о чем написано выше), указывать конкретные тестовые сьюты для запуска, если не хотим запускать всю пачку и так далее. Также он умеет собирать статистику по упавшим тестам и в случае если мы хотим запускать их несколько раз при падениях- делать rerun.

Также для поддержания чистоты кодовой базы мы пользуемся линтером ktlint. Этот линтер форматирует код согласно официальному Android Kotlin Style Guide, причем запускается он автоматически каждый раз при коммите. Работает он в связке с pre-commit для удобства.

Что касается отчетов, кроме Allure (о котором будет ниже), отчеты о прогонах постятся в TestRail через самописный скрипт на python, который парсит отчеты и потом постит результаты через API TestRail. Соответственно, для каждого кейса проставляется статус и ссылка на конкретный отчет в Allure.

Прогоны для smoke и regression запускаются руками в gitlab с указанием созданного рана в TestRail (этот момент планируется автоматизировать в ближайшем будущем).

Отчеты

Используем довольно классический Allure, который находится внутри Kaspresso. Это довольно широко распространенный и популярный фреймворк от Яндекса, который позволяет формировать удобные, красивые и читаемые отчеты, в которых можно посмотреть результаты прохождения тестов по разным категориям, сьютам и даже по конкретным методам.

Да, тут нет ничего необычного, но мы покажем, как выглядит стандартный отчет после прохождения тестов (в один поток):

Классический вид Allure отчета
Классический вид Allure отчета

“Внутрянка” упавшего теста выглядит вот так:

Полная информация по падению теста внутри отчета
Полная информация по падению теста внутри отчета

Здесь мы видим (из важного):

  • Полный stacktrace.

  • Поле Owner - здесь мы используем название команды, которая является владельцем данного теста.

  • Ссылка на тест-кейс в TestRail.

  • Скриншот приложения в момент падения теста.

  • Скринкаст приложения в момент падения теста.

  • Дамп из logcat

Выше представлен один из редких случаев - файл скринкаста весит 6 мегабайт, обычно он не превышает 2-3 мегабайта.

Зачем мы сделали скринкасты? 

Из плюсов можно отметить:

  • легко можно отследить полный флоу теста;

  • проверка работоспособности теста;

  • проверка на скорость прохождения теста.

Из минусов:

  • Если тесты оказываются громоздкими (чего стараемся избегать), то скринкаст может превышать 10 мб за видео, что может засорять раннеры.

Проблемы

Думаю, важно поделиться теми проблемами, с которыми мы столкнулись. Возможно, это предостережет Вас от выбора данного фреймворка, ориентируясь на Ваши бизнес-задачи и условия в конкретно Вашей компании.

  • Отсутствие обкатанных примеров и документации в общем доступе.

  • Требуется достаточно мощное железо для постоянной работы в Android Studio:  сборка приложений, постоянно открытые эмуляторы и так далее. Можно страдать с вариантами ноутбуков с i7/16, но постоянно будет использоваться своп от ssd, что будет в целом сильно снижать ресурс ноутбука. Желательно брать технику от 32 гб оперативы. По нашим наблюдениям, хорошо работает на макбуках M1/32 или M1/64 версиях и стационарниках на Ryzen9/64.

    *шутка про java и всю оперативку, которую она съела*

  • Требуется дополнительное обучение сотрудников - про него расскажем отдельной обширной статьей.

  • Готовых специалистов на рынке немного, от этого они достаточно дорогие.

  • В связи с тем, что во многих структурах БКС обширно практиковалось и практикуется исключительно ручное тестирование, в компании нет фундаментального понимания необходимости наличия автоматизированных проверок. Пришлось потратить некоторое время на разъяснение плюсов автотестов. Такие вопросы периодически поднимаются снова, но, конечно, безусловная часть нашей работы - это общение и донесение важных ценностей культуры контроля качества для людей смежных профессий, которые, безусловно, могут быть слабо погружены в тему в силу разных обстоятельств. Это их не оправдывает, но такие ситуации могут случаться, поэтому честно делимся своим опытом на этот счет.


Хотим упомянуть в статье…

…часть важных участников вышеописанных событий.

Виталий Круглов

Ведущий специалист в команде автоматизации, идеолог проекта, набил много шишек, “собаку съел”, и так и не смог выспаться

Хайрутдинов Антон

Внешний консультант по внедрению автотестов

Сидоров Константин

Руководитель QA управления, начал всю эту трансформацию

Дёмин Сергей

Руководитель направления автоматизации, грамотно держит на плаву все процессы и направляет ребят

Малышкин Артём

Руководитель QA в фичатим, помогает продвигать автотесты среди команд

Мишин Илья

SDET, ответственный за инфраструктуру

Сидельников Николай

Руководитель QA команды разработки торгового функционала

Романенко Илья

Старший QA в команде разработки торгового функционала

А также всех QA инженеров Мира Инвестиций, кто научился, пишет и использует автотесты в своей ежедневной работе.

UPD 22.04.22:

Согласно комментарию @alex-tiurin, его фреймворк теперь называется Ultron и позволяет работать с RecyclerView.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Какие технологии предпочитаете вы для автотестирования Mobile?
21.05% Appium 8
7.89% Espresso 3
55.26% Kaspresso 21
13.16% Другой вариант 5
2.63% Ultron 1
Проголосовали 38 пользователей. Воздержались 6 пользователей.
Теги:
Хабы:
+5
Комментарии 16
Комментарии Комментарии 16

Публикации

Информация

Сайт
bcs.career
Дата регистрации
Дата основания
Численность
5 001–10 000 человек
Местоположение
Россия
Представитель
Давыдова Любовь

Истории