Мокирование
Полное руководство по перехвату и модификации сетевых запросов в Playwright Python: route, fulfill, abort, continue, HAR-файлы и практические примеры.
Основы мокирования в Playwright
Мокирование (перехват сетевых запросов) — одна из ключевых возможностей Playwright. Она позволяет перехватывать HTTP-запросы, которые делает страница, и подменять ответы, блокировать запросы или модифицировать их на лету. Это незаменимо для тестирования сценариев с недоступными API, для ускорения тестов и для проверки поведения страницы в особых условиях.
Важно: Перехват работает на уровне протокола CDP (Chrome DevTools Protocol), что даёт полный контроль над сетевыми запросами. В отличие от сервис-воркеров или прокси, перехват в Playwright прозрачен для приложения и работает на любом сайте.
page.route() — базовый перехват запросов
Метод page.route() регистрирует обработчик для запросов, соответствующих указанному URL-паттерну. Паттерн может быть строкой (с glob-символами) или регулярным выражением.
from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch() page = browser.new_page() # Перехват всех запросов к API page.route("https://api.example.com/**", lambda route: route.fulfill( status=200, content_type="application/json", body='{"status": "ok"}' )) page.goto("https://example.com")
Glob-паттерны для URL
Playwright поддерживает glob-паттерны для фильтрации URL. Это удобный способ перехватить группу запросов без написания сложных регулярных выражений.
# Все CSS-файлы page.route("**/*.css", lambda route: route.abort()) # Все изображения page.route("**/*.{png,jpg,jpeg,gif,svg}", lambda route: route.abort()) # Все запросы к определённому поддомену page.route("https://analytics.example.com/**", lambda route: route.abort()) # Все POST-запросы к API page.route("**/api/**", lambda route: route.fulfill(...)) # Комбинация: все шрифты и иконки page.route("**/*.{woff,woff2,ttf,eot,ico}", lambda route: route.abort()) # Все запросы, кроме основного документа page.route("**/*", lambda route: route.continue_() if route.request.resource_type == "document" else route.abort())
route.fulfill() — подмена ответа
Метод route.fulfill() позволяет вернуть произвольный ответ вместо реального запроса к серверу. Можно указать статус, заголовки, тело (строкой, байтами, JSON или из файла).
# Простая подмена с JSON-ответом page.route("**/api/users", lambda route: route.fulfill( status=200, content_type="application/json", body='[{"id": 1, "name": "Иван"}, {"id": 2, "name": "Мария"}]' )) # Подмена с использованием json-параметра page.route("**/api/user", lambda route: route.fulfill( status=200, content_type="application/json", json={"id": 1, "name": "Тестовый пользователь", "role": "admin"} )) # Подмена с ответом из файла page.route("**/api/data", lambda route: route.fulfill( status=200, path="mocks/response.json" )) # Подмена с кастомными заголовками page.route("**/api/users", lambda route: route.fulfill( status=200, headers={"X-Custom-Header": "mock-value", "Access-Control-Allow-Origin": "*"}, body='[]' )) # Имитация ошибки сервера (500) page.route("**/api/error", lambda route: route.fulfill( status=500, content_type="application/json", body='{"error": "Internal Server Error", "code": "SERVER_ERROR"}' )) # Имитация таймаута (медленный ответ) import time page.route("**/api/slow", lambda route: ( time.sleep(10), route.fulfill(status=200, body='"slow response"') ))
route.abort() — блокировка запроса
Метод route.abort() отменяет запрос с указанной причиной ошибки. Это полезно для блокировки нежелательных ресурсов (реклама, аналитика, шрифты).
# Блокировка с причиной "aborted" (по умолчанию) page.route("**/*.css", lambda route: route.abort()) # Различные причины ошибки page.route("https://ads.example.com/**", lambda route: route.abort("connectionrefused")) page.route("https://tracker.example.com/**", lambda route: route.abort("accessdenied")) page.route("https://cdn.example.com/**", lambda route: route.abort("internetdisconnected")) # Блокировка только скриптов page.route("**/*.js", lambda route: route.abort("aborted")) # Блокировка изображений для ускорения тестов page.route("**/*.{png,jpg,jpeg,gif,svg,webp}", lambda route: route.abort()) # Блокировка внешних ресурсов, но разрешение локальных page.route("**/*", lambda route: route.continue_() if route.request.url.startswith("http://localhost") else route.abort())
Важно: Причины ошибок для abort: "aborted", "accessdenied", "addressunreachable", "blockedbyclient", "blockedbyresponse", "connectionaborted", "connectionclosed", "connectionfailed", "connectionrefused", "connectionreset", "internetdisconnected", "namenotresolved", "timedout", "failed".
route.continue_() — модификация запроса
Метод route.continue_() позволяет пропустить запрос, изменив его параметры: URL, метод, заголовки или тело. Запрос будет отправлен на сервер с изменениями.
# Продолжить запрос без изменений page.route("**/api/**", lambda route: route.continue_()) # Изменить заголовки запроса page.route("**/api/**", lambda route: route.continue_( headers={ "Authorization": "Bearer mock-token", "X-Test": "true" } )) # Изменить URL запроса (перенаправление) page.route("**/api/v1/**", lambda route: route.continue_( url=route.request.url.replace("/api/v1/", "/api/v2/") )) # Изменить метод запроса page.route("**/api/delete/**", lambda route: route.continue_( method="GET" )) # Изменить тело POST-запроса page.route("**/api/users", lambda route: route.continue_( post_data='{"name": "Мокированный", "role": "admin"}' )) # Комбинация: изменить заголовки и добавить параметр в URL page.route("**/api/search", lambda route: route.continue_( url=route.request.url + "&mock=true", headers={"X-Mock": "true"} ))
Совет: Используйте route.continue_(), когда нужно изменить запрос, но не подменять ответ полностью. Например, для добавления тестовых заголовков, подмены URL или изменения тела запроса.
route.fetch() — выполнение реального запроса из обработчика
Метод route.fetch() выполняет реальный HTTP-запрос к серверу (как если бы перехвата не было). Это полезно, когда нужно выполнить реальный запрос, но при необходимости изменить его результат.
import json def handle_route(route): # Выполнить реальный запрос к серверу response = route.fetch() # Модифицировать ответ data = response.json() data["mocked"] = True data["original_length"] = len(data) # Вернуть модифицированный ответ route.fulfill( status=response.status, headers=response.headers, body=json.dumps(data) ) page.route("**/api/users", handle_route) # Пример: добавить элемент в список ответа def add_item_to_response(route): response = route.fetch() data = response.json() data["items"].append({"id": 999, "name": "Подставной элемент"}) route.fulfill(status=response.status, json=data) page.route("**/api/items", add_item_to_response)
page.unroute() — удаление обработчика
Метод page.unroute() удаляет зарегистрированный обработчик для указанного паттерна. Если обработчик не был указан, удаляются все обработчики для данного паттерна.
# Удаление конкретного обработчика def handler(route): route.fulfill(status=200, body="mock") page.route("**/api/data", handler) # ... тест с моком ... page.unroute("**/api/data", handler) # Удаление всех обработчиков для паттерна page.unroute("**/api/**") # Удаление всех обработчиков вообще page.unroute("**/*") # Контекстный менеджер для временного мока page.route("**/api/test", lambda route: route.fulfill(body="temp")) # ... выполнение теста с моком ... page.unroute("**/api/test") # Теперь запросы снова идут на реальный сервер
context.route() — перехват для всего контекста
Метод context.route() работает аналогично page.route(), но перехватывает запросы во всех страницах данного браузерного контекста. Это полезно для установки глобальных моков.
# Перехват на уровне контекста (работает для всех страниц) context.route("**/analytics/**", lambda route: route.abort()) # Глобальная подмена API для всех страниц контекста context.route("**/api/config", lambda route: route.fulfill( json={"feature_enabled": True, "max_items": 50} )) # Удаление обработчика контекста context.unroute("**/analytics/**") # Все страницы в этом контексте не загружают шрифты context.route("**/*.woff2", lambda route: route.abort())
page.route_from_har() — ответы из HAR-файла
Playwright может перехватывать запросы и возвращать ответы, сохранённые в HAR-файле (HTTP Archive). Это мощный инструмент для воспроизведения тестов с заранее записанными данными.
# Использование HAR-файла для мокирования page.route_from_har("test-data/har/api-response.har") # Использование с фильтрацией URL page.route_from_har( "test-data/har/example.har", url="**/api/**" # только API-запросы из HAR ) # Обновление HAR-файла (update = True) page.route_from_har( "test-data/har/example.har", update=True # перезаписать HAR новыми данными ) # Запись HAR-файла при первом запуске context = browser.new_context(record_har_path="test-data/har/example.har") page = context.new_page() page.goto("https://example.com") # После завершения HAR-файл будет содержать все запросы context.close() # Использование записанного HAR в тестах page.route_from_har("test-data/har/example.har") page.goto("https://example.com") # Все запросы будут взяты из HAR, а не выполнены реально
Примечание: HAR-файлы (HTTP Archive) содержат полную информацию о запросах и ответах. Playwright может как записывать HAR (через опцию record_har_path в контексте), так и воспроизводить из него ответы. Это идеально для тестов, которые должны работать офлайн или с фиксированными данными.
Практические примеры
Пример 1: Мок API для тестирования
Полный пример теста, который мокирует API и проверяет отображение данных на странице.
from playwright.sync_api import sync_playwright, expect import json def test_user_list_with_mock_api(): mock_users = [ {"id": 1, "name": "Анна", "status": "active"}, {"id": 2, "name": "Борис", "status": "inactive"}, {"id": 3, "name": "Виктория", "status": "active"} ] with sync_playwright() as p: browser = p.chromium.launch() page = browser.new_page() # Перехват запроса к API и возврат мок-данных page.route("**/api/users", lambda route: route.fulfill( status=200, content_type="application/json", body=json.dumps(mock_users) )) page.goto("https://example.com/users") # Проверка, что данные отображаются expect(page.get_by_text("Анна")).to_be_visible() expect(page.get_by_text("Борис")).to_be_visible() expect(page.get_by_text("Виктория")).to_be_visible() browser.close()
Пример 2: Блокировка рекламы и аналитики
Блокировка нежелательных запросов для ускорения тестов и устранения внешних зависимостей.
def test_page_without_ads(page): # Блокировка рекламы и аналитики page.route("**/ads/**", lambda route: route.abort()) page.route("**/analytics/**", lambda route: route.abort()) page.route("**/tracker/**", lambda route: route.abort()) # Блокировка изображений (для ускорения) page.route("**/*.{png,jpg,jpeg,gif}", lambda route: route.abort()) # Навигация на страницу page.goto("https://example.com") # Проверка основного контента expect(page.get_by_role("heading", name="Добро пожаловать")).to_be_visible()
Пример 3: Подмена изображения на тестовое
Замена изображений на тестовые заглушки для изоляции тестов.
def test_image_mock(page): # Подмена всех изображений на тестовую заглушку page.route("**/*.{png,jpg,jpeg,gif,webp}", lambda route: route.fulfill( status=200, content_type="image/png", path="mocks/test-image.png" # файл-заглушка 1x1 пиксель )) page.goto("https://example.com/gallery") # Все изображения загружены (заглушки) images = page.get_by_role("img").all() assert len(images) == 10
Пример 4: Мокирование ошибок сети
Тестирование поведения страницы при различных сетевых ошибках.
def test_error_scenarios(page): # Тест 1: ошибка 500 от API page.route("**/api/data", lambda route: route.fulfill( status=500, content_type="application/json", body='{"error": "Server Error"}' )) page.goto("https://example.com/data") expect(page.get_by_text("Ошибка загрузки данных")).to_be_visible() # Тест 2: таймаут запроса (очень медленный ответ) page.route("**/api/data", lambda route: ( time.sleep(60), # долгий ответ route.fulfill(status=200, body='"ok"') )) page.goto("https://example.com/data") expect(page.get_by_text("Таймаут соединения")).to_be_visible() # Тест 3: блокировка запроса page.route("**/api/data", lambda route: route.abort("connectionrefused")) page.goto("https://example.com/data") expect(page.get_by_text("Сервер недоступен")).to_be_visible()
Пример 5: Динамическая модификация ответа
Перехват реального ответа и добавление в него тестовых данных.
def test_inject_test_data(page): def modify_response(route): # Выполняем реальный запрос response = route.fetch() # Модифицируем JSON-ответ data = response.json() data["test_mode"] = True data["notification"] = "Это тестовое окружение" # Добавляем элемент в массив if "items" in data: data["items"].insert(0, {"id": "test-001", "name": "Тестовый элемент"}) route.fulfill( status=response.status, headers=response.headers, body=json.dumps(data) ) page.route("**/api/config", modify_response) page.goto("https://example.com") # Проверка, что тестовые данные отображаются expect(page.get_by_text("Это тестовое окружение")).to_be_visible()
Заключение
Мокирование в Playwright — мощный механизм для контроля над сетевыми запросами в тестах. Основные возможности:
- page.route() и context.route() — перехват запросов по glob-паттернам или регулярным выражениям
- route.fulfill() — подмена ответа с полным контролем над статусом, заголовками и телом
- route.abort() — блокировка нежелательных запросов (реклама, аналитика, внешние ресурсы)
- route.continue_() — модификация запроса перед отправкой на сервер
- route.fetch() — выполнение реального запроса из обработчика с возможностью модификации ответа
- page.route_from_har() — воспроизведение ответов из HAR-файла
- page.unroute() — удаление обработчиков