Когда UI-тестов десять, можно писать локаторы прямо в тестах. Когда их пятьсот и меняется кнопка входа, приходится править пятьсот файлов. Page Object решает именно эту проблему.
Идея
Каждая страница (или крупный компонент) представлена классом. Класс знает локаторы и умеет действия на языке предметной области. Тест знает сценарий и проверки.
class LoginPage:
def __init__(self, page):
self.page = page
self.email = page.get_by_label("Email")
self.password = page.get_by_label("Пароль")
self.submit = page.get_by_role("button", name="Войти")
def open(self):
self.page.goto("/login")
return self
def login(self, email, password):
self.email.fill(email)
self.password.fill(password)
self.submit.click()
return DashboardPage(self.page)
def test_login(page, user):
dashboard = LoginPage(page).open().login(user.email, user.password)
expect(dashboard.greeting).to_have_text(f"Привет, {user.name}")
Локатор кнопки поменялся, правка в одном месте. Тест читается как сценарий.
Правила хорошего Page Object
- Методы описывают действия пользователя (
login,add_to_cart), а не технику (click_button_3). - Метод, который ведёт на другую страницу, возвращает её объект: получается fluent-цепочка.
- Проверки чаще держат в тестах. Page Object отдаёт состояние (
greeting,items_count()), а тест решает, что ожидать. Так один объект служит и позитивным, и негативным сценариям. - Никаких
sleepи магических индексов внутри: ожидания и локаторы такие же аккуратные, как в тестах.
Компоненты
Шапка, корзина-виджет, таблица, модалка встречаются на многих страницах. Их выносят в компоненты (Page Component), а страница их собирает:
class Header {
constructor(private root: Locator) {}
cartBadge = () => this.root.getByTestId('cart-count');
}
class ProductPage {
readonly header: Header;
constructor(private page: Page) {
this.header = new Header(page.getByRole('banner'));
}
}
Компонент ищет элементы внутри своего корня, поэтому две таблицы на странице не путаются.
Page Factory и Screenplay
- Page Factory (Java): аннотации
@FindByиPageFactory.initElementsсоздают ленивые прокси элементов. Удобно, но в современных проектах часто обходятся без неё. - Screenplay: тест описан через актёров, их возможности и задачи (
actor.attemptsTo(Login.with(user))). Масштабируется на большие наборы, но порог входа выше.
Слои фреймворка
tests/ сценарии и проверки
pages/, components/ Page Object
api/ клиенты для подготовки данных
data/ билдеры и фабрики тестовых данных
fixtures/ conftest или test.extend
config/ окружения, URL, учётки из переменных
Тестовые данные создаются билдерами с разумными значениями по умолчанию: тест переопределяет только то, что важно для сценария (build_user(role="admin")). Для случайных реалистичных данных берут Faker, но с фиксированным seed, если важна воспроизводимость.
Частые ошибки
- «Божественный» Page Object на 2000 строк для всего приложения.
- Проверки внутри Page Object, из-за которых метод нельзя использовать в негативном тесте.
- Наследование страниц ради переиспользования вместо композиции компонентов.
- Хранить найденные элементы, а не локаторы (протухшие ссылки).
Что спросят на собесе
- Что такое Page Object и какую проблему он решает?
- Где держать проверки: в тестах или в Page Object?
- Как организовать повторяющиеся элементы, например шапку и таблицы?
- Из каких слоёв состоит ваш фреймворк?
- Как вы готовите тестовые данные?