Когда я познакомился с Kotlin DSL, я подумал: отличная вещь, жаль, что при разработке продуктов она не пригодится.
Однако я ошибался: он помог нам создать очень лаконичный и элегантный способ написания сквозных UI-тестов в Android.
О сервисе, тестовых данных и почему все не так просто Сначала немного контекста о нашем сервисе, чтобы вы поняли, почему мы приняли те или иные решения.
Мы помогаем соискателям и работодателям найти друг друга:
- работодатели регистрируют свои компании и размещают вакансии
- соискатели ищут вакансии, добавляют их в избранное, подписываются на результаты поиска, создают резюме и отправляют ответы
Вы скажете: «Так создайте заранее тестовых работодателей и соискателей, а потом работайте с ними на тестах».
Но здесь есть пара проблем:
- во время тестов меняем данные;
- тесты выполняются параллельно.
У них практически есть боевая обстановка, но реальных данных нет. В связи с этим при добавлении новых данных индексация происходит практически мгновенно.
Для добавления данных на стенд мы используем специальные методы подгонки.
Они добавляют данные напрямую в базу данных и моментально индексируют их:
Доступ к приборам возможен только из локальной сети и только для тестовых стендов.interface TestFixtureUserApi { @POST("fx/employer/create") fun createEmployerUser(@Body employer: TestEmployer): Call<TestEmployer> }
Методы вызываются из теста непосредственно перед запуском стартового Activity. DSL Теперь мы подошли к самой пикантной части.
Как указываются данные для теста? initialisation{
applicant {
resume {
title = "Резюме на аналогичную вакансию"
isOptional = true
resumeStatus = ResumeStatus.APPROVED
}
resume {
title = "Еще какое-то резюме"
}
}
employer {
vacancy {
title = "Резюме на аналогичную вакансию"
}
vacancy {
title = "Резюме на аналогичную вакансию"
description = "Working hard"
}
vacancy {
title = "Резюме на аналогичную вакансию"
description = "Working very hard"
}
}
}
В блоке инициализации создаём необходимые для теста сущности: в примере выше мы создали одного претендента с двумя резюме, а также одного работодателя, предоставившего несколько вакансий.
Чтобы исключить ошибки из-за пересечения тестовых данных, мы генерируем уникальный идентификатор теста и каждой сущности.
Отношения между сущностями Каково основное ограничение при работе с DSL? Из-за древовидной структуры довольно сложно построить связи между разными ветвями дерева.
Например, в нашем приложении для соискателей есть раздел «Подходящие вакансии для резюме».
Чтобы вакансии появлялись в этом списке, нам необходимо настроить их таким образом, чтобы они ассоциировались с резюме текущего пользователя.
initialisation {
applicant {
resume {
title = "TEST_VACANCY_$uniqueTestId"
}
}
employer {
vacancy {
title = "TEST_VACANCY_$uniqueTestId"
}
}
}
Для этого используется уникальный идентификатор теста.
Таким образом, при работе с приложением к данному резюме рекомендуются данные вакансии.
Кроме того, важно отметить, что в этом списке не будет других вакансий.
Инициализация однотипных данных Что делать, если вам нужно сделать много вакансий? Это как скопировать каждый блок? Конечно же нет! Создаем метод с блоком вакансий, в котором указано необходимое количество вакансий и преобразователь для их диверсификации в зависимости от уникального идентификатора.
initialisation {
employer {
vacancyBlock {
size = 10
transformer = {
it.also { vacancyDsl ->
vacancyDsl.description = "Some description with text ${vacancyDsl.uniqueVacancyId}"
}
}
}
}
}
В блоке vacancyBlock указываем, сколько клонов вакансий нам нужно создать и как их трансформировать в зависимости от порядкового номера.
Работа с данными в тесте Пока тест выполняется, работать с данными становится очень просто.
У нас есть доступ ко всем данным, которые мы создаем.
В нашей реализации они хранятся в специальных обертках коллекций.
Из них можно получить данные как по порядковому номеру вакансии (vacancies[0]), так и по тегу, который можно задать в dsl (vacancies["моя вакансия"]), и по ярлыкам (vacancies.first()).
TaggedItemContainer class TaggedItemContainer<T>(
private val items: MutableList<TaggedItem<T>>
) {
operator fun get(index: Int): T {
return items[index].
data } operator fun get(tag: String): T { return items.first { it.tag == tag }.
data } operator fun plusAssign(item: TaggedItem<T>) { items += item } fun forEach(action: (T) -> Unit) { for (item in items) action.invoke(item.data) } fun first(): T { return items[0].
data } fun second(): T { return items[1].
data } fun third(): T { return items[2].
data } fun last(): T { return items[items.size - 1].
data
}
}
Почти в 100% случаев при написании тестов мы используем методы first() и Second(), оставляя остальные для гибкости.
Ниже приведен пример теста с инициализацией и действиями по его выполнению.
Какао initialisation {
applicant {
resume {
title = "TEST_VACANCY_$uniqueTestId"
}
}
}.
run { mainScreen { positionField { click() } jobPositionScreen { positionEntry(vacancies.first().
title)
}
searchButton {
click()
}
}
}
Что не вписывается в DSL Могут ли все данные поместиться в DSL? Нашей целью было сделать DSL максимально кратким и простым.
В нашей реализации из-за того, что порядок указания соискателей и работодателей не важен, не представляется возможным подогнать их отношения – ответы.
Создание ответов уже выполняется в последующем блоке операциями над уже созданными на сервере сущностями.
Реализация DSL Как вы поняли из статьи, алгоритм указания тестовых данных и выполнения теста следующий:
- Часть DSL при инициализации анализируется;
- На основе полученных значений на сервере создаются тестовые данные;
- Выполняется дополнительный блок преобразования, в котором можно указать ответы;
- Тест проводится с окончательным набором данных.
@TestCaseDslMarker
class TestCaseDsl {
val applicants = mutableListOf<ApplicantDsl>()
val employers = mutableListOf<EmployerDsl>()
val uniqueTestId = CommonUtils.unique
fun applicant(block: ApplicantDsl.() -> Unit = {}) {
val applicantDsl = ApplicantDsl(
uniqueTestId,
uniqueApplicantId = CommonUtils.unique
applicantDsl.block()
applicants += applicantDsl
}
fun employer(block: EmployerDsl.() -> Unit = {}) {
val employerDsl = EmployerDsl(
uniqueTestId = uniqueTestId,
uniqueEmployerId = CommonUtils.unique
employerDsl.block()
employers += employerDsl
}
}
В методе заявителя мы создаем ApplicantDsl. ЗаявительDSL
@TestCaseDslMarker
class ApplicantDsl(
val uniqueTestId: String,
val uniqueApplicantId: String,
var tag: String? = null,
var login: String? = null,
var password: String? = null,
var firstName: String? = null,
var middleName: String? = null,
var lastName: String? = null,
var email: String? = null,
var siteId: Int? = null,
var areaId: Int? = null,
var resumeViewLimit: Int? = null,
var isMailingSubscription: Boolean? = null
) {
val resumes = mutableListOf<ResumeDsl>()
fun resume(block: ResumeDsl.() -> Unit = {}) {
val resumeDslBuilder = ResumeDsl(
uniqueTestId = uniqueTestId,
uniqueApplicantId = uniqueApplicantId,
uniqueResumeId = CommonUtils.unique
)
resumeDslBuilder.apply(block)
this.resumes += resumeDslBuilder
}
}
Затем выполняем над ним операции из блока Block: ApplicantDsl.() -> Unit. Именно такая конструкция позволяет нам легко работать с полями ApplicantDsl в нашем DSL. Обратите внимание, что uniqueTestId и uniqueApplicantId (уникальные идентификаторы для соединения сущностей друг с другом) уже установлены на момент выполнения блока и мы можем получить к ним доступ.
Подобным образом структурирован блок инициализации изнутри: fun initialisation(block: TestCaseDsl.() -> Unit): Initialisation {
val testCaseDsl = TestCaseDsl().
apply(block)
val testCase = TestCaseCreator.create(testCaseDsl)
return Initialisation(testCase)
}
Мы создаем тест, применяем к нему действия блока, затем с помощью TestCaseCreator создаем данные на сервере и помещаем их в коллекции.
Функция TestCaseCreator.create() довольно проста — мы перебираем данные и создаем их на сервере.
Подводные камни и идеи
Некоторые тесты очень похожи и отличаются только входными данными и способами управления их отображением (например, когда в вакансии указаны разные валюты).В нашем случае таких тестов было немного, и мы решили не загромождать DSL специальным синтаксисом.
Во времена, когда не существовало DSL, индексирование данных занимало много времени, и чтобы сэкономить время, мы проводили множество тестов в одном классе и создавали все данные в статическом блоке.Не делайте этого — это лишит вас возможности перезапустить неудачный тест. Дело в том, что во время запуска неудавшегося теста мы могли изменить исходные данные на сервере.
Например, мы могли бы добавить вакансию в избранное.
Тогда при перезапуске теста нажатие на звездочку приведет, наоборот, к удалению вакансии из списка избранных, а это поведение, которого мы не ожидаем.
Полученные результаты Такой способ указания тестовых данных значительно упростил работу с тестами: При написании тестов не нужно думать о том, есть ли сервер и в каком порядке нужно инициализировать данные; Все объекты, которые можно установить на сервере, легко перечисляются в подсказках IDE; Появился единый способ инициализации и соединения данных друг с другом.
Похожие материалы Если вас заинтересовал наш подход к UI-тестированию, то прежде чем начать, предлагаю вам ознакомиться со следующими материалами:
- Типобезопасные строители — официальная документация на сайте kotlinlang.org;
- Kotlin DSL: теория и практика — отличный доклад с jPoint 2018 о kotlin dsl и его декодировании;
- Какао — как снова сделать UI-тестирование великолепным — базовые знания о фреймворке Kakao для UI-тестов;
- Как перестать бояться и начать писать UI-тесты с помощью Kakao — доклад о Kakao с AppsConf 2019. Пока доступны только слайды, потом будет видео.
Теги: #Android #Разработка Android #Тестирование мобильных приложений #автотест #dsl #kakao #fixtures
-
Гренландия
19 Oct, 24 -
Отзывы О Бонусах Fusion Hq
19 Oct, 24 -
Видео С Митапа Avito Data Science
19 Oct, 24 -
Мини Лайфхак Для Ластфм
19 Oct, 24