Привет!
Я — Урманчеев Станислав, QA Automation Engineer на проекте «Лояльность» в Mир Plat.Form (НСПК). Хочу поделиться с читателями Хабра нашим опытом в создании и развитии фреймворка для автоматизации тестов на Appium.
Какие проблемы мы собрали по пути, к чему пришли в итоге и почему не стоит усложнять жизнь тестировщикам сложным API для тестирования – читайте под катом.
Дисклеймер: о Kotlin dsl есть подробная статья на Хабре и документация на Kotlinlang.
Какая проблема стояла перед командой тестирования?
Немного о продукте, для которого мы создали наш фреймворк:
Мобильное приложение «Привет Мир!» для iOS/Android c регулярными релизами раз в 2-4 недели. Основной функционал — взаимодействие с программой лояльности платежной системы «Мир»:
-
Регистрация клиента.
-
Добавление и удаление карт.
-
Просмотр и участие в акциях.
В целом функционал и клиентский путь несложный. Команде тестирования Мир Plat.Form (НСПК) нужно в сжатые сроки провести регрессионное тестирование и ПСИ.
Наша основная боль — сокращение временных ресурсов на поддержку и написание тестов. С такими вводными мы начали выбирать инструменты.
Возможные альтернативы
У нас была возможность попробовать различные связки языков и фреймворков для автоматизации.
Kotlin/Swift |
Kaspresso + XCUITest
|
· Поддержка тестов внутри двух проектов становилась довольно затруднительной; · Значительная часть отведенного на регресс времени уходила на сборку проекта внутри корпоративной сети, починку тестов; |
Java |
Cucumber + Appium |
· Дополнительный слой абстракции в лице Gherkin больше создавал проблемы, чем их решал; · Отчеты удобно читать; |
В сторону Cucumber смотрели, но для UI-тестирования он избыточен.
Относительно небольшое количество функционала и тот факт, что автотестирование для приложения делалось с 0, позволило попробовать несколько технологий и выбрать наиболее подходящие под проект. В итоге остановились на стеке:
-
Kotlin
-
Allure
-
Appium
-
Wiremock standalone
Зачем писать свою обертку поверх Appium?
Есть отличный фреймворк Kaspresso, его стиль тестов во многом вдохновлял меня, но в определенный момент было принято решение перейти на кроссплатформенные тесты.
В поисках чего-то похожего был найден этот проект. Akow давно не поддерживают, контрибьютить в opensource не позволяла политика команды — в итоге мы решили написать свой инструмент.
Минусы решений под один проект всем широко известны, но я перечислю плюсы:
-
Ничего лишнего, пишем наиболее лаконичное API для текущего проекта;
-
Легко изменять, не надо форкать или открывать PR в сторонний проект;
-
Повышение вовлеченности тестировщиков — у всех есть возможность внести новый функционал.
Ближе к коду
Главные идеи:
-
Улучшение семантики кода, используя возможности Kotlin;
-
Генерация Allure-шагов для отчета;
-
Изолирование слоев фреймворка для упрощения дальнейшего развития.
Прежде чем приступать к написанию инструмента (пускай небольшого и для тестирования), было бы неплохо определиться с архитектурой. Сейчас нам не потребуются сложные сиквенс-диаграммы, в основе будет Appium. Именно он будет взаимодействовать с устройствами.
Начнем с общего интерфейса для всех UI-элементов в тестах:
interface IUIElement {fun click () fun checkDisplayed() fun checkText() fun sendKeys() }
Создадим контрактный интерфейс IUIElement и добавим функции, которые в дельнейшем реализуем в классе AppiumElement. Этот интерфейс поможет нам также для реализации паттерна проектирования “class AppiumElement(private val by: By, val name: String = "[название_элемента]") : IUIElement { override fun click() = step("Клик по элементу [$name]") { screenshot() driver.findElement(by).click() } override fun checkDisplayed(): Unit = step("Проверка отображения элемента [$name]") { assertTrue("Элемент [$name] не отобразился", driver.findElement(by).isDisplayed) } /** More code*/
Конструктор класса принимает два параметра: Генерация шагов при каждом обращении — функция, значительно упрощающая разбор отчетов, написание тестов и освобождающая от необходимости вручную описывать шаги. К тому же это является хорошим шагом в сторону концепции “тест-кейсы как код”, не прибегая к использованию Cucumber. Для получения ссылки на объект Помимо генерации шагов при обращении к UI приложения, стоит оборачивать проверки API-запросов внутри мобильного приложения. Kotlin даёт широкие возможности для сокращения Boilerplate кода. Вы можете написать новые функции для класса из сторонней библиотеки. Добавим функцию классу Без использования функции-расширения и генерации Еще пример, но уже для Collection: Удобный способ вызова метода (без точки и скобок для вызова), требует указания как получателя, так и параметра. В примере используем его для создания экземпляра После создание элементов приближается к декларативному стилю: Объявление тех же констант, но без использования новых функций выглядит так: Именованные параметры помогают лучше понимать параметры и разбирать код, особенно когда вызываются перегруженные функции или функции с большим количеством входных параметров. Но инфиксная запись делает код тестов ‘чище’. Kotlin позволяет определить для типов ряд встроенных операторов. Для определения оператора для типа определяется функция с ключевым словом operator. Но сейчас мы перегрузим не математические операторы, а то, к чему не так часто обращаются тестировщики в явном виде — оператор invoke. Он является оператором вызова (функции, метода), в круглых скобках транслируется в invoke с соответствующим числом аргументов. Более подробно вы можете почитать в документации, а я продолжу. В блоке кода ниже пример использования перегрузки оператора, функция invoke принимает Также добавим object Mock: В конечном итоге мы получаем расширяемое, но в то же время простое API для тестирования, реализацию паттерна — @Before fun setUp() { Mock{ policy { jsonBody("src/test/resources/policy.json") } } onBoarding { skip() } inputPhoneNumber { phoneFiled.sendKeys("1111111111") continueButton.click() } loginSMS { inputSMS() } createPinCode { pinCodeTab.setPin(1111) } } @Test @DisplayName("Проверка UI") fun checkMainPageUITest() { main { assert { checkUI() } } }
Ниже в блоке кода паттерн Page Object. Здесь я хочу обратить внимание на несколько вещей: Создание в классе отдельной функции assert и класс Asserts для выделения всех функций с проверками в отдельный класс и вызова их только в контексте метода Таким образом все наши шаги по взаимодействию с AppiumElement становятся вложенными Выглядит он отлично: Все шаги сгенерировались автоматически; Код документирует сам себя, освобождая время тестировщика. В дальнейшем из таких отчетов можно создавать тест-кейсы в Allure TestOps, но это уже отдельная история. Написание собственного инструмента не такая костыльная и страшная задача, как может показаться с первого взгляда. Для закрытия большей части задач UI-тестирования хватает 4-7 классов/интерфейсов и нескольких дней. Взамен команда тестирования получит гибкий инструмент, предоставляющий удобное под конкретные задачи API для взаимодействия с тестовым фреймворком (в нашем случае Appium).val by: By
локатор для поиска элемента, val name: String
название добавления элемента для Allure-шага. Вложенные шаги и Page object
MainPage
создадим функцию main, которая принимает block: MainPage.()
функции класса MainPage
и вызывает внутри Allure-шага, название шага берется из tag: String
из конструктора класса. Таким образом в отчете все получается сгруппировано в виде вложенных шагов и помогает сразу понять экран, на котором выполнялись действия/проверки.companion object { fun main(block: MainPage.() -> Unit): MainPage = MainPage().apply { Allure.step(this.tag) { block()} } }
fun verifyRequest(count: Int = 1) { step("Проверка [по URL и query] запроса [$matcher], был отправлен [$count раз]") { verify(exactly(count), anyRequestedFor(urlMatching(matcher))) } }
Добавим немного сахара
By
, чтобы искать TextView
по переданной строке.fun byTextView(text: String): By.ByXPath = By.ByXPath("//android.widget.TextView[@text='$text']") val profileTitle = element(byTextView("Текст, который проверяет на экране"), "Текст для allure шага")
Allure.step
код выглядел бы вот так. Можно вынести в Utils-класс функцию byTextView
, но этот вариант нам нравится больше, доступ к нужному методу есть сразу из класса By
. step("Проверка отображения элемента [$name]") { assertTrue("Элемент [$name] не отобразился", driver.findElement(By.ByXPath("// android.widget.TextView[@text='$text’]") .isDisplayed)) }
fun Collection<IUIElement>.verifyElements() = this .forEach { it.verifyElement() }
Infix запись функций
AppiumElement
.infix fun String.byClassName(name: String): AppiumElement = AppiumElement(By.className(this))
val searchButton = "Кнопка поиска" byClassName "SearchButtonClass" val title = "текст_для_локатора" withName "описание для allure отчета"
val searchButton =AppiumElement(selector = By.className("SearchButtonClass"), name = "Кнопка поиска") val title =AppiumElement(selector = By.ByXPath("// android.widget.TextView[@text='$text']") name = "Текст заголовка")
Перегрузка оператора invoke
block: Endpoint.() -> Unit
, которые выполняются в apply:class Endpoint(override val pathMatcher: String = ".*") : IEndpointStub { operator fun invoke(block: Endpoint.() -> Unit) = apply(block) val onMatch: ForwardChainExpectation = mockServer.`when`(request().withPath(pathMatcher)) override fun jsonBody(filePath: String) { onMatch.respond( response() .withBody(loadText(filePath)) .withStatusCode(200) .withHeader("Content-Type", "application/ json;charset=UTF-8")) } }
object Mock { operator fun invoke(block: Mock.() -> Unit) = apply(block) val profile get() = Endpoint(".+profile") /** More endpoint’s*/ }
Что получаем в итоге
fun main
, которая возвращает ссылку на наш class MainPage
и принимает блок кода, который оборачивается в Allure.step
и выполняется в apply. class MainPage : BasePage("Главная") { val bottomToolbar by lazyUnsafe { MainBottomToolbarElement() } val searchButton = "Кнопка поиска" byClassName "SearchButtonClass" val title = "Текст для локатора" withName "описание для allure отчета" fun assert(block: MainPage.Asserts.() -> Unit): MainPage = apply { Asserts().block() } inner class Asserts { fun checkUI() = listOf(transferButton, returnedMoney, title).verifyElements() } companion object { fun main(block: MainPage.() -> Unit): MainPage = MainPage().apply { Allure.step(this.tag) { block() } } }
Как выглядит отчет
Итоги
ссылка на оригинал статьи https://habr.com/ru/articles/685198/
Добавить комментарий