$ sudo teach|
    $ sudo teach|
    IT school
  • Telegram
  • Партнёрам
  • Все курсы
$ sudo teach IT
OOO "SALEPROFIT"Контакты и реквизитыIT-Park Logo

Школа

  • Блог
  • Проверить сертификат

Сотрудничество

  • Стать учителем
  • Партнёрская программа
  • О проекте

Право

  • Оферта
  • Политика конфиденциальности

© 2023–2026 $ sudo teach IT™. All Rights Reserved. Public user contributions licensed under CC BY-SA 4.0 license with attribution required
TelegramGitHubYouTube
ГлавнаяБлогПишем безопасные автотесты для веба: Page Object, стабильные селекторы и параллельный запуск

Пишем безопасные автотесты для веба: Page Object, стабильные селекторы и параллельный запуск

$ sudo teach IT
·1 августа 2026 г.·9 мин·64
Пишем безопасные автотесты для веба: Page Object, стабильные селекторы и параллельный запуск

Покажем, как строить надёжную тестовую архитектуру для UI: Page Object, стратегии поиска элементов без флейков, работа с ожиданиями и запуск тестов параллельно в CI. Пройдём путь от первого “зелёного” теста до устойчивого набора.

Содержание
Почему UI-тесты флейчат: реальные причиныАрхитектура UI: Page Object как каркасЧто такое Page Object (и чем он полезен)Пример структуры проектаМинимальный Page Object на практике (Python + Playwright)Стабильные селекторы: стратегия вместо «угадывания»Почему data-testid — де-факто стандартЧто делать, если data-testid нетИзбегаем типичных флейков селекторовПрактика: ограничения и контекст локатораОжидания: детерминизм вместо “sleep”Автоожидание и смысл “правильных ожиданий”Пример: ожидание результата после кликаКогда нужно ожидание сетевых запросовГраницы ожиданий: что нельзя делатьИзоляция тестов: чтобы параллельность не ломала данныеПочему параллельный запуск выявляет скрытые зависимостиМодели изоляции: три подходаПример: уникальные данные для параллельных тестовПараллельный запуск в CI: практические деталиЧто важно на уровне конфигурацииПример команды запускаКонфликт ресурсов: что обычно ломается первымНормализация окружения в CIОт первого зелёного теста до устойчивого набора: путь развитияШаг 1. Сделайте тест “правильным”, а не просто проходящимШаг 2. Введите соглашение по селекторамШаг 3. Сделайте тесты независимыми по даннымШаг 4. Подключите параллельный запуск и посмотрите на ошибкиШаг 5. Приведите регресс к воспроизводимостиПример: связка Page Object + стабильные ожидания + сценарий тестаТипичные ошибки, которые “незаметно” портят архитектуру

Автотесты для веба редко ломаются «из-за кода теста». Чаще они ломаются из‑за хрупкости вокруг UI: селекторы начинают совпадать не там, ожидания оказываются недостаточными (или наоборот — избыточными), а параллельный запуск в CI вскрывает скрытые зависимости между тестами. В итоге команда тратит больше времени на «чинить зелёные» и обновлять селекторы, чем на проверку регрессий.

В этой статье разберём путь от первого «зелёного» теста до набора, который переживает изменения фронтенда, работает стабильно и может исполняться параллельно в CI. Я буду говорить в первую очередь о подходах и архитектуре, но местами покажу конкретный код: Page Object, стратегии поиска элементов без флейков, ожидания и параллелизацию.


Почему UI-тесты флейчат: реальные причины

