В этой статье хочу поделиться инструментом, который я понемногу разрабатывал и улучшал при наличии свободного времени. Применяя для упрощения своих задач, над которыми я работал в разных компаниях за это время.
Работа в автоматизации тестирования стала моей оффициальной профессией несколько лет назад. За это время мне довелось позаниматься очень широким спектром задач, в зависимости от компаний, где я работал. Начиная с тестирования пользовательских интерфейсов различных сайтов, web и мобильных приложений, обязанности заводили меня глубоко в тестирование сложных API сервисов и даже автоматизацию десктопных программ.
Первые годы я плотно занимался тестированием Web приложений на Java, хотя начинал не с неё. Разумеется, перепробовав много подходов, я нашёл для себя любимый stack, который применял при наличии свободы выбора. Для меня это сложилось в TestNg + Selenium + Allure. Естественно, мне приходилось работать со знаменитым Cucumber, а потом и вообще с чудовищьным (на мой взгляд) Serenity и ещё многими другими решениями, включая кастомные разработки разных людей.
Моё расположение никогда не лежало в сторону BDD, так как чаще всего я сам читал отчёты и проверял тесты, и в 100% случаев сам составлял сценарии с Gherkin (иногда по предоставленным сценариям, чаще нет). Так называемое удобство, дающее BDD пользователю, сводилось к минимуму, а вот взамен требовало многое — небоскрёбы из обёрток классов, сложности с наследованием, необходимость создавать кучу дубликатов мизерных действий, абсолютное неудобство передачи данных между шагами и процессами и прочие трудности.
Потом пришёл Selenide, сделавший отказ от BDD-фреймворков ещё более обоснованным (хотя не сразу и не без оговорок). Тем не менее, BDD-отчёты мне в принципе нравились – они почти всегда были более содержательными и легко читаемыми, чем отчёты, собираемые через аннотации @Step в Allure, и куда более читаемыми, чем встроенное автологирование Allure.
Если же хотелось сделать отчёт более содержательным и интуитивно понятным без BDD-подходов и фреймворков, приходилось бы, как и в BDD, оборачивать каждое минимальное действие в обёртку шага. Например:
public class FormPage {
SelenideElement btnSave = $(".btn-primary").as("Save");
}
Данный способ предоставит простейший автогенерированный лог на событие btnSave.click(); – что-то типа "Step1: Click $(Save)".
Проблема с обёртками в сложных сценариях
Конечно, можно сказать, что любой сценарий состоит из последовательности простых действий. Может, в идеальном мире, так и было бы, но если попробовать написать тест для CRM, содержащей десятки таблиц, множество виджетов в каждой записи, сложные фильтрации, поисковые запросы, загрузки, импорты данных, динамические события и прочее – итоговый тест может состоять из 50–60 строк, где каждая строка является достаточно сложным шагом. После этого, взглянув на автогенерированный отчёт, спорить о его "легкочитаемости" будет крайне сложно.
Однако, если я хочу увидеть в отчёте нормальное описание шага, например:
"Click on the <Save> button of the <User settings dialog>"
то придётся писать обёртку:
@Step("Click on the <Save> button of the <User settings dialog>")
public void clickBtnSave() {
btnSave.click();
}
Либо можно, например, так:
public class FormPage {
SelenideElement btnSave = $(".btn-primary").as("the <Save> button of the <User settings widget>");
}
В отчёте это будет выглядеть лучше, но, во-первых, не настолько, а во-вторых, если эта кнопка располагается в каждой записи длинного списка пользователей, где каждая строка является сложным виджетом с множеством различных элементов, включая кнопку, мы получим ElementsCollection, отфильтруем её по какому-то устойчивому параметру, найдём нужные и нажмём на кнопку в каждой записи. Все действия в отчёте будут выглядеть одинаково.
Сложности с коллекциями элементов
В итоге мы вернёмся к большому недостатку BDD-подхода — будем плодить тонны обёрточных методов для каждого мизерного клика, а таких в web-приложениях может быть сотни. И это только для клика! Прибавьте другие действия, контекстные меню, операции с различными полями ввода, события, проверки от простого текста до html-атрибутов и т.д. и умножьте на количество элементов в приложении. Каждое малейшее действие требует конкретного описания, а если пойти по BDD-подходу, то тестовый проект может оказаться намного больше тестируемого приложения, что неоднократно подтверждалось на практике.
Еще большую боль, как с BDD-фреймворками, так и без них, доставляют операции с коллекциями элементов. Всевозможные поиски, фильтрации и проверки требуют конкретных описаний шагов, которые плодятся со страшной скоростью.
Например, в таблице данных пользователей может быть 30 и более параметров:
public class FormPage {
ElementsCollection btnSave = $$(".user-record").as("Users settings list");
}
Что если нам нужно сделать проверку каждого в отдельности из 30 параметров и для каждого создать отдельный метод с описанием? То есть:
public class FormPage {
ElementsCollection users = $$(".user-record").as("Users settings list");
public void assertEmail(String userId, String expectedEmail) {
String targetEmail = users.filter(Condition.text(userId)).first().$x("div[@id='#email-container']").text();
Assertions.assertThat(targetEmail).as("User email").isEqualTo(expectedEmail);
}
}
И делать так для остальных 29 параметров? Конечно, нет. Тут надо сказать, что следует создать DataObject с параметрами пользователя и написать универсальный метод, который будет брать этот объект и проходить по каждому полю, сравнивая все параметры посредством Soft Assert. Да, это здоровое решение, но всё равно придётся писать логику, а о красивом автологируемом отчёте можно забыть.
То, что Allure отчёты Selenide генерировались автоматически, меня не устраивало. Ведь спустя время, чтобы вспомнить какой-то сценарий и понять, что произошло, приходилось напрягаться даже мне, не говоря уже о том, если отчёт смотрит человек, не знакомый с кодом. Кроме того, в некоторых случаях отчёты приходилось делать не на английском языке.
Цель создания фреймворка
Идеального инструмента не бывает, но я задумывался и пытался найти решение, которое позволит исключить большинство сложностей любого подхода. Мне хотелось сделать так, чтобы всё, что я делал, – это определял сущности страниц, виджетов и элементов, и писал сценарий, а всё остальное делал бы фреймворк сам. Таким образом мне почти не пришлось бы писать логику шагов, а она сама организовывалась бы на основании структуры кода и типов данных, не ограничивая свободный стиль Selenide. При этом фреймворк должен автоматически собирать и логировать все вызываемые действия, максимально приближая их к красивым BDD-отчётам и предоставляя возможность редактировать этот процесс по желанию.
Мои рассуждения
-
Нужно взять стандартную и понятную структуру, но добавить больше конкретики.
-
Каждая сущность должна иметь ожидаемые свойства и методы поведения, большинство из которых реализованы сразу из коробки.
-
Каждая сущность, будь то страница, виджет или элемент, должна понимать, где она находится и к чему принадлежит.
-
Все сущности должны быть легко расширяемыми; должна быть возможность добавлять любые кастомные элементы, компоненты и доработки.
-
Фреймворк должен на 99% самостоятельно собирать метаданные объектов.
-
Фреймворк должен опираться на Allure и Selenide, не ограничивая их возможности.
-
Я должен наконец избавиться от поиска элементов через фильтрацию, а вообще от поиска в списках, и забыть как о страшном сне.
-
Код должен повторяться минимум (DRY). Даже объекты страниц должны быть легко расширяемыми и переиспользуемыми.
-
Фреймворк должен включать множество заранее реализованных действий пользователя с браузером, которые логируются в отчёты.
-
Я должен мочь редактировать логируемые шаги и определять степень их подробности динамически.
-
И, наконец, красивые отчёты должны собираться на разных языках, а не только на английском.
Структура PageObject
Начиная с пункта 1, я хотел видеть примерно такую структуру. Я указываю аннотацией классу, что он является страницей. Элементы и виджеты страницы должны быть представлены соответствующими типами этих элементов.
PageObject с простыми элементами
@PageObject
public class FormPage {
@Name("Login")
@Locator(css = "#login")
protected TextField fieldLogin;
@Name("Email")
@Locator(css = "#email")
protected TextField fieldEmail;
@Name("Password")
@Locator(css = "#password")
protected TextField fieldPassword;
// ... другие поля
}
PageObject с виджетами
@PageObject
public class HomePage {
@Name("Navigation bar")
NavBar navBar;
@Name("Search block")
SearchBlock searchBlock;
@Name("Gallery block")
GalleryBlock galleryBlock;
@Name("Footer")
Footer footer;
}
Описание виджетов
@Widget
public static class NavBar extends AbstractWidget {
@Name("Images")
@Locator(css = ".navbar-nav .nav-item:nth-child(1)")
protected Link linkPhotos;
@Name("Stickers")
@Locator(css = ".navbar-nav .nav-item:nth-child(2)")
protected Link linkStickers;
@Name("Icons")
@Locator(css = ".navbar-nav .nav-item:nth-child(3)")
protected Link linkIcons;
}
Где:
-
@PageObject — указывает, что класс является страницей;
-
@Widget — указывает, что класс является виджетом (частью страницы);
-
@Name — присваивает элементу или виджету конкретное название;
-
@Locator — один из способов указать путь до объекта на HTML-странице.
Я набросал такую схему и стал работать над её реализацией.
Я решил не выпендриваться и просто сложил название из технологий в фундаменте – получилось "Allurium". Далее будут рассмотрен пример, иллюстрирующий небольшую часть возможностей.
Полный набор примеров и описаний, а также документацию можно будет найти на сайте фреймворка
Подключить зависимость максимально просто
<dependency>
<groupId>io.github.ipk-tools</groupId>
<artifactId>allurium</artifactId>
<version>2.0.0</version>
</dependency>
Либо если у вас Gradle
implementation group: 'io.github.ipk-tools', name: 'allurium', version: '2.0.0'
В связи с ограничениями на размеры статей, я рассмотрю максимально урезанный вариант вступительного примера - заполнение примитивной формы, с разными типами полей ввода.
Форму можно посмотреть на этой странице
Подобно тому как это делали бы в selenide, сначала нужно написать Page Object. Но в случае с Allurium всё должно быть жёстко структурировано и типизировано, чтобы каждый вид элемента имел свой правильный тип, а фреймворк понимал, как с ним работать, и составлял корректное описание в отчёте.
@PageObject
@Getter
@Accessors(fluent = true)
public class FormPage extends Page {
@Name("Login")
@Locator(css = "#login")
protected TextField fieldLogin;
@Name("Email")
@Locator(css = "#email")
protected TextField fieldEmail;
@Name("Password")
@Locator(css = "#password")
protected TextField fieldPassword;
@Name("Rank")
protected TextField fieldRank = $textField("#rank");
@Name("Date")
protected TextField fieldDate = _$textField("//input[@id='date']");
@Name("Telephone")
@Locator(css = "#tel")
protected TextField fieldTelephone;
@Name("Avatar")
@Locator(css = "#file")
protected UploadField uploadAvatar;
@Name("Gender Male")
@Locator(css = "#male")
protected Button radioBtnMale;
@Name("Gender Female")
@Locator(css = "#female")
protected Button radioBtnFemale;
@Name("Preferable time - Morning")
@Locator(css = "#checkbox1")
protected CheckBox ckbMorning;
@Name("Preferable time - Evening")
@Locator(css = "#checkbox2")
protected CheckBox ckbEvening;
@Name("Experience")
@Locator(css = "#experience")
protected DropdownSelect ddExperience;
@Name("Working Days")
@Locator(css = "#multiple-select")
protected Select selectWorkingDays;
@Name("Reset")
@Locator(css = ".btn-secondary")
protected Button btnReset;
@Name("Submit")
@Locator(css = ".btn-primary")
protected Button btnSubmit;
}
В структуре Allurium фундаментальными являются классы, отмеченные аннотацией @PageObject. Они служат контейнерами, в которых располагаются виджеты и простые элементы. Одна страница = один PageObject. Объекты страниц могут быть наследуемыми и расширяемыми без ограничений.
Внутри нашего PageObject FormPage располагаются простые элементы. Но не все элементы имеют тип SelenideElement — каждый элемент имеет соответствующий ему тип:
-
TextField — текстовое поле ввода
-
UploadField — поле загрузки файла
-
CheckBox — чекбокс
-
DropdownSelect — выпадающий список (Select) с выбором одной опции
-
Select — список (Select) с возможностью выбора нескольких опций
-
Button — кнопка
Объявление элемента
При объявлении элемента, кроме типа, нужно всегда присваивать ему имя. Я предпочитаю использовать аннотацию @Name("name") вместо вызова .as("name"). Это позволяет создавать экземпляры в нужный момент без лишних сложностей и выглядит более эстетично. Однако можно присваивать имя и через конструктор объекта. Например, вместо:
@Name("Submit")
@Locator(css = ".btn-primary")
protected Button btnSubmit;
можно написать:
protected Button btnSubmit = $button(".btn-primary", "Submit");
Либо, если экземпляр уже создан, через setName("some-name"):
Button btnSubmit = $button(".btn-primary");
btnSubmit.setName("Submit");
Далее необходимо указать локатор. Это можно сделать разными способами:
@Name("Submit")
protected Button btnSubmit = $button(".btn-primary");
Где ".btn-primary" — CSS-селектор кнопки. Примитивные элементы, такие как кнопки, текстовые поля и блоки текста, уже описаны во фреймворке. Экземпляр создаётся статическим методом, аналогично созданию SelenideElement:
protected SelenideElement btnSubmit = Selenide.$(".btn-primary").as("Submit");
Длинный вариант был бы:
@Name("Submit")
protected Button btnSubmit = Button.$button(".btn-primary");
А короткий вариант через static метод:
@Name("Submit")
protected Button btnSubmit = $button(".btn-primary");
Если перед символом $ поставить нижнее подчёркивание (_), как в примере ниже, элемент будет искаться по XPath:
@Name("Date")
protected TextField fieldDate = _$textField("//input[@id='date']");
Можно создать экземпляр через конструктор:
@Name("Submit")
protected Button btnSubmit = new Button(".btn-primary");
Или с помощью аннотации @Locator, аналогично @FindBy в Selenium:
@Name("Submit")
@Locator(css = ".btn-primary")
protected Button btnSubmit;
В этом случае экземпляр объявлять не обязательно — он автоматически создаётся фреймворком, если у элемента присутствует аннотация @Locator.
@Locator может принимать следующие типы локаторов:
css — эквивалент By.cssSelector(".btn-primary")
xpath — эквивалент By.xpath("//*@classs='btn-primary']")
className — эквивалент By.className("btn-primary")
id — эквивалент By.id("#id")
Для открытия браузера и загрузки страницы вместо Selenide.open("url"); используется UiSteps.openBrowser(url); — этот шаг также записывается в отчёт. UiSteps — класс со статическими методами, не привязанными к конкретной странице или элементу, которые фреймворк трактует как шаги и логирует в отчёт.
Теперь напишем сценарий
public class ITExampleAlluriumTest extends TestBaseAllurium {
@Test
@Feature("Form")
@DisplayName("Filling the example form")
public void fillTheForm() {
URL imageUrl = this.getClass().getClassLoader().getResource("img/testicon.png");
UiSteps.openBrowser(formPageUrl);
formPage.fieldLogin().assertVisible();
formPage.fieldLogin().write("John");
formPage.fieldLogin().assertHasCssClass("form-control");
formPage.fieldLogin().assertCurrentValue("John");
formPage.fieldEmail().write("john.doe@gmail.com");
formPage.fieldEmail().assertCurrentValue("john.doe@gmail.com");
formPage.fieldPassword().write("Password12345!");
formPage.fieldRank().write("10");
formPage.fieldDate().clearAndWrite("11.11.2011");
formPage.fieldTelephone().write("199887688");
formPage.uploadAvatar().uploadFile(new File(imageUrl.getFile()));
formPage.radioBtnMale().click();
formPage.radioBtnMale().assertEnabled();
formPage.radioBtnFemale().assertDisabled();
formPage.ckbMorning().check();
formPage.ckbMorning().assertChecked();
formPage.ckbEvening().assertUnchecked();
formPage.ddExperience().select("2 years");
formPage.selectWorkingDays().select("Monday");
formPage.selectWorkingDays().select("Friday");
formPage.btnSubmit().click();
}
}
Выполним и посмотрим Allure отчёт

В этом отчёте обозначены все типы элементов, на мой взгляд более комфортным языком описывается совершаемое действие. Видны все применённые ассерты, но отсутствует лишние логи. Такой отчёт я получаю автоматически.
Одним из наиболле сильных сторон Allurium по моему мнению получилось сильно облегчить работу со списками. Фреймфорк заставляет организовывать структуру таким образом, чтобы приктически полностью отказаться от фильтраций для поска нужных элементов, какой-бы глубины вложенности они ни были.
Тоесть список может содеждать в себе виджеты, которые содержат другие списки, а те ещё списки и т.д. Но для поиска нужных элементов нам не понадобиться писать никакой логики.
Однако чтобы придти к этой концепции нужно начать как всегда с простейших примеров. Один из них рассмотрим здесь.
Для примера возьмём страницу расположенныую здесь
Тут можно найти 2 простых списка. Один список полей, второй список кнопок.
Опишем Page Object
@PageObject
@Getter
@Accessors(fluent = true)
public class SimpleListsPage extends Page {
@Name("Input fields")
protected ListWC<TextField> inputTextFields = new ListWC<>(By.xpath("//input[contains(@class, 'form-control')]"));
@Name("Bird names")
@ListLocator(css = ".mt-5 .btn-primary")
protect
Описываем сценарий в котором заполним все поля и нажмём все кнопки
Смотрим отчёт

Если в Selenide для работы со списками используется класс ElementsCollection, то в Allurium для этой цели применяется класс ListWC. В данном примере мы создаём два списка простых элементов интерфейса — полей ввода и кнопок.
-
Список, как и элемент или виджет, должен иметь имя, присваиваемое аннотацией @Name.
-
При объявлении списка необходимо создать его экземпляр, хотя бы с пустым конструктором, то есть new ListWC<>();
-
Селектор для элементов списка можно указать через аргумент конструктора с использованием класса By, как в примере с полями ввода:
@Name("Input fields")
protected ListWC<TextField> inputTextFields = new ListWC<>(By.xpath("//input[contains(@class, 'form-control')]"));
Также селектор для списка можно задать через аннотацию @ListLocator (например, для списка кнопок):
@Name("Buttons bird names")
@ListLocator(css = ".mt-5 .btn-primary")
protected ListWC<Button> listBirdNameButtons = new ListWC<>();
Найти элемент в списке можно различными способами:
-
Как в обычной коллекции, по индексу: list.get(index)
-
Через специальный метод get("string id"). Например, в списке кнопок, где уникальным является текст:
simpleListsPage.listBirdNameButtons().get("Flamingo").click();
ListWC спроектирован таким образом, чтобы искать элементы самостоятельно, в зависимости от их типа и присвоенного им id. Которое может выбираться автоматически либо присваиваться как захочет разработчик.
Кнопка является простейшим тегом, когда мы вызовем метод 'get', фреймворк понимает, что 'id' кнопки как правило является текст. И в первую очередь найдёт кнопку с заданным текстом. В случае с полями, фреймворк будет в первую очередь ориентироваться на единственный уникальный идентификатор "id", т.к. в данном случае больше не за что зацепиться. Таким образом. Вызывая метод 'get' на списке, фреймворк понимает в зависимости от типа как ему работать с этим элементом или виджетом и как осущесвлять поиск.
Однако это не накладывает никаких ограничений на кастомизацию и позволит осуществлять сколь угодно глубокий поиск в сложных виджетах лишь вызывая метод "get" который залогирует в отчёт подробный шаг без лишних неуместных структур.
С широким спектром этих примеров можно ознакомиться в документации в разделе - FAQ
Заключение
Я уверен что в данный момент фреймворк не лишён багов, т.к. почти не коммуницирую с людьми пользовавшимися предыдущими версиями, а для моих задач более чем хватает.
Так же нужно иметь ввиду, что фреймворк сильно завязан на аспектное программирование, что не может не накладывать некоторые издержки на производительность несмотря на множество оптимизаций, если сравнивать с написанием идентичных тестов на чистом Selenide.
Буду рад если кому-то это хоть немного упростит работу, как делает мне. И было бы хорошо если потестировал кто-то кроме меня, т.к. глаз уже замылился.
Я подготовил насколько можно подробную документацию и разъяснение со множеством примеров включая отдельный проект на Github c примерами. Там можно посмотреть как правильно подключать, версии и т.д.
Прорезюмирую ресурсы:
Исходный код Allurium
Проект с примерами
Сайт с документацией и примерами
Автор: Yaarven