Мастерство мокирования в Playwright: от основ до сложных архитектурных решений

Углублённый курс для QA-инженеров по управлению сетевым слоем приложения. Студенты научатся изолировать фронтенд, имитировать серверные сбои и создавать гибкие тестовые сценарии на JavaScript.

1. Введение в мокирование: роль изоляции в архитектуре автоматизированных тестов

Введение в мокирование: роль изоляции в архитектуре автоматизированных тестов

Представьте, что вы тестируете функционал оформления заказа в крупном интернет-магазине. Для успешного прохождения сценария вам нужно: авторизоваться, выбрать товар, который есть в наличии на складе, применить действующий промокод, выбрать доступный пункт выдачи и, наконец, провести оплату через внешний банковский шлюз. В понедельник утром тест падает, потому что база данных склада ушла на техническое обслуживание. В обед он падает снова, так как тестовый промокод истек. К вечеру — третья неудача: банковский шлюз в песочнице (sandbox) вернул ошибку 503. В итоге за весь день вы не проверили ни строчки собственного кода фронтенда, потратив часы на выяснение причин во внешних системах.

Эта ситуация — классический пример «хрупких тестов» (flaky tests), которые зависят от факторов, находящихся вне зоны контроля тестировщика. Именно здесь на сцену выходит концепция изоляции и мокирования.

Философия изоляции в автоматизации

В современной разработке программное обеспечение редко представляет собой монолит. Чаще это созвездие микросервисов, сторонних API, баз данных и облачных функций. Когда мы пишем автоматизированный тест, перед нами всегда стоит выбор: проверять всю цепочку взаимодействий целиком (End-to-End или E2E) или изолировать тестируемый компонент от его окружения.

Изоляция — это процесс отделения тестируемой системы (System Under Test, SUT) от её зависимостей. Если мы тестируем фронтенд-приложение, нашими зависимостями являются бэкенд-сервисы и сторонние интеграции.

Почему реальные данные — это не всегда хорошо?

Многие начинающие автоматизаторы стремятся к «максимальной реалистичности», полагая, что тесты на реальных данных надежнее. Однако на практике полная зависимость от бэкенда в UI-тестах порождает ряд критических проблем:

  • Недетерминированность. Реальные данные постоянно меняются. Сегодня в списке товаров пять позиций, завтра — три. Тест, завязанный на конкретные значения, будет требовать постоянной правки.
  • Сложность подготовки состояния. Чтобы проверить экран «У вас нет заказов», вам нужно создать нового пользователя. Чтобы проверить «Золотой статус лояльности», нужно программно совершить покупок на 100 000 руб. Это занимает время и перегружает тестовую логику.
  • Медлительность. Сетевые запросы к реальному серверу, обработка в БД и передача ответа по протоколу HTTP занимают сотни миллисекунд. В масштабе тысячи тестов это превращается в часы ожидания в CI/CD пайплайне.
  • Стоимость ошибок. Тестирование удаления аккаунта на реальном бэкенде требует последующего восстановления данных, иначе повторный запуск теста невозможен.
  • Мокирование (Mocking) — это техника, позволяющая заменить реальный ответ сервера заранее подготовленными данными, которые перехватываются на уровне сетевого уровня браузера.

    Моки, стабы и спаи: разбираемся в терминологии

    В профессиональной среде часто используют термины «мок», «стаб» и «дублер» как синонимы, но для качественного проектирования тестов важно понимать нюансы. Профессор Джерард Месарош в своей книге «Шаблоны тестирования xUnit» выделяет несколько типов тестовых дублеров (Test Doubles).

    Dummy (Пустышка)

    Объекты, которые передаются в методы, но никогда не используются. В контексте сетевых запросов это может быть пустой JSON-ответ для запроса, результат которого не важен для текущего теста, но необходим, чтобы приложение не «упало» с ошибкой.

    Stub (Заглушка)

    Стаб предоставляет заранее определенные ответы на вызовы во время теста. Если приложение запрашивает профиль пользователя, стаб всегда вернет:

    Стабы не меняют своего поведения в зависимости от входных параметров и не проверяют, как именно их вызвали.

    Mock (Мок)

    Это более сложный инструмент. Моки настраиваются так, чтобы они ожидали определенных вызовов. Они могут проверять: * Был ли вызван конкретный URL? * Какие параметры были переданы в теле POST-запроса? * Сколько раз произошел вызов?

    В Playwright грань между стабом и моком размыта, так как API page.route() позволяет одновременно и подменять данные (stubbing), и проверять факты взаимодействия (mocking).

    Spy (Шпион)

    Шпионы записывают информацию о вызовах, не подменяя реализацию. В тестировании UI это используется редко, так как обычно нам важнее именно контроль над данными, но иногда полезно «подсмотреть», ушел ли запрос на аналитику, не прерывая его выполнение.

    Архитектурное место мокирования в пирамиде тестирования

    Чтобы понять, когда использовать мокирование в Playwright, нужно взглянуть на пирамиду тестирования. Традиционно UI-тесты находятся на её вершине и считаются самыми дорогими. Однако использование моков позволяет сдвинуть часть этих тестов «ниже», делая их более похожими на интеграционные тесты фронтенда.

    > «Мокирование превращает нестабильный E2E-тест в контролируемый контрактный тест пользовательского интерфейса».

    Рассмотрим три стратегии:

  • Чистый E2E (No Mocks): Тест проходит через браузер, бэкенд, базу данных и внешние API.
  • Плюс:* Максимальная уверенность в работоспособности всей системы. Минус:* Высокая хрупкость, медленная скорость.
  • Тестирование с мокированием внешних сервисов: Бэкенд реальный, но запросы к платежным системам или картам мокируются.
  • Плюс:* Снижение стоимости тестов, исключение зависимости от чужого API. Минус:* Требуется настройка моков на стороне бэкенда или через прокси.
  • UI-изоляция (Full Mocking): Фронтенд запускается в браузере, но ВСЕ запросы к бэкенду перехватываются Playwright и заменяются моками.
  • Плюс:* Мгновенная скорость, 100% стабильность, возможность протестировать любые состояния UI. Минус:* Мы не знаем, совместим ли наш фронтенд с текущей версией реального бэкенда.

    В рамках этого курса мы сфокусируемся на третьем варианте — UI-изоляции. Это мощнейший инструмент в руках QA-инженера, позволяющий тестировать интерфейс в «лабораторных условиях».

    Преимущества мокирования для QA-инженера

    1. Тестирование негативных сценариев

    Попробуйте заставить реальный банковский сервер вернуть ошибку «Карта просрочена» ровно в 15:00. Это сложно. С моками в Playwright это занимает одну строку кода: route.fulfill({ status: 402, body: JSON.stringify({ error: 'Card Expired' }) }); Вы можете имитировать 404 (не найдено), 500 (ошибка сервера), 403 (нет доступа) и проверять, как UI обрабатывает эти ситуации: рисует ли он красивые «пустые состояния» или падает в «белый экран».

    2. Моделирование редких состояний данных

    Представьте задачу: проверить отображение списка транзакций, если их больше 1000 (пагинация) и если их ровно 0. Вместо того чтобы наполнять базу данных тысячами записей, вы просто подменяете JSON-ответ. Вы можете создать «идеального пользователя», у которого заполнены все поля профиля, и «минималистичного пользователя» без аватара и описания, чтобы проверить верстку.

    3. Независимость от готовности бэкенда

    Часто разработка фичи на фронтенде идет параллельно с бэкендом. Если контракт (формат данных) уже согласован, вы можете начать писать автотесты на еще не существующий функционал, используя моки. Когда бэкенд будет готов, вам останется только включить интеграционные тесты.

    4. Ускорение цикла обратной связи (Feedback Loop)

    Тесты с моками выполняются в разы быстрее. Это позволяет запускать их на каждый коммит в Git, обеспечивая разработчиков мгновенной информацией о том, не сломали ли они UI.

    Риски и ограничения: когда моки могут навредить

    Было бы непрофессионально утверждать, что мокирование — это серебряная пуля. У этого подхода есть существенный риск: ложноположительные результаты.

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

    Для минимизации этого риска используются следующие подходы: * Контрактное тестирование: Отдельные тесты проверяют, что реальный бэкенд возвращает данные именно в том формате, который мы используем в моках. * Гибридная стратегия: 80% тестов — быстрые и изолированные (с моками), 20% — полноценные E2E (без моков) для проверки критических путей (Smoke-тесты).

    Как Playwright работает с сетью (общий обзор)

    Playwright оперирует на уровне протокола CDP (Chrome DevTools Protocol) или аналогичных механизмов в других браузерах. Это позволяет ему вставать «посередине» между движком браузера и сетевой картой.

    Когда браузер пытается отправить запрос, Playwright перехватывает его. В этот момент у нас есть три пути:

  • Пропустить запрос (Continue): Запрос уходит на реальный сервер.
  • Изменить запрос (Modify): Мы можем добавить заголовок, изменить параметры или тело запроса перед отправкой.
  • Подменить ответ (Fulfill): Мы блокируем отправку запроса и сразу возвращаем браузеру наш подготовленный ответ.
  • Этот механизм работает крайне эффективно, так как не требует установки прокси-серверов или изменения кода самого приложения. Приложение даже не «подозревает», что данные, которые оно получило, ненастоящие.

    Практический пример: от хаоса к порядку

    Рассмотрим кейс: страница личного кабинета, где отображается баланс пользователя.

    Без мокирования:

  • Тест заходит на страницу.
  • Запрос GET /api/v1/balance уходит на сервер.
  • Сервер лезет в БД, считает сумму.
  • Возвращает 100.00');.
  • Результат: Тест всегда стабилен, выполняется за миллисекунды и проверяет именно то, что UI корректно отображает данные из API.

    Изоляция как стандарт индустрии

    В крупных технологических компаниях (Google, Meta, Яндекс) изоляция тестов является обязательным требованием. Это связано с тем, что инфраструктурные расходы на поддержание «всегда работающего» тестового окружения для тысяч разработчиков колоссальны. Мокирование позволяет каждому инженеру иметь «свой собственный бэкенд» прямо внутри браузера.

    Более того, современный подход Backend for Frontend (BFF) отлично сочетается с мокированием. Поскольку UI часто общается с промежуточным слоем, мокирование этого слоя позволяет полностью протестировать логику отображения, фильтрации и первичной обработки данных, не затрагивая тяжелые корпоративные системы.

    Математическая модель надежности тестов

    Если рассмотреть надежность теста как вероятность успешного прохождения всех его этапов, то для E2E-теста она выглядит так:

    Где: * — вероятность отсутствия багов в UI (то, что мы хотим проверить). * — вероятности доступности сети, бэкенда, базы и внешних сервисов.

    Если каждая из систем доступна с вероятностью (что неплохо), то итоговая вероятность того, что тест не упадет по «инфраструктурным» причинам, для цепочки из 5 зависимостей составит: . Это значит, что 5% запусков будут ложноотрицательными. В масштабах 1000 тестов — это 50 упавших тестов каждое утро.

    При мокировании формула упрощается:

    Поскольку (надежность Playwright) стремится к , надежность вашего теста начинает напрямую зависеть только от качества вашего кода.

    Подготовка к работе с Playwright

    Для того чтобы эффективно использовать мокирование, необходимо понимать, как устроены HTTP-запросы. В следующих главах мы будем постоянно оперировать такими понятиями как: * Method (Метод): GET, POST, PUT, DELETE, PATCH. * Status Code (Код ответа): 200 (OK), 201 (Created), 400 (Bad Request), 401 (Unauthorized), 403 (Forbidden), 404 (Not Found), 500 (Internal Server Error). * Headers (Заголовки): Content-Type, Authorization, Cookie. * Body (Тело запроса/ответа): Обычно в формате JSON.

    Playwright предоставляет объект route, который является ключом к управлению сетевым трафиком. Освоение этого объекта — это переход от простого «нажимателя кнопок» к инженеру, который полностью контролирует среду выполнения своего приложения.

    Использование моков — это не просто технический прием, это смена парадигмы. Мы перестаем воспринимать приложение как «черный ящик», зависящий от воли случая и состояния серверов, и начинаем рассматривать его как предсказуемую систему, где каждый входной сигнал (ответ API) приводит к четко определенному выходному результату (состоянию интерфейса).

    Впереди нас ждет глубокое погружение в синтаксис и методы Playwright, которые сделают ваши тесты быстрыми, надежными и по-настоящему профессиональными.

    2. Сетевые возможности Playwright: механизмы перехвата и маршрутизации трафика

    Сетевые возможности Playwright: механизмы перехвата и маршрутизации трафика

    Представьте, что вы тестируете интерфейс управления ядерным реактором или, что более приземленно, систему банковских переводов. Чтобы проверить, как кнопка «Подтвердить» реагирует на секундную потерю связи или на специфическую ошибку сервера, вам пришлось бы физически выдергивать кабель из роутера или просить бэкенд-разработчиков временно «сломать» базу данных. В мире современной автоматизации это звучит как анахронизм. Playwright предлагает элегантное решение: он встает «прослойкой» между браузером и сетью, превращаясь в программируемый прокси-сервер, который позволяет вам манипулировать каждым байтом, проходящим через сетевой уровень.

    Анатомия сетевого перехвата: как Playwright видит трафик

    Чтобы эффективно управлять сетевыми запросами, необходимо понимать, на каком уровне происходит вмешательство. Большинство инструментов автоматизации прошлого полагались на внедрение JavaScript-кода в страницу (monkey-patching объекта XMLHttpRequest или функции fetch). Это имело массу ограничений: такие скрипты часто конфликтовали с кодом приложения, не видели запросы, инициированные расширениями браузера, или запросы, происходящие в момент загрузки самой страницы.

    Playwright работает иначе. Он взаимодействует напрямую с протоколом отладки браузера (Chrome DevTools Protocol для Chromium, аналогичные механизмы для Firefox и WebKit). Это означает, что перехват происходит на уровне сетевого стека самого браузера. Когда браузер собирается отправить запрос, он «спрашивает» Playwright: «У тебя есть инструкции для этого URL?».

    Процесс маршрутизации в Playwright строится вокруг двух ключевых сущностей:

  • Маршрут (Route): Объект, представляющий конкретный перехваченный запрос.
  • Обработчик (Handler): Функция, которая решает судьбу этого запроса.
  • Когда вы вызываете метод page.route(url, handler), вы регистрируете своего рода «таможенный пост». Каждый запрос, чей URL соответствует заданному шаблону, будет приостановлен, пока ваш обработчик не примет решение: пропустить его дальше, подменить ответ или заблокировать вовсе.

    Глобальный и локальный контексты маршрутизации

    В Playwright существует иерархия объектов, и понимание того, где именно вы настраиваете перехват, критически важно для архитектуры тестов.

    * BrowserContext.route(): Устанавливает правила для всех страниц внутри данного контекста. Это полезно, если вы хотите глобально заблокировать аналитику (например, Google Analytics или Sentry) для всей тестовой сессии. * Page.route(): Ограничивает область действия правил конкретной вкладкой. Это наиболее частый сценарий, когда мокирование специфично для конкретного тест-кейса.

    Важно помнить о порядке регистрации. Если у вас есть правила и на уровне контекста, и на уровне страницы, Playwright сначала проверяет правила страницы. Если совпадений нет, он переходит к уровню контекста. Если же несколько правил на одном уровне соответствуют запросу, сработает то, которое было зарегистрировано последним (LIFO — Last In, First Out). Это позволяет «перекрывать» общие правила более специфичными в конкретных тестах.

    Селекторы URL: от строк до регулярных выражений

    Эффективная маршрутизация начинается с точного выбора целей. Playwright предоставляет три способа фильтрации запросов:

    1. Строковые шаблоны (Glob patterns) Самый простой способ. Вы можете использовать подстановочные знаки, такие как (любая последовательность символов внутри сегмента пути) или * (любая последовательность, включая вложенные папки). Например, */api/v1/users/ перехватит запрос к профилю любого пользователя, но не затронет вложенные ресурсы вроде /settings.

    2. Регулярные выражения Когда логика выбора становится сложной, на помощь приходят регулярные выражения. Это незаменимо, если нужно перехватить запросы к нескольким эндпоинтам сразу или исключить определенные расширения файлов. Пример: /.\/api\/(customers|orders)\/./ — перехватит запросы и к покупателям, и к заказам.

    3. Функции-предикаты Самый мощный инструмент. Вы передаете функцию, которая принимает объект URL и возвращает true или false.

    Здесь мы перехватываем запросы к каталогу только в том случае, если в URL присутствует параметр сортировки. Это позволяет избежать избыточного мокирования и делать тесты более «умными».

    Три пути обработки: Fulfill, Continue, Abort

    Когда запрос попал в «сети» вашего маршрута, у вас есть три основных сценария действий.

    Сценарий 1: Fulfill (Исполнение)

    Это и есть классическое мокирование. Вы не даете запросу уйти на реальный сервер, а сами формируете ответ. Метод route.fulfill() позволяет настроить: * Status: HTTP-код (200, 404, 500 и т.д.). * Headers: Заголовки ответа (например, Content-Type: application/json). * Body / Path: Тело ответа в виде строки, буфера или путь к файлу на диске.

    Представьте ситуацию: вы тестируете отображение списка товаров. Реальный API возвращает 50 позиций, что затрудняет проверку верстки пагинации. С помощью fulfill вы можете моментально вернуть ровно 3 товара с заранее известными названиями и ценами.

    Сценарий 2: Continue (Продолжение)

    Иногда вам не нужно подменять ответ, но нужно изменить сам запрос перед отправкой. Метод route.continue() позволяет: * Изменить метод запроса (например, с GET на POST). * Добавить или изменить заголовки (например, подставить тестовый токен авторизации). * Изменить тело запроса (postData).

    Это часто используется в сценариях «проксирования», когда тест выступает в роли посредника, корректирующего поведение системы на лету.

    Сценарий 3: Abort (Прерывание)

    Метод route.abort() имитирует сетевую ошибку. Вы можете указать причину падения: failed, timedout, internetdisconnected, accessdenied и другие. Это критически важно для проверки отказоустойчивости фронтенда. Как поведет себя форма регистрации, если запрос на проверку уникальности email упадет по таймауту? Покажется ли пользователю понятное сообщение или интерфейс «залипнет» с бесконечным лоадером?

    Жизненный цикл объекта Route и асинхронность

    Работа с сетью в Playwright — это всегда асинхронный процесс. Когда срабатывает обработчик маршрута, выполнение кода теста не останавливается автоматически. Playwright ожидает, что вы либо вызовете один из терминальных методов (fulfill, continue, abort), либо вернете Promise, который разрешится позже.

    Рассмотрим нюанс: если вы забыли вызвать терминальный метод внутри page.route(), запрос останется в состоянии «Pending» (ожидание) до тех пор, пока не сработает системный таймаут браузера. Это одна из самых частых причин «зависания» тестов у новичков.

    > Важное правило: Каждый путь исполнения внутри вашего обработчика route должен завершаться вызовом метода управления запросом.

    Глубокое погружение: Перехват ответов и Fetch API

    Playwright позволяет не только подменять ответы «вслепую», но и модифицировать реальные ответы сервера. Это техника «Man-in-the-Middle» внутри вашего теста.

    Алгоритм выглядит так:

  • Перехватываем запрос.
  • Позволяем ему дойти до сервера и получить ответ с помощью route.fetch().
  • Получаем данные ответа, модифицируем их (например, меняем одно поле в JSON).
  • Отправляем модифицированный ответ в браузер через route.fulfill().
  • Это избавляет от необходимости описывать огромные JSON-объекты в коде тестов. Если вам нужно проверить только то, как интерфейс отображает статус «Премиум-пользователь», вы можете взять реальный ответ профиля и программно изменить в нем только поле isPremium: true.

    Пример логики:

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

    Масштабируемость и повторное использование маршрутов

    В крупных проектах количество моков может исчисляться сотнями. Описывать их все внутри test.step или beforeEach — путь к нечитаемому коду. Профессиональный подход подразумевает создание «библиотеки моков» или использование паттерна Mock Manager.

    Вы можете вынести логику мокирования в отдельные классы-помощники:

    Такой подход делает тесты декларативными. Вместо того чтобы вникать в детали сетевых протоколов, читатель теста видит намерение: userMocks.mockProfileError().

    Безопасность и побочные эффекты

    При работе с маршрутизацией важно помнить о «чистоте» окружения. Маршруты, установленные в одном тесте, не должны влиять на другие. Playwright автоматически очищает маршруты page.route() при закрытии страницы или завершении теста, если они были объявлены внутри test. Однако, если вы используете browserContext.route(), будьте осторожны при переиспользовании контекста между тестами.

    Еще один нюанс — кэширование. Браузер может закэшировать ответ сервера, и при повторном запросе Playwright может не вызвать ваш обработчик route, так как запрос не уйдет в сеть, а будет взят из дискового кэша. Чтобы избежать этого, рекомендуется отключать кэш в тестовом контексте:

    Или добавлять в заголовки ответа мока Cache-Control: no-cache.

    Сравнение механизмов: Playwright vs Service Workers

    Иногда возникает вопрос: зачем использовать Playwright API, если в приложении уже есть Service Worker, который тоже умеет перехватывать запросы? Разница в уровне контроля. Service Worker — это часть кода приложения. Тест не должен зависеть от внутренней реализации кэширования приложения. Более того, Playwright может перехватывать запросы самого Service Worker'а к сети. Используя page.route(), вы находитесь «выше» по иерархии и имеете абсолютную власть над трафиком, независимо от того, какие технологии используются внутри фронтенда.

    Работа с WebSocket и специфическими типами трафика

    Хотя основной фокус мокирования обычно направлен на REST API (HTTP/HTTPS), современные приложения активно используют WebSocket для передачи данных в реальном времени. Playwright предоставляет экспериментальные возможности для работы с WebSocket, но классический page.route() для них не предназначен, так как WebSocket — это не серия отдельных запросов, а постоянное соединение.

    Для работы с WebSocket в Playwright используется событие page.on('websocket'), которое позволяет инспектировать фреймы, но полноценное «мокирование» сообщений внутри сокета требует более сложной обвязки. Для большинства задач тестирования UI достаточно мокировать HTTP-запросы, которые инициируют установку сокет-соединения или возвращают начальные данные.

    Конфликты маршрутов и отладка

    Когда проект растет, возникают ситуации, когда один запрос попадает под два разных правила route. Как мы уже упоминали, Playwright использует принцип LIFO. Однако на практике это может привести к трудноуловимым багам, когда «старый» мок в beforeEach перекрывается «новым» в теле теста, но разработчик ожидал иного поведения.

    Для отладки сетевых правил в Playwright предусмотрены отличные инструменты:

  • Playwright Inspector: Позволяет пошагово проходить тест и видеть, какие маршруты активны.
  • Trace Viewer: Самый мощный инструмент. В разделе "Network" вы можете кликнуть на любой запрос и увидеть, был ли он «Fulfilled by route». Если запрос был перехвачен, Playwright покажет, какая именно часть кода за это отвечала.
  • Логирование: Вы всегда можете добавить console.log внутрь обработчика маршрута, чтобы убедиться, что он вообще срабатывает.
  • Этот простой прием сэкономит часы отладки при настройке сложных регулярных выражений.

    Проектирование стабильных селекторов

    Последний аспект, на котором стоит остановиться — это устойчивость ваших правил маршрутизации к изменениям в коде. Использование полных URL (например, https://api.staging.example.com/v1/users) — плохая практика. При переходе с окружения staging на production ваши тесты сломаются.

    Всегда используйте относительные шаблоны или переменные окружения. Playwright отлично справляется с относительными путями, если задан baseURL в конфигурации. Однако для page.route() лучше использовать паттерны, начинающиеся с **/api/..., чтобы тест оставался работоспособным независимо от домена, на котором запущено приложение.

    Маршрутизация трафика в Playwright — это не просто функция подмены данных. Это мощный архитектурный рычаг, который позволяет изолировать фронтенд от капризов бэкенда, имитировать катастрофические сбои сети за миллисекунды и создавать детерминированные тесты там, где раньше царил хаос изменчивых данных. Овладение этим инструментом переводит тестировщика из разряда «записывателя сценариев» в разряд инженера, управляющего состоянием системы.