QA Interview Hub · База знань · Тести

Selenium + Java

Page Object

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(). Це зручно, але довгі ланцюжки важко дебажити, тож розривайте їх осмисленими змінними там, де потрібні перевірки.