В статье будет рассмотрен подход позволяющий существенно упростить создание и поддержку тестовой end-to-end (e2e) библиотеки. Также, будет показано как использование расписания в e2e тестировании помогает даже тогда, когда в реальной системе расписание отсутствует. Центральной идеей данного подхода является возможность совмещения действий над системой и всех необходимых при этом проверках в вызове одной функции. А по ходу статьи мы будем героически решать возникающие на этом пути сложности. Для упрощения объяснения концепции мы будем использовать сферический абстрактный интернет магазин с надуманными проблемами. Очень упрощённо автоматизированное тестирование можно представить в виде последовательности вызова функций. Некоторые из функций при этом совершают какое-то действие с системой, например создают, обновляют или удаляют какие-то объекты, а другие функции проверяют, что объекты действительно создались, изменились или были удалены. Самым очевидным способом будет сделать эти функции простыми обёртками над api/sql запросами с явной передачей всех необходимых для этого параметров. И если для функций-действий это не доставляет особых проблем, то для функций-проверок приходится передавать практически одни и те же параметры (а иногда проверяемых параметров больше сотни) по всему сценарию. Такой подход провоцирует copy&paste, а также усложняет чтение сценария. Рассмотрим это на примере: Чтобы избежать дублирования достаточно создать “виртуальное” представление объекта тестируемой системы в нашей тестовой библиотеке. А во время функций-действий не только выполнять API запрос, но и обновлять состояние нашего внутреннего объекта. Сам объект при этом будет похож на класс в распространенных языках программирования с поддержкой ООП и выглядит примерно так: При таком подходе наш пример можно переписать так: Сценарий при этом стал более читаемым, тестируемые объекты обрели явные имена (client1 в примере), а усилия на его создание и поддержку существенно сократились. В дальнейшем мы во всех примерах будем исходить именно из объектного подхода в автоматизации. Уже из описания следует, что в таких системах часть событий происходит по внутреннему расписанию, при этом в ряде случаев эти события затрагивают сразу большие группы тестовых сценариев. Пример: в целях экономии на доставке владельцы нашего магазина решили все заказы клиента за день отправлять одной посылкой. Для этого в 18:00 неизвестного часового пояса через cron запускается скрипт, который агрегирует заказы клиентов за день. Нам нужно убедиться, что агрегация осуществляется корректно, особенно с учётом того, что есть приоритетные заказы, которые должны быть отправлены сразу после оформления. При реальном тестировании наступления 18:00 никто естественно не ждёт, а просто вызывает нужный скрипт и/или используют виртуальный перевод времени тестируемой системы. Внутреннее расписание нашей библиотеки будет состоять из специальных блоков, которые мы назовём globalSteps. Эти блоки выполняются в заранее определённом порядке, а переход на следующий globalStep будет выполнен только после выполнения задач всех сценариев предыдущего блока.  Рассмотрим на примере: Сначала будут выполнены задачи из globalStep(order1) для сценариев scenario1 и scenario2 (в auxiliary сценарии у нас нет никаких задач для этого шага), после чего мы перейдём к globalStep(order2) и т.д. Вспомогательный (auxiliary) сценарий нужен для того, чтобы выполнять задачи внутреннего расписания тестируемой системы, а задач функционального тестирования в нём быть не должно. На псевдоязыке scenario1 можно описать так: А auxiliary так: Другим, менее очевидным способом использования расписания в тестовой библиотеке, является тестирование растянутых по времени событий. Поясню это на примере: после создания заказа клиенту должно быть отправлено письмо, но эмпирическим путём выяснено, что письмо приходит не мгновенно, а с задержкой до 10 секунд. Тогда создание заказа вместе с проверкой будут выглядеть так: ord1 = mkOrder(...) wait(10s) ord1.checkEmail() Если у нас будет 100 подобных сценариев и они будут выполняться последовательно, то только время ожидание составит 100*10s = 1000s ~ 16.66min, что достаточно неприятно. Но, если мы применим подход с globalSteps, то этого можно избежать:  auxilary scenario1 scenarioN makeOrder  ord1=mkOrder(...) ord1=mkOrder(...) wait sleep(10s)   checkEmail  ord1.checkEmail() ord1.checkEmail() На этом экспресс курс по системам с расписанием завершён и мы переходим непосредственно к нашей концепции. Реальные сценарии легко бьются на небольшие автономные блоки, которые включают в себя одиночное действие (action) над системой и все необходимые при этом проверки. Постараемся это описать в виде некоего события (event). Если посмотреть на последний пример, то наш event обобщённо можно описать так: действие (action) <пауза> реакция системы (feedback) Чтобы не вызывать паузу непосредственно в нашем event, введём специальный блок для выполнения отложенных действий и назовём его feedback. Теперь наше событие в псевдокоде можно описать так: Из этого примера видно следующее: у нас создаётся новый объект newOrder, который очень удобно передавать как параметр другим функциям вызов функции checkEmail() вызывается отложено в feedback блоке объект newOrder возвращается из event для его дальнейшего использования в сценариях если бы не feedback блок, то event ничем не отличается от обычной функции При этом внимательный и хорошо подкованный читатель возможно заметил, что feedback очень похож на вызов лямбда функции. Без лишних, деталей в нашем реальном проекте, реализованном на Kotlin, эта функция выглядела бы примерно так: Другими словами, вся описываемая здесь машинерия легко выразима в mainstream языках программирования поддерживающих лямбда-выражения.  К сожалению, технические детали реализации концепции сильно перегрузят статью, поэтому в конце будет просто приведена ссылка на минимально работающий пример на Kotlin. Как мы выяснили ранее, еvent сам по себе это просто функция, но из-за наличия feedback части она может вызываться только в специальном контексте. Создадим этот контекст расширив концепцию globalSteps добавив в него новые блоки выполнения и назовём это stage. Stage будет состоять минимум из трёх уже знакомых частей: action <pause> feedback Главное отличие stage от event в том, что stage является частью расписания и именно в нём выполняются events (один или несколько) и общая для всех сценариев пауза. Другими словами, можно сказать, что stage состоит из двух globalSteps (action и feedback) разделённых паузой. На псевдоязыке создание расписания можно описать так: Напомню, что как и в случае с globalSteps, сценарий будет выполняться в порядке добавления stages в расписание. Порядок вызова stages в самих сценариях при этом игнорируется. Далее концепция stage будет расширена. Все проверки тестовой системы принципиально можно поделить на два вида проверка внутреннего состояния системы (через sql, RestAPI и т.д.) ожидание активного ответа в виде некоего сообщения (e-mail, sms, mq-message и т.д.) Вышеописанные feedback проверки отлично работают с сообщениями, но проверки внутреннего состоянии имеют ряд особенностей: Часто api реальной системы возвращает информацию только по группе объектов, например все заказы одного клиента. В таких случаях проверки одиночных объектов не работают. Если мы пропустим проверку какого-то сообщения системы, то это легко вычислить, например сравнив количество выполненных проверок и реальное количество сообщений. В противовес этому, пропущенную sql/api-проверку обнаружить гораздо сложнее. Правильно составленный sql/api-запрос к системе позволяет заметить неучтённые изменения соседних объектов. Рассмотрим пример для последнего пункта: делаем два заказа, но один оплачиваем и его статус сразу меняется на подтверждённый, а другой, нет и спустя какое-то время, он отменяется без нашего участия. Если мы будем выполнять все проверки через feedback вариант, то получим следующее: Эту проблему можно решить через агрегированную проверку, при которой в одном sql/api-запросе будут возвращаться сразу все заказы нашего клиента. Сценарий от этого не станет правильным (мы где-то пропустили order2.reject()), но ошибка станет видна в отчёте тестирования. Т.е. мы хотим, чтобы при изменении хотя бы  одного из объектов сценария выполнялись все агрегированные проверки объектов данного типа. Реализовать это достаточно просто: нужно иметь списки всех объектов сценария разбитые по типам (например clients, orders и т.д.) устанавливать флаг наличия обновления на группе объектов одного типа в соответствующих events написать код, который, при наличии флага, выполнит все агрегированные проверки и сбросит этот флаг Посмотрим что будет происходить в stage("createOrder") с учётом вышесказанного. Внутри нашего stage добавим блок stateCheck, а порядок выполнения теперь будет выглядеть так: action <pause> stateCheck feedback Внутри блока stateCheck будет происходить примерно следующее: Надеюсь, что приведённый код добавил немного ясности к описанию этого блока. Остаётся только добавить пару моментов их практического применения: Внутри stateCheck можно добавить сразу несколько проверок, что особенно удобно когда нужно всесторонне протестировать разные api endpoints. Один event может устанавливать флаг isUpdated сразу для нескольких типов объектов (например и для orders, и для clients). Иногда, во время выполнения action части, система не возвращает внутренний идентификатор объекта (например номер заказа), но он становится известен во время stateCheck. В этом случае удобно делать связывание внутренних идентификаторов реальной системы с нашими виртуальными объектами на этом шаге. После чего в feedback проверках можно использовать этот идентификатор как ключ. Если вам кажется, что проверка сразу всех однотипных объектов является излишней, при изменении хотя бы одного из них, то учтите, что внутри одного сценария часто находятся потенциально взаимозависимые объекты. Также stateCheck проверки являются “срезами” состояний системы, что сильно помогает при расследовании проблем. Рассмотрим достаточно сложную для нашей автоматизации ситуацию: пусть магазин не имеет собственной службы доставки, а курьерская служба с которой мы работаем имеет очень низкий уровень развития IT. Из-за этого информация о получении заказа приходит не в реальном времени, а в конце их операционного дня, причём данные отправляются в виде большого текстового файла в котором есть информация сразу по всем доставленным заказам за день. Нам нужно проверить, что файл корректно обрабатывается. Для этого мы должны сформировать его на основании данных заказов из наших тестовых сценариев. В какой-то один сценарий это вынести нельзя по разным причинам: в большинстве сценариев заказ должен завершиться доставкой, т.е. почти в каждом из сценариев должно быть что-то вроде order1.delivered() может быть огромное число тестовых случаев связанных именно с доставкой и описание их всех в одном сценарии будет адом, а этого мы хотим избежать в некоторых сценариях, факт доставки будет лишь предварительным условием, например, в случаях возврата товара Получается, что данные для факта доставки должны быть доступны сразу из всех сценариев, а чтобы не нарушать изолированность внутренних объектов сценария (таких как client, order и т.д.) нам придётся ввести некий общедоступный globalContext. По мере необходимости в этот общий контекст будут добавляться свои объекты, например globalContext::deliveryConfirmation, а в сценариях это будет выглядеть примерно так: Проблема с формированием общего для всех доставок файла решена, но при этом в расписании пока нет места для выполнения проверок, ведь stage("ConfirmDelivery-sendFile") выполняется уже после окончания stage("ConfirmDelivery-add") и его stateCheck/feedback блоков. Давайте попробуем это исправить добавив проверки в отдельный stage. Рассмотрим это на примере из двух сценариев: Scenario1 (работа с заказом) и Auxiliary (тут будет отправка файла). Чтобы было понятно запишем их выполняемые действия в хронологическом порядке: Это “прекрасно” работает, но выглядит несколько коряво… Чтобы выполнить это всё за один stage, просто добавим новый туда блок для “ConfirmDelivery-sendFile” из auxiliary, назовём его innerStep. Т.е. stage теперь будет выглядеть так: action innerStep <pause> stateCheck feedback Так же объединим обновление globalContext и проверки из checkDelivery в новый event: После чего сценарии приобретают нормальный вид: На этом наполнение блоками нашего stage будем считать законченным. А вот про globalContext хочется добавить пару замечаний: в реальном тестировании в нём обычно содержится очень много информации: там может быть список номеров пластиковых карт для тестовых платежей, внутренняя копия товаров с ценами и т.д. также, там обязательно должен быть общий счётчик, на основании которого генерируются уникальные для всех сценариев идентификаторы (смотрите реализацию функции generate в коде на github) Другими словами globalContext в реальном тестировании играет очень важную роль. Опишем свойства идеального, с моей точки зрения, сценария: Не содержит вызова проверок, они выполняются автоматически. Наши внутренние объекты должны иметь удобные для тестировщика имена, например client1, order2 и т.д. Это упрощает написание и восприятие длинных сценариев. Для простых случаев ожидаемый результат действий должен вычисляться автоматически. Например, стоимость заказа можно рассчитать сложив стоимость его позиций, и не требовать явного указания в сценарии Если переданные в event данные не позволяют сразу определить поведение системы, то это должно указываться явно. Например, можем специально попробовать создать двух пользователей с одним e-mail, что не допускается нашим магазином. В этом случае требуется явное указание об ошибке. Сценарий не должен содержать ветвлений, иначе это просто минимум два сценария. По сути он должен быть декларативным. Выполнять проверки автоматически с помощью events/stages мы научились ранее, а объектный подход дал нам понятные имена. То, что нужно указывать явно, а что лучше считать в events автоматически, приходит с опытом. Ну и отсутствие ветвлений непосредственно в сценариях – это просто культура их написания. Всё перечисленное в сумме даёт очень компактные и легко читаемые сценарии. Давайте посмотрим это на простом примере: Реализация данной идеи в реальном проекте сделана на язык Kotlin. Он позволяет создавать свой собственный DSL язык, что, я считаю, критичным при описании сценариев. Также, Kotlin позволяет задавать значения по-умолчанию для входных параметров функций, что сильно облегчает поддержку тестовой библиотеки в условиях постоянно развиваемой тестируемой системы Минимальная реализация представлена на github. Для простоты восприятия там нет связи с каким-либо фрэймворком автоматизации или системами построения отчётов, а также удалены многие полезные мелочи, такие как: работа с конфигами, фильтрация запуска сценариев на основании их медатаданных и т.д. Но при этом всё, что связано с реализацией расписания, там сохранено в максимально близком к реальному проекту виде. Из-за вышеописанных ограничений всё “тестирование” представляет из себя создание и обновление “виртуальных” объектов и вывод их состояния на экран. Объединение двух достаточно простых идей: объектного подхода и использование events/stages в расписании, позволило превратить ад автоматизации e2e тестирования в достаточно интересный и высокоэффективный процесс. При этом, получилось достигнуть всех целей, которые были заложены при разработке данной концепции: проверки больше не теряются ошибки сценариев хорошо видны в отчётах сценарии понятны даже людям, не вовлечённых в процесс тестирования написать простой сценарий можно буквально за 15 минут автоматизация помогает в ручном тестирование при создании предварительных условий поддержка тестовой библиотеки достаточно простая, т.к. при изменение функционала тестируемой системы обычно нужно поправить только логику нескольких events, а не все сценарии размылась граница между ручными тестировщиками и автоматизаторами тестирования, т.к. для написание автоматических сценариев не требуется хороших навыков программирования но минимум один хороший программист в команде должен быть, чтобы создать каркас библиотеки и помогать другим тестировщикам (иногда без опыта программирования) со сложными проблемами. А также из-за огромного числа вещей происходящих в автоматическом режиме, складывается впечатление, что здесь не обошлось без щепотки магии.