automationpage-objectarchitectureplaywrightseleniumqa

Архитектура UI-автотестов: Page Object и что идёт после него

Первый мой автотест-сьют выглядел честно и просто: открыть страницу, найти поле, ввести текст, нажать кнопку, проверить результат. Двадцать тестов — красота, всё зелёное, пишутся за вечер. Проблемы начинаются позже, и всегда одинаково: тестов становится двести, дизайнер меняет кнопку логина, и ты правишь один и тот же селектор в сорока файлах. Или вход переехал с отдельной страницы на модалку — и половина сьюта краснеет не потому что баг, а потому что «войти» теперь делается иначе.

Вот тут обычно и приходит мысль про «архитектуру автотестов». И с ней важно не переусердствовать — в обе стороны.

Зачем вообще архитектура

Сначала договоримся, зачем она. Не ради красоты и не потому что «так принято». Хороший автотест-сьют меряется не количеством тестов, а стоимостью изменения — сколько мест надо тронуть, когда в продукте поменялась одна вещь. Если ответ «одно» — архитектура хорошая. Если «поиск по всему проекту и сорок правок» — плохая, сколько бы модных паттернов ты сверху ни навесил.

Page Object — что это на самом деле

Page Object обычно объясняют как «класс на каждую страницу». Это описание, но не суть. Суть: Page Object прячет КАК за ЧТО. Тест говорит, что делает пользователь — «войти под таким-то», «добавить товар в корзину». А как именно это происходит в DOM — какие селекторы, клики, ожидания — знает только Page Object. Тест не должен знать, что кнопка входа это button[data-testid=login]. Он знает только loginPage.loginAs(user).

Побочный эффект этого разделения — ровно то, что чинит боль из начала. Селектор кнопки живёт в одном месте. Поменялась вёрстка — правишь один метод в одном объекте, а не сорок тестов.

Что класть в Page Object, а что нет

Самый частый антипаттерн — ассерты внутри Page Object. Соблазн понятный: раз объект знает про страницу, пусть он и проверяет. Но тогда Page Object начинает решать, что важно проверить, — а это работа теста. Правильно так: Page Object отдаёт состояние (текст ошибки, видим ли элемент, сколько строк в таблице) или возвращает другой Page Object (после успешного логина — DashboardPage). А сравнивает с ожиданием — тест.

Единственное исключение, которое я себе разрешаю, — проверка, что мы вообще на нужной странице (базовое «страница загрузилась»): её удобно держать в объекте, чтобы падать рано и понятно.

И ещё: Page Object — про поведение, не про карту элементов. Объект с полусотней полей-локаторов и без единого метода-действия — это не Page Object, это словарь селекторов. Ценность в методах, которые говорят на языке пользователя.

Слои выше Page Object

Page Object — не потолок. Как только сьют растёт, появляются вещи, которые в модель «страница» не влезают.

Компоненты. Хедер, футер, модалка подтверждения, тост, дейт-пикер — куски, которые живут на многих страницах. Держать их в каждом Page Object — копипаста. Выносим в отдельные компонентные объекты, а страницы их переиспользуют. Тогда «закрыть баннер о cookie» описано один раз.

Setup через API, а не через UI. Самая недооценённая часть. Если тесту нужен залогиненный пользователь с товаром в корзине — не прокликивай логин и добавление товара через интерфейс в каждом тесте. Это медленно и хрупко: сломалась регистрация — краснеет всё, хотя проверял ты совсем другое. Готовь состояние через API или базу, а через UI делай ровно то, что тестируешь. Сам логин достаточно проверить в паре отдельных тестов на логин.

Тест-данные. Хардкод вроде «[email protected] / Password123» расползается и связывает тесты между собой: два теста дерутся за одного юзера — привет флак. Лучше фабрики и билдеры, которые создают свежие данные под конкретный тест, с уникальностью там, где она важна (email с uuid/таймстампом). Про это был отдельный пост — тест-данные тихо ломают сьют сильнее, чем локаторы.

Где переусложняют

Теперь честно про обратную крайность — её я вижу чаще, чем «нет архитектуры вообще». BasePage, от которого наследуются все страницы, распухший до тридцати «утилитных» методов, которыми пользуются три страницы из тридцати. Наследование ради наследования. Обёртки над обёртками над драйвером — «свой фреймворк» поверх Playwright, который и так фреймворк. Абстракции, придуманные заранее, под дублирование, которого ещё нет.

Правило, которое меня спасает: не абстрагируй заранее. Первое повторение — терпим. Второе — замечаем. На третьем — выносим. До третьего раза ты не знаешь, какая абстракция правильная, и почти наверняка угадаешь неверно — а ломать неудачную абстракцию дороже, чем убрать копипасту.

И критерий сверху: абстракция должна упрощать чтение теста, а не добавлять слой, в который надо проваливаться. Если, чтобы понять сценарий теста, приходится открыть три файла, — архитектура работает против тебя.

Как выглядит здоровый тест

Простой критерий, по которому я оцениваю сьют: открой тест и прочитай его вслух. Если получается связный сценарий пользователя — «вошёл, добавил товар, оформил заказ, увидел подтверждение» — без единого селектора, css и xpath в теле теста — архитектура на месте. Если в тесте торчат //div[2]/span и sleep(3000) — нет, сколько бы Page Object’ов рядом ни лежало.

Коротко — что забрать с собой

  • Локаторы — в одном месте (Page Object или компонент). Тест не знает про css.
  • Page Object без ассертов: отдаёт состояние или следующий объект, проверяет тест.
  • Setup через API/БД, через UI — только то, что реально тестируешь.
  • Повторяющиеся куски интерфейса — компоненты, не копипаста по страницам.
  • Не абстрагируй заранее — правило трёх: сначала работающий дубль, потом абстракция.
  • Тест читается как сценарий пользователя. Селектор в теле теста — запах.

Почитать: Martin Fowler — PageObject · Playwright — Page Object Models · Selenium — Page Object Models