Прежде чем проектировать архитектуру, важно понять источник нестабильности. Самые частые причины флейков в вебе:

  1. Хрупкие селекторы
    Выбрали CSS по классу, который поменялся, или по структуре DOM, которая «логически не должна меняться», но меняется вместе с версткой.

  2. Гонки (race conditions)
    Тест ищет элемент до того, как он появился, или кликает по оверлею/анимации. В результате иногда действие проходит, иногда — нет.

  3. Смешение состояния между тестами
    Тесты используют один и тот же пользователь/сессию/корзину/данные. При параллельном запуске эффект усиливается.

  4. Неправильные ожидания
    Вместо wait until visible — фиксированный sleep. Или наоборот: ожидание «любого» состояния вместо «конкретного результата».

  5. Сеть/фоновые запросы
    UI реагирует на запросы с задержкой, а тест ждёт только DOM-изменение без учета реальной готовности данных.

  6. Непредсказуемая среда
    Разный размер экрана, разные часовые пояса, разная версия браузера/драйвера, нестабильный DNS/загрузка ассетов.

Хорошая архитектура тестов минимизирует эти источники на уровне:

  • селекторов (стабильность),
  • ожиданий (детерминизм),
  • изоляции тестов (отсутствие гонок),
  • запуска (параллельность без конфликта ресурсов).

Архитектура UI: Page Object как каркас

Что такое Page Object (и чем он полезен)

Page Object — это паттерн, где представление страницы (или её части) оформляется как объект с методами действий и проверок. Идея: логика поиска и взаимодействия с элементами живёт рядом с «страницей», а тесты — становятся читаемыми сценариями, а не набором find_element(...).

Ключевой момент: Page Object — это не “ещё один файл с селекторами”, а механизм:

  • централизовать стратегии поиска,
  • закрепить соглашения (например, использовать data-testid),
  • спрятать детали ожиданий,
  • упростить рефакторинг при изменениях UI.

Пример структуры проекта

Один из рабочих вариантов:

  • tests/
    • e2e/
      • flows/ — высокоуровневые сценарии (минимум селекторов)
      • pages/ — Page Objects
      • fixtures/ — фикстуры окружения (пользователь, тестовые данные)
  • ui/
    • selectors/ — иногда можно выделить общие селекторы
    • helpers/ — общие функции ожидания и действий
  • config/ — параметры CI, переменные окружения

Минимальный Page Object на практике (Python + Playwright)

Ниже пример для Playwright (он удобен ожиданиями и автоожиданием). Если вы используете Selenium — принципы те же, но ожидания придётся писать более явно.

code
# tests/e2e/pages/login_page.py
from playwright.sync_api import Page, expect

class LoginPage:
    def __init__(self, page: Page, base_url: str):
        self.page = page
        self.base_url = base_url

        # Селекторы лучше хранить как константы внутри page object
        self._email = page.locator("[data-testid='login-email']")
        self._password = page.locator("[data-testid='login-password']")
        self._submit = page.locator("[data-testid='login-submit']")
        self._error = page.locator("[data-testid='login-error']")

    def open(self):
        self.page.goto(f"{self.base_url}/login")

    def login(self, email: str, password: str):
        self._email.fill(email)
        self._password.fill(password)
        self._submit.click()

    def assert_error_message(self, expected: str):
        # Ожидаем видимость конкретного сообщения
        expect(self._error).to_be_visible()
        expect(self._error).to_have_text(expected)

Обратите внимание на два аспекта:

  1. data-testid: это “контракт” между тестами и UI, а не попытка угадывать классы.
  2. Ожидания в Page Object: тесты меньше знают о деталях DOM и о том, “как именно” ждать.

Стабильные селекторы: стратегия вместо «угадывания»

Почему data-testid — де-факто стандарт

В идеале фронтенд-команда добавляет атрибуты тестовой идентификации (или вы договариваетесь о них). Тогда селекторы становятся:

  • стабильными при изменениях верстки,
  • независимыми от классов,
  • менее чувствительными к перестройке DOM.

Минимальный набор:

  • data-testid="..." на элементах, которые нужно находить,
  • иногда data-test-id / data-qa — зависит от команды, но важно единообразие.

Что делать, если data-testid нет

Если проект уже живёт без этих атрибутов, важно не впадать в крайность “всё переделать сейчас”. Практика показывает: лучше внедрять тестовые идентификаторы точечно — на узлах, которые чаще всего ломают UI-тесты.

Как точечно улучшать селекторы:

  • Не используйте селекторы вида .btn.primary.large для ключевых действий.
  • Не используйте nth-child или жесткие цепочки div > form > ..., если структура может меняться.
  • Используйте селекторы по роли/тексту, если UI семантичен:
    get_by_role("button", name="Sign in") (Playwright) — часто устойчивее.
  • Проверяйте уникальность селектора: тест должен быть уверен, что на странице один элемент.

Избегаем типичных флейков селекторов

  1. Селектор совпадает больше чем с одним элементом
    Тогда действие может пройти по “не тому”, что нашлось первым.

  2. Элемент есть, но не интерактивен
    Например, скрыт модальным окном или перекрыт. Решение: ожидать visible и/или enabled.

  3. Селектор по тексту слишком общий
    “Submit” встречается в нескольких местах. Лучше селекторы с контекстом (Page Object) или с уточнением по родителю.

Практика: ограничения и контекст локатора

Даже если селектор стабильный, важно держать его в рамках контекста компонента или страницы.

code
# tests/e2e/pages/cart_page.py
from playwright.sync_api import Page, expect

class CartPage:
    def __init__(self, page: Page):
        self.page = page
        self._items = page.locator("[data-testid='cart-items']")
        self._empty_state = page.locator("[data-testid='cart-empty']")
        self._checkout_button = page.locator("[data-testid='cart-checkout']")

    def assert_empty(self):
        expect(self._empty_state).to_be_visible()
        expect(self._items).to_have_count(0)

    def checkout(self):
        expect(self._checkout_button).to_be_enabled()
        self._checkout_button.click()

Ожидания: детерминизм вместо “sleep”

Автоожидание и смысл “правильных ожиданий”

Проблема фиксированных задержек (sleep) в том, что вы:

  • либо ждёте слишком мало,
  • либо ждёте слишком долго,
  • и главное — “время” не равно “готовности”.

Правильные ожидания — это ожидания состояния, например:

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

Пример: ожидание результата после клика

code
# tests/e2e/flows/test_login_failure.py
from playwright.sync_api import Page
from tests.e2e.pages.login_page import LoginPage

def test_login_shows_error(page: Page, base_url: str):
    login = LoginPage(page, base_url)
    login.open()
    login.login("wrong@example.com", "wrong-password")
    login.assert_error_message("Неверный логин или пароль")

Здесь ожидание внутри assert_error_message гарантирует, что тест не “угадывает”, а проверяет реальное состояние.

Когда нужно ожидание сетевых запросов

Иногда UI меняется быстро, но данные ещё не загрузились. Тогда вы тестируете “не тот момент”. Для Playwright можно ждать конкретный запрос или ответ — это снижает флейки в интеграционных сценариях.

Пример (идея, которую можно адаптировать):

code
# Пример: ожидание ответа поиска
def test_search_results_are_loaded(page, base_url):
    page.goto(f"{base_url}/catalog")

    # Ждём конкретный API-запрос
    with page.expect_response(lambda resp: "/api/search" in resp.url and resp.status == 200):
        page.get_by_test_id("search-input").fill("laptop")
        page.get_by_test_id("search-submit").click()

    # Затем проверяем UI-результат
    page.get_by_test_id("search-results").locator("[data-testid='product-card']").first.wait_for()

На практике это стоит применять точечно: там, где DOM-изменение не гарантирует готовность данных.

Границы ожиданий: что нельзя делать

  • Не делайте ожидание “любой элемент видим” — это маскирует проблемы.
  • Не проверяйте “текст появился где-то” без привязки к контейнеру.
  • Не используйте глобальные селекторы из тестов — переносите в Page Object.

Изоляция тестов: чтобы параллельность не ломала данные

Почему параллельный запуск выявляет скрытые зависимости

Если тесты последовательно не мешают друг другу, это не значит, что они независимы. Параллельность “раскроет” общие сущности:

  • один и тот же пользователь,
  • одна и та же корзина,
  • общий набор тестовых данных,
  • одинаковые временные идентификаторы,
  • общий файл/папка/ключ в окружении.

Модели изоляции: три подхода

  1. Пер-тестовый аккаунт / пер-тестовая сессия
    Создаём пользователя на каждый тест (или на каждый worker). Дороже, зато надёжнее.

  2. Одинаковая фикстура данных, но уникальные “ключи” действий
    Например, товары или сущности создаются с уникальными названиями по uuid.

  3. Транзакционный подход / сброс состояния
    После теста откатываем изменения или чистим таблицы.

Выбор зависит от вашей инфраструктуры. В CI проще всего внедрить уникальные ключи и чистку по окончании теста, а аккаунты — автоматизировать через API.

Пример: уникальные данные для параллельных тестов

code
import uuid

def unique_email():
    return f"test_{uuid.uuid4().hex}@example.com"

Дальше:

  • создаём пользователя с таким email,
  • используем email/идентификатор в проверках,
  • после теста удаляем пользователя через API (если возможно).

Параллельный запуск в CI: практические детали

Что важно на уровне конфигурации

У параллельного запуска есть минимум три задачи:

  1. Распределить тесты по воркерам
  2. Изолировать окружение
  3. Стабильно поднять браузеры и артефакты

Если вы используете Playwright, он уже поддерживает параллельность — но вы должны обеспечить корректное окружение.

Пример команды запуска

Допустим, у вас Playwright:

  • параллельно по файлам (workers)
  • с сохранением артефактов при падении.

Вариант CLI (идея):

code
pytest -q --maxfail=1
# или если запуск идет напрямую Playwright:
# npx playwright test --workers=4

Если тесты на Python через pytest, параллельность обычно делают через pytest-xdist:

code
pytest -n auto

Конфликт ресурсов: что обычно ломается первым

  1. Файлы и директории артефактов
    Важно, чтобы скриншоты/логи сохранялись с уникальными путями (часто это автоматически, но проверьте).

  2. Глобальные куки/профили браузера
    Если вы используете один профиль на весь прогон — появятся конфликты. Лучше каждый worker создаёт свой context.

  3. Экономия на “дорогом” setup
    Общий “seed” в начале может вступать в гонку. Setup должен быть либо:

    • до параллельности (и тогда неизменным),
    • либо per-worker/per-test.

Нормализация окружения в CI

Стабильность выше, когда вы фиксируете:

  • версию браузера,
  • размер viewport (если UI респонсивный),
  • язык/часовой пояс (в зависимости от приложения),
  • переменные окружения.

В идеале тесты должны быть детерминированы не только логически, но и “по среде”.


От первого зелёного теста до устойчивого набора: путь развития

Шаг 1. Сделайте тест “правильным”, а не просто проходящим

Даже один тест можно привести к устойчивости:

  • перенесите селекторы в Page Object,
  • добавьте ожидания результата,
  • исключите sleep там, где можно ожидать состояние.

Критерий: тест не должен падать при умеренных задержках сети.

Шаг 2. Введите соглашение по селекторам

Определите правило:

  • ключевые элементы имеют data-testid,
  • в тестах нет “сырых” селекторов,
  • допускается выбор селектора по роли/тексту только если UI семантически устойчив.

Шаг 3. Сделайте тесты независимыми по данным

Как только появляется второй тест, вы рискуете начать “влиять друг на друга”. Начните с простого:

  • уникальные идентификаторы,
  • чистка данных после теста,
  • отсутствие общих глобальных переменных.

Шаг 4. Подключите параллельный запуск и посмотрите на ошибки

Параллельный запуск — это не финальная настройка. Его лучше включать рано, хотя бы с небольшим числом воркеров (например, 2–4), чтобы быстро обнаружить:

  • конфликт пользователей,
  • конфликт ресурсов,
  • отсутствие ожиданий по сети.

