Репозиторий объектов (object repository) и объект страницы (page object) - два способа хранить знание о пользовательском интерфейсе в одном месте, а не повторять его в каждом автотесте. Объект страницы - класс в коде тестов, который представляет страницу или её часть и предоставляет тестам её услуги: ввести учётные данные, открыть входящие, прочитать сообщение об ошибке. Репозиторий объектов - хранилище описаний элементов в виде данных, устроенное как страницы и их элементы; шаги тестов ссылаются на элементы по имени. В обоих случаях изменение интерфейса отражается в одном месте, а тесты остаются прежними.
Мартин Фаулер впервые описал эту идею под названием Window Driver. Название page object распространил проект Selenium, а в сентябре 2013 года Фаулер изложил паттерн в статье «PageObject» на своём сайте. Сейчас паттерн Page Object описан в документации Selenium («Page object models») и в документации Playwright («Page object models»). Программа ISTQB Advanced Level Test Automation Engineering (версия 2.0, 2024 год) называет модель объектов страниц среди паттернов проектирования автоматизации тестирования. Репозиторий объектов пришёл из практики инструментов автоматизации, где шаги тестов строятся из ключевых слов или собираются без кода; само тестирование на основе ключевых слов стандартизировано в ISO/IEC/IEEE 29119-5 (текущая редакция 2024 года).
Проблема дублирования локаторов
UI-тест находит элементы с помощью локаторов и выполняет над ними действия. Виды локаторов и порядок их выбора описаны в статье Локаторы в UI-автотестах. Когда локаторы записаны прямо в тестах, каждый тест, который проходит через страницу, повторяет их.
Два теста страницы входа на Playwright с локаторами внутри тестов:
import { test, expect } from '@playwright/test';
test('user signs in with valid credentials', async ({ page }) => {
await page.goto('https://example.com/login');
await page.getByLabel('Email').fill('anna@example.com');
await page.getByLabel('Password').fill('correct-password');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByRole('heading', { name: 'Inbox' })).toBeVisible();
});
test('user sees an error with a wrong password', async ({ page }) => {
await page.goto('https://example.com/login');
await page.getByLabel('Email').fill('anna@example.com');
await page.getByLabel('Password').fill('wrong-password');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByRole('alert')).toHaveText('Invalid email or password');
});Три локатора формы повторяются в обоих тестах. В наборе обычно десятки тестов, которые сначала выполняют вход. Если подпись кнопки меняется с «Sign in» на «Log in», все они падают, и каждый приходится править. Кроме того, тест смешивает два вида знания: что проверяется и как устроена страница. Документация Selenium называет обе проблемы на собственном примере: локаторы перемешаны с тестом, и одни и те же локаторы повторяются в каждом тесте, который использует страницу.
Влияние на поддержку измерено в промышленном исследовании Леотты, Клериси, Риччи и Спадаро (ICSTW 2013). Два набора по 25 тест-кейсов для одного веб-приложения приводили в соответствие с новым релизом. Набор без объектов страниц потребовал 124 минуты и 90 изменённых строк кода. Набор с 19 объектами страниц - 43 минуты и 11 строк. На старте набор с объектами страниц потребовал больше кода: 3320 строк против 2891.
Паттерн Page Object
Услуги вместо разметки
Фаулер описывает объект страницы как обёртку над HTML-страницей или её фрагментом с API в терминах приложения. Он должен позволять тесту делать и видеть всё, что может пользователь. Методы объекта страницы говорят языком приложения: «войти как», «число непрочитанных писем», а не «найти элемент по такому селектору и нажать на него». Текстовое поле представлено строкой, флажок - логическим значением, кнопка - действием.
Документация Selenium описывает тот же класс с двух сторон. Тестам он предоставляет услуги. По отношению к странице он единственный глубоко знает структуру её HTML. В результате во всём наборе тестов есть только одно место, которое знает, как устроена конкретная страница.
Объект страницы не обязан охватывать всю страницу. Фаулер рекомендует строить объекты для значимых элементов интерфейса и следовать структуре, понятной пользователю, а не разметке вёрстки.
Навигация и возвращаемые объекты
Действие, которое переводит пользователя на другую страницу, возвращает объект этой страницы. Успешный вход возвращает страницу входящих, неуспешный - саму страницу входа. Документация Selenium рекомендует моделировать разные исходы одного действия разными методами: в её примере это loginAs и loginAsExpectingError. Когда метод меняет тип возвращаемого значения, тесты, которые опираются на старую навигацию, перестают компилироваться, и ошибка обнаруживается до запуска.
Фаулер упоминает альтернативу: объекты страниц возвращают общий контекст браузера, а тест сам решает, какой объект страницы создать дальше. Так знание о навигации остаётся в тесте.
Проверки внутри объектов страниц
Источники расходятся в том, может ли объект страницы содержать проверки (assertions).
| Источник | Позиция |
|---|---|
| Фаулер, «PageObject» (2013) | Предпочитает объекты страниц без проверок; допустимое исключение - проверка инвариантов страницы |
| Документация Selenium | Объекты страниц никогда не выполняют проверок; единственное исключение - проверка, что загрузилась нужная страница |
| Документация Playwright | Её пример объекта страницы вызывает expect внутри метода getStarted |
| Сторонники проверок, в изложении Фаулера | Проверки в объектах страниц уменьшают дублирование и дают более понятные сообщения об ошибках |
Довод против - разделение ответственности. Объект страницы даёт доступ к странице, а ожидаемый результат определяет тест. Для повторяющихся проверок Фаулер предлагает общие вспомогательные функции проверок. Если объект страницы проверяет сам, тест, которому нужно проверить противоположный исход, не может переиспользовать этот метод.
Пример на Playwright
Те же два теста с объектами страниц. Страница входа знает свои локаторы и предоставляет два действия с разными исходами:
import { type Locator, type Page } from '@playwright/test';
import { InboxPage } from './inbox-page';
export class LoginPage {
readonly page: Page;
readonly email: Locator;
readonly password: Locator;
readonly signInButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.email = page.getByLabel('Email');
this.password = page.getByLabel('Password');
this.signInButton = page.getByRole('button', { name: 'Sign in' });
this.errorMessage = page.getByRole('alert');
}
async goto() {
await this.page.goto('https://example.com/login');
}
async signInAs(email: string, password: string): Promise<InboxPage> {
await this.submit(email, password);
return new InboxPage(this.page);
}
async signInExpectingError(email: string, password: string): Promise<LoginPage> {
await this.submit(email, password);
return this;
}
private async submit(email: string, password: string) {
await this.email.fill(email);
await this.password.fill(password);
await this.signInButton.click();
}
}Страница входящих открывает только то, что нужно тестам:
import { type Locator, type Page } from '@playwright/test';
export class InboxPage {
readonly heading: Locator;
constructor(page: Page) {
this.heading = page.getByRole('heading', { name: 'Inbox' });
}
}Тесты описывают сценарий и ожидаемый результат. Проверки остаются в тестах:
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/login-page';
test('user signs in with valid credentials', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
const inbox = await loginPage.signInAs('anna@example.com', 'correct-password');
await expect(inbox.heading).toBeVisible();
});
test('user sees an error with a wrong password', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.signInExpectingError('anna@example.com', 'wrong-password');
await expect(loginPage.errorMessage).toHaveText('Invalid email or password');
});Когда меняется подпись кнопки, меняется одна строка LoginPage. Тесты не правятся. Класс открывает локаторы как свойства только для чтения, чтобы тесты могли использовать веб-ориентированные проверки Playwright (web-first assertions), которые ждут ожидаемого состояния; документация Playwright делает так же в своём примере.
Компоненты и фрагменты страниц
Многие части интерфейса повторяются на нескольких страницах: шапка с поиском и меню аккаунта, таблица с постраничной навигацией, карточка товара в списке. Фаулер писал, что название «panel object» подошло бы лучше, чем page object, потому что паттерн применим к любой значимой части экрана. Документация Selenium называет такие классы объектами компонентов страницы (page component objects). Компонент можно включить в объект страницы, и он может содержать другие компоненты.
В Playwright компонент удобно строить на корневом локаторе: все его локаторы ищутся внутри этого корня.
import { type Locator } from '@playwright/test';
export class HeaderComponent {
readonly searchBox: Locator;
readonly accountMenu: Locator;
constructor(root: Locator) {
this.searchBox = root.getByRole('searchbox');
this.accountMenu = root.getByRole('button', { name: 'Account' });
}
async search(query: string) {
await this.searchBox.fill(query);
await this.searchBox.press('Enter');
}
}Объект страницы создаёт компонент со своим корнем, например new HeaderComponent(page.getByRole('banner')). Один и тот же класс обслуживает все страницы, где есть шапка. Программа ISTQB CTAL-TAE описывает также модель потока (flow model): дополнительный фасад над объектами страниц, который хранит действия пользователя, охватывающие несколько страниц, чтобы шаги тестов переиспользовались в разных сценариях.
Репозиторий объектов
Страницы и элементы как данные
В репозитории объектов описание интерфейса - не код, а данные. Репозиторий устроен как иерархия: папки, страницы, элементы. У каждого элемента есть имя, уникальное в пределах страницы, и локатор. Шаги тестов ссылаются на элемент по странице и имени, а инструмент подставляет локатор во время выполнения.
Пример структуры репозитория для сценария входа. Он условный: форматы у инструментов разные, общего стандарта для них не найдено.
{
"pages": [
{
"name": "Login page",
"path": "/login",
"elements": [
{ "name": "Email field", "locator": { "kind": "LABEL", "value": "Email" } },
{ "name": "Password field", "locator": { "kind": "LABEL", "value": "Password" } },
{ "name": "Sign in button", "locator": { "kind": "ROLE", "value": "button", "name": "Sign in" } },
{ "name": "Error message", "locator": { "kind": "ROLE", "value": "alert" } }
]
},
{
"name": "Inbox page",
"path": "/inbox",
"elements": [
{ "name": "Inbox heading", "locator": { "kind": "ROLE", "value": "heading", "name": "Inbox" } }
]
}
]
}Документация OpenText Functional Testing (прежнее название - UFT One) описывает свой репозиторий объектов как хранилище тестовых объектов, у каждого из которых есть набор описывающих свойств. Она различает локальный репозиторий, привязанный к одному действию, и общий репозиторий, которым несколько тестов пользуются в режиме только для чтения. Та же документация рекомендует общий репозиторий для тестов на основе ключевых слов и для объектов, свойства которых часто меняются.
Шаги на основе ключевых слов
Глоссарий ISTQB определяет тестирование на основе ключевых слов (keyword-driven testing) как технику написания сценариев, при которой тестовые сценарии содержат высокоуровневые ключевые слова, а вспомогательные файлы - низкоуровневые сценарии, которые их реализуют. ISO/IEC/IEEE 29119-5 описывает эталонный подход к нему, требования к фреймворкам и инструментам, общий формат обмена данными, уровни иерархических ключевых слов и начальный список общих низкоуровневых ключевых слов, например для ввода данных или проверки значений. Программа CTAL-TAE добавляет, что тест-кейсы на основе ключевых слов - это списки или таблицы шагов и что в их написании могут участвовать тест-аналитики и бизнес-аналитики, а сами ключевые слова реализуют инженеры по автоматизации тестирования.
Второй тест примера в виде шагов с ключевыми словами, которые ссылаются на репозиторий:
| Шаг | Ключевое слово | Страница | Элемент | Значение |
|---|---|---|---|---|
| 1 | Open page | Login page | ||
| 2 | Enter text | Login page | Email field | anna@example.com |
| 3 | Enter text | Login page | Password field | wrong-password |
| 4 | Click | Login page | Sign in button | |
| 5 | Check text | Login page | Error message | Invalid email or password |
В шагах нет локаторов. Ключевое слово «Click» реализовано во фреймворке один раз, элемент «Sign in button» описан в репозитории один раз. Тестировщик, который не пишет код, может собирать новые тесты из готовых ключевых слов и элементов.
Поддержка при изменении интерфейса
Главное назначение репозитория - изменение интерфейса обрабатывается в одном месте. Типичные изменения и работа, которую они требуют:
- изменилась подпись или атрибут: правится локатор одного элемента, и все шаги, которые на него ссылаются, при следующем запуске используют новое значение;
- новый элемент: он один раз добавляется на свою страницу и становится доступен всем тестам;
- удалённый элемент: перед удалением инструмент или команда находит все шаги, которые на него ссылаются, потому что эти шаги упадут;
- перенесённый элемент: он переносится на другую страницу или в компонент, ссылки в шагах обновляются;
- новая версия интерфейса: репозиторий версионируется вместе с тестами, чтобы тестовый прогон использовал описания, которые соответствуют тестируемой сборке.
Изменения репозитория проходят ревью так же, как код. Изменение одного локатора затрагивает все тесты, которые используют элемент, и неверный локатор ломает их все сразу.
Правила именования
Имена в репозитории читают люди, которые пишут шаги тестов, поэтому имя описывает элемент, а не его реализацию. Распространённые правила:
- страница называется так, как её видит пользователь: «Login page», «Order details», а не по имени файла шаблона;
- имя элемента сочетает смысл и тип: «Sign in button», «Email field», «Error message»;
- локатор не повторяется в имени: «button#submit-2» перестаёт быть правдой после первого изменения разметки;
- имена уникальны в пределах страницы, а один и тот же элемент на разных страницах называется одинаково;
- повторяющиеся части (шапка, меню, диалоги) хранятся как отдельные страницы или компоненты, а не копируются на каждую страницу.
Паттерн Screenplay
Паттерн Screenplay - альтернатива объектам страниц, в которой тест описывается как действующие лица (actors), выполняющие задачи. По статье его авторов (InfoQ, 2016), Энтони Маркано придумал его в 2007-2008 годах, Энди Палмер развивал его с 2009 года, а Ян Молак дал ему название Journey Pattern в 2013 году. Авторы представляют его как применение принципов проектирования SOLID к автоматизированным приёмочным тестам. По их доводам, крупные объекты страниц нарушают принципы единственной ответственности и открытости-закрытости, потому что новые тесты часто требуют менять существующие классы страниц.
Документация Serenity BDD описывает элементы паттерна: действующие лица, способности (abilities, например работа с браузером), задачи (tasks), взаимодействия (interactions) и вопросы (questions). Задача составляется из взаимодействий:
public class Login {
public static Task asAStandardUser() {
return Task.where(
Open.url("https://www.saucedemo.com/"),
Enter.theValue("standard_user").into("#user-name"),
Enter.theValue("secret_sauce").into("#password"),
Click.on("#login-button")
);
}
}Тест после этого читается как toby.attemptsTo(Login.asAStandardUser()). Serenity BDD также хранит локаторы как именованные цели (targets), например Target.the("User name").locatedBy(".user-name"), которые объявляются в отдельных классах и переиспользуются задачами и вопросами. Таким образом, Screenplay меняет организацию действий, а локаторы по-прежнему хранятся в одном месте.
Локаторы в тестах, объект страницы и репозиторий объектов
Таблица сравнивает три способа размещения локаторов.
| Локаторы в тестах | Объект страницы | Репозиторий объектов | |
|---|---|---|---|
| Где живут локаторы | В каждом тесте | В классах страниц и компонентов | В хранилище страниц и элементов |
| Форма | Код | Код | Данные, правятся в инструменте или файле |
| Кто правит | Разработчик тестов | Разработчик тестов | Разработчик тестов или тестировщик без кода |
| Повторное использование действий | Копирование | Методы объектов страниц | Ключевые слова фреймворка |
| Изменение локатора | В каждом тесте | В одном классе | В одном элементе |
| Проверки | В тесте | В тесте, по большинству источников | В шагах проверки теста |
| Типичное применение | Небольшие наборы, прототипы | Фреймворки на коде | Инструменты на ключевых словах и без кода |
Объект страницы и репозиторий объектов решают одну задачу на разных уровнях. Объект страницы хранит локаторы и действия вместе, в коде. Репозиторий объектов хранит только описания элементов, а действия - общие ключевые слова. Объекты страниц могут выражать сложное поведение страницы: ожидание определённого состояния, составные операции, разные исходы. Репозиторий делает описание интерфейса доступным людям, которые не пишут код.
Подходы сочетаются. Фреймворк на коде может загружать локаторы из файла репозитория в объекты страниц, а инструмент на ключевых словах - группировать элементы в компоненты, похожие на объекты страниц. В обоих случаях в тестах нет локаторов, и изменение интерфейса не требует правки тестов.
Термины
| Термин | Значение |
|---|---|
| Объект страницы (page object) | Класс в коде тестов, который представляет страницу или её часть и предоставляет тестам её услуги, скрывая локаторы |
| Объект компонента страницы (page component object) | Объект страницы для повторяющейся части страницы, например шапки или карточки товара; включается в страницы и в другие компоненты |
| Репозиторий объектов (object repository) | Хранилище описаний элементов интерфейса, устроенное как страницы и элементы, на которые шаги тестов ссылаются по имени |
| Тестирование на основе ключевых слов (keyword-driven testing) | Техника написания сценариев, при которой шаги теста состоят из высокоуровневых ключевых слов, реализованных вспомогательными низкоуровневыми сценариями |
| Модель потока (flow model) | Фасад над объектами страниц, который хранит действия пользователя, охватывающие несколько страниц, для повторного использования в тестах |
| Паттерн Screenplay (Screenplay pattern) | Паттерн проектирования тестов, в котором действующие лица со способностями выполняют задачи из взаимодействий и задают вопросы о системе |
Источники
- PageObject, Martin Fowler, 10 September 2013(откроется в новой вкладке) (на английском)
- Page object models, документация Selenium(откроется в новой вкладке) (на английском)
- Page object models, документация Playwright(откроется в новой вкладке) (на английском)
- ISTQB Certified Tester Advanced Level Test Automation Engineering Syllabus v2.0, ISTQB, 2024(откроется в новой вкладке) (на английском)
- ISO/IEC/IEEE 29119-5:2024, Software testing, Part 5: Keyword-driven testing(откроется в новой вкладке) (на английском)
- Keyword-driven testing, глоссарий ISTQB (зеркало imbus)(откроется в новой вкладке) (на английском)
- Leotta, Clerissi, Ricca, Spadaro. Improving Test Suites Maintainability with the Page Object Pattern: An Industrial Case Study, ICSTW 2013(откроется в новой вкладке) (на английском)
- Test objects in object repositories, справка OpenText Functional Testing(откроется в новой вкладке) (на английском)
- Beyond Page Objects: Next Generation Test Automation with Serenity and the Screenplay Pattern, InfoQ, 2016(откроется в новой вкладке) (на английском)
- Screenplay Fundamentals, документация Serenity BDD(откроется в новой вкладке) (на английском)