Добавить в корзинуПозвонить
Найти в Дзене

Паттерн Handlers в UI автотестах: как перестать размазывать if/elif по тестам

В UI‑автотестах часто встречается ситуация: Если пытаться решать это “в лоб” в Page Object, он быстро превращается в набор ветвлений: Потом таких мест становится десятки — и поддержка деградирует. Паттерн variant handlers (или просто handlers) решает именно это: он выделяет слой, который подбирает реализацию под конкретный шаблон страницы и даёт тестам единый интерфейс действий. Подписывайтесь на мой канал в Телеграмм, чтобы ничего не пропустить. Ну или на канал в VK, если хотите видеть новые статьи у себя в ленте. Ты разделяешь сущности на три слоя: 1) Контракт страницы — что тест хочет сделать (в терминах сценария), без привязки к конкретной вёрстке
Примеры: 2) Handlers (варианты) — как именно это сделать для каждого шаблона
Примеры: 3) Resolver (диспетчер) — выбирает подходящий handler по “якорям” (признакам) текущей отрисовки По сути это комбинация: Page Object хорошо отвечает на “где что находится”, но страдает, когда: Handlers позволяют Page Object остаться “тонким”: он делегиру
Оглавление

В UI‑автотестах часто встречается ситуация:

  • бизнес‑сценарий один (“каталог”, “карточка”, “форма оплаты”),
  • но UI отрисовывается по-разному: A/B, фичефлаги, разные шаблоны, разные устройства/разрешения, разные типы данных,
  • количество блоков/карточек/виджетов может быть разным.

Если пытаться решать это “в лоб” в Page Object, он быстро превращается в набор ветвлений:

Потом таких мест становится десятки — и поддержка деградирует. Паттерн variant handlers (или просто handlers) решает именно это: он выделяет слой, который подбирает реализацию под конкретный шаблон страницы и даёт тестам единый интерфейс действий.

Подписывайтесь на мой канал в Телеграмм, чтобы ничего не пропустить.

Ну или на канал в VK, если хотите видеть новые статьи у себя в ленте.

Идея паттерна

Ты разделяешь сущности на три слоя:

1) Контракт страницы — что тест хочет сделать (в терминах сценария), без привязки к конкретной вёрстке
Примеры:

  • items() — вернуть карточки
  • open_item(i) — открыть i‑ю карточку
  • apply_filter(name)
  • get_total_count()

2) Handlers (варианты)как именно это сделать для каждого шаблона
Примеры:

  • CatalogGridHandler (сеткой)
  • CatalogListHandler (списком)
  • CatalogCompactHandler (компакт)

3) Resolver (диспетчер) — выбирает подходящий handler по “якорям” (признакам) текущей отрисовки

По сути это комбинация:

  • Strategy: разные стратегии взаимодействия с одной “страницей по смыслу”
  • Chain of Responsibility: перебор кандидатов, “первый подошёл — он и есть активный вариант”
  • иногда Factory: создание правильной реализации

Чем это отличается от “просто Page Object”

Page Object хорошо отвечает на “где что находится”, но страдает, когда:

  • разные шаблоны имеют разные контейнеры/локаторы,
  • на одном шаблоне есть элементы, которых нет на другом,
  • разные структуры требуют разных шагов (например, сначала раскрыть панель, затем кликнуть).

Handlers позволяют Page Object остаться “тонким”: он делегирует поведение выбранному варианту.

Важно: два типа “различий” в UI (и как их хендлить)

Тип A: структура реально разная (разные контейнеры/иерархия)

Это твой основной кейс для variant handlers.
Пример: карточки лежат в [data-testid=catalog-grid] vs [data-testid=catalog-list].

Тип B: структура та же, но элементов больше/меньше (опциональные блоки)

Это часто решается внутри одного handler через:

  • “мягкие” методы получения (optional),
  • ожидания, которые допускают 0 элементов,
  • отдельные методы has_*() / try_*().

На практике почти всегда микс: несколько вариантов структуры + опциональные куски внутри.

Архитектура: контракт + варианты + resolver

Ниже “скелет” на Python. Я пишу максимально нейтрально: это одинаково применимо и к Selenium, и к Playwright (отличатся будут только API locator/count/click).

1) Контракт (Protocol)

Protocol помогает держать дисциплину: каждый handler обязан реализовать одинаковые методы, иначе статический анализатор (mypy/pyright) начнёт ругаться.

-2

2) Конкретные handlers (варианты)

Ключевая часть — matches(): определяем вариант по якорям.

-3
-4

(В реальном коде ты, конечно, используешь self.page, я оставляю здесь читаемость как приоритет.)

3) Resolver (выбор варианта)

-5

4) Публичный Page Object, который делегирует

-6

В итоге тесты получают единый интерфейс:

-7

Разное количество элементов: как не превращать это в flaky

Тут частая ловушка: “на одном шаблоне блоков меньше” — и тесты падают, потому что ожидали наличие.

Практичный подход:

1) Чётко разделять “обязательное” и “опциональное”

В контракте удобно иметь методы:

  • has_promo_banner() -> bool
  • promo_banner_text() -> str (и пусть он падает, если баннера быть не должно)
  • или promo_banner_text_or_none() -> Optional[str]

2) Ожидания должны соответствовать бизнес‑инвариантам

Если “карточек может быть 0” — не используй ожидание “минимум 1”.
Если “карточка должна быть хотя бы одна” — наоборот, фиксируй это как явное утверждение в PageObject.

3) Стабилизировать работу со списками

Примеры стабилизации:

  • ждать “загрузка завершена” (спиннер исчез / запросы закончились),
  • потом читать список,
  • потом действовать.

Когда handlers — перебор, и что делать вместо

Если отличие только в “иногда кнопка скрыта” и селекторы те же — иногда достаточно:

  • одного Page Object,
  • пары методов try_click_*().

Но как только:

  • разные контейнеры,
  • разные локаторы,
  • разные шаги взаимодействия,
  • много мест с ветвлением,

— variant handlers почти всегда окупаются.

Рекомендованный формат структуры проекта (чтобы было удобно поддерживать)

Обычно хорошо ложится так:

  • pages/catalog/page.py — публичный CatalogPage
  • pages/catalog/variants/grid.py — GridCatalogHandler
  • pages/catalog/variants/list.py — ListCatalogHandler
  • pages/catalog/variants/resolver.py — resolve_variant, список вариантов, диагностика
  • pages/catalog/contracts.py — CatalogVariant protocol

Плюс: тесты читают “язык предметной области”, а не “язык верстки”.

Спасибо, что дочитали до конца 🙌
До встречи в следующих статьях!

-8