Перехват сетевых запросов, стабы, моки API, модификация ответов сервера
Перехват сетевых запросов — одна из самых мощных возможностей Playwright. Вы можете заглушить API, подменить ответ, замедлить запрос или вернуть ошибку — всё это без изменения кода приложения.
Тестирование UI не должно зависеть от бэкенда. Моки дают:
page.route(pattern, handler) перехватывает запрос до отправки:
# Заглушить API пользователей
await page.route('**/api/users', lambda route: route.fulfill(
status=200,
json=[
{'id': 1, 'name': 'Иван', 'email': 'ivan@example.com'},
{'id': 2, 'name': 'Мария', 'email': 'maria@example.com'},
],
))
await page.goto('/users')
# Страница получает заглушённые данные
assert page.get_by_text('Иван').is_visible()
assert page.get_by_text('Мария').is_visible()Глоб-паттерн **/api/users совпадает с любым URL, заканчивающимся на /api/users.
Handler получает объект route и обязан выполнить одно из трёх действий:
# 1. Вернуть подставной ответ
await page.route('**/api/data', lambda route: route.fulfill(
status=200,
json={'key': 'value'},
))
# 2. Пропустить запрос к серверу
await page.route('**/api/analytics', lambda route: route.continue_())
# 3. Отменить запрос (имитация ошибки сети)
await page.route('**/api/tracking', lambda route: route.abort('failed'))Важно: handler обязан вызвать одно из трёх действий. Если handler ничего не вызовет, запрос зависнет до таймаута.
Вместо lambda используйте обычную функцию для сложной логики:
def handle_users_route(route):
# Можно читать тело запроса
post_data = route.request.post_data_json
if post_data and post_data.get('role') == 'admin':
route.fulfill(status=200, json={'id': 1, 'role': 'admin'})
else:
route.fulfill(status=200, json={'id': 2, 'role': 'viewer'})
await page.route('**/api/users', handle_users_route)Передайте функцию вместо строки для точной фильтрации:
def only_post_requests(route):
return route.request.method == 'POST' and '/api/users' in route.request.url
await page.route(only_post_requests, lambda route: route.fulfill(
status=201,
json={'id': 42, 'created': True},
))route.continue_() позволяет изменить запрос:
# Подменить заголовок авторизации
await page.route('**/api/**', lambda route: route.continue_(
headers={
**route.request.headers,
'Authorization': 'Bearer test-token',
}
))
# Изменить метод
await page.route('**/api/old-endpoint', lambda route: route.continue_(
method='POST',
url='https://api.example.com/new-endpoint',
))Параметр delay замедляет ответ — полезно для тестирования лоадеров:
await page.route('**/api/slow-data', lambda route: route.fulfill(
status=200,
json={'data': 'loaded'},
delay=2000, # 2 секунды задержки
))
await page.goto('/dashboard')
# Проверяем, что лоадер виден
assert page.locator('.spinner').is_visible()
# Ждём данные
assert page.get_by_text('loaded').is_visible()Иногда нужно не заменить ответ полностью, а изменить часть реального ответа:
async def modify_response(route):
# Пропускаем запрос к серверу
response = await route.fetch()
# Читаем JSON
data = await response.json()
# Модифицируем
data['features'].append('premium')
# Возвращаем изменённый ответ
await route.fulfill(
status=response.status,
json=data,
)
await page.route('**/api/config', modify_response)route.fetch() выполняет реальный запрос к серверу и возвращает APIResponse. Это позволяет читать ответ, модифицировать и возвращать изменённую версию.
# Ошибка 500 — Internal Server Error
await page.route('**/api/crash', lambda route: route.fulfill(
status=500,
json={'error': 'Internal Server Error'},
))
# Ошибка 403 — Forbidden
await page.route('**/api/admin', lambda route: route.fulfill(
status=403,
json={'error': 'Доступ запрещён'},
))
# Сетевая ошибка — как будто сервер недоступен
await page.route('**/api/offline', lambda route: route.abort('failed'))Тестирование обработки ошибок:
await page.route('**/api/submit', lambda route: route.fulfill(status=500))
await page.get_by_role('button', name='Отправить').click()
assert page.get_by_text('Произошла ошибка').is_visible()# Снять все моки
await page.unroute_all()
# Снять мок для конкретного паттерна
await page.unroute('**/api/users')
# Снять мок с handler'ом (если их несколько)
await page.unroute('**/api/users', specific_handler)unroute_all() полезен в teardown фикстуры, чтобы мок одного теста не повлиял на другой.
Для чистоты тестов используйте контекстный менеджер page.expect_request():
# Ожидание, что тест вызовет конкретный запрос
with page.expect_request('**/api/save') as request_info:
await page.get_by_role('button', name='Сохранить').click()
request = request_info.value
assert request.method == 'POST'
assert 'important_data' in request.post_data_json❌ Забыть вызвать fulfill, continue_ или abort. Запрос зависнет до таймаута (30 секунд). Handler обязан завершить запрос.
❌ Использовать fulfill(status=500) для имитации недоступности сервера. HTTP 500 — это ответ сервера. Сетевая ошибка — это abort('failed'). Для UI это разные сценарии: при 500 страница может показать одно сообщение, при network error — другое.
❌ Один мок на все тесты в файле. Моки, установленные в одном тесте, могут повлиять на другие. Снимайте моки в teardown: yield + await page.unroute_all().
✅ Правильный подход: устанавливать моки в фикстуре или начале теста, снимать в teardown, использовать expect_request для проверки исходящих запросов.
/users: заглушите GET /api/users и проверьте, что имена отображаются.POST /api/submit статусом 500 и проверьте сообщение об ошибке.route.fetch() для модификации реального ответа: добавьте поле в JSON перед возвратом.delay=3000 и проверьте, что спиннер виден до загрузки данных.Изучите аутентификацию и авторизацию, чтобы научиться сохранять сессии и обходить login-формы.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.