Page Object – это шаблон проектирования, который широко используется в автоматизированном тестировании и позволяет разделять логику выполнения тестов от их реализации. Page Object как бы моделирует страницы тестируемого приложения в качестве объектов в коде. В результате его использования у вас получатся отдельные классы, отвечающие за работу с HTML каждой конкретной веб-страницы. Такой подход значительно уменьшает объем повторяющегося кода, потому что одни и те же объекты страниц можно использовать в различных тестах. Основное преимущество Page Object заключается в том, что в случае изменения пользовательского интерфейса, можно выполнить исправление только в одном месте, а не исправлять каждый тест, в котором этот интерфейс используется.
Класс PageObject не обязательно должен представлять собой всю страницу. Он может быть частью страницы, которая часто используется на сайте (или даже на одной странице). Это может быть, например, пагинатор или меню. Основной принцип состоит в том, что есть только один класс в Вашем проекте, который знает о структуре HTML конкретной страницы или ее части.
Разделение логики и реализации
Существует большая разница между логикой тестирования (что проверить) и его реализацией (как проверить). Пример тестового сценария: «Пользователь вводит неверный логин или пароль, нажимает кнопку входа, получает сообщение об ошибке». Этот сценарий описывает логику теста, в то время как реализация содержит в себе такие действия как поиск полей ввода на странице, их заполнение, проверку полученной ошибки и т.д. И если, например, измениться способ вывода сообщения об ошибке, то это никак не повлияет на сценарий теста, все также нужно будет ввести неверные данные, нажать кнопку входа и проверить ошибку. Но это напрямую затронет реализацию теста — необходимо будет изменить метод получающий и обрабатывающий сообщение об ошибке. При разделении логики теста от его реализации автотесты становятся более гибкими и их, как правило, легче поддерживать.
Page Object в Selenium
Паттерн Page Object в Selenium реализован с помощью библиотеки PageFactory и класса страницы. Page Object представляет собой отдельный класс, содержащий локаторы элементов, методы для работы с ними и конструктор принимающий в качестве параметра объект WebDriver. Методы класса Page Object могут возвращать объекты других Page Object классов. С помощью этого можно воссоздать копию переходов и поведения веб-приложения. Например, метод успешной регистрации в классе RegistrationPage должен возвращать экземпляр HomePage, потому что после регистрации на сайте пользователя перенаправляет на домашнюю страницу. Одним из следствий такого подхода является то, что необходимо моделировать как успешные, так и неуспешные методы. Или, например, в случае если нажатие на элемент может открывать различные страницы в зависимости от условий, то также необходимо создавать разные методы для каждого необходимого случая:
public class RegistrationPage { private WebDriver driver; public RegistrationPage(WebDriver driver) { this.driver = driver; } public HomePage registerUserSuccess(User user) { // успешная регистрация и переход на домашнюю страницу } public RegistrationPage registerUserError(User user) { // регистрация пользователя с неверно заполненными полями // вывод ошибки, остаемся на той же странице } }
Поиск элементов на странице можно осуществлять в методах используя driver.findElement
, а можно объявить элементы в классе декларативно, используя аннотацию @FindBy
.
import java.util.List; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; import org.openqa.selenium.support.PageFactory; public class HomePage { private WebDriver driver; /** * Имя пользователя */ @FindBy(id = "profilename") private WebElement userName; /** * Выход */ @FindBy(linkText = "Выход") private WebElement exit; /** * Элементы меню */ @FindBy(className = "menu_item") private List<WebElement> menuItems; public HomePage(WebDriver driver) { PageFactory.initElements(driver, this); this.driver = driver; } }
@FindBy
в качестве параметров принимает те же механизмы поиска, что и методы класса By
.
Аннотация @FindBy
работает только с использованием PageFactory
. Без вызова PageFactory.initElements(driver, this);
при обращении к элементам Вы получите NullPointerException
. PageFactory инициализирует элементы при помощи вызова поиска driver.findElement
. Инициализация веб-элементов происходит на странице не вовремя вызова методаinitElements
, PageFactory использует, так называемую, LAZY инициализацию. То есть, поиск элемента будет осуществляться только при обращении к нему в ходе выполнения теста. Если вы никогда не используете элемент в PageObject, то findElement
для него никогда не будет вызван. Так объявленный в классе несуществующий на странице элемент вызовет исключение только при попытке его использовать.
PageFactory поддерживает инициализацию элементов по умолчанию. Это позволяет опускать аннотацию @FindBy
, и в этом случае имя поля в классе выступает как ID
или name
элемента на HTML-странице. Сперва драйвер ищет элемент по соответствию id, и если элемент не найден, то затем по имени класса. Инициализация без @FindBy
, естественно, не работает для List<WebElement>
, поскольку не принято иметь несколько элементов с одинаковым ID
или name
на странице.
Каждый раз при обращении к элементу драйвер будет снова и снова осуществлять его поиск на странице. Для сложных AJAX-приложений это как раз то, что нужно. Но для ускорения выполнения существует возможность кешировать найденные элементы. Тогда поиск будет осуществляться только один раз при первом обращении к элементу.
@FindBy(id = "profilename") @CacheLookup private WebElement userName;
Но делайте это только если вы уверены, что:
- элемент всегда будет на странице;
- элемент не будет меняться;
- Вы не покидаете HTML страницу и возвращаетесь обратно (StaleElementReferenceException).
Пример класса Page Object:
import org.junit.Assert; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; import org.openqa.selenium.support.PageFactory; public class RegistrationPage { private static String URL_MATCH = "registration"; private WebDriver driver; /** * Логин */ @FindBy(id = "userLogin") private WebElement login; /** * Пароль */ @FindBy(id = "regPassword") private WebElement password; /** * Пароль подтверждение */ @FindBy(id = "passwordConfirmation") private WebElement passwordConfirm; /** * E-mail */ @FindBy(id = "UserEmail") private WebElement email; /** * Кнопка зарегистрировать */ @FindBy(id = "submitRegistration") private WebElement bSubmitRegister; /** * Сообщение об ошибке */ @FindBy(id = "error-message") private WebElement registerError; public RegistrationPage(WebDriver driver) { // проверить, что вы находитесь на верной странице if (!driver.getCurrentUrl().contains(URL_MATCH)) { throw new IllegalStateException( "This is not the page you are expected" ); } PageFactory.initElements(driver, this); this.driver = driver; } /** * Регистраци пользователя * @param user - {@link User} */ private void registerUser(User user) { System.out.println(driver.getTitle()); login.sendKeys(user.login); password.sendKeys(user.password); passwordConfirm.sendKeys(user.passwordConfirmation); email.sendKeys(user.email); bSubmitRegister.click(); } /** * Успешная регистрация пользователя * @param user - {@link User} * @return {@link HomePage} */ public HomePage registerUserSuccess(User user) { registerUser(user); return new HomePage(driver); } /** * Неуспешная регистрация * @param user - {@link User} * @return {@link RegistrationPage} */ public RegistrationPage registerUserError(User user) { registerUser(user); return new RegistrationPage(driver); } /** * Проверить сообщение об ошибке * @param user - {@link User} * @return {@link RegistrationPage} */ public RegistrationPage checkErrorMessage(String errorMessage) { Assert.assertTrue("Error message should be present", registerError.isDisplayed()); Assert.assertTrue("Error message should contains " + errorMessage, registerError.getText().contains(errorMessage)); return this; } }
Вызов методов страницы регистрации в тесте:
@Test public void registerUserTest() { driver.get("http://HOST_NAME/registration"); User user = User.createValidUser(); user.email = "not_valid_email"; RegistrationPage registrationPage = new RegistrationPage(driver); registrationPage .registerUserError(user) .checkErrorMessage(errorMessage) ; user = User.createValidUser(); registrationPage .registerUserSuccess(user) // ... // вызов методов HomePage ; }
Интересно было почитать. Помню писал свою реализацию Page Factory, когда библиотеки еще не было. Отличный был опыт. И за статью спасибо 🙂
Спасибо большое!
Татьяна жаль что вы ничего больше не пишите в своем блоге((( каждый раз возвращаюсь к прочтению статей, и узнаю для себя что-то новое, вы никогда не думали о том , чтобы книжку издать?
Большое спасибо, очень приятно узнать, что работа была проделана не зря.
К сожалению, сейчас нет времени продолжать писать статьи, но я еще обязательно вернусь к этой теме.
P.S. Про книгу никогда не думала))))
Я пока только учусь, и мне уже не поможет, но для будущих поколений не могли бы вы прикрепить код проекта? А то для новичка путаници много..
Но описано классно, спасибо.
почему все элементы страницы всегда private?
Потому что должен соблюдаться принцип инкапсуляции. Доступ к полям класса должен предоставляться через интерфейс, а не напрямую. Приведенных методов должно быть достаточно для работы с данной страницей. Если же появляется новая логика, которая требует обращения к приватным полям — добавляйте новый паблик-метод, реализующий её и общайтесь с объектами через него.
это называется инкапсуляция
спасибо, очень доступно.
скажите, а почему основные методы описываются в классе PageObject? Это как-то ускоряет работу или как?
я все абсолютно методы держу в хелпере. В хелпере же инициализирую нужную страницу. а в тесте обращаюсь только к методам из хелпера. А если у меня будут методы в классе PageObject, смогу ли я так просто к ним обращаться из теста?
Подскажите, пожалуйста, а почему вы называете Page Object шаблоном проектирования? Идея определить классы для объектов, с которыми приходится иметь дело, лежит в основе ООП. Например, если бы я разрабатывал ПО для тестирования интерфейсов мобильных приложений, то скорее всего, определил бы Screen Object’ы, а если бы мариновал огурцы — Cucumber Object’ы 🙂
Этот код не запускается. В registerUser(User user) передаются нулл всё время.
Я что-то не так делаю?
User с соответсвующими полями создан.