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

Патерни

Page Object Model

Яку проблему вирішує Page Object

Без Page Object локатори і дії з UI розкидані по всіх тестах. Змінився data-testid кнопки логіну, і ви правите 40 файлів. Тести читаються як набір кліків, а не як бізнес-сценарій.

Page Object (POM) інкапсулює сторінку або екран: локатори і дії живуть в одному класі, тест викликає методи з бізнес-назвами. Головні вигоди:

  • Одна точка зміни. Локатор змінився, правите один рядок.
  • Читабельність. await loginPage.login(user) замість трьох fill і click.
  • Повторне використання. Однакові дії не копіюються між тестами.

У Playwright є важлива особливість: Locator лінивий. Він не шукає елемент у момент створення, а тільки при дії чи перевірці. Тому локатори можна спокійно створювати в конструкторі як readonly поля, навіть якщо сторінка ще не відкрита.

Важливо розуміти, що POM це не "клас на кожен URL". Межі Page Object визначає зміст: логічний екран, модальне вікно, складна форма. Якщо сторінка величезна, її розбивають на компоненти (наступний розділ).

На співбесіді: Page Object інкапсулює локатори і дії сторінки в одному класі, тому при зміні UI я правлю одне місце, а тести читаються як бізнес-сценарії. У Playwright локатори ліниві, тому я оголошую їх у конструкторі як readonly поля.

Мінімальний Page Object у Playwright

import { type Page, type Locator, expect } from '@playwright/test';

export class LoginPage {
  readonly email: Locator;
  readonly password: Locator;
  readonly submit: Locator;
  readonly error: Locator;

  constructor(private readonly page: Page) {
    this.email = page.getByLabel('Email');
    this.password = page.getByLabel('Password');
    this.submit = page.getByRole('button', { name: 'Sign in' });
    this.error = page.getByRole('alert');
  }

  async goto() {
    await this.page.goto('/login');
  }

  async login(email: string, password: string) {
    await this.email.fill(email);
    await this.password.fill(password);
    await this.submit.click();
  }
}

Тест:

test('shows error for wrong password', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto();
  await loginPage.login('user@test.com', 'wrong');
  await expect(loginPage.error).toHaveText('Invalid credentials');
});

Ключові моменти: page приходить ззовні через конструктор (не глобальна змінна), методи async і повертають Promise, локатори публічні, щоб тест міг робити web-first перевірки expect(locator). Методи описують намір користувача (login), а не механіку (clickSubmitButton). Якщо дія веде на іншу сторінку, метод може повертати новий Page Object, але в Playwright це не обов'язково, бо page той самий.

На співбесіді: Мій Page Object отримує page у конструкторі, тримає локатори як readonly поля і має async методи з бізнес-назвами. Локатори я лишаю доступними, щоб робити перевірки через expect(locator) у тесті.

Асерти в Page Object, god objects і наслідування

Асерти всередині Page Object

Класична дискусія. Мартін Фаулер радить тримати асерти в тестах: Page Object описує сторінку, тест вирішує, що перевіряти. Компромісний підхід, популярний у Playwright: дозволені методи-очікування стану, наприклад expectLoaded() з await expect(this.heading).toBeVisible(), бо це синхронізація, а не бізнес-перевірка. А от verifyOrderTotalIs100() у Page Object ховає мету тесту і погано перевикористовується.

God Page Object

Клас на 1500 рядків з усіма елементами застосунку. Ознаки: десятки не пов'язаних методів, постійні merge-конфлікти, ніхто не знає, що там є. Лікування: розбити на сторінки і компоненти (хедер, таблиця, модалка).

Ланцюжки наслідування

BasePage -> AuthenticatedPage -> AdminPage -> AdminUsersPage. Через рік зміна в BasePage ламає все, а щоб зрозуміти метод, треба пройти 4 файли. Кращий варіант: мінімальний або відсутній базовий клас, спільні частини через композицію (readonly header = new Header(page)).

Інші помилки

  • Методи повертають boolean замість того, щоб дати тесту локатор (isVisible() не чекає, на відміну від expect().toBeVisible()).
  • page.waitForTimeout() у методах замість auto-waiting.
  • Page Object створює тестові дані або ходить в API: це вже інша відповідальність.
На співбесіді: Я тримаю бізнес-асерти в тестах, а в Page Object допускаю лише очікування стану сторінки типу expectLoaded. Великі сторінки ділю на компоненти, а замість глибоких ієрархій наслідування використовую композицію.