Page Object Model: що це й правила
Page Object — клас, що описує сторінку (або її частину) застосунку: де на ній елементи й що з нею можна робити. Тест говорить мовою користувача (loginPage.loginAs(user)), а не мовою локаторів. Коли верстка змінюється, правлять один клас, а не сотні тестів.
public class LoginPage {
private final WebDriver driver;
private final By email = By.id("email");
private final By password = By.id("password");
private final By submit = By.cssSelector("[data-testid='login-submit']");
public LoginPage(WebDriver driver) { this.driver = driver; }
public DashboardPage loginAs(String user, String pass) {
driver.findElement(email).sendKeys(user);
driver.findElement(password).sendKeys(pass);
driver.findElement(submit).click();
return new DashboardPage(driver);
}
}
Правила (збігаються з тим, що радить документація Selenium):
- локатори й деталі взаємодії — всередині класу, тест їх не бачить;
- методи — це послуги сторінки (
loginAs, addToCart), а не «клікни кнопку»; метод, що веде на іншу сторінку, повертає її Page Object;
- перевірки (assert) — у тестах, а не в Page Object; виняток — перевірка в конструкторі, що відкрито потрібну сторінку;
- Page Object рідко віддає назовні сам
WebDriver чи сирі WebElement;
- повторювані блоки (шапка, меню, таблиця) виносять у окремі компоненти, які вкладають у сторінки.
Типова помилка — «божественний» клас на всю сторінку й методи на кожну кнопку без бізнес-сенсу.
PageFactory, компоненти й fluent-стиль
PageFactory — допоміжний клас Selenium: поля з @FindBy ініціалізуються викликом PageFactory.initElements(driver, this). Кожне поле — проксі, що шукає елемент при кожному зверненні (з @CacheLookup — один раз, і тоді елемент може застаріти, stale).
public class LoginPage {
@FindBy(id = "email") private WebElement email;
@FindBy(id = "password") private WebElement password;
public LoginPage(WebDriver driver) {
PageFactory.initElements(driver, this);
}
}
Чому команди відходять від нього:
- забули
initElements — отримали NullPointerException замість зрозумілої помилки;
- локатор не можна параметризувати (рядок таблиці за іменем, кнопка за текстом) без окремих методів із
By;
- рефлексія й проксі ускладнюють налагодження та композицію, а виграш над звичайним
By-полем мінімальний;
- документація Selenium згадує його лише в застарілому розділі, рекомендації не дає.
Альтернатива — поля By і явні методи пошуку з очікуваннями; вони прозоріші та краще дружать з Java-композицією.
Компоненти. Шапка, бічне меню, модалка, рядок таблиці — окремі класи, що працюють усередині кореневого елемента (SearchContext), а не всієї сторінки. Їх повторно використовують у багатьох Page Object.
public class HeaderComponent {
private final WebDriver driver;
private final WebElement root;
public HeaderComponent(WebDriver driver, WebElement root) {
this.driver = driver;
this.root = root;
}
public SearchPage search(String text) {
root.findElement(By.name("q")).sendKeys(text + Keys.ENTER);
return new SearchPage(driver);
}
}
Fluent-стиль: метод повертає this або наступну сторінку, тож сценарій читається ланцюжком: new LoginPage(driver).open().loginAs(u, p).openCart(). Це зручно, але довгі ланцюжки важко дебажити, тож розривайте їх осмисленими змінними там, де потрібні перевірки.