Когда познакомился с Kotlin DSL, подумал: отличная штука, жалко в продуктовой разработке она не пригодится. Однако, я был неправ: он нам помог сделать очень лаконичный и элегантный способ написания End-to-end UI тестов в Android.   Для начала немного контекста про наш сервис, чтобы вам было понятно, почему мы приняли те или иные решения. Мы помогаем соискателям и работодателям найти друг друга: Для того чтобы имитировать реальные пользовательские сценарии и убедиться, что на них приложение работает корректно, нам нужно создать на сервере все эти тестовые данные. Вы скажете: “Так создайте тестовых работодателей и соискателей заранее, а потом в тестах уже с ними и работайте”. Но тут есть пара проблем:  End-to-end тесты запускаются на тестовых стендах. На них практически боевое окружение, но отсутствуют реальные данные. В связи с этим при добавлении новых данных индексация происходит почти моментально. Чтобы добавить на стенд данные, мы используем специальные методы фикстуры. Они добавляют данные прямиком в базу данных и моментально проводят индексацию: Фикстуры доступны только из локальной сети и только для тестовых стендов. Методы вызываются из теста непосредственно перед запуском стартового Activity. Вот мы и дошли до самого сочного. Как же задаются данные для теста?  В блоке initialisation мы заводим необходимые для теста сущности: в примере выше мы создали одного соискателя с двумя резюме, а также одного работодателя, который предоставил несколько вакансий. Чтобы исключить ошибки, связанные с пересечением тестовых данных, мы генерируем уникальный идентификатор для теста и для каждой сущности. Связи между сущностями В чем основное ограничение при работе с DSL? Из-за его древовидности довольно сложно построить связи между различными ветками дерева.  К примеру, в нашем приложении для соискателей есть раздел “Подходящие вакансии для резюме”. Чтобы в этом списке появились вакансии, нам нужно задать их таким образом, чтобы они были связаны с резюме текущего пользователя. Для этого используется уникальный идентификатор теста. Таким образом, при работе с приложением заданные вакансии рекомендуются для данного резюме. Кроме того, важно отметить, что никакие другие вакансии в этом списке не появятся. Инициализация однотипных данных А что если нужно сделать много вакансий? Это каждый блок так копировать? Разумеется нет! Делаем метод с блоком вакансий, в котором указывается необходимое число вакансий и трансформер, чтобы разнообразить их в зависимости от уникального идентификатора. В блоке vacancyBlock мы указываем, сколько клонов вакансий нам нужно создать и как трансформировать их в зависимости от порядкового номера. Работа с данными в тесте Во время выполнения теста работа с данными становится очень простой. Нам доступны все созданные нами данные. В нашей реализации они хранятся в специальных обертках для коллекций. Из них можно получить данные как по порядковому номеру задания ( vacancies[0]), так по тэгу, который можно задать в dsl (vacancies[“my vacancy”]), и по шорткатам (vacancies.first() Практически в 100% случаях при написании тестов мы используем методы first() и second(), остальные держим для гибкости. Ниже привел пример теста с инициализацией и с шагами на Kakao Что не помещается в DSL Все ли данные можно уместить в DSL? Мы преследовали цель оставить DSL максимально лаконичным и простым. В нашей реализации из-за того, что порядок задания соискателей и работодателей не важен, не получается уместить их взаимосвязь — отклики.
Создание откликов уже выполняется в последующем блоке операциями над уже созданными на сервере сущностями. Как вы поняли из статьи, алгоритм задания тестовых данных и выполнения теста следующий: Разбор данных из блока initialisation Что там за магия происходит? Рассмотрим, как конструируется верхнеуровневый элемент TestCaseDsl: В методе applicant мы создаем ApplicantDsl.  Затем мы выполняем над ним операции из блока block: ApplicantDsl.() -> Unit. Именно эта конструкция позволяет нам легко оперировать с полями ApplicantDsl в нашей DSL. Обратите внимание, что uniqueTestId и uniqueApplicantId (уникальные идентификаторы для связи сущностей между собой) на момент выполнения блока уже заданные и мы можем к ним обращаться. Блок initialisation изнутри устроен похожим образом: Мы создаем тест, применяем к нему действия блока, далее при помощи TestCaseCreator создаем данные на сервере и укладываем их в коллекции. Функция TestCaseCreator.create() устроена довольно просто — мы перебираем данные и создаем их на сервере. В нашем случае, таких тестов оказалось немного, и мы решили не загромождать DSL специальным синтаксисом Не делайте так — это сделает для вас невозможным перезапуск упавшего теста. Дело в том, что во время запуска упавшего теста мы могли поменять исходные данные на сервере. К примеру, мы могли добавить вакансию в избранное. Тогда при перезапуске теста нажатие на звездочку приведёт уже наоборот к удалению вакансии из списка избранного, а это уже поведение, которые мы не ожидаем. Такой способ задания тестовых данных очень упростил работу с тестами:
При написании тестов не нужно думать о том, есть ли сервер и в каком порядке нужно инициализировать данные;
Все сущности, которые можно задать на сервере, легко выпадают в подсказках IDE;
Появился единый способ инициализации и связи данных между собой. Материалы по теме Если Вас заинтересовал наш подход к UI-тестированию, то перед тем как начать, предлагаю ознакомиться со следующими материалами: Что дальше Данная статья является первой из серии про инструменты и высокоуровневые фреймворки для написания и поддержки UI тестов в Android. По мере выхода новых частей я буду их прилинковывать к данной статье. B2C CTO @hh.ru