Martin Fowler: термин «интеграционный тест» стал крайне расплывчатым и используется для обозначения совершенно разных подходов
Предлагается выделять два типа интеграционных тестов:
Узкие интеграционные тесты (Narrow Integration Tests)
Тестируют взаимодействие кода сервиса с внешним компонентом (база данных или другой сервис), при этом сам внешний компонент заменяется тестовым двойником (mock или stub).
Широкие интеграционные тесты (Broad Integration Tests)
Проверяют взаимодействие между компонентами, используя их реальные экземпляры. Требуют развертывания сложного тестового окружения
Цель сквозных (End-to-End, E2E) тестов: валидировать полный сценарий (user flow) через всю систему, как если бы это делал реальный человек
Можно сказать, что идет валидация пользовательских прецедентов (use cases)
Пример:
E2E-тест для интернет-магазина может включать в себя:
Проходит через все слои приложения (UI, бэкенд, БД, платежные шлюзы и сервисы уведомлений)

Наибольший интерес представляют UI-тесты, поскольку, как правило, пользовательи взаимодействуют с системой через пользовательский интерфейс.
Обобщим:
«Плавающий» тест (flaky test) — тест, который при многократном запуске на одном и том же коде и в одном и том же окружении показывает разные результаты (то проходит успешно, то падает без видимых причин).
Когда команда видит, что сборка в CI/CD постоянно «красная» из-за плавающих тестов, она перестает доверять результатам (эрозия доверия). Разработчики начинают игнорировать упавшие тесты, предполагая, что это очередное ложное срабатывание, и могут пропустить реальный баг (регрессию).
Каждый упавший плавающий тест требует времени на анализ. Команде нужно выяснить, была ли это реальная проблема или очередной сбой автоматизации.
Влияние небольшого процента нестабильности может быть катастрофическим для всего набора тестов
Пусть есть набор из 300 UI-тестов, и каждый из них имеет надежность 99.5% (вероятность ложного падения 0.5%). Тогда вероятность того, что весь набор тестов пройдет успешно: (около 22.2%). В 77.8% случаев сборка будет падать из-за ложных срабатываний.
Причины нестабильности:
Асинхронные операции и проблемы синхронизации
Главная причина нестабильности в современных веб-приложениях, построенных на фреймворках (React, Angular или Vue). Пользовательский интерфейс динамичен. После загрузки страницы в фоне запускается множество процессов (API-запросы, рендеринг компонентов, анимации).
Скрипт, работающий по принципу «найти элемент -> выполнить действие», часто пытается взаимодействовать с элементом до того, как тот появился в DOM, стал видимым или кликабельным
Хрупкие стратегии локаторов
Локатор — нструкция, по которой инструмент автоматизации находит элемент на странице (ID, CSS-класс, XPath). Если локатор привязан к деталям реализации, а не к семантическому значению элемента, он становится хрупким.
Использование автоматически сгенерированных CSS-классов или абсолютных XPath-путей приведет к тому, что тест сломается при малейшем изменении верстки.
Управление тестовыми данными и состоянием
Идеальный тест должен быть независимым (результат не должен зависеть от других тестов). На практике тесты часто используют общие данные или состояние (одна и та же запись о пользователе в БД). Если один тест изменяет эти данные (удаляет пользователя), а другой тест рассчитывает на их наличие, второй тест упадет. Актуально при параллельном запуске тестов.
Нестабильность окружения
Тесты могут успешно проходить на локальной машине разработчика, но падать в CI-окружении. Причины: разница в версиях браузеров, операционных систем, медленная сеть, недоступность тестовых сервисов или внешних API.
WebDriver (2006) — стандарт W3C, реализованный как REST API протокол. Особенность: наличие браузерного драйвера (например, chromedriver, geckodriver) в виде промежуточного звена между скриптом и браузером. Этот драйвер работает как HTTP-сервер, который принимает команды от тестового скрипта, транслирует их в специфичный для браузера протокол и выполняет в браузере.
Схема: тестовый скрипт -> HTTP-запрос -> браузерный драйвер → внутренний протокол браузера → браузер.

