Хабрахабр

[Перевод] Революция или эволюция Page Object Model?

Всем привет! Меня зовут Артём Соковец. Хочу поделиться переводом своей статьи об Atlas: реинкарнации фреймворка HTML Elements, где представлен совершенно иной подход работы с Page Object.

Page Element, ScreenPlay, Loadable Component, Chain of invocations… Перед тем, как перейти к деталям, хочу спросить: сколько обёрток для Page Object вы знаете?

А что будет, если взять 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

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

Ваш путеводитель в мире 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.

Основной точкой входа для описания web-страниц является интерфейс WebPage, а для мобильных экранов — Screen. Два других модуля atlas-webdriver и atlas-appium используются для разработки автоматизированных скриптов UI web и UI mobile. Концептуально 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)

Вам не нужно заботиться о таких исключениях, как NotFoundException, StaleElementReferenceException и WebDriverException, а также можно забыть о применении явных и неявных ожиданий Selenium API. В Atlas есть встроенные повторные попытки.

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 частотой опроса раз в секунду.

Для всех элементов поиск будет происходить в течение 3 секунд, а в случае с одним составит 20. Также можем настроить ожидание для конкретного элемента с помощью аннотации Retry.

@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<RepositoryCard> hovercards();
}

Использование:

onSite().onContributorsPage().hovercards().waitUntil(hasSize(4));

Также есть возможность фильтровать элементы и конвертировать их в список другого вида.

Smart Assertions

Как ранее упоминалось, в сущностях AtlasWebElement и AtlasMobileElement используются методы should, waitUntil для работы с проверками (утверждениями).

Для чего это сделано? Чтобы сэкономить время при разборе отчётов прогона автоматизированных сценариев. Большинство функциональных проверок выполняются в конце сценария: они интересны специалисту функционального тестирования, а промежуточные проверки – специалисту автоматизированного тестирования. Следовательно, если функциональность продукта не работает, логично бросать исключение AssertationError, в ином случае — RuntimeException.

В Allure сразу будет видно, с чем мы имеем дело: либо у нас дефект продукта (в работу берёт специалист ФТ), либо сломался автотест (разбирается специалист АТ).

Модель расширений

Модель расширения Atlas похожа на модель расширения JUnit 5. У пользователя есть возможность переопределить базовый функционал инструмента либо внедрить свой функционал. Если вам интересно, посмотрите исходный код. Модули atlas-webdriver и atlas-appium построены на расширениях.

Бывают моменты, когда стандартный клик по элементам не отрабатывает, тогда можно воспользоваться JS-кликом. Разберём абстрактный пример: требуется разработать UI-автотесты для браузера Internet Explorer 11 (кое-где он ещё используется). Вы решаете на время переопределить клик на всём тестовом проекте.

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 добавлены три метода: скролл экрана вверх/вниз (scrollUp/scrollDown) и клик на элемент с удержанием (longPress). Работа с мобильным элементом (AtlasMobileElement) происходит аналогично работе с веб-элементом AtlasWebElement.

Один элемент описывается как для платформы iOS, так и для Android. Приведу пример главного экрана приложения Wikipedia. Также описывают параметризованную кнопку.

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

Каждый метод при вызове имеет четыре события: Before, Pass, Fail. Логирование событий возможно реализовать с помощью специального листенера (интерфейс Listener). After.

Ниже представлен пример Allure Listener, который можно найти по ссылке. Используя данный интерфейс, можно организовать отчётность.

Далее подключаем слушатель при инициализации класса Atlas.

atlas = new Atlas(new WebDriverConfiguration(driver)).listener(new AllureListener());

Вышеуказанным способом возможно создать listener для различных систем репортинга (например, для ReportPortal).

Подключение

6. Для автоматизации UI web достаточно прописать зависимость atlas-webdriver и указать последнюю актуальную версию (на момент написания этого текста актуальна версия 1. 0).

Maven:

<dependency> <groupId>io.qameta.atlas</groupId> <artifactId>atlas-webdriver</artifactId> <version>${atlas.version}</version>
</dependency>

Gradle:

dependencies { сompile 'io.qameta.atlas:atlas-webdriver:1.+' }

Аналогичным образом поступаем, если требуется автоматизировать UI Mobile.

Maven:

<dependency> <groupId>io.qameta.atlas</groupId> <artifactId>atlas-appium</artifactId> <version>${atlas.version}</version>
</dependency>

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 extends WebPage> T onPage(Class<T> 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 — это гибкий инструмент с большими возможностями. Его можно настроить под конкретный тестовый проект так, как удобно вашей команде и вам. Заняться разработкой кроссплатформенных тестов и т.д.

Есть Telegram-чат @atlashelp. Доступны видеозаписи докладов о нём с конференций Heisenbug, Selenium Camp и Nexign QA Meetup.

С помощью данного инструмента вы сможете сократить значительное количество строк кода (проверено на проектах таких компаний, как Яндекс, СберТех и Тинькофф).

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть