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

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

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

А что будет, если взять Page Object с реализацией на интерфейсе, прикрутить Proxy Pattern и добавить немного функциональности Java 8?

Если интересно, предлагаю перейти под кат.

skd9msauqsfknsh8sw9sxyrj2dk.png

Введение


При использовании стандартного шаблона проектирования 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.

-smm8wwthpyo0xu7lrweew0ew54.png

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

Элементы


othnql8naaytd5_voumj6ev9uwy.png

В поставке инструмента идут два специализированных класса для работы с UI-элементами (аналог класса WebElement).

xyn06vwigi41-bob1cxykbq-7dq.png

AtlasWebElement и AtlasMobileElement дополнены методами should и waitUntil. (рассмотрение данных методов будет далее в статье).

Инструмент предоставляет возможность создания своих компонентов с помощью расширения вышеуказанных классов, что позволяет создать кроссплатформенный элемент.

Основные возможности


Рассмотрим подробнее функциональность инструмента:

efyd-c1u5ywgk7ql59ablf3aw5g.png

Интерфейсы вместо классов

При описании стандартных PageObject используются интерфейсы вместо классов.

public interface MainPage extends WebPage, WithHeader {
    @FindBy("//a[contains(text(), 'Or start a free trial of Enterprise Server')]")
    AtlasWebElement trial();
}


В данном примере описывается ссылка на стартовой странице GitHub.

Параметризация элементов

Представим, что у нас есть форма с полями:

uumbwjd2-nqpladvu1m6khwmxg0.png

Чтобы её описать, требуется создать 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.

_pca39zx5r4iw6kkwy9azeqy4d4.png

Опишем данный блок (большинство веб-элементов опущено):

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 выпадающих меню и одно поле поиска:

bfpkdgqfxasiualhznwxmis_nd8.png

Создадим собственный элемент 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, можно описать их один раз и подключать там, где требуется. Экономия времени и количества строк кода налицо.

jkmbhwzovztryyxlgoxu-slikau.png

Методы по умолчанию

В 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, куда вводим текст, далее появляется выпадающий список, элементы появляются не сразу.

-ry7uy6gnjyp02kyy94ikb8prde.png

Для таких случаев есть сущность 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.

fxrylhza6j8ng2c8hawoaniixvc.png

x3jhqvyysqmclcpoe7recbi8tim.png

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

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

nrretjnxyedry2guei6w0p1opqa.png

У пользователя есть возможность переопределить базовый функционал инструмента либо внедрить свой функционал. Модель расширения 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.

vftokukrzuynpobt2yjtulqy2g8.png

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

x3fo3cmcfe7beqygkorupy3ngxo.png

Далее подключаем слушатель при инициализации класса 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.

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

© Habrahabr.ru