[Перевод] Революция или эволюция Page Object Model?
Всем привет! Меня зовут Артём Соковец. Хочу поделиться переводом своей статьи об Atlas: реинкарнации фреймворка HTML Elements, где представлен совершенно иной подход работы с Page Object.
Перед тем, как перейти к деталям, хочу спросить: сколько обёрток для Page Object вы знаете? Page Element, ScreenPlay, Loadable Component, Chain of invocations…
А что будет, если взять Page Object с реализацией на интерфейсе, прикрутить Proxy Pattern и добавить немного функциональности Java 8?
Если интересно, предлагаю перейти под кат.
Введение
При использовании стандартного шаблона проектирования PageObject возникает ряд проблем:
Дублирование элементов
public class MainPage {
@FindBy(xpath = ".//div[@class = 'header']")
private Header header;
}
public class AnyOtherPage {
@FindBy(xpath = ".//div[@class = 'header']")
private Header header;
}
Здесь блок Header используется в различных классах PageObject.
Отсутствие параметризации у элементов
public class EditUserPage {
@FindBy(xpath = "//div[text()='Text_1']")
private TextBlock lastActivity;
@FindBy(xpath = "//div[text()='Text_2']")
private TextBlock blockReason;
}
В этом примере описываются элементы страницы редактирования настроек пользователя. Два элемента TextBlock содержат практически идентичный локатор с разницей только в текстовом значении («Text_1» и «Text_2»).
Однотипный код
public class UserPage {
@FindBy(xpath = "//div[text()='Телефон']/input")
private UfsTextInput innerPhone;
@FindBy(xpath = "//div[text()='Email']/input")
private UfsTextInput email;
@FindBy(xpath = "//button[text()='Сохранить']")
private UfsButton save;
@FindBy(xpath = "//button[text()='Список']")
private UfsButton toUsersList;
}
В повседневной работе можно встретить Page Object, состоящие из множества строк кода с однотипными элементами. В дальнейшем такие классы «неудобно» поддерживать.
Большой класс с шагами (steps)
public class MainSteps {
public void hasText(HtmlElement e, Matcher m)
public void hasValue(HtmlElement e, Matcher m)
public void linkContains(HtmlElement e, String s)
public void hasSize(List e, Matcher m)
public void hasItem(List e, Matcher m)
//...
}
Со временем разрастается класс шагов для работы с элементами. Требуется более пристальное внимание, чтобы не было дубликатов методов.
Ваш путеводитель в мире Page Object
Реинкарнация фреймворка HTML Elements направлена на решение вышеописанных проблем, уменьшение количество строк кода тестового проекта, более продуманную работу со списками и ожиданиями, а также тонкую настройку инструмента под себя благодаря системе расширений.
Atlas — Java-фреймворк нового поколения для разработки UI-автотестов с реализацией паттерна Page Object через интерфейсы. Данный подход предоставляет возможность множественного наследования при построении дерева элементов, что в итоге обеспечивает лаконичный код ваших автотестов.
Основным нововведением фреймворка является использование интерфейсов вместо стандартных классов.
Вот так выглядит описание главной страницы github.com:
public interface MainPage extends WebPage, WithHeader {
@FindBy("//a[contains(text(), 'Or start a free)]")
AtlasWebElement trial();
}
В приведённом коде описывается главная страница сайта GitHub с одним элементом и множественным наследованием от слоёв WebPage и WithHeader (пример дан исключительно для учебных целей, поэтому большинство веб-элементов опущено).
Архитектура фреймворка
На текущий момент Atlas состоит из трёх модулей:
- atlas-core
- atlas-webdriver
- atlas-appium
В atlas-core описана основная функциональность обработки Page Object’ов с помощью интерфейсов. Сама идея использования интерфейсов была взята из известного инструмента Retrofit.
Два других модуля atlas-webdriver и atlas-appium используются для разработки автоматизированных скриптов UI web и UI mobile. Основной точкой входа для описания web-страниц является интерфейс WebPage, а для мобильных экранов — Screen. Концептуально atlas-webdriver и atlas-appium построены на расширениях (пакет *.extension).
Элементы
В поставке инструмента идут два специализированных класса для работы с UI-элементами (аналог класса WebElement).
AtlasWebElement и AtlasMobileElement дополнены методами should и waitUntil. (рассмотрение данных методов будет далее в статье).
Инструмент предоставляет возможность создания своих компонентов с помощью расширения вышеуказанных классов, что позволяет создать кроссплатформенный элемент.
Основные возможности
Рассмотрим подробнее функциональность инструмента:
Интерфейсы вместо классов
При описании стандартных PageObject используются интерфейсы вместо классов.
public interface MainPage extends WebPage, WithHeader {
@FindBy("//a[contains(text(), 'Or start a free trial of Enterprise Server')]")
AtlasWebElement trial();
}
В данном примере описывается ссылка на стартовой странице GitHub.
Параметризация элементов
Представим, что у нас есть форма с полями:
Чтобы её описать, требуется создать 11 переменных с аннотацией @FindBy и, при необходимости, объявить getter.
Используя Atlas, потребуется лишь один параметризованный элемент AtlasWebElement.
public interface MainPage extends WebPage {
@FindBy("//div[text()='{{ text }}']/input")
AtlasWebElement input(@Param("text") String text);
}
Код автоматизированного теста выглядит следующим образом:
@Test
public void simpleTest() {
onMainPage().input("First Name").sendKeys("*");
onMainPage().input("Postcode").sendKeys("*");
onMainPage().input("Email").sendKeys("*");
}
Обращаемся к нужной странице, вызываем метод с параметром и выполняем требуемые действия с элементом. Метод с параметром описывает конкретное поле.
Множественное наследование
Ранее упоминалось, что блок (например, Header), который используется в разных Page Object — это дублирование кода.
Есть header GitHub.
Опишем данный блок (большинство веб-элементов опущено):
public interface Header extends AtlasWebElement {
@FindBy(".//input[contains(@class,'header-search-input')]")
AtlasWebElement searchInput();
}
Далее создадим слой, который можно подключить к любой странице:
public interface WithHeader {
@FindBy("//header[contains(@class,'Header')]")
Header header();
}
Расширяем главную страницу блоком header.
public interface MainPage extends WebPage, WithHeader {
@FindBy("//a[contains(text(), 'Or start a)]")
AtlasWebElement trial();
}
В целом можно создать больше слоёв и подключить их к нужной странице. В примере ниже подключаем с главной странице слои header, footer, sidebar.
public interface MainPage extends WithHeader, WithFooter, WithSidebar {}
Пойдём дальше. Header содержит 4 кнопки, 3 выпадающих меню и одно поле поиска:
Создадим собственный элемент Button, и одним элементом опишем четыре кнопки.
public interface Button extends AtlasWebElement {
@FindBy(".//a[contains(., '{{ value }}')]")
AtlasWebElement selectButton(@Param("value") String value);
}
Подключим кнопку button к слою header. Таким образом расширим функциональность шапки GitHub.
public interface Header extends WithButton {
…
}
Отдельный элемент Button можно подключать к различным слоям веб-сайта и быстро получить на нужной странице требуемый элемент.
Пример:
@Test
public void simpleTest() {
onMainPage().open("https://github.com");
onMainPage().header().button("Priсing").click();
}
Во второй строке теста происходит обращение к шапке сайта, далее вызываем параметризированную кнопку со значением «Pricing» и выполняем клик.
На тестируемом сайте может быть довольно много элементов, которые повторяются от страницы к странице. Чтобы не описывать их все с помощью стандартного подхода Page Object, можно описать их один раз и подключать там, где требуется. Экономия времени и количества строк кода налицо.
Методы по умолчанию
В Java 8 появились методы по умолчанию, которые используются, чтобы заранее определить нужную функциональность.
Допустим, у нас есть «вредный» элемент: например, чекбокс, который то включен, то выключен. Через него проходит много сценариев. Требуется включать чекбокс, если он выключен:
if(onMainPage().rentFilter().checkbox("Кирпич").getAttribute("class").contains("disabled")) {
onMainPage().rentFilter().checkbox("Кирпич").click();
}
Чтобы не хранить весь этот код в классе шагов, возможно поместить его рядом с элементом в виде метода по умолчанию.
public interface Checkbox extends AtlasWebElement {
@FindBy("//...")
AtlasWebElement checkBox((@Param("value") String value);
default void selectCheckbox(String value) {
if (checkBox(value).getAttribute("class").contains("disabled")) {
checkBox(value).click();
}
}
}
Теперь шаг в тесте будет выглядеть так:
onMainPage().rentFilter().selectCheckbox("Кирпич");
Другой пример, в котором требуется совместить очистку и ввод символов в поле.
onMainPage().header().input("GitHub").clear();
onMainPage().header().input("GitHub").sendKeys("*");
Определим метод, который очищает поле и возвращает сам элемент:
public interface Input extends AtlasWebElement {
@FindBy("//xpath")
AtlasWebElement input(@Param("value") String value);
default AtlasWebElement withClearInput(String value) {
input(value).clear();
return input(value);
}
}
В тестовом методе шаг выглядит следующим образом:
onMainPage().header().withClearInput("GitHub").sendKeys("Atlas");
Таким образом можно запрограммировать требуемое поведение в элементе.
Повторные попытки (Retry)
В Atlas есть встроенные повторные попытки. Вам не нужно заботиться о таких исключениях, как NotFoundException, StaleElementReferenceException и WebDriverException, а также можно забыть о применении явных и неявных ожиданий Selenium API.
onSite().onSearchPage("Junit 5").repositories().waitUntil(hasSize(10));
Если на каком-то этапе цепочки вы поймали исключение, фаза повторяется с начала.
Есть возможность самостоятельно настраивать интервал времени, в течение которого можно выполнять повторение, или частоту повторения.
Atlas atlas = new Atlas(new WebDriverConfiguration(driver))
.context(new RetryerContext(new DefaultRetryer(3000L, 1000L, Collections.singletonList(Throwable.class))));
Ожидаем в течение трёх секунд c частотой опроса раз в секунду.
Также можем настроить ожидание для конкретного элемента с помощью аннотации Retry. Для всех элементов поиск будет происходить в течение 3 секунд, а в случае с одним составит 20.
@Retry(timeout = 20_000L, polling = 2000L)
@IOSFindBy(xpath = "//XCUIElementTypeSearchField[@name='Search Wikipedia']")
@AndroidFindBy(xpath = "//*[contains(@text, 'Search Wikipedia')]")
AtlasMobileElement searchWikipedia();
Работа со списками
Из коробки инструмент предоставляет работу со списками. Что это значит? Есть поле с тегом input, куда вводим текст, далее появляется выпадающий список, элементы появляются не сразу.
Для таких случаев есть сущность ElementsCollection. С её помощью происходит работа со списками.
public interface ContributorsPage extends WebPage, WithHeader {
@FindBy(".//ol[contains(@class, 'contrib-data')]//li[contains(@class, 'contrib-person')]")
ElementsCollection hovercards();
}
Использование:
onSite().onContributorsPage().hovercards().waitUntil(hasSize(4));
Также есть возможность фильтровать элементы и конвертировать их в список другого вида.
Smart Assertions
Как ранее упоминалось, в сущностях AtlasWebElement и AtlasMobileElement используются методы should, waitUntil для работы с проверками (утверждениями).
Для чего это сделано? Чтобы сэкономить время при разборе отчётов прогона автоматизированных сценариев. Большинство функциональных проверок выполняются в конце сценария: они интересны специалисту функционального тестирования, а промежуточные проверки — специалисту автоматизированного тестирования. Следовательно, если функциональность продукта не работает, логично бросать исключение AssertationError, в ином случае — RuntimeException.
В Allure сразу будет видно, с чем мы имеем дело: либо у нас дефект продукта (в работу берёт специалист ФТ), либо сломался автотест (разбирается специалист АТ).
Модель расширений
У пользователя есть возможность переопределить базовый функционал инструмента либо внедрить свой функционал. Модель расширения Atlas похожа на модель расширения JUnit 5. Модули atlas-webdriver и atlas-appium построены на расширениях. Если вам интересно, посмотрите исходный код.
Разберём абстрактный пример: требуется разработать UI-автотесты для браузера Internet Explorer 11 (кое-где он ещё используется). Бывают моменты, когда стандартный клик по элементам не отрабатывает, тогда можно воспользоваться JS-кликом. Вы решаете на время переопределить клик на всём тестовом проекте.
onMainPage().header().button("en").click();
Как это сделать?
Создаём расширение, которое реализует интерфейс MethodExtension.
public class JSClickExt implements MethodExtension {
@Override
public Object invoke(Object proxy, MethodInfo methodInfo, Configuration config) {
final WebDriver driver = config.getContext(WebDriverContext.class)
.orElseThrow(() -> new AtlasException("Context doesn't exist")).getValue();
final JavascriptExecutor js = (JavascriptExecutor) driver;
js.executeScript("arguments[0].click();", proxy);
return proxy;
}
@Override
public boolean test(Method method) {
return method.getName().equals("click");
}
}
Переопределяем два метода. В методе test () задаём, что переопределяем метод click. Метод invoke реализует требуемую логику. Теперь клик по элементу будет происходить через JavaScript.
Подключаем расширение следующим образом:
atlas = new Atlas(new WebDriverConfiguration(driver, "https://github.com"))
.extension(new JSClickExt());
С помощью расширений возможно создать поиск локаторов для элементов в БД и реализовать другие интересные возможности — всё зависит от вашей фантазии и потребностей.
Единая точка входа к PageObject’ам (WebSite)
Инструмент позволяет хранить все ваши Pages в одном месте и в дальнейшем работать только через сущность Site.
public interface GitHubSite extends WebSite {
@Page
MainPage onMainPage();
@Page(url = "search")
SearchPage onSearchPage(@Query("q") String value);
@Page(url = "{profile}/{project}/tree/master/")
ProjectPage onProjectPage(@Path("profile") String profile, @Path("project") String project);
@Page
ContributorsPage onContributorsPage();
}
Дополнительно Page’ам возможно задавать быстрый url, query-параметры и path-сегменты.
onSite().onProjectPage("qameta", "atlas").contributors().click();
В строчке выше передаются два path-сегмента (qameta и atlas), что преобразовывается в адрес github.com/qameta/atlas/tree/master. Основное преимущество такого подхода в том, что возможно сразу открыть требуемую страницу без прокликивания до неё.
@Test
public void usePathWebSiteTest() {
onSite().onProjectPage("qameta", "atlas").contributors().click();
onSite().onContributorsPage().hovercards().waitUntil(hasSize(4));
}
Работа с мобильным элементом
Работа с мобильным элементом (AtlasMobileElement) происходит аналогично работе с веб-элементом AtlasWebElement. Дополнительно в AtlasMobileElement добавлены три метода: скролл экрана вверх/вниз (scrollUp/scrollDown) и клик на элемент с удержанием (longPress).
Приведу пример главного экрана приложения Wikipedia. Один элемент описывается как для платформы iOS, так и для Android. Также описывают параметризованную кнопку.
public interface MainScreen extends Screen {
@Retry(timeout = 20_000L, polling = 2000L)
@IOSFindBy(xpath = "//XCUIElementTypeSearchField[@name='Search Wikipedia']")
@AndroidFindBy(xpath = "//*[contains(@text, 'Search Wikipedia')]")
AtlasMobileElement searchWikipedia();
@IOSFindBy(id = "{{ value }}")
AtlasMobileElement button(@Param("value") String value);
}
Тесты выглядят аналогичным образом:
public void simpleExample() {
onMainScreen().searchWikipedia().click();
onSearchScreen().search().sendKeys("Atlas");
onSearchScreen().item("Atlas LV-3B").swipeDownOn().click();
onArticleScreen().articleTitle().should(allOf(displayed(), text("Atlas LV-3B")));
}
В примере выше мы открываем главную страницу Wikipedia, щёлкаем по поисковой строке, вводим текст Atlas, далее прокручиваем до элемента списка со значением Atlas LV-3B и переходим в его представление. Последняя строчка проверяет, что заголовок отображается и содержит требуемое значение.
Listener
Логирование событий возможно реализовать с помощью специального листенера (интерфейс Listener). Каждый метод при вызове имеет четыре события: Before, Pass, Fail. After.
Используя данный интерфейс, можно организовать отчётность. Ниже представлен пример Allure Listener, который можно найти по ссылке.
Далее подключаем слушатель при инициализации класса Atlas.
atlas = new Atlas(new WebDriverConfiguration(driver)).listener(new AllureListener());
Вышеуказанным способом возможно создать listener для различных систем репортинга (например, для ReportPortal).
Подключение
Для автоматизации UI web достаточно прописать зависимость atlas-webdriver и указать последнюю актуальную версию (на момент написания этого текста актуальна версия 1.6.0).
Maven:
io.qameta.atlas
atlas-webdriver
${atlas.version}
Gradle:
dependencies { сompile 'io.qameta.atlas:atlas-webdriver:1.+' }
Аналогичным образом поступаем, если требуется автоматизировать UI Mobile.
Maven:
io.qameta.atlas
atlas-appium
${atlas.version}
Gradle:
dependencies { сompile 'io.qameta.atlas:atlas-appium:1.+' }
Использование
После подключения зависимости в свой проект необходимо инициализировать инстанс класса Atlas.
@Before
public void startDriver() {
driver = new ChromeDriver();
atlas = new Atlas(new WebDriverConfiguration(driver));
}
В конструктор Atlas передаем инстанс конфигураци, а также драйвер.
На текущий момент есть две конфигурации: WebDriverConfiguration и AppiumDriverConfiguration. Каждая конфигурация содержит определенные расширения по умолчанию.
Далее определим метод, который будет создавать все PageObject.
private T onPage(Class page) {
return atlas.create(driver, page);
}
Пример простенького тестового сценария:
@Test
public void simpleTest() {
onPage(MainPage.class).open("https://github.com");
onPage(MainPage.class).header().searchInput().sendKeys("Atlas");
onPage(MainPage.class).header().searchInput().submit();
}
Открываем сайт, обращаемся к слою header, в нём ищем текстовое поле (search input), вводим текст и нажимаем ввод.
Итоги
В заключение хочу отметить, что Atlas — это гибкий инструмент с большими возможностями. Его можно настроить под конкретный тестовый проект так, как удобно вашей команде и вам. Заняться разработкой кроссплатформенных тестов и т.д.
Доступны видеозаписи докладов о нём с конференций Heisenbug, Selenium Camp и Nexign QA Meetup. Есть Telegram-чат @atlashelp.
С помощью данного инструмента вы сможете сократить значительное количество строк кода (проверено на проектах таких компаний, как Яндекс, СберТех и Тинькофф).