Шаг 5. Приведите регресс к воспроизводимости

Когда тест падает, он должен оставлять подсказки:

  • trace/video (если используете),
  • скриншот,
  • текст/состояние,
  • логи страницы.

Это ускоряет исправления в разы. И именно это часто отличает набор “для галочки” от реального инструмента.


Пример: связка Page Object + стабильные ожидания + сценарий теста

Соберём короткий пример: логин, затем проверка состояния главной страницы.

code
# tests/e2e/pages/home_page.py
from playwright.sync_api import Page, expect

class HomePage:
    def __init__(self, page: Page):
        self.page = page
        self._welcome = page.locator("[data-testid='welcome-message']")
        self._logout = page.locator("[data-testid='logout-button']")

    def assert_logged_in(self, email: str):
        expect(self._welcome).to_be_visible()
        expect(self._welcome).to_contain_text(email)

    def logout(self):
        self._logout.click()
code
# tests/e2e/flows/test_login_success.py
from playwright.sync_api import Page
from tests.e2e.pages.login_page import LoginPage
from tests.e2e.pages.home_page import HomePage
import uuid

def test_login_success(page: Page, base_url: str, api_client):
    # api_client создаёт пользователя через API; email уникальный
    email = f"u_{uuid.uuid4().hex}@example.com"
    password = "StrongPassword!123"
    api_client.create_user(email=email, password=password)

    login = LoginPage(page, base_url)
    login.open()
    login.login(email, password)

    home = HomePage(page)
    home.assert_logged_in(email)

    home.logout()

Здесь важны детали:

  • создание пользователя через API, чтобы UI-тест был сфокусирован на UI логине;
  • уникальный email для параллельности;
  • ожидания внутри страниц.

Типичные ошибки, которые “незаметно” портят архитектуру

  1. Смешивание ответственности
    Когда тесты содержат логику ожиданий, а Page Object не содержит ничего, кроме констант — вы получаете “полутесты”, которые сложно поддерживать.

  2. Тесты “знают” DOM
    Чем больше селекторов

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

Автор

$ sudo teach IT

Автоматизация тестирования на Python

Курс по теме

Автоматизация тестирования на Python

На курсе вы освоите фреймворки для автотестов (pytest, Selenium, Playwright), работу с веб-интерфейсами (CSS), тестирование API (requests, httpx), базы данных (SQL) и паттерны проектирования (Page Object), развертывание тестов на CI/CD

Открыть курс

Продолжите обучение

Все курсы
Python – для начинающих!

Python – для начинающих!

С нуля до профессионального уровня. Подходит для всех. Учитесь каждый день и овладейте самым популярным языком программирования.

Перейти к курсу
Протоколы интернета - продвинутый курс

Протоколы интернета - продвинутый курс

Что происходит под капотом интернета: TCP и QUIC на уровне полей и состояний, NAT и прямые соединения, туннели и VPN-протоколы, прокси, анонимные и децентрализованные сети, приватность и разборы реальных взломов. 19 модулей, 133 темы, 19 тестов.

Перейти к курсу
Ботостроение Telegram

Ботостроение Telegram

Лёгкий, быстрый и доступный способ познакомиться с миром ботостроения в Telegram. Видео, конспекты, практика и помощь – всё у нас на курсе.

Перейти к курсу
Протоколы интернета - простыми словами

Протоколы интернета - простыми словами

Как данные находят дорогу от вашего устройства до сайта. IP, UDP, TCP, DHCP, DNS, HTTP, TLS и HTTPS - на бытовых аналогиях, без жаргона, формул и командной строки. 10 модулей, 55 уроков, 10 тестов.

Перейти к курсу

Другие статьи

Пишем CLI для разработчика на Python: команды, конфиги и корректные exit codes по стандарту проекта
пишем

Пишем CLI для разработчика на Python: команды, конфиги и корректные exit codes по стандарту проекта

Построим удобный интерфейс командной утилиты: структура подкоманд, чтение конфигов, валидация входных параметров и единая логика ошибок с понятными exit code. Сфокусируемся на UX для тех, кто запускает CLI в CI и в скриптах.

15 сентября 2026 г.
1380
Пишем корректные bash-скрипты: обработка ошибок, строгие режимы и безопасные переменные
пишем

Пишем корректные bash-скрипты: обработка ошибок, строгие режимы и безопасные переменные

Покажем best practices: set -euo pipefail, ловля ошибок, корректное quoting и проверка входных параметров. Разберём мини-проект с устойчивым поведением.

24 июля 2026 г.
970
Как выучить программирование самому и бесплатно: с чего начать и в каком порядке
выучить

Как выучить программирование самому и бесплатно: с чего начать и в каком порядке

Разберём понятный маршрут для новичка: как выбрать первый язык, что учить в первую очередь, какие мини-проекты делать и как не забросить занятия. Дам бесплатные ориентиры и план, чтобы знания складывались в портфолио.

21 сентября 2026 г.
1940
Что такое HTML и чем он отличается от CSS — объясню простыми словами и на примерах
такое

Что такое HTML и чем он отличается от CSS — объясню простыми словами и на примерах

Разберём, из чего состоит веб-страница: что делает HTML (смысл и структура) и что делает CSS (оформление). В конце соберёте мини-страницу и поймёте, что нужно учить в каком порядке.

30 октября 2026 г.
100
Cargo для продакшена: workspaces, фичи и стратегия версий зависимостей
cargo

Cargo для продакшена: workspaces, фичи и стратегия версий зависимостей

Разберём, как организовать Rust workspace, управлять optional features, фиксировать версии библиотек и уменьшать “дрейф” зависимостей при командной разработке и CI.

18 сентября 2026 г.
1940
Что такое SQL и зачем он нужен новичку: как думать о данных простыми словами
такое

Что такое SQL и зачем он нужен новичку: как думать о данных простыми словами

Разберём, что такое базы данных и SQL без сложной теории: как выбирать данные, фильтровать их и собирать таблицы в отчёт. В конце — короткий план, как потренироваться и собрать первые запросы для портфолио.

28 октября 2026 г.
220

Комментарии

Станьте первым в обсуждении

Поделитесь мнением о статье или задайте вопрос автору

Содержание

Почему UI-тесты флейчат: реальные причиныАрхитектура UI: Page Object как каркасЧто такое Page Object (и чем он полезен)Пример структуры проектаМинимальный Page Object на практике (Python + Playwright)Стабильные селекторы: стратегия вместо «угадывания»Почему data-testid — де-факто стандартЧто делать, если data-testid нетИзбегаем типичных флейков селекторовПрактика: ограничения и контекст локатораОжидания: детерминизм вместо “sleep”Автоожидание и смысл “правильных ожиданий”Пример: ожидание результата после кликаКогда нужно ожидание сетевых запросовГраницы ожиданий: что нельзя делатьИзоляция тестов: чтобы параллельность не ломала данныеПочему параллельный запуск выявляет скрытые зависимостиМодели изоляции: три подходаПример: уникальные данные для параллельных тестовПараллельный запуск в CI: практические деталиЧто важно на уровне конфигурацииПример команды запускаКонфликт ресурсов: что обычно ломается первымНормализация окружения в CIОт первого зелёного теста до устойчивого набора: путь развитияШаг 1. Сделайте тест “правильным”, а не просто проходящимШаг 2. Введите соглашение по селекторамШаг 3. Сделайте тесты независимыми по даннымШаг 4. Подключите параллельный запуск и посмотрите на ошибкиШаг 5. Приведите регресс к воспроизводимостиПример: связка Page Object + стабильные ожидания + сценарий тестаТипичные ошибки, которые “незаметно” портят архитектуру