Дополнительный шаг через HTTP вносит задержку (latency)
Преимущества: стандартизация и кросс-браузерная поддержка. При этом возможности ограничены базовыми пользовательскими взаимодействиями (клик, ввод текста), протокол не предоставляет доступа к внутренним механизмам браузера (перехват сетевых запросов, подписка на события консоли).
Другой подход: Chrome DevTools Protocol (CDP). Это протокол на основе JSON-RPC, работающий поверх WebSockets. Прямое соединение тестового скрипта с портом отладки браузера.

CDP не является универсальным стандартом, хотя другие браузеры развивают схожие протоколы
При этом, команда Playwright создает патчи для основных браузерных движков (Chromium, Firefox, WebKit), чтобы предоставить унифицированный API.
Playwright продвигает философию приоритезации атрибутов, видимых пользователю.
В основе лежит использование ARIA (Accessible Rich Internet Applications)-ролей. Локатор высшего приоритета: getByRole(). ARIA — набор атрибутов, которые придают семантическое значение элементам для вспомогательных технологий, таких как скринридеры.
Раньше было просто:
<button>, <a>, <input>, семантика ясна. Сегодня веб-приложения включают сложные компоненты, созданные из<div> и <span>: кастомные выпадающие списки, слайдеры, табы, модальные окна.
Для обычного пользователя стилизованный под кнопку блок выглядит как кнопка. Для скринридера — это текстовый блок без какой-либо функции. Пользователь с нарушениями зрения не поймет, что на этот элемент можно нажать.
ARIA решает эту проблему, добавляя элементам роли и атрибуты:
Пользователи не ищут элементы по ID или CSS-классу, они ищут "кнопку с текстом "Сохранить", "ссылку на главную" или "поле для ввода имени"
А еще getByRole() — инструмент, который подталкивает разработчиков к созданию более доступных продуктов (Accessibility, a11y).
Функциональные тесты могут успешно проходить, даже если пользовательский интерфейс визуально сломан (элементы накладываются друг на друга, цвета некорректны, "верстка поехала"). Для решения проблемы применяется визуально-регрессионное тестирование (VRT).
Есть и другие локаторы:
| Приоритет | Тип локатора | Обоснование | Пример |
|---|---|---|---|
| 1 | getByRole |
Наиболее устойчив. Отражает семантическую роль элемента (button, checkbox) для пользователя |
page.getByRole('button', { name: 'Войти' }) |
| 2 | getByLabel |
Подходит для элементов форм. Находит элемент ввода по тексту связанного с ним <label> |
page.getByLabel('Имя пользователя') |
| 3 | getByPlaceholder |
Подходит для полей ввода, у которых нет явного <label>, но есть текст-подсказка |
page.getByPlaceholder('Введите ваш email') |
| 4 | getByText |
Находит элемент по его текстовому содержимому. Удобен для заголовков, параграфов и других неинтерактивных элементов | page.getByText('Добро пожаловать!') |
| 5 | getByAltText |
Находит изображения по их альтернативному тексту (alt). Важно для доступности |
page.getByAltText('Логотип') |
| 6 | getByTitle |
Находит элементы по их атрибуту title |
page.getByTitle('Закрыть всплывающее окно') |
| 7 | getByTestId |
Используется, когда другие локаторы недоступны (data-testid — специальный атрибут для тестов) |
page.getByTestId('main-logo') |
| 8 | CSS / XPath | Крайняя мера | page.locator('.avatar') |
Принцип VRT:
Инструменты вроде Applitools интегрируются с Playwright и автоматизируют этот процесс в рамках CI.
Качественный тест не просто подтверждает, что кнопка работает. Он подтверждает, что она воспринимаема всеми пользователями (через ARIA-локаторы) и выглядит правильно (через VRT)
Page Object Model (POM) — паттерн проектирования, в котором каждая веб-страница приложения представляется в виде класса. Класс инкапсулирует локаторы элементов этой страницы и методы для взаимодействия с ними. Это позволяет отделить логику тестов от логики взаимодействия.
Пример организации файлов:
playwright-pom-lecture/
├── pages/
│ ├── __init__.py
│ └── login_page.py
├── tests/
│ ├── __init__.py
│ └── test_login.py
Класс LoginPage:
from playwright.sync_api import Page, expect
class LoginPage:
def __init__(self, page: Page):
self.page = page
# локаторы элементов на странице
self.username_input = page.get_by_label("Username")
self.password_input = page.get_by_label("Password")
self.login_button = page.get_by_role("button", name="Login")
self.flash_message = page.locator("#flash")
def navigate(self):
self.page.goto("https://hype.pmifi.ru/login")
def login(self, username, password):
self.username_input.fill(username)
self.password_input.fill(password)
self.login_button.click()
def check_success_message(self):
expect(self.flash_message).to_contain_text("You logged into a secure area!")
def check_error_message(self):
expect(self.flash_message).to_contain_text("Your username is invalid!")
Тесты:
# tests/test_login.py
import pytest
from playwright.sync_api import Page
from pages.login_page import LoginPage
def test_successful_login(page: Page):
login_page = LoginPage(page)
login_page.navigate()
login_page.login("gunenkov", "SuperSecretPassword!")
login_page.check_success_message()
expect(page).to_have_url("https://hype.pmifi.ru/account")
def test_failed_login(page: Page):
login_page = LoginPage(page)
login_page.navigate()
login_page.login("wronguser", "wrongpassword")
login_page.check_error_message()
Тело теста читается как последовательность шагов сценария, а не как набор команд
Недостаток: потенциальное нарушение SRP, так как классы отвечают и за поиск, и за действие с элементами (в примере выше еще и за проверки)
Альтернатива Screenplay Pattern — более строгое разделение ответственностей.
Основные элементы:
Пример:
screenplay_example/
├── abilities/
│ └── browse_the_web.py
├── actors/
│ └── actor.py
├── interactions/
│ ├── click.py
│ └── fill.py
├── questions/
│ └── text.py
├── tasks/
│ └── login.py
└── tests/
└── test_login.py
Способность
from playwright.sync_api import Page
class BrowseTheWeb:
def __init__(self, page: Page):
self.page = page
@staticmethod
def with_playwright(page: Page):
return BrowseTheWeb(page)
Актор
class Actor:
def __init__(self, name: str):
self.name = name
self.abilities = {}
def can(self, ability):
self.abilities[ability.__class__.__name__] = ability
return self
def attempts_to(self, task):
task.perform_as(self)
def asks_for(self, question):
return question.answered_by(self)
Взаимодействия:
class Click:
def __init__(self, locator: str):
self.locator = locator
@staticmethod
def on(locator: str):
return Click(locator)
def perform_as(self, actor):
page = actor.abilities["BrowseTheWeb"].page
page.click(self.locator)
class Fill:
def __init__(self, locator: str, text: str):
self.locator = locator
self.text = text
@staticmethod
def in_field(locator: str, with_text: str):
return Fill(locator, with_text)
def perform_as(self, actor):
page = actor.abilities["BrowseTheWeb"].page
page.fill(self.locator, self.text)
Задача:
from ..interactions.click import Click
from ..interactions.fill import Fill
class Login:
def __init__(self, username, password):
self.username = username
self.password = password
@staticmethod
def with_credentials(username, password):
return Login(username, password)
def perform_as(self, actor):
actor.attempts_to(Fill.in_field("#username", with_text=self.username))
actor.attempts_to(Fill.in_field("#password", with_text=self.password))
actor.attempts_to(Click.on("#login-button"))
Вопрос:
class Text:
def __init__(self, locator: str):
self.locator = locator
@staticmethod
def of(locator: str):
return Text(locator)
def answered_by(self, actor):
page = actor.abilities["BrowseTheWeb"].page
return page.inner_text(self.locator)
И вот тест:
from playwright.sync_api import Page
from ..actors.actor import Actor
from ..abilities.browse_the_web import BrowseTheWeb
from ..tasks.login import Login
from ..questions.text import Text
def test_successful_login(page: Page):
mikhail = Actor("Mikhail").can(BrowseTheWeb.with_playwright(page))
page.goto("https://test.pmifi.ru")
alex.attempts_to(Login.with_credentials("admin", "admin"))
welcome_message = alex.asks_for(Text.of("#welcome-message"))
assert "Welcome, admin!" in welcome_message
Когда вызывается какое-либо действие (locator.click()) Playwright не пытается сразу же кликнуть на элемент. Он запускает серию проверок на интерактивность (actionability checks) и ждет, пока все они не будут пройдены. Если проверки не проходят в течение заданного таймаута (по умолчанию 30 секунд), тест падает с понятной ошибкой.
| Проверка | Описание | Почему это предотвращает нестабильность |
|---|---|---|
| Присоединен (Attached) | Элемент должен быть присоединен к DOM-дереву страницы | Предотвращает ошибки, когда тест пытается взаимодействовать с элементом, который уже был удален со страницы в результате какого-либо действия |
| Видим (Visible) | Элемент должен быть видимым: иметь ненулевые размеры и не иметь стилей display: none или visibility: hidden |
Гарантирует, что пользователь мог бы увидеть этот элемент. Предотвращает попытки кликнуть на скрытые элементы |
| Стабилен (Stable) | Ограничивающая рамка (bounding box) элемента не должна меняться в течение нескольких кадров анимации. | Решает проблему взаимодействия с анимированными элементами Playwright дождется окончания анимации, прежде чем выполнить действие |
| Доступен (Enabled) | Элемент не должен иметь атрибут disabled |
Предотвращает клики по неактивным кнопкам или полям ввода, которые часто бывают заблокированы во время фоновой загрузки данных |
| Получает события (Receives events) | Элемент не должен быть перекрыт другим элементом (например, модальным окном или всплывающим уведомлением) | Гарантирует, что клик по элементу не будет перехвачен другим, находящимся поверх него. Playwright проверяет, что именно целевой элемент является конечной точкой для события мыши |
Перехват запроса и его замена кастомным JSON-ответом:
from playwright.sync_api import Page, Route
def test_mock_api_response(page: Page):
mocked_json_response = [
{"name": "Клубничка", "id": 21},
{"name": "Черника", "id": 22}
]
page.route(
"**/api/v1/berries",
lambda route: route.fulfill(
status=200,
json=mocked_json_response
)
)
# предполагается, что с этой страницы отправится запрос на получение
# ягод
page.goto("https://wildberries.ru/hype-client")
assert page.get_by_text("Клубничка").is_visible()
assert page.get_by_text("Черника").is_visible()
Контекст — изолированная сессия со своими вкладками, cookie и т.д
Возможности эмуляции:
from playwright.sync_api import expect, Playwright
def test_tokyo_user_on_pixel_5(playwright: Playwright):
# Убираем лишний аргумент page
pixel_5 = playwright.devices["Pixel 5"]
# Используем 'with', чтобы контекст закрылся автоматически
with playwright.chromium.new_context(
**pixel_5,
locale="en-US",
timezone_id="Asia/Tokyo",
geolocation={"latitude": 35.6895, "longitude": 139.6917},
permissions=["geolocation"]
) as context:
page = context.new_page()
page.goto("https://www.google.com/maps")
# Согласие на обработку cookie
page.get_by_role("button", name="すべて同意").click()
# Кнопка "Мое местоположение"
page.get_by_role("button", name="現在地を表示").click()
# Проверки
expect(page).to_have_url(r".*@35\.6.*,139\.6.*")
timezone_string = page.evaluate("() => new Date().toString()")
assert "JST" in timezone_string or "Japan Standard Time" in timezone_string
Другие полезные возможности фреймворка:
Codegen
Инструмент для генерации кода тестов. Можно запустить Codegen, вручную выполнить действия в браузере, Playwright запишет их и преобразует в готовый код
Playwright Inspector
Инструмент для отладки, который позволяет пошагово выполнять тест, ставить точки останова, инспектировать страницу в любой момент времени и подбирать локаторы
Trace Viewer
При падении теста Playwright может сгенерировать файл трассировки, который можно открыть в Trace Viewer. Он содержит полную запись выполнения теста: видео, снимки DOM до и после каждого действия, сетевые запросы, логи консоли и исходный код теста