Kotlin Dsl, Исправления И Элегантные Тесты Пользовательского Интерфейса В Android

Когда я познакомился с Kotlin DSL, я подумал: отличная вещь, жаль, что при разработке продуктов она не пригодится.

Однако я ошибался: он помог нам создать очень лаконичный и элегантный способ написания сквозных UI-тестов в Android.

Kotlin DSL, исправления и элегантные тесты пользовательского интерфейса в Android

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

Мы помогаем соискателям и работодателям найти друг друга:

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

Вы скажете: «Так создайте заранее тестовых работодателей и соискателей, а потом работайте с ними на тестах».

Но здесь есть пара проблем:

  1. во время тестов меняем данные;
  2. тесты выполняются параллельно.

Тестовая среда и приспособления Сквозные испытания проводятся на испытательных стендах.

У них практически есть боевая обстановка, но реальных данных нет. В связи с этим при добавлении новых данных индексация происходит практически мгновенно.

Для добавления данных на стенд мы используем специальные методы подгонки.

Они добавляют данные напрямую в базу данных и моментально индексируют их:

  
  
  
  
  
  
  
  
   

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 при инициализации анализируется;
  • На основе полученных значений на сервере создаются тестовые данные;
  • Выполняется дополнительный блок преобразования, в котором можно указать ответы;
  • Тест проводится с окончательным набором данных.

Парсинг данных из блока инициализации Что за волшебство там происходит? Давайте посмотрим, как устроен элемент TestCaseDsl верхнего уровня:

@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-тестированию, то прежде чем начать, предлагаю вам ознакомиться со следующими материалами:

Что дальше Эта статья — первая в серии об инструментах и высокоуровневых платформах для написания и поддержки UI-тестов в Android. По мере выхода новых частей я буду ссылаться на них в этой статье.

Теги: #Android #Разработка Android #Тестирование мобильных приложений #автотест #dsl #kakao #fixtures

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.