Вступление
В этой статье мы разберем, как писать UI автотесты на Python, используя проверенные подходы и лучшие практики автоматизации. Мы поэтапно рассмотрим ключевые паттерны, такие как PageObject, PageComponent и PageFactory, подробно объясняя, когда и зачем они применяются, а также чем они отличаются друг от друга.
Цель статьи — не просто показать работу с этими паттернами, а разобрать их «на атомы» и объяснить, как они помогают сделать тесты масштабируемыми, читаемыми и легко поддерживаемыми.
Мы реализуем тестовый проект на основе специально подготовленного веб-приложения — UI Course, которое разработано для практики написания UI автотестов. Исходный код приложения доступен в моем репозитории на GitHub: UI Course GitHub.
Для запуска тестов в CI/CD мы будем использовать GitHub Actions, но все примеры легко адаптируются под другие системы, такие как Jenkins, GitLab CI или CircleCI — отличия будут касаться лишь синтаксиса и конфигурации пайплайнов.
Сценарий
Прежде чем погрузиться в реализацию паттернов и архитектуры автотестов, давайте определим, какой именно сценарий мы собираемся автоматизировать.
Намеренно выберем максимально простой и понятный сценарий, чтобы сосредоточиться на структуре автотестов, а не на логике приложения. Это поможет лучше понять, как применяются паттерны PageObject, PageComponent и PageFactory на практике.
Сценарий будет включать следующие шаги:
Открытие страницы регистрации: /#/auth/registration
Ввод данных нового пользователя:
Email
,Username
,Password
.Нажатие кнопки "Registration" и проверка перехода на страницу "Dashboard": /#/dashboard
Проверка наличия всех ключевых элементов на странице "Dashboard".
Технологии
Перед тем как приступить к реализации, определим инструменты и библиотеки, с которыми будем работать:
Python 3.12 — для написания автотестов
Playwright — инструмент для взаимодействия с браузером, управления страницами и проверки UI.
Pytest — тестовый фреймворк
Pytest Playwright — интеграция Playwright с pytest, позволяет запускать тесты с фикстурами и настройками Playwright.
Pydantic Settings — удобная работа с конфигурацией проекта через переменные окружения и
.env
файлы.Allure — для генерации детализированного отчёта
Почему Playwright, а не Selenium?
На момент написания статьи выбор между Playwright и Selenium — это выбор между современностью и устаревшими решениями. Ниже приведены аргументы в пользу Playwright.
Преимущества Playwright:
Скорость и надёжность. Playwright работает напрямую через DevTools Protocol, в отличие от Selenium, который использует прослойку WebDriver. Это делает Playwright быстрее и менее подверженным флаки-тестам.
Простая установка. Установка Playwright — это буквально две команды:
pip install playwright pytest-playwright
,playwright install
. В случае с Selenium всё куда сложнее: нужно вручную скачивать драйверы под конкретные браузеры и следить за их актуальностью.Видео и скриншоты "из коробки". Playwright умеет записывать видео без дополнительной настройки. Это сильно упрощает отладку.
Встроенные ожидания. Каждый метод Playwright уже содержит встроенные ожидания (waits). Не нужно писать кастомные ожидания и “костыли”, как часто бывает в Selenium.
Playwright Trace Viewer. Уникальный инструмент, позволяющий в деталях анализировать каждый шаг выполнения теста. Особенно полезен при отладке и CI-ошибках.
Асинхронность. Playwright изначально асинхронный (на базе asyncio), что позволяет эффективно масштабировать тесты. Есть и синхронная обёртка, но под капотом всё работает асинхронно.
Поддержка WebKit. В отличие от Selenium, Playwright поддерживает тестирование в браузере WebKit (на котором основан Safari), что критично для кросс-браузерного тестирования.
Перехват и моки запросов. Playwright позволяет мокать и подменять HTTP-запросы, что полезно для изоляции UI-слоя при тестировании.
Поддержка нескольких вкладок и фреймов. Работа с несколькими вкладками и iframes в Playwright реализована гораздо удобнее, чем в Selenium.
Интуитивный API. API у Playwright современный, читаемый и минималистичный — в отличие от Selenium, где код часто перегружен.
Почему Selenium всё ещё используют?
Несмотря на устаревание, Selenium по-прежнему встречается в проектах. Основные причины:
Неосведомленность. Многие специалисты просто не знакомы с Playwright или не успели глубоко его изучить.
Наследие. В компаниях уже написаны сотни и тысячи тестов на Selenium. Переписывать — дорого, долго и страшно.
Переход в процессе. Некоторые команды уже планируют или начали миграцию, но процесс небыстрый.
Рынок вакансий. Selenium всё ещё встречается в требованиях, особенно в legacy-проектах. Новички делают выбор в его пользу из-за "частотности", не зная, что это временно.
Сопротивление новому. Есть компании и специалисты, которые придерживаются "если работает — не трогай", и не спешат менять инструменты.
Вывод
Playwright — это современный, мощный и удобный инструмент, который уже сегодня вытесняет Selenium из новых проектов. Если вы только начинаете или планируете развивать тестовую инфраструктуру — смело делайте выбор в пользу Playwright. Его функциональность, простота и активное развитие говорят сами за себя.
Паттерны автоматизации UI-тестов
Чтобы автотесты были поддерживаемыми, читаемыми и масштабируемыми, важно не просто писать тест-кейсы "как получится", а использовать проверенные архитектурные подходы — паттерны автоматизации.
В этой статье мы будем использовать сразу три ключевых паттерна: PageObject, PageComponent и PageFactory. Вместе они создают иерархичную, гибкую и масштабируемую структуру для тестов.
PageObject
PageObject — это самый распространённый и базовый паттерн в UI-автоматизации. Суть его в том, чтобы представить каждую страницу (или экран, если речь о мобильном приложении) в виде класса.
Атрибуты класса — это элементы страницы (кнопки, поля ввода и т.д.).
Методы класса — действия с этими элементами (клик, ввод текста, проверка состояния и т.д.).
Таким образом, тест больше не "размазывается" по локаторам и сырым действиям, а обращается к странице как к объекту:
registration_page.fill_email("test@example.com")
registration_page.submit()
Page Object решает задачу структурирования страниц, но он не даёт универсального подхода к переиспользуемым компонентам, которые встречаются на разных страницах.
PageComponent
PageComponent — следующий уровень абстракции. Он нужен тогда, когда в интерфейсе есть повторяющиеся компоненты, которые встречаются на разных страницах. Например:
Навигационное меню (Navbar)
Боковая панель (Sidebar)
Карточка товара
Модальные окна
Повторяющиеся блоки и секции
Если такой компонент вшивать напрямую в PageObject, то возникает дублирование и костыли: один и тот же Navbar реализуется на каждой странице по-своему, хотя логика у него одна и та же.
PageComponent решает это через композицию:
Каждый компонент оформляется в виде отдельного класса.
Внутри класса описываются атомарные элементы и действия, связанные с этим компонентом.
Компоненты встраиваются внутрь PageObject'ов как атрибуты, используя композицию.
Пример:
class DashboardPage:
def __init__(self, page):
self.navbar = NavbarComponent(page)
self.sidebar = SidebarComponent(page)
Таким образом, логика компонентов не дублируется, и можно централизованно поддерживать и переиспользовать их поведение.
PageFactory
PageFactory — менее известный паттерн, особенно в Python-сообществе. Чаще он встречается в Java-проектах, но с лёгкостью адаптируется и под Python.
Если PageObject — про страницы, PageComponent — про компоненты, то PageFactory — про атомарные элементы.
Суть в том, чтобы каждый элемент на странице — будь то кнопка, инпут, иконка, текст, чекбокс — представить в виде объекта одного типа, с универсальным поведением.
class Input:
def __init__(self, locator):
self.locator = locator
def fill(self, text): ...
def clear(self): ...
def is_disabled(self): ...
Этот инпут не знает ничего о странице или компоненте, в котором он находится — он просто умеет взаимодействовать с конкретным элементом DOM.
Такой подход позволяет:
убрать дублирование поведения для однотипных элементов,
централизовать логику (например, ввод текста, очистку, валидацию),
и, главное, переиспользовать атомарные элементы в любых PageObject или PageComponent.
Почему это мощно?
Вместе три паттерна образуют многоуровневую архитектуру, применимую к любым пользовательским интерфейсам:
Уровень | Назначение |
---|---|
Page Object | Представляет собой страницу или экран |
Page Component | Отвечает за переиспользуемые блоки и виджеты |
Page Factory | Управляет базовыми элементами: инпутами, кнопками, иконками и другими атомарными элементами |
Эта структура легко масштабируется на:
веб-приложения (как в нашем случае),
мобильные приложения (через Appium, например),
десктопные интерфейсы.
Логирование
Перед тем как приступить к написанию PageObject, PageComponent и PageFactory, важно сразу внедрить единое логирование. Это поможет:
быстрее находить причину падений,
анализировать тесты на CI/CD,
понимать, какие действия происходили на каждом этапе.
Создадим логгер-билдер — функцию, которая возвращает сконфигурированный логгер для любого компонента теста: страницы, компонента или самого теста.
import logging
def get_logger(name: str) -> logging.Logger:
"""
Создаёт и возвращает логгер с заданным именем.
Логгер:
- Устанавливает уровень логирования DEBUG
- Выводит логи в консоль (stdout)
- Использует формат: "дата | имя логгера | уровень | сообщение"
:param name: Имя логгера (обычно имя модуля или класса)
:return: Конфигурированный logging.Logger
"""
logger = logging.getLogger(name) # Создаём логгер с указанным именем
logger.setLevel(logging.DEBUG) # Устанавливаем уровень логирования DEBUG
handler = logging.StreamHandler() # Создаём обработчик, который пишет в stdout
handler.setLevel(logging.DEBUG) # Тоже уровень DEBUG
formatter = logging.Formatter(
'%(asctime)s | %(name)s | %(levelname)s | %(message)s'
) # Формат логов: время | имя логгера | уровень | сообщение
handler.setFormatter(formatter) # Применяем формат к обработчику
logger.addHandler(handler) # Добавляем обработчик к логгеру
return logger # Возвращаем готовый логгер
В каждом компоненте, странице или тесте можешь создать логгер вот так:
from tools.logger import get_logger
logger = get_logger("NAME_OF_MY_LOGGER")
А дальше логировать действия:
logger.info("Открываем страницу регистрации")
logger.debug("Вводим email test@example.com")
logger.error("Не удалось найти кнопку 'Submit'")
PageFactory
Несмотря на то, что паттерн PageFactory находится на самом нижнем уровне архитектуры (после PageObject и PageComponent), реализацию стоит начинать именно с него. Причина проста — страницы и компоненты будут состоять из атомарных элементов (кнопок, текстов, полей ввода и т.д.), и наличие абстракции этих элементов необходимо уже на начальном этапе.
Реализуемые элементы:
Button
— элемент кнопкиInput
— поле вводаText
— текст на страницеImage
— изображениеLink
— ссылка
Базовый класс BaseElement
Все элементы на странице (будь то кнопка, текст или поле ввода) имеют общую базовую функциональность: получение локатора, проверка видимости, клик, проверка текста и другое. Вынесем эту логику в отдельный класс BaseElement
, от которого будут наследоваться остальные элементы.
import allure
from playwright.sync_api import Page, Locator, expect
from tools.logger import get_logger
logger = get_logger("BASE_ELEMENT")
class BaseElement:
"""
Базовый элемент страницы.
Предоставляет общие методы взаимодействия с любым UI-элементом,
включая клик, проверку видимости, проверку текста и получение локатора.
"""
def __init__(self, page: Page, locator: str, name: str) -> None:
"""
:param page: Экземпляр страницы Playwright
:param locator: Значение data-testid для поиска элемента (может содержать плейсхолдеры)
:param name: Название элемента (для логирования и аллюра)
"""
self.page = page
self.name = name
self.locator = locator
@property
def type_of(self) -> str:
"""
Возвращает тип элемента. Переопределяется в потомках.
"""
return "base element"
def get_locator(self, nth: int = 0, **kwargs) -> Locator:
"""
Формирует локатор элемента по data-testid с возможностью подстановки аргументов.
:param nth: Индекс элемента, если на странице несколько одинаковых
:param kwargs: Аргументы для форматирования локатора
:return: Локатор элемента
"""
locator = self.locator.format(**kwargs)
step = f'Getting locator with "data-testid={locator}" at index "{nth}"'
with allure.step(step):
logger.info(step)
return self.page.get_by_test_id(locator).nth(nth)
def click(self, nth: int = 0, **kwargs):
"""
Выполняет клик по элементу.
:param nth: Индекс элемента (по умолчанию 0)
:param kwargs: Аргументы для форматирования локатора
"""
step = f'Clicking {self.type_of} "{self.name}"'
with allure.step(step):
locator = self.get_locator(nth, **kwargs)
logger.info(step)
locator.click()
def check_visible(self, nth: int = 0, **kwargs):
"""
Проверяет, что элемент видим на странице.
:param nth: Индекс элемента
:param kwargs: Аргументы для форматирования локатора
"""
step = f'Checking that {self.type_of} "{self.name}" is visible'
with allure.step(step):
locator = self.get_locator(nth, **kwargs)
logger.info(step)
expect(locator).to_be_visible()
def check_have_text(self, text: str, nth: int = 0, **kwargs):
"""
Проверяет, что у элемента присутствует заданный текст.
:param text: Ожидаемый текст
:param nth: Индекс элемента
:param kwargs: Аргументы для форматирования локатора
"""
step = f'Checking that {self.type_of} "{self.name}" has text "{text}"'
with allure.step(step):
locator = self.get_locator(nth, **kwargs)
logger.info(step)
expect(locator).to_have_text(text)
Конструктор класса — принимает страницу, локатор (
data-testid
) и название элементаtype_of
— позволяет переопределить "тип" элемента для логов и аллюраget_locator
— получает Locator черезdata-testid
, поддерживает индексацию и шаблоныclick
— кликает по элементуcheck_visible
— проверяет, что элемент отображается на страницеcheck_have_text
— проверяет, что элемент содержит указанный текст
На основе BaseElement
будут построены специализированные элементы: Button
, Input
, Text
, Image
. Они добавят свою специфику (например, метод fill()
для инпута) и помогут сделать тесты максимально читаемыми.
Важность использования кастомных data-testid атрибутов для локаторов
При автоматизации тестирования важно правильно выбирать подходы к локаторам элементов на странице. Обратите внимание на то, как инициализируется локатор в примере кода: self.page.get_by_test_id(locator).nth(nth)
. Этот подход использует атрибуты data-testid
, которые помогают однозначно идентифицировать элементы на странице.
Почему лучше использовать кастомные атрибуты, такие как data-testid?
Настоятельно рекомендую использовать именно кастомные атрибуты типа data-testid
, qa-id
, data-test-id
, qa-data-id
и другие подобные. Важно понимать, что название атрибута не имеет значения, главное — это его предназначение: обеспечение уникальной и стабильной идентификации элементов на странице.
Использование кастомных атрибутов вместо универсальных подходов, таких как CSS-селекторы или XPath, имеет несколько преимуществ:
Стабильность: В отличие от XPath или CSS-селекторов, которые могут зависеть от структуры HTML или классов, атрибуты
data-testid
независимы от дизайна страницы и редизайнов. Если изменения касаются внешнего вида или структуры, тесты сdata-testid
не потребуют переписывания локаторов.Читаемость и поддерживаемость: Тесты, использующие кастомные атрибуты, легко читаемы и поддерживаемы. Вам не нужно анализировать сложные цепочки XPath или идентификаторы классов, чтобы понять, что именно вы тестируете. Это позволяет улучшить удобство работы как для тестировщиков, так и для разработчиков.
Избежание споров о локаторах: Одним из самых популярных вопросов является, что лучше использовать для поиска элементов — CSS или XPath? Ответ: ни одно, ни другое не является идеальным. Кастомные атрибуты
qa-id
илиdata-testid
— это наиболее надежное решение. Они позволяют избежать долгих споров и создают стабильную основу для локаторов.
Зачем это нужно?
Задумайтесь: что лучше — потратить неделю или две на добавление кастомных атрибутов по всему приложению, или же в дальнейшем сталкиваться с постоянными проблемами с тестами и локаторами?
Без использования кастомных атрибутов локаторы в виде длинных и сложных CSS-селекторов или XPath могут быть подвержены постоянным изменениям, особенно при редизайне страниц. Это приведет к тому, что тесты будут ломаться и требовать частых изменений, что затруднит масштабирование и поддержку тестов.
С другой стороны, потратив неделю или две на добавление атрибутов data-testid
или аналогичных по всему приложению, вы создадите надежную базу для написания стабильных тестов, которые не потребуют изменений после каждого редизайна.
Как добавить data-testid?
Для успешного использования этого подхода есть два пути:
Попросить фронтенд-разработчиков: Фронтенд-разработчики, будь то React, Vue, Angular или даже мобильные разработки, прекрасно понимают важность и простоту добавления кастомных атрибутов. Попросите их добавить уникальные атрибуты в ваш проект.
Сделать это самостоятельно: На самом деле, добавление кастомных атрибутов несложно, и вы можете научиться делать это сами. Это поможет вам получить больший контроль над тестами и улучшить их стабильность.
Почему важно?
Использование кастомных qa-id
атрибутов — это основа, на которой строятся хорошие практики автоматизации тестирования UI. К сожалению, многие команды игнорируют этот подход и пытаются обходиться без него, что приводит к созданию ненадежных и трудных в поддержке тестов. В результате такие тесты становятся нестабильными, а время на их поддержку растет.
Например, в мобильной автоматизации тестирования автотесты без использования кастомных атрибутов для идентификации элементов вообще не пишутся. Это становится обязательным стандартом, позволяющим повысить стабильность и предсказуемость тестов.
Итог
Итак, кастомные атрибуты — это не просто опция, а необходимая база для стабильных и поддерживаемых тестов. Использование data-testid
или аналогичных атрибутов позволяет вам создать четкую и надежную систему локаторов, которая избавит от необходимости переделывать тесты при каждом изменении дизайна. Потратить неделю на их внедрение — это гораздо меньший ресурс, чем потом тратить месяцы на исправление тестов, которые постоянно ломаются.
Класс Button
Класс Button
представляет собой элемент кнопки на странице и является расширением базового элемента BaseElement
. В этом классе добавляются методы, специфичные для кнопки, такие как проверка состояния кнопки (включена или выключена). В остальном, структура класса и методы аналогичны классу BaseElement
.
import allure
from playwright.sync_api import expect
from elements.base_element import BaseElement
from tools.logger import get_logger
logger = get_logger("BUTTON")
class Button(BaseElement):
"""
Класс для работы с кнопками на странице. Наследует базовые методы от BaseElement и добавляет
специфичные методы для работы с кнопками, такие как проверка состояния (включена/выключена).
"""
@property
def type_of(self) -> str:
"""
Возвращает тип элемента, в данном случае "button".
Используется для унификации логирования и обработки элементов.
"""
return "button"
def check_enabled(self, nth: int = 0, **kwargs):
"""
Проверяет, что кнопка активна (включена). Используется для тестирования сценариев,
когда кнопка должна быть доступна для клика.
:param nth: Индекс кнопки, если на странице несколько одинаковых элементов.
:param kwargs: Дополнительные аргументы для локатора.
:raises AssertionError: Если кнопка не активна.
"""
step = f'Checking that {self.type_of} "{self.name}" is enabled'
with allure.step(step):
locator = self.get_locator(nth, **kwargs)
logger.info(step)
expect(locator).to_be_disabled()
def check_disabled(self, nth: int = 0, **kwargs):
"""
Проверяет, что кнопка не активна (выключена). Используется для тестирования сценариев,
когда кнопка должна быть недоступна для клика.
:param nth: Индекс кнопки, если на странице несколько одинаковых элементов.
:param kwargs: Дополнительные аргументы для локатора.
:raises AssertionError: Если кнопка активна.
"""
step = f'Checking that {self.type_of} "{self.name}" is disabled'
with allure.step(step):
locator = self.get_locator(nth, **kwargs)
logger.info(step)
expect(locator).to_be_disabled()
Класс Button
— это расширение базового класса BaseElement
, который предназначен для работы с кнопками на веб-странице. В классе реализованы специфичные для кнопки методы, такие как проверка состояния активности кнопки. Этот подход позволяет централизованно хранить логику работы с элементами интерфейса и повторно использовать ее в разных частях тестов.
Имплементация классов для других элементов страницы, таких как поля ввода, текстовые блоки и изображения, делается аналогичным образом. Основное отличие — это добавление методов, специфичных для каждого типа элемента, которые позволяют тестировать конкретное поведение на странице (например, активность кнопки или текстовое содержимое элемента).
Input
Класс Input
расширяет базовый класс BaseElement
и используется для взаимодействия с полями ввода на странице. Этот класс включает методы, которые позволяют заполнять поля ввода и проверять их значения.
import allure
from playwright.sync_api import expect, Locator
from elements.base_element import BaseElement
from tools.logger import get_logger
logger = get_logger("INPUT")
class Input(BaseElement):
"""
Класс для работы с полями ввода на странице. Наследует базовые методы от BaseElement
и добавляет специфичные методы для работы с полями ввода, такие как заполнение значений
и проверка значений в поле.
"""
@property
def type_of(self) -> str:
"""
Возвращает тип элемента, в данном случае "input".
Это полезно для унификации работы с различными типами элементов.
"""
return "input"
def get_locator(self, nth: int = 0, **kwargs) -> Locator:
"""
Получает локатор для поля ввода с возможностью параметризации и выбором индекса.
"""
# Вызываем родительский метод для получения базового локатора и добавляем локатор для input
# Может быть необходимо если на странице нужно как-то хитро получать поле ввода
return super().get_locator(nth, **kwargs).locator('input')
def fill(self, value: str, nth: int = 0, **kwargs):
"""
Заполняет поле ввода заданным значением.
:param value: Значение, которое нужно ввести в поле.
:param nth: Индекс, если на странице несколько одинаковых элементов.
:param kwargs: Дополнительные аргументы для параметризации локатора.
:raises AssertionError: Если поле не найдено или не доступно для ввода.
"""
step = f'Fill {self.type_of} "{self.name}" to value "{value}"'
with allure.step(step):
locator = self.get_locator(nth, **kwargs)
logger.info(step)
locator.fill(value)
def check_have_value(self, value: str, nth: int = 0, **kwargs):
"""
Проверяет, что поле ввода содержит заданное значение.
:param value: Ожидаемое значение в поле ввода.
:param nth: Индекс, если на странице несколько одинаковых элементов.
:param kwargs: Дополнительные аргументы для параметризации локатора.
:raises AssertionError: Если значение в поле не соответствует ожидаемому.
"""
step = f'Checking that {self.type_of} "{self.name}" has a value "{value}"'
with allure.step(step):
locator = self.get_locator(nth, **kwargs)
logger.info(step)
expect(locator).to_have_value(value)
Text
from elements.base_element import BaseElement
class Text(BaseElement):
"""
Класс для работы с текстовыми элементами на странице.
Наследует все основные методы от BaseElement, предоставляя возможность
работать с текстом как элементом на странице.
"""
@property
def type_of(self) -> str:
"""
Возвращает тип элемента. В случае текста возвращается "text".
"""
return "text"
Image
from elements.base_element import BaseElement
class Image(BaseElement):
"""
Класс для работы с изображениями на странице.
Наследует все основные методы от BaseElement, предоставляя возможность
работать с изображениями как элементами на странице.
"""
@property
def type_of(self) -> str:
"""
Возвращает тип элемента. В случае изображения возвращается "image".
"""
return "image"
Link
from elements.base_element import BaseElement
class Link(BaseElement):
"""
Класс для работы с ссылками на странице.
Наследует все основные методы от BaseElement, предоставляя возможность
работать с ссылками как элементами на странице.
"""
@property
def type_of(self) -> str:
"""
Возвращает тип элемента. В случае ссылки возвращается "link".
"""
return "link"
Все три класса (Text
, Image
, и Link
) реализуют минимальное расширение от BaseElement
. Они все переопределяют метод type_of
, чтобы указать тип соответствующего элемента на странице. В остальном, они могут использовать общие методы из базового класса для взаимодействия с элементами на странице, такие как проверка видимости и другие базовые операции.
Это позволяет нам создавать универсальные и расширяемые элементы, которые можно использовать в тестах, не создавая для каждого нового типа элемента дополнительные повторяющиеся методы.
Преимущества использования PageFactory в автоматизации тестирования
1. Семантическое разделение
Использование PageFactory помогает перейти от работы с абстрактными локаторами к конкретным элементам на странице. Вместо того чтобы манипулировать неясными локаторами, мы работаем с элементами, которые явно представляют собой кнопки, поля ввода и другие видимые компоненты. Это делает код более читаемым и понятным.
Пример: Если вам нужно взаимодействовать с кнопкой "Submit", вы прямо указываете, что работаете именно с кнопкой, а не с абстрактным локатором. Это упрощает понимание теста, даже если его читает новый разработчик или тестировщик.
2. Инкапсуляция
PageFactory инкапсулирует логику взаимодействия с элементами. Каждый элемент имеет свои методы, такие как click
или check_visible
, которые выполняют всю необходимую логику внутри себя. Это упрощает работу, так как не нужно беспокоиться о мелких деталях, таких как ожидание видимости элемента или правильный клик. Вместо этого, каждый метод становится единым источником истины.
Пример: Чтобы проверить видимость элемента, можно использовать несколько способов:
assert locator.is_visible()
expect(locator).to_be_visible()
locator.wait_for(state='visible')
Все эти методы делают одно и то же, но верный вариант — expect(locator).to_be_visible()
, потому что он корректно ожидает появления элемента. Если элемент не появляется, будет выброшена понятная ошибка AssertionError
. Используя PageFactory, мы гарантируем, что все тесты используют одинаковый способ проверки видимости, что предотвращает возможные ошибки и несоответствия в реализации.
Без PageFactory такая ситуация может привести к хаосу в проекте: каждый тестировщик может использовать свой способ проверки видимости, что приведет к дискуссиям и увеличению времени на поддержание кода. В PageFactory логика централизована, и все тесты используют один метод, что гарантирует правильность работы и упрощает сопровождение кода.
3. Разделение интерфейсов
В PageFactory каждый элемент предоставляет строго определённый набор методов, соответствующих его типу. Например, для чекбокса будет доступен метод для изменения его состояния, а для поля ввода — метод для ввода текста. Это предотвращает ошибки, такие как попытки применить методы, которые не подходят для конкретного элемента.
Пример: Если мы работаем с полем ввода, то элемент будет содержать только методы, соответствующие полю ввода, такие как fill()
. Если попытаться вызвать метод изменения состояния чекбокса, это вызовет ошибку, что предотвращает использование некорректных действий для различных типов элементов.
4. Логирование и Allure шаги
PageFactory предоставляет встроенные возможности для добавления логирования и шагов в отчет Allure. Это позволяет нам автоматически генерировать читаемые шаги для всех действий с элементами, делая отчет более информативным.
Пример: При клике на кнопку или проверке видимости элемента, Allure шаг может выглядеть так:
Checking that button "Login" is visible
Мы можем явно указать, с каким элементом работаем, что делает отчет более понятным для автоматизаторов, тестировщиков и даже бизнес-аналистов. При добавлении нового элемента достаточно создать новый класс, переопределить свойство type_of
и новый элемент автоматически будет работать с Allure и логированием.
5. Переопределение функциональности
PageFactory дает возможность переопределить функциональность для конкретных элементов. Это открывает гибкость в случае, если стандартный способ взаимодействия с элементом не подходит. Например, если на странице есть поле ввода, требующее уникальной логики для поиска локатора, мы можем просто переопределить метод get_locator
.
Пример: Если на странице есть сложное поле ввода, которое требует печать с клавиатуры для работы с ним, мы можем создать новый элемент KeyboardInput
, переопределить метод fill()
для работы с клавиатурным вводом и использовать его в тестах.
class KeyboardInput(Input):
def fill(self, value: str, nth: int = 0, **kwargs):
# Специфичная логика для ввода с клавиатуры
pass
6. Динамическое форматирование локаторов
PageFactory поддерживает динамическое форматирование локаторов, что решает проблему работы с динамическими элементами на странице. Вместо того чтобы создавать множество разных локаторов или методы для их форматирования, мы можем использовать шаблоны и передавать параметры в метод.
Пример:
dynamic_input = Input(page, "dynamic-input-{index}", "Dynamic input")
dynamic_input.fill(index=100)
Здесь index
будет подставлен в строку локатора, создавая динамически нужный локатор, например, dynamic-input-100
.
7. Вербозность и ясность отчетов
PageFactory позволяет не только добавить шаги в Allure, но и логировать их с полным контекстом. Это означает, что отчеты и логи будут максимально понятными. Например:
Click "Submit" button
Fill input "Username" with value "test_user"
Такой подход делает отчеты и логи более информативными и полезными для всех участников процесса — как для автоматизаторов, так и для тестировщиков или бизнес-аналистов.
8. Фокус на бизнес-логику
Все преимущества, описанные выше, позволяют нам сосредоточиться на тестировании бизнес-логики, а не на решении технических вопросов, таких как динамическое форматирование локаторов или логирование. Все это вынесено на уровень PageFactory, что позволяет значительно упростить процесс написания тестов.
9. Простота
Последним, но не менее важным преимуществом является простота реализации PageFactory. Этот подход не требует сложных техник или магии, все сделано с использованием стандартных принципов ООП. Это делает подход универсальным и легко адаптируемым для любых языков программирования, будь то Python, Java или JavaScript, а также для мобильных тестов.
PageComponent
Теперь реализуем компоненты, которые понадобятся для написания сценариев. Мы будем применять паттерн PageComponent — он позволяет логически структурировать страницу, разбивая её на отдельные, переиспользуемые части (компоненты). Внутри компонентов мы будем использовать элементы, реализованные с помощью PageFactory.
Такая архитектура делает тесты более читаемыми и масштабируемыми. Нам понадобятся несколько компонентов:
Форма регистрации
Тулбар на странице панели управления
Компонент графика/чарта
Навигационная панель (Navbar)
BaseComponent
Все компоненты будут наследоваться от BaseComponent
. Это базовый класс, который задаёт интерфейс для всех дочерних компонентов и предоставляет базовые методы, общие для всех. Как и любой паттерн, PageComponent всегда начинается с реализации базового компонента.
BaseComponent сам по себе не описывает конкретный участок интерфейса, он служит основой для остальных компонентов.
from typing import Pattern
import allure
from playwright.sync_api import Page, expect
from tools.logger import get_logger
logger = get_logger("BASE_COMPONENT")
class BaseComponent:
"""
Базовый компонент страницы.
Содержит общие методы и интерфейс для всех дочерних компонентов.
"""
def __init__(self, page: Page):
"""
Конструктор базового компонента.
:param page: Экземпляр страницы Playwright.
"""
self.page = page
def check_current_url(self, expected_url: Pattern[str]):
"""
Проверяет, соответствует ли текущий URL страницы заданному шаблону.
:param expected_url: Шаблон (регулярное выражение), которому должен соответствовать URL.
"""
step = f'Checking that current url matches pattern "{expected_url.pattern}"'
# Добавляем шаг в Allure отчёт
with allure.step(step):
# Записываем шаг в лог
logger.info(step)
# Проверяем, что URL соответствует заданному шаблону
expect(self.page).to_have_url(expected_url)
BaseComponent
принимает в конструкторе объект Page от Playwright и сохраняет его какself.page
. Это позволит любому дочернему компоненту обращаться к странице напрямую.Метод
check_current_url
— удобный базовый метод, который позволяет проверять текущий URL с использованием регулярного выражения. Это может пригодиться для проверки, что мы действительно на нужной странице.Метод использует:
Allure для отчёта: добавляется шаг с описанием проверки.
Логгер для записи информации в файл или консоль.
Playwright expect() для ожидания соответствия URL.
RegistrationForm
Реализуем компонент формы регистрации — RegistrationFormComponent
. Он будет отвечать за взаимодействие с полями ввода email
, username
и password
. Этот компонент унаследован от BaseComponent
и использует элементы типа Input
, реализованные в PageFactory.
Компонент предоставляет два основных метода:
fill
— для заполнения формыcheck_visible
— для проверки, что форма отобразилась и все значения в ней соответствуют ожидаемым

/components/registration_form_component.py
import allure
from playwright.sync_api import Page
from components.base_component import BaseComponent
from elements.input import Input
class RegistrationFormComponent(BaseComponent):
"""
Компонент формы регистрации. Содержит поля: Email, Username, Password.
Предоставляет методы для заполнения и проверки отображения формы.
"""
def __init__(self, page: Page):
"""
Инициализирует элемент формы регистрации.
:param page: Экземпляр Playwright Page
"""
super().__init__(page)
# Поле ввода email
self.email_input = Input(page, "registration-form-email-input", "Email")
# Поле ввода username
self.username_input = Input(page, "registration-form-username-input", "Username")
# Поле ввода password
self.password_input = Input(page, "registration-form-password-input", "Password")
@allure.step("Fill registration form")
def fill(self, email: str, username: str, password: str):
"""
Заполняет форму регистрации заданными значениями.
:param email: Email пользователя
:param username: Имя пользователя
:param password: Пароль
"""
# Заполнение email и проверка, что значение введено корректно
self.email_input.fill(email)
self.email_input.check_have_value(email)
# Заполнение username и проверка, что значение введено корректно
self.username_input.fill(username)
self.username_input.check_have_value(username)
# Заполнение password и проверка, что значение введено корректно
self.password_input.fill(password)
self.password_input.check_have_value(password)
@allure.step("Check that registration form is visible")
def check_visible(self, email: str, username: str, password: str):
"""
Проверяет, что форма регистрации отображается корректно и содержит указанные значения.
:param email: Ожидаемое значение email
:param username: Ожидаемое значение username
:param password: Ожидаемое значение пароля
"""
# Проверка email-поля
self.email_input.check_visible()
self.email_input.check_have_value(email)
# Проверка username-поля
self.username_input.check_visible()
self.username_input.check_have_value(username)
# Проверка password-поля
self.password_input.check_visible()
self.password_input.check_have_value(password)
Важно! В методах fill
и check_visible
параметры передаются напрямую. Это нормально для маленьких форм. Однако если полей становится больше, или компоненты вложены, лучше использовать @dataclass для группировки параметров:
from dataclasses import dataclass
@dataclass
class RegistrationFormFillParams:
email: str
username: str
password: str
Тогда метод fill
будет выглядеть так:
def fill(self, params: RegistrationFormFillParams):
self.email_input.fill(params.email)
self.username_input.fill(params.username)
self.password_input.fill(params.password)
Такой подход:
упрощает передачу параметров
снижает вероятность ошибок при передаче значений
повышает читаемость тестов
В нашем примере это не критично, но полезно запомнить при работе с более сложными формами и вложенными структурами.
DashboardToolbarViewComponent
Этот компонент отвечает за проверку видимости тулбара (панели инструментов) на дашборде. Основной его задачей является проверка заголовка панели — текста Dashboard.
Он может использоваться, например, сразу после логина или при переходе на основную страницу, чтобы убедиться, что пользователь действительно оказался на дашборде.

/components/dashboard_toolbar_view_component.py
import allure
from playwright.sync_api import Page
from components.base_component import BaseComponent
from elements.text import Text
class DashboardToolbarViewComponent(BaseComponent):
"""
Компонент панели инструментов на дашборде.
Используется для проверки отображения заголовка 'Dashboard'.
"""
def __init__(self, page: Page):
"""
Инициализация компонента панели инструментов.
:param page: Экземпляр страницы Playwright
"""
super().__init__(page)
# Элемент, отображающий заголовок панели инструментов
self.title = Text(page, 'dashboard-toolbar-title-text', 'Dashboard')
@allure.step("Check visible dashboard toolbar view")
def check_visible(self):
"""
Проверяет, что панель инструментов отображается корректно:
- Заголовок панели виден
- Заголовок содержит текст 'Dashboard'
"""
# Проверка, что заголовок виден на экране
self.title.check_visible()
# Проверка, что заголовок содержит правильный текст
self.title.check_have_text('Dashboard')
Мы используем
Text
, унаследованный отBaseElement
, поэтому у нас уже встроены шаги Allure и логирование.Метод
check_visible
включает две проверки:элемент отображается на странице
текст элемента соответствует ожидаемому значению "Dashboard"
Это — хорошая базовая проверка, которая может использоваться во многих smoke-тестах или sanity-check'ах после авторизации.
ChartViewComponent
Компонент, представляющий график (чарт) на странице. Используется для проверки отображения заголовка и самого графика по заданному identifier
и chart_type
.

/components/chart_view_component.py
import allure
from playwright.sync_api import Page
from components.base_component import BaseComponent
from elements.image import Image
from elements.text import Text
class ChartViewComponent(BaseComponent):
"""
Компонент графика (чарта), который может отображаться на дашборде или в аналитике.
:param identifier: Базовый ID виджета (например, "students", "scores", "courses")
:param chart_type: Тип графика (например, "bar", "line", "pie")
"""
def __init__(self, page: Page, identifier: str, chart_type: str):
super().__init__(page)
# Элемент заголовка графика
self.title = Text(page, f'{identifier}-widget-title-text', 'Title')
# Элемент изображения самого графика
self.chart = Image(page, f'{identifier}-{chart_type}-chart', 'Chart')
@allure.step('Check visible chart view "{title}"')
def check_visible(self, title: str):
"""
Проверяет, что чарт отображается корректно:
- Заголовок виден и содержит переданный текст
- График (image) виден на странице
:param title: Ожидаемый заголовок графика
"""
self.title.check_visible()
self.title.check_have_text(title)
self.chart.check_visible()
Использование
identifier
иchart_type
позволяет переиспользовать компонент для различных графиков на странице.В методе
check_visible
мы валидируем два элемента: заголовок и сам график.Шаг allure динамически подставляет
title
, что делает отчёт понятным.
NavbarComponent
Компонент навигационной панели. Отвечает за отображение заголовка приложения и приветствия пользователя.

/components/navbar_component.py
import allure
from playwright.sync_api import Page
from components.base_component import BaseComponent
from elements.text import Text
class NavbarComponent(BaseComponent):
"""
Компонент верхнего меню (navbar), где отображаются:
- Название приложения
- Приветствие пользователя
"""
def __init__(self, page: Page):
super().__init__(page)
# Заголовок с названием приложения
self.app_title = Text(page, 'navigation-navbar-app-title-text', 'App title')
# Заголовок приветствия
self.welcome_title = Text(page, 'navigation-navbar-welcome-title-text', 'Welcome title')
@allure.step("Check visible navbar")
def check_visible(self, username: str):
"""
Проверяет, что navbar отображается корректно:
- Название приложения видно и соответствует 'UI Course'
- Приветствие содержит имя пользователя
:param username: Имя пользователя, которое ожидается в приветствии
"""
self.app_title.check_visible()
self.app_title.check_have_text('UI Course')
self.welcome_title.check_visible()
self.welcome_title.check_have_text(f'Welcome, {username}!')
Компонент можно использовать сразу после входа в систему для валидации корректного отображения интерфейса.
Приветственное сообщение проверяется динамически, что особенно полезно в тестах с разными пользователями.
Использование
Text
позволяет переиспользовать уже готовую обёртку с логами и шагами.
PageObject
В этой секции реализуем паттерн Page Object Model (POM) — один из самых распространённых подходов к структурированной автоматизации UI.
PageObject — это объект, представляющий страницу приложения или её часть. Каждый такой объект инкапсулирует поведение страницы: навигацию, действия и проверки. Это позволяет:
Повысить читаемость и переиспользуемость кода
Сократить дублирование
Сделать поддержку тестов проще
В нашем примере будет две страницы:
RegistrationPage
— страница регистрацииDashboardPage
— страница панели управления
Но перед этим начнём с базовой страницы, от которой будут наследоваться все остальные.
BasePage
Базовая страница, от которой наследуются все остальные. Содержит наиболее общие методы, такие как открытие URL, перезагрузка и проверка текущего URL.
from typing import Pattern
import allure
from playwright.sync_api import Page, expect
from tools.logger import get_logger
logger = get_logger("BASE_PAGE")
class BasePage:
"""
Базовый класс для всех PageObject-страниц.
Содержит общие действия:
- переход по URL
- перезагрузка страницы
- проверка текущего URL
"""
def __init__(self, page: Page):
"""
:param page: Экземпляр страницы Playwright
"""
self.page = page
def visit(self, url: str):
"""
Открывает страницу по заданному URL и ждёт полной загрузки.
:param url: URL страницы
"""
step = f'Opening the url "{url}"'
with allure.step(step):
logger.info(step)
# Переход на указанный URL
self.page.goto(url, wait_until='networkidle')
def reload(self):
"""
Перезагружает текущую страницу и ждёт загрузки DOM.
"""
step = f'Reloading page with url "{self.page.url}"'
with allure.step(step):
logger.info(step)
# Перезагрузка страницы
self.page.reload(wait_until='domcontentloaded')
def check_current_url(self, expected_url: Pattern[str]):
"""
Проверяет, что текущий URL соответствует ожидаемому регулярному выражению.
:param expected_url: Ожидаемый URL как регулярное выражение (Pattern)
"""
step = f'Checking that current url matches pattern "{expected_url.pattern}"'
with allure.step(step):
logger.info(step)
# Проверка соответствия текущего URL
expect(self.page).to_have_url(expected_url)
visit(url)
— переходит на указанный адрес и ждёт, пока завершатся все сетевые запросы.reload()
— перезагружает текущую страницу и ожидает загрузки DOM.check_current_url()
— использует регулярное выражение (Pattern[str]), что позволяет гибко проверять URL (например, с параметрами, токенами и т.п.).Все действия обёрнуты в шаги allure, что делает отчёты читаемыми и информативными.
Логирование с использованием кастомного логгера
BASE_PAGE
поможет быстро локализовать проблему в логах.
RegistrationPage
Это PageObject для страницы регистрации. Он инкапсулирует все взаимодействия с элементами на странице регистрации, такие как форму регистрации и кнопки.

import re
from playwright.sync_api import Page
from components.registration_form_component import RegistrationFormComponent
from elements.button import Button
from elements.link import Link
from pages.base_page import BasePage
class RegistrationPage(BasePage):
"""
Страница регистрации (Registration Page).
Включает элементы:
- Форма регистрации
- Кнопка для перехода на страницу входа
- Кнопка для отправки формы регистрации
Наследуется от BasePage.
"""
def __init__(self, page: Page):
"""
Инициализация страницы регистрации.
:param page: Экземпляр страницы Playwright
"""
super().__init__(page)
# Компоненты страницы
self.registration_form = RegistrationFormComponent(page) # Форма регистрации
# Элементы страницы
self.login_link = Link(page, "registration-page-login-link", "Login") # Ссылка на страницу входа
self.registration_button = Button(page, "registration-page-registration-button", "Registration") # Кнопка отправки формы
def click_registration_button(self):
"""
Клик на кнопку "Зарегистрироваться" и проверка перехода на страницу панели управления.
:raises AssertionError: Если URL не соответствует ожидаемому
"""
# Нажатие на кнопку регистрации
self.registration_button.click()
# Проверка, что мы перенаправлены на страницу панели управления
self.check_current_url(re.compile(".*/#/dashboard"))
Конструктор
__init__(self, page: Page):
Инициализирует компоненты и элементы страницы, используя элементы на странице, такие как форма регистрации, кнопки и ссылки.
Наследуется от базового класса
BasePage
, предоставляя функционал для перехода по страницам и проверки URL.
Метод
click_registration_button(self):
Кликает по кнопке регистрации.
После клика выполняется проверка, что URL страницы соответствует ожидаемому, что означает успешный переход на страницу панели управления.
DashboardPage
Это PageObject для страницы панели управления. Он инкапсулирует взаимодействие с различными компонентами на странице, такими как панели навигации, различные графики и панель инструментов.
from playwright.sync_api import Page
from components.chart_view_component import ChartViewComponent
from components.dashboard_toolbar_view_component import DashboardToolbarViewComponent
from components.navbar_component import NavbarComponent
from pages.base_page import BasePage
class DashboardPage(BasePage):
"""
Страница панели управления (Dashboard Page).
Включает компоненты:
- Панель навигации (Navbar)
- Графики по оценкам, курсам, студентам и активностям
- Панель инструментов для управления
Наследуется от BasePage.
"""
def __init__(self, page: Page):
"""
Инициализация страницы панели управления.
:param page: Экземпляр страницы Playwright
"""
super().__init__(page)
# Компоненты страницы
self.navbar = NavbarComponent(page) # Панель навигации
self.scores_chart_view = ChartViewComponent(page, "scores", "scatter") # График по оценкам
self.courses_chart_view = ChartViewComponent(page, "courses", "pie") # График по курсам
self.students_chart_view = ChartViewComponent(page, "students", "bar") # График по студентам
self.activities_chart_view = ChartViewComponent(page, "activities", "line") # График по активностям
self.dashboard_toolbar_view = DashboardToolbarViewComponent(page) # Панель инструментов
def check_visible_students_chart(self):
"""
Проверка видимости графика по студентам.
:raises AssertionError: Если график не виден или не имеет правильного текста
"""
self.students_chart_view.check_visible('Students')
def check_visible_courses_chart(self):
"""
Проверка видимости графика по курсам.
:raises AssertionError: Если график не виден или не имеет правильного текста
"""
self.courses_chart_view.check_visible('Courses')
def check_visible_activities_chart(self):
"""
Проверка видимости графика по активностям.
:raises AssertionError: Если график не виден или не имеет правильного текста
"""
self.activities_chart_view.check_visible('Activities')
def check_visible_scores_chart(self):
"""
Проверка видимости графика по оценкам.
:raises AssertionError: Если график не виден или не имеет правильного текста
"""
self.scores_chart_view.check_visible('Scores')
Конструктор
__init__(self, page: Page):
Этот конструктор инициализирует компоненты, которые будут присутствовать на странице панели управления:
NavbarComponent
— панель навигации.ChartViewComponent
— различные компоненты графиков для разных категорий (оценки, курсы, студенты, активности).DashboardToolbarViewComponent
— панель инструментов для управления на странице.
Методы
check_visible_*_chart(self):
Каждый из этих методов проверяет, видим ли определённый график на странице.
Внутри каждого метода вызывается метод
check_visible
для соответствующего графика (например, для студентов —self.students_chart_view.check_visible('Students')
).Если график не виден или текст не совпадает, будет выброшено исключение
AssertionError
.
Плюсы и минусы PageObject
В практике часто можно встретить мнение, что PageObject — это лучший паттерн для UI автотестов, и что он буквально спасает от всех проблем. Возможно, в отдельных случаях это действительно так, но, как и любой другой паттерн, PageObject имеет свои недостатки. Давайте кратко рассмотрим их:
Минусы PageObject
1. Сложность в реализации
PageObject добавляет дополнительный оверхед, и иногда это делает написание тестов более сложным, особенно для новичков. Прежде чем писать тест, необходимо реализовать сам PageObject. Для простых страниц это не представляет особых трудностей, но если страница становится большой и сложной, то может возникнуть множество проблем с реализацией. В таких случаях могут появиться неаккуратные решения, что только усложняет написание тестов.
Итог: PageObject не плох, но для эффективной работы с ним требуется опыт, и это не всегда легко.
2. PageObject сам по себе не полноценен
Если мы рассматриваем простые страницы, такие как LoginPage
или RegistrationPage
, то PageObject может быть вполне достаточен. Но когда приложение становится более сложным, одного PageObject уже недостаточно. Он не решает многие вопросы:
Как работать с повторяющимися компонентами, например, Navbar или Sidebar?
Как правильно интегрировать Allure шаги в отчеты?
Как реализовать логирование?
Как динамически форматировать локаторы?
Для таких задач приходится использовать другие паттерны, такие как PageComponent или PageFactory.
3. Проблемы при рефакторинге
PageObject не всегда удобен при рефакторинге и редизайне страниц. Если завтра разработчики переделают значительную часть приложения или изменят дизайн, то PageObject может оказаться неэффективным. В этом случае придется переработать все страницы. Этот момент также следует учитывать при проектировании UI тестов.
Плюсы PageObject
1. Упрощение тестов
PageObject помогает сделать тесты короче, читабельнее и удобнее для написания. С помощью PageObject можно инкапсулировать логику взаимодействия с компонентами страницы, что уменьшает дублирование кода и улучшает структуру тестов.
2. Переиспользуемость
PageObject позволяет переиспользовать готовые страницы в различных тестах. Это особенно полезно при работе с крупными приложениями, где одни и те же страницы могут быть задействованы в нескольких тестах, что значительно упрощает поддержку тестов.
Заключение
PageObject — это отличный паттерн для организации UI автотестов, но его нужно использовать в "умелых руках" и в комбинации с другими паттернами. Он имеет как плюсы, так и минусы, и важно понимать обе стороны, чтобы эффективно использовать его в проекте.
Настройки UI автотестов
Один из лучших подходов к управлению конфигурацией автотестов — централизованное хранение всех настроек. Это позволяет:
легко переопределять параметры через .env
использовать удобные типы данных (HttpUrl, DirectoryPath)
не менять код при изменении значений.
В этом примере мы реализуем простую, но гибкую систему управления конфигурацией с помощью pydantic-settings — это надстройка над Pydantic, специально разработанная для работы с переменными окружения
Вот ключевые параметры, которые мы вынесем в конфигурацию:
app_url
— URL тестируемого приложенияheadless
— запуск браузера в headless-режиме (если true, браузер запускается "в фоне", без UI — удобно для CI)videos_dir
— путь, куда сохраняются видео при выполнении тестов (если включена запись)tracing_dir
— путь для сохранения результатов Playwright Trace Viewer (подробный trace каждого теста)expect_timeout
— таймаут в миллисекундах для всех expect() ожиданий
from typing import Self
from pydantic import HttpUrl, DirectoryPath
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
"""
Конфигурация UI автотестов. Все значения загружаются из .env файла или переменных окружения.
:param app_url: URL тестируемого приложения.
:param headless: Флаг запуска браузера в headless-режиме.
:param videos_dir: Путь к директории для сохранения видео.
:param tracing_dir: Путь к директории для сохранения Playwright trace-файлов.
:param expect_timeout: Таймаут ожиданий Playwright в миллисекундах.
"""
model_config = SettingsConfigDict(
env_file='.env', # Загрузка переменных из файла .env
env_file_encoding='utf-8',
env_nested_delimiter='.' # Позволяет использовать вложенные переменные, если потребуется
)
app_url: HttpUrl
headless: bool
videos_dir: DirectoryPath
tracing_dir: DirectoryPath
expect_timeout: float
@classmethod
def initialize(cls) -> Self:
"""
Инициализирует экземпляр Settings. Если директории для видео и трейсинга не существуют — создаёт их.
:return: Инициализированный объект Settings
:raises ValueError: если директории не могут быть созданы или невалидны
"""
videos_dir = DirectoryPath("./videos")
tracing_dir = DirectoryPath("./tracing")
videos_dir.mkdir(exist_ok=True)
tracing_dir.mkdir(exist_ok=True)
return Settings(videos_dir=videos_dir, tracing_dir=tracing_dir)
Важно: зачем нужен метод initialize и что он делает
Метод initialize()
— это классовый метод в классе Settings
, который выполняет сразу несколько важных задач:
1. Инициализация экземпляра Settings
Он создает и возвращает полностью готовый объект настроек, в котором все значения будут загружены из:
.env
файла (если он есть);переменных окружения (если переменных в
.env
не хватает).
2. Создание директорий videos
и tracing
(если они не существуют)
Перед созданием экземпляра Settings
, метод вручную создаёт две директории:
videos_dir = DirectoryPath("./videos")
tracing_dir = DirectoryPath("./tracing")
videos_dir.mkdir(exist_ok=True)
tracing_dir.mkdir(exist_ok=True)
videos_dir
— используется для сохранения видео-записей выполнения тестов.tracing_dir
— используется для сохранения трейсов (отладочных логов) от Playwright, которые можно открыть через Trace Viewer.
Это особенно важно при первом запуске проекта, когда этих директорий ещё нет в файловой системе.
.env файл
Создадим .env файл в корне проекта со следующим содержимым:
APP_URL="https://nikita-filonov.github.io/qa-automation-engineer-ui-course"
HEADLESS=true
EXPECT_TIMEOUT=15000
Теперь, чтобы использовать настройки, достаточно вызвать метод initialize()
:
settings = Settings.initialize()
print(settings.app_url)
print(settings.headless)
print(settings.videos_dir)
print(settings.tracing_dir)
print(settings.expect_timeout)
Важно! Глобальную переменную settings
добавлять не будем — вместо этого будем инициализировать settings
на уровне фикстур.
Фикстуры
В UI-тестировании крайне важно, чтобы каждый тест выполнялся в изолированном окружении, с чистым состоянием браузера, и при этом имел доступ к необходимым объектам страниц и настройкам.
Для этого мы реализуем следующие фикстуры:
chromium_page
– создаёт новую страницу браузера Chromium перед каждым тестом, с видео и трейсингом.registration_page
– возвращает готовую страницу регистрации.dashboard_page
- возвращает готовую страницу дашбордаsettings
– инициализирует настройки один раз на всю тестовую сессию.
Почему Pytest плагины, а не conftest.py?
Для объявления и управления фикстурами будем использовать Pytest плагины. Плагины удобнее и гибче, чем объявления фикстур в conftest.py, потому что:
Нет необходимости заботиться о расположении conftest.py.
Фикстуры из плагинов доступны глобально во всём проекте, вне зависимости от структуры тестов.
Использование conftest.py оправдано только для специфических групп тестов. В противном случае файлы conftest.py разрастаются до 1000+ строк, что затрудняет поддержку.
import pytest
from config import Settings
@pytest.fixture(scope="session")
def settings() -> Settings:
"""
Фикстура создаёт объект с настройками один раз на всю тестовую сессию.
:return: Экземпляр класса Settings с загруженными конфигурациями.
"""
return Settings.initialize()
import uuid
import allure
import pytest
from playwright.sync_api import Playwright, Page, expect
from config import Settings
from pages.dashboard_page import DashboardPage
from pages.registration_page import RegistrationPage
@pytest.fixture
def chromium_page(playwright: Playwright, settings: Settings) -> Page:
"""
Фикстура для запуска браузера Chromium и создания новой страницы.
- Использует настройки из фикстуры `settings`.
- Устанавливает глобальный таймаут ожиданий.
- Включает запись видео и трейсинга (screenshots, snapshots, source).
- После завершения теста:
- сохраняет трейс в `settings.tracing_dir`;
- прикрепляет видео и трейс к Allure-отчету;
- закрывает браузер.
:param playwright: Объект Playwright, предоставляемый pytest-playwright.
:param settings: Настройки проекта.
:yield: Новый объект `Page` для каждого теста.
"""
expect.set_options(timeout=settings.expect_timeout)
browser = playwright.chromium.launch(headless=settings.headless)
context = browser.new_context(
base_url=f"{settings.app_url}/",
record_video_dir=settings.videos_dir
)
context.tracing.start(screenshots=True, snapshots=True, sources=True)
page = context.new_page()
yield page
tracing_file = settings.tracing_dir.joinpath(f'{uuid.uuid4()}.zip')
context.tracing.stop(path=tracing_file)
browser.close()
allure.attach.file(tracing_file, name='trace', extension='zip')
allure.attach.file(page.video.path(), name='video', attachment_type=allure.attachment_type.WEBM)
@pytest.fixture
def dashboard_page(chromium_page: Page) -> DashboardPage:
"""
Фикстура для инициализации страницы дашборда.
:param chromium_page: Страница браузера Chromium, созданная через фикстуру.
:return: Объект `DashboardPage` для использования в тестах.
"""
return DashboardPage(page=chromium_page)
@pytest.fixture
def registration_page(chromium_page: Page) -> RegistrationPage:
"""
Фикстура для инициализации страницы регистрации.
:param chromium_page: Страница браузера Chromium, созданная через фикстуру.
:return: Объект `RegistrationPage` для использования в тестах.
"""
return RegistrationPage(page=chromium_page)
Чтобы фикстуры автоматически подключались во всех тестах проекта, добавим следующую строчку в корневой файл conftest.py:
pytest_plugins = (
"fixtures.pages",
"fixtures.settings"
)
Теперь фикстуры settings
, chromium_page
, registration_page
, dashboard_page
будут доступны глобально.
App routes
Для удобного и безопасного управления URL-роутами внутри приложения — вынесем все маршруты в отдельный Enum. Это позволит:
избежать дублирования строк с путями;
минимизировать опечатки;
централизованно изменять маршруты при необходимости;
использовать автодополнение в IDE и повысить читаемость кода.
from enum import Enum
class AppRoute(str, Enum):
"""
Enum со всеми основными маршрутами приложения.
Наследуется от str, чтобы значения можно было использовать как обычные строки.
Примеры:
- переходы в тестах;
- проверка текущего URL;
- редиректы.
"""
LOGIN = "./#/auth/login" # Страница входа
REGISTRATION = "./#/auth/registration" # Страница регистрации
DASHBOARD = "./#/dashboard" # Главная страница (дашборд)
COURSES = "./#/courses" # Список курсов
COURSES_CREATE = "./#/courses/create" # Создание нового курса
UI тесты
Теперь создадим UI автотест, который проверяет успешную регистрацию пользователя. Тест будет использовать:
PageObject, PageComponent, PageFactory — для взаимодействия со страницами и компонентами;
Фикстуры — для инициализации браузера, страниц и глобальных настроек (через pytest).
Сценарий теста
Открыть страницу регистрации: #/auth/registration
Заполнить данные нового пользователя:
Email
,Username
,Password
Нажать кнопку Registration
Проверить, что открылся Dashboard: #/dashboard
Убедиться, что на Dashboard отображаются все нужные элементы:
Navbar
,Toolbar
, Графики:Scores
,Courses
,Students
,Activities
import allure
import pytest
from pages.dashboard_page import DashboardPage
from pages.registration_page import RegistrationPage
from tools.routes import AppRoute
@pytest.mark.regression
@pytest.mark.registration
class TestRegistration:
@allure.title("Successful registration")
def test_successful_registration(
self,
dashboard_page: DashboardPage,
registration_page: RegistrationPage
):
# 1. Переход на страницу регистрации
registration_page.visit(AppRoute.REGISTRATION)
# 2. Проверка, что форма регистрации отображается
registration_page.registration_form.check_visible(email="", username="", password="")
# 3. Заполнение формы регистрации
registration_page.registration_form.fill(
email="user@example.com",
username="Playwright",
password="qwerty"
)
# 4. Клик по кнопке регистрации
registration_page.click_registration_button()
# 5. Проверка отображения элементов на Dashboard
dashboard_page.navbar.check_visible("Playwright") # Navbar с именем пользователя
dashboard_page.dashboard_toolbar_view.check_visible() # Toolbar на дашборде
dashboard_page.check_visible_scores_chart() # График оценок
dashboard_page.check_visible_courses_chart() # График курсов
dashboard_page.check_visible_students_chart() # График студентов
dashboard_page.check_visible_activities_chart() # График активности
@pytest.mark.regression
и@pytest.mark.registration
— метки для группировки и фильтрации тестов.@allure.title
— заголовок, который будет отображён в отчёте Allure.Используемые фикстуры
dashboard_page
иregistration_page
автоматически создаются через pytest, благодаря pytest-плагинам.
Важно!
Совокупность описанных выше подходов позволяет создавать максимально чистые и понятные UI тесты — без визуального "шума" в виде ручных Allure шагов, логирования, лишней инициализации и прочих технических деталей.
Весь вспомогательный код и логика вынесены на уровень PageObject, PageComponent и PageFactory, что позволяет:
Скрыть всю внутреннюю кухню взаимодействия с UI;
Сфокусироваться только на бизнес-логике;
Сделать тесты читаемыми и поддерживаемыми.
Фокус на главном — бизнес-логика
Когда мы читаем тест, мы должны сразу понимать:
“Что именно проверяется и как это влияет на бизнес”.
Если внутри теста появляются:
Явные шаги Allure (
allure.step(...)
);Логирование (
print
,logger.info
,loguru
);Инициализация браузера, страниц, окружения;
Повторы или технические детали...
…тест быстро теряет читаемость и становится тяжело поддерживаемым. В таких условиях начинаются "костыли", появляются дубли, тесты ломаются от малейших изменений, а рефакторинг превращается в боль.
Хорошо читаемый тест — это когда:
Видно, что тестируется;
Понятно, с какими компонентами мы работаем;
Легко добавить, изменить или удалить шаг.
Масштаб важен
Если у вас в проекте 5 тестов — можно писать их хоть в одном файле, без фикстур, паттернов и структур. Но такие проекты — редкость. На практике:
UI-тестов обычно сотни;
API-тестов — тысячи.
В таких условиях архитектура, структура и паттерны критически важны.
Удобная отладка через логирование
Благодаря встроенному логированию, при запуске можно сразу понять, что делает тест в каждый момент времени. Пример логов:
2025-04-03 23:35:25,613 | INPUT | INFO | Checking that input "Password" has a value "qwerty"
2025-04-03 23:35:25,619 | BASE_ELEMENT | INFO | Getting locator with "data-testid=registration-page-registration-button" at index "0"
2025-04-03 23:35:25,619 | BASE_ELEMENT | INFO | Clicking button "Registration"
2025-04-03 23:35:25,706 | BASE_PAGE | INFO | Checking that current url matches pattern ".*/#/dashboard"
2025-04-03 23:35:25,790 | BASE_ELEMENT | INFO | Getting locator with "data-testid=navigation-navbar-app-title-text" at index "0"
Такой вывод:
Показывает где именно произошёл сбой;
Помогает быстро анализировать логику работы компонентов;
Используется даже при CI-прогоне, когда нет доступа к браузеру.
Регистрация pytest-маркировок
Добавим pytest-маркировки в pytest.ini, чтобы избежать предупреждений при запуске.
[pytest]
addopts = -s -v
python_files = *_tests.py test_*.py
python_classes = Test*
python_functions = test_*
markers =
regression: Маркировка для регрессионных тестов.
registration: Маркировка для тестов, связанных с регистрацией пользователей.
Запуск на CI/CD
Настроим workflow-файл для автоматического запуска UI-тестов в GitHub Actions, генерации Allure-отчета с сохранением истории и публикации его на GitHub Pages.
name: UI tests # Название workflow, отображается в интерфейсе GitHub Actions
on:
push:
branches:
- main # Запускаем workflow при пуше в ветку main
pull_request:
branches:
- main # И при открытии pull request'а в main
jobs:
run-tests: # Джоб для запуска тестов
runs-on: ubuntu-latest # Используем последнюю версию Ubuntu как CI-окружение
steps:
- name: Check out repository
uses: actions/checkout@v4 # Клонируем репозиторий в CI-окружение
- name: Set up Python
uses: actions/setup-python@v5 # Устанавливаем Python
with:
python-version: '3.12' # Указываем версию Python
- name: Install dependencies
run: |
python -m pip install --upgrade pip # Обновляем pip
pip install -r requirements.txt # Устанавливаем зависимости проекта
playwright install --with-deps # Устанавливаем Playwright и его зависимости
- name: Run UI tests with pytest and generate Allure results
run: |
pytest -m regression --alluredir=allure-results --numprocesses 2
# Запускаем тесты с меткой "regression"
# --alluredir=allure-results сохраняет результаты в папку allure-results
# --numprocesses 2 - выполняем тесты в 2 потока (ускоряет выполнение)
- name: Upload Allure results
if: always() # Всегда выполняем (даже если предыдущий шаг упал)
uses: actions/upload-artifact@v4 # Загружаем артефакт в CI
with:
name: allure-results # Имя артефакта
path: allure-results # Путь к директории с результатами Allure
publish-report: # Джоб для публикации отчёта
needs: [ run-tests ] # Выполняется после успешного (или завершённого) run-tests
runs-on: ubuntu-latest # Тоже запускается в Ubuntu
steps:
- name: Check out repository
uses: actions/checkout@v4 # Клонируем репозиторий
with:
ref: gh-pages # Клонируем ветку gh-pages
path: gh-pages # Указываем путь, куда клонировать
- name: Download Allure results
uses: actions/download-artifact@v4 # Загружаем ранее загруженные результаты
with:
name: allure-results # Имя артефакта
path: allure-results # Путь куда распаковать
- name: Allure Report action from marketplace
uses: simple-elf/allure-report-action@v1.12 # Генерируем Allure-отчёт
if: always() # Выполняем даже при ошибке
with:
allure_results: allure-results # Путь к результатам
allure_history: allure-history # Папка с историей отчётов (для графиков и сравнения)
- name: Deploy report to Github Pages
if: always() # Выполняем даже если предыдущие шаги упали
uses: peaceiris/actions-gh-pages@v4 # Деплой на GitHub Pages
with:
github_token: ${{ secrets.GITHUB_TOKEN }} # Используем встроенный GitHub токен
publish_branch: gh-pages # Ветка для публикации отчёта
publish_dir: allure-history # Папка с готовым HTML отчётом
Ссылки на документацию для всех использованных actions можно найти ниже:
Разрешения для Workflow
Если сейчас запустить тесты на GitHub Actions то, будет ошибка, говорящая о том, что у github token из workflow по умолчанию нет прав на записть в репзоиторий

Для исправления этой ошибки необходимо вручную изменить настройки прав workflow:
Откройте вкладку Settings в репозитории GitHub.
Перейдите в раздел Actions → General.
Прокрутите страницу вниз до блока Workflow permissions.
Выберите опцию Read and write permissions.
Нажмите кнопку Save для сохранения изменений.
После выполнения этих шагов можно отправить код с UI-тестами в удалённый репозиторий.
Запуск тестов и генерация Allure-отчёта
После коммита изменений во вкладке Actions появится новый workflow, в котором автоматически запустятся тесты.

Если тесты пройдут успешно, Allure-отчёт будет сгенерирован и загружен в ветку gh-pages, после чего автоматически запустится workflow pages build and deployment. Этот процесс публикует Allure-отчёт на GitHub Pages, делая его доступным для просмотра в браузере.
Важно! Перед запуском workflow необходимо убедиться, что в репозитории существует ветка gh-pages. Если ветка отсутствует, её необходимо создать в удалённом репозитории, иначе публикация Allure-отчёта на GitHub Pages не будет работать.
Проверка настроек GitHub Pages
Если workflow pages build and deployment не запустился, необходимо проверить настройки GitHub Pages:
Откройте вкладку Settings в репозитории.
Перейдите в раздел Pages → Build and deployment.
Убедитесь, что параметры соответствуют настройкам на скриншоте ниже.
На этой же странице будет отображаться виджет со ссылкой на опубликованный Allure-отчёт.

Доступ к Allure-отчётам
Каждый отчёт публикуется на GitHub Pages с уникальным идентификатором workflow, в котором он был сгенерирован.
Все сгенерированные Allure-отчёты также можно найти в ветке gh-pages.
Перейдя по ссылке на GitHub Pages, можно открыть сгенерированный Allure-отчёт с историей результатов тестирования.

По итогу, после корректной настройки, при каждом новом запуске тестов Allure-отчёт будет автоматически обновляться и сохранять историю предыдущих прогонов.

Благодаря подходу с PageFactory и чётко реализованной архитектуре PageObject/PageComponent, мы получаем максимально детализированные шаги в Allure-отчёте без необходимости вручную прописывать @allure.step
.
Каждое взаимодействие с компонентом автоматически логируется и отображается в виде шагов. Это делает отчёты понятными на всех уровнях детализации:
На верхнем уровне — читаемое бизнес-описание шагов, соответствующее сценарию теста.
При раскрытии — подробности: какой компонент использовался, какой локатор применялся и с каким индексом он искался.

В дополнение к шагам, в Allure-отчёт автоматически добавляются вложения:
Видео выполнения теста, которое можно просмотреть прямо в отчёте. Это особенно полезно для анализа визуальных и анимационных багов.
Архив trace (
.zip
), который можно скачать и загрузить в Playwright Trace Viewer для максимально детального анализа теста.

Trace Viewer позволяет интерактивно исследовать всё, что происходило в браузере: DOM-снимки, сетевые запросы, переходы по страницам, скриншоты, события и другое.

Заключение
Все ссылки на код, отчеты и запуски тестов в CI/CD можно найти на моем GitHub: