Перехоплення й мокання мережі
Зміст
Автотест, що відкриває сторінку в браузері, майже ніколи не працює ізольовано. Сторінка робить десятки мережевих запитів (requests) — по дані користувача, список замовлень, конфігурацію фіч-флагів, аналітику. Кожен такий запит іде на справжній бекенд (backend), а бекенд — це окрема система зі своєю базою даних, своїми затримками й своїми збоями. Саме тут народжується більшість болю в наскрізному (end-to-end) тестуванні: тест червоний не тому, що фронтенд зламався, а тому, що API відповів на 300 мілісекунд повільніше, повернув інакший порядок елементів або просто «моргнув» пʼятисоткою.
Перехоплення (interception) і мокання (mocking) мережі — це набір технік, які дають тесту контроль над тим, що браузер отримує у відповідь на свої запити. Замість покладатися на реальний сервер, ми вклинюємося між браузером і мережею й самі вирішуємо: пропустити запит, підмінити відповідь, сповільнити її або взагалі обірвати зʼєднання.
Навіщо мокати відповіді в тестах
Мокання відповіді (mocking a response) означає, що тест сам формує тіло (body), заголовки (headers) і код статусу (status code) HTTP-відповіді, а браузер отримує цю рукотворну відповідь так, ніби вона прийшла з сервера. Фронтенд при цьому нічого не підозрює: для нього це звичайний мережевий обмін.
Причин робити це кілька, і всі вони прямо повʼязані з якістю автотестів.
Детермінізм. Реальний бекенд повертає живі дані, які змінюються: сьогодні на дашборді 12 замовлень, завтра 15, а тестова база «поповзла» після чужого прогону. Автотест, що перевіряє текст «У вас 12 замовлень», стає флакі (flaky) вже наступного дня. Замокана відповідь завжди однакова, тож результат тесту залежить лише від коду, а не від стану чужої системи.
Ізоляція стану. Одна з головних причин флаку — спільний стан між тестами. Якщо тести ходять у справжню базу, вони заважають один одному: паралельний прогон створює гонки (race conditions), а «брудні» дані від попереднього тесту ламають наступний. Мок розриває цей звʼязок — кожен тест отримує рівно той стан, який задекларував.
Швидкість. Запит на реальний сервіс — це десятки, а іноді й сотні мілісекунд. Помножте на кількість запитів і кількість тестів, і отримаєте хвилини на порожньому місці. Локальний мок відповідає майже миттєво.
Крайові випадки (edge cases), які важко відтворити «по-справжньому». Як змусити продакшн-API повернути 500? Як показати користувачеві порожній список, коли в базі повно даних? Як перевірити поведінку інтерфейсу при обриві зʼєднання? На реальному бекенді це або неможливо, або вимагає складних маніпуляцій. Мок робить такий сценарій одним рядком коду.
Незалежність від готовності бекенда. Класична ситуація: контракт API узгоджено, а сам ендпоінт ще пишуть. За мокнутим контрактом фронтенд-тести можна писати вже зараз, не чекаючи чужої роботи.
Важливо бачити й зворотний бік: замокавши відповідь, ми навмисно перестаємо перевіряти реальну інтеграцію. До ризиків цього повернемося наприкінці — це не деталь, а ключове проєктне рішення.
Як влаштоване перехоплення
Концептуально перехоплення завжди має однакову форму, незалежно від інструмента. Тестовий фреймворк реєструє правило: «для запитів, що підходять під ось цей шаблон URL, викликай ось цю функцію-обробник». Коли браузер збирається зробити такий запит, фреймворк зупиняє його й передає керування обробнику. Обробник отримує обʼєкт маршруту (route) і мусить ухвалити одне з трьох рішень.
| Рішення | Що відбувається | Типове застосування |
|---|---|---|
| Пропустити (continue) | Запит іде далі на реальний сервер; дорогою можна змінити URL, метод, заголовки чи тіло | Логування, підміна заголовка авторизації, часткова модифікація |
| Підмінити (fulfill) | Сервер не викликається взагалі, браузер отримує рукотворну відповідь | Повне мокання даних, помилки, порожні стани |
| Обірвати (abort) | Запит завершується мережевою помилкою | Імітація втрати зʼєднання, недоступності сервісу |
Кілька властивостей цієї моделі, які варто засвоїти одразу, бо вони пояснюють типові помилки новачків:
- Правило спрацьовує лише для запитів, зроблених після його реєстрації. Тому маршрут треба ставити до дії, яка тригерить запит (до переходу на сторінку, до кліку) — інакше тест «спізниться» й перехопить порожнечу.
- Мок вибірковий: запити, що не підійшли під жоден шаблон, ідуть на реальний сервер. Ви мокаєте те, що потрібно, решта живе своїм життям.
- Якщо на один шаблон зареєстровано кілька обробників, спрацьовують вони у зворотному до реєстрації порядку — тобто першим бере запит останній зареєстрований, і він може явно передати керування попередньому. Це дозволяє покласти загальний мок у налаштування (setup), а в окремому тесті перекрити його вужчим правилом.
Playwright
У Playwright перехоплення будується навколо реєстрації маршруту (route). Ви кажете сторінці або контексту (context): «слухай запити за цим шаблоном», і передаєте обробник. Усередині обробника викликаєте один з методів обʼєкта маршруту — fulfill, continue або abort.
// Підміна: браузер отримає порожній список замовлень
await page.route('**/api/orders', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ orders: [] }),
});
});
Тіло відповіді у fulfill можна задати рядком, буфером або взяти з файлу — інлайн-JSON зручний для дрібних моків, а файл — для великих реалістичних payload-ів. Під час continue можна перевизначити параметри вихідного запиту (URL, метод, заголовки, тіло) — це шлях для часткової модифікації, коли підміняти всю відповідь не треба. Коли на шаблон навішано кілька обробників, попередньому в ланцюжку керування передають методом fallback.
Область дії маршруту буває на рівні сторінки (page) або цілого контексту браузера (context). Контекстний маршрут зручний, коли той самий мок потрібен усім вкладкам у тесті — наприклад, глушіння аналітики чи фіч-флагів. Playwright також уміє відтворювати відповіді з HAR-файлу через окремий механізм — про нього нижче.
CodeceptJS
CodeceptJS сам не працює з браузером напряму, а робить це через хелпер (helper) — найчастіше на базі Playwright. Тому перехоплення концептуально те саме, лише з іншим синтаксисом і через обʼєкт I.
// перед дією, що робить запит
I.mockRoute('**/api/user', (route) => route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ name: 'Тест Юзер', role: 'admin' }),
}));
I.amOnPage('/profile');
I.see('Тест Юзер');
// прибираємо мок, щоб він не протік у наступні тести
I.stopMockingRoute('**/api/user');
Історично в CodeceptJS існував і другий шлях — окремий хелпер на базі Polly.js (пакет @codeceptjs/mock-request), який записує й відтворює HTTP-обмін і надає методи на кшталт I.mockRequest() та I.stopMocking(). Важливий нюанс: цей хелпер працює з рушіями Puppeteer і WebDriver, але не з Playwright — для Playwright-хелпера користуються саме I.mockRoute()/I.stopMockingRoute(), показаними вище. Тобто Polly-шлях корисний тоді, коли потрібен режим запису-відтворення на не-Playwright рушії, а не ручне формування відповідей.
Нюанс. Сигнатури й опції
mockRoute/stopMockingRouteта@codeceptjs/mock-requestзмінювалися між релізами — звір з документацією версії у своєму проєкті.
Прибирання моку в кінці (stopMockingRoute) — не косметика, а частина ізоляції стану: залишений маршрут перехоплюватиме запити наступних тестів у тому самому воркері й давав би плаваючі падіння, які важко діагностувати.
Загальний висновок: інструменти різні, а модель одна — «шаблон URL → обробник → одне з трьох рішень». Опанувавши її, ви легко переносите знання між фреймворками.
Stub чи mock: у чому різниця
Терміни «стаб» (stub) і «мок» (mock) прийшли з юніт-тестування. Узагальнювальний термін «тестовий двійник» (test double) і його таксономію ввів Джерард Мезарос у книзі xUnit Test Patterns, а популяризувала цей словник стаття Мартіна Фаулера «Mocks Aren’t Stubs», що прямо посилається на роботу Мезароса. Стаб і мок часто вживають як синоніми, хоча означають вони різне. У контексті мережі різниця така.
Заглушка (stub) просто повертає заздалегідь заготовлену відповідь. Її завдання — дати системі валідні дані, щоб тест міг рухатися далі. Стаб не цікавить, скільки разів його викликали й з якими аргументами.
Мок (mock) — це заглушка з очікуваннями (expectations). Він не лише віддає відповідь, а й перевіряє сам факт і характер взаємодії: чи був викликаний ендпоінт, скільки разів, з яким тілом чи заголовками. Тобто мок бере участь у перевірці (assertion) через верифікацію поведінки.
| Ознака | Заглушка (stub) | Мок (mock) |
|---|---|---|
| Основна мета | Дати відповідь | Дати відповідь + перевірити виклик |
| Перевіряє факт виклику | Ні | Так |
| Знає про аргументи запиту | Не обовʼязково | Так, це частина перевірки |
| Типова фраза | «нехай сервер поверне ось це» | «переконаймося, що фронт надіслав саме це» |
Приклад різниці на практиці. Якщо ви лише хочете, щоб сторінка показала профіль, — вам потрібен стаб: «на /api/user віддай ось цей JSON». Якщо ж ви перевіряєте, що кнопка «Зберегти» справді надсилає PATCH з правильним тілом, — вам потрібен мок: ви перехоплюєте запит, читаєте його тіло й робите assertion на вміст.
// Мок: перевіряємо, що фронт надіслав правильні дані
let sentBody = null;
await page.route('**/api/profile', async (route) => {
sentBody = route.request().postDataJSON();
await route.fulfill({ status: 200, body: '{}' });
});
await page.click('#save');
expect(sentBody).toEqual({ name: 'Оновлене імʼя' });
Строго кажучи, у таксономії Мезароса те, що ми тут робимо, ближче до шпигуна (spy): за визначенням шпигун — це стаб, який ще й записує інформацію про те, як його викликали. Ми записуємо факт виклику й перевіряємо його після дії, а не задаємо жорсткі очікування наперед, як робить «чистий» мок. У мережевому тестуванні цю деталь зазвичай не розрізняють, але корисно памʼятати: assertion на вміст запиту й фіксоване очікування «має бути викликано рівно раз» — це різні за строгістю підходи.
На практиці межа розмита, і більшість інструментів дають обидві можливості одним і тим самим механізмом. Тримати різницю в голові корисно: вона підказує, що саме перевіряє тест — стан інтерфейсу після відповіді (стаб) чи коректність вихідного запиту (мок).
Підміна даних: коли й навіщо
Підміна даних — найчастіший сценарій мокання. Замість створювати десь у базі особливий стан, ми віддаємо потрібну відповідь прямо в тесті. Ось типові сценарії, які важко чи довго відтворювати «по-справжньому».
| Сценарій | Що мокаємо | Що перевіряємо |
|---|---|---|
| Порожній стан (empty state) | Відповідь з порожнім масивом | Показ заглушки «Немає даних», а не спінера чи помилки |
| Дуже великий список | Відповідь з тисячами елементів | Пагінацію, віртуалізацію, продуктивність рендера |
| Особлива роль | Профіль з role: admin | Наявність адмінських кнопок без реального адмін-акаунта |
| Локалізація й формати | Дати, валюти, довгі рядки в різних мовах | Верстку, обрізання тексту, форматування |
| Прикордонні значення | Нуль, відʼємні числа, дуже довгі імена | Обробку крайових випадків без «домовляння» з бекендом |
| Фіч-флаги | Конфігурацію з увімкненою/вимкненою фічею | Обидві гілки інтерфейсу незалежно від реального стану флагів |
Ключова цінність: підміна даних робить сценарій, який у реальній системі трапляється раз на місяць, доступним у кожному прогоні й повністю передбачуваним. Це прямо зменшує флак, бо тест більше не залежить від того, чи знайдеться в базі потрібний запис.
// Порожній стан
await page.route('**/api/notifications', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ items: [], total: 0 }),
}));
Як відтворити 500 чи повільну відповідь
Перевірка «щасливого шляху» (happy path) — це половина роботи. Друга половина — як інтерфейс поводиться, коли щось іде не так. Мокання тут незамінне, бо змусити живий сервер збоїти на вимогу зазвичай неможливо.
Помилки сервера
Найпростіший спосіб — повернути відповідь з відповідним кодом статусу. Ось коди, які найчастіше імітують у тестах, і що вони мають означати для фронтенда.
| Код | Значення | Що перевіряємо в UI |
|---|---|---|
| 500 | Internal Server Error | Загальне повідомлення про помилку, збереження введених даних |
| 503 | Service Unavailable | Повідомлення «сервіс тимчасово недоступний», ретрай |
| 429 | Too Many Requests | Обробку обмеження частоти, повідомлення про ліміт |
| 502 / 504 | Bad Gateway / Gateway Timeout | Поведінку при проблемах на проксі/шлюзі |
| 401 / 403 | Unauthorized / Forbidden | Редірект на логін чи повідомлення про брак прав |
// Імітація 500
await page.route('**/api/checkout', (route) =>
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Internal Server Error' }),
}));
await page.click('#pay');
await expect(page.locator('.error-banner')).toBeVisible();
Тут добре видно, як мокання зʼєднується з локаторами (locators) й очікуваннями (waits): ми штучно створюємо умову й перевіряємо, що зʼявився саме той елемент, який має показатися при помилці. Варто перевіряти не лише появу банера, а й те, що застосунок не втрачає введені дані та не «зависає» в стані завантаження назавжди.
Повільна відповідь
Щоб перевірити стани завантаження — спінери, скелетони, блокування кнопки на час запиту — потрібна затримка (latency). Її роблять просто: в обробнику витримують паузу, а потім пропускають або підміняють запит.
// Штучна затримка 5 секунд, потім реальна відповідь
await page.route('**/api/report', async (route) => {
await new Promise((resolve) => setTimeout(resolve, 5000));
await route.continue();
});
Ця техніка ловить цілий клас багів: спінер, що не зникає; кнопку, яку можна натиснути двічі й відправити дубль запиту; таймаут, що спрацьовує зарано. Зверніть увагу на звʼязок з очікуваннями: якщо тест жорстко чекає елемент з коротким таймаутом, штучна затримка допоможе виявити, що очікування налаштоване занадто оптимістично й у реальній повільній мережі тест сипатиметься.
Обрив зʼєднання
Щоб зʼімітувати повну недоступність мережі, запит обривають замість того, щоб на нього відповідати.
// Мережева помилка
await page.route('**/api/**', (route) => route.abort('failed'));
Аргумент abort — це код помилки з фіксованого набору Playwright: за замовчуванням 'failed', а серед інших доступні 'timedout', 'connectionrefused', 'internetdisconnected', 'namenotresolved', 'connectionreset' та ще кілька. Самі назви кодів однакові незалежно від рушія, бо це перелік рівня Playwright; а от який конкретно мережевий стан вони зімітують у певному браузері (Chromium, Firefox, WebKit), може відрізнятися в деталях.
Окремо варто згадати обмеження пропускної здатності (throttling) — імітацію повільного 3G, а не просто затримки одного запиту. Затримка в обробнику стосується конкретного запиту, а throttling впливає на весь мережевий канал. Такі речі зручніше робити або через протокол налагодження браузера (у Chromium — CDP-команди керування мережею), або через зовнішній проксі, про який далі.
HAR-файл
HAR (HTTP Archive) — це формат для запису мережевого обміну між браузером і серверами. Файл має розширення .har і всередині є звичайним JSON: список усіх запитів і відповідей з їхніми URL, методами, заголовками, тілами, часами й кодами статусів. Формат виріс із роботи над інструментами розробника в браузерах (початок — приблизно 2009 рік), а близько 2012-го його чернетку подали до W3C Web Performance Working Group. Офіційним стандартом він так і не став: чернетку не фіналізували й не опублікували, тож HAR лишається де-факто стандартом спільноти, а не рекомендацією W3C чи RFC. Попри це його вміють експортувати DevTools усіх основних браузерів (вкладка Network → Export/Save as HAR) та проксі-інструменти.
Спрощена структура HAR виглядає так:
{
"log": {
"version": "1.2",
"entries": [
{
"request": {
"method": "GET",
"url": "https://api.example.com/orders",
"headers": [{ "name": "Accept", "value": "application/json" }]
},
"response": {
"status": 200,
"content": {
"mimeType": "application/json",
"text": "{\"orders\":[]}"
}
}
}
]
}
}
Навіщо це AQA. По-перше, HAR — головний артефакт для діагностики впалого тесту: приклавши HAR до звіту, ви даєте розробнику точну картину, які запити пішли, з якими заголовками й що повернулося. По-друге, HAR лягає в основу режиму запису-відтворення (record and replay): ви один раз записуєте реальний обмін у HAR, а далі тести відтворюють відповіді з файлу, не торкаючись сервера. Playwright, наприклад, уміє відтворювати відповіді з HAR на рівні сторінки чи контексту (концептуально — «для цих запитів бери відповіді з ось цього файлу»); при цьому збіг шукається за URL і HTTP-методом, а для POST — ще й за тілом запиту.
Такий підхід — золота середина між живим бекендом і ручними моками: дані реалістичні, бо колись справді прийшли з сервера, але тест від сервера вже не залежить. Дві сторони застереження. Перша: HAR старіє — якщо API змінив контракт, старий запис почне брехати, і тест лишатиметься зеленим на застарілих даних (про це нижче в ризиках). Друга, суто безпекова: HAR містить усе, зокрема заголовки авторизації, токени й куки (cookies). Перед тим як покласти .har у репозиторій, його треба почистити від секретів — інакше ви закомітите живі облікові дані.
Проксі-інструменти: Charles і mitmproxy
Перехоплення в тесті вклинюється всередині фреймворка. Але є й зовнішній підхід — проксі (proxy), який стоїть між браузером (чи будь-яким застосунком) і мережею й бачить увесь трафік. Це корисно там, куди тестовий route не дістає: мобільні застосунки, десктоп-клієнти, трафік, що йде повз керований Playwright контекст.
Charles — комерційний GUI-проксі (написаний на Java, працює на macOS, Windows, Linux). Дає наочну картину трафіку й потужні інструменти маніпуляції: Map Local (підмінити відповідь локальним файлом), Map Remote (перенаправити запит на інший хост), Rewrite (переписати заголовки чи тіло за правилами), Breakpoints (зупинити запит і відредагувати вручну на льоту) та throttling для імітації повільної мережі.
mitmproxy — безкоштовний інструмент з відкритим кодом. Має інтерактивний термінальний інтерфейс (mitmproxy), вебінтерфейс (mitmweb) і неінтерактивний режим для скриптів (mitmdump). Головна його сила для автоматизації — аддони (addons) на Python: ви пишете скрипт, що програмно змінює будь-який запит чи відповідь, і легко вбудовуєте його в CI.
| Ознака | Charles | mitmproxy |
|---|---|---|
| Ліцензія | Комерційна (платна) | Відкритий код, безкоштовний |
| Інтерфейс | Графічний | Термінальний + веб + CLI |
| Скриптованість | Обмежена, через правила | Повна, аддони на Python |
| Поріг входу | Нижчий, наочний | Вищий, зате гнучкіший |
| Автоматизація в CI | Незручно | Природно (mitmdump) |
Обидва працюють і з HTTPS, але для цього браузер чи пристрій має довіряти кореневому сертифікату проксі (його встановлюють вручну). Без цього зашифрований трафік проксі побачить лише як нерозбірливий потік — це наслідок того, що TLS і задуманий захищати обмін від посередника.
Коли обирати проксі замість перехоплення в тесті. Проксі виграє, якщо трафік іде повз ваш тестовий фреймворк (нативний застосунок, стороннє SDK), якщо потрібно розслідувати наживо реальну сесію або якщо треба глобальний throttling. Перехоплення в тесті виграє за детермінізмом, простотою в CI та тим, що живе прямо в коді тесту поряд з перевірками. Для суто вебового E2E перехоплення зазвичай зручніше; проксі — інструмент дослідження й нестандартних випадків.
Ризики надмірного мокання
Мокання — гострий інструмент, і ним легко порізатися. Головна небезпека формулюється одним реченням: замокавши все, ви тестуєте свої припущення про бекенд, а не сам бекенд. Тест стає зеленим і водночас відірваним від реальності.
Дрейф контракту (contract drift). Ви замокали /api/user, що повертає поле name. За півроку бекенд перейменував його на fullName. Реальний застосунок зламався, а ваш тест і далі зелений, бо мок нічого не знає про зміну. Це найпідступніший клас проблем: що більше моків, то більша ймовірність, що якийсь із них тихо розійшовся з дійсністю. HAR-записи страждають від цього так само — вони теж застигають у минулому.
Хибна впевненість. Зелений набір мокнутих тестів створює відчуття, що інтеграція працює. Але жоден з них не перевіряв справжній мережевий шлях: автентифікацію, CORS, серіалізацію, реальні коди статусів. Інтеграційні баги проскакують крізь таку сітку непоміченими.
Крихкі перевірки на деталі запиту. Мок, що перевіряє точне тіло запиту до останнього поля, ламається від будь-якої несуттєвої зміни — доданого заголовка аналітики, нового необовʼязкового параметра. Такий тест дає багато шуму й мало сигналу.
Вартість підтримки. Кожен мок — це код, який хтось має оновлювати разом зі зміною API. Сотні моків легко перетворюються на паралельну «тіньову» копію бекенда, яку ніхто не синхронізує.
Практичні орієнтири, як тримати баланс:
- Мокайте зовнішнє й некероване: сторонні платіжні шлюзи, аналітику, флакі-залежності. Не мокайте без потреби власний основний API в наскрізних тестах — саме його інтеграцію ви й хочете перевірити.
- Тримайте кілька справжніх E2E-тестів на живому бекенді для критичних шляхів. Хай вони повільніші й трохи флакіші — вони ловлять те, чого мок не побачить ніколи.
- Для перевірки узгодженості з бекендом використовуйте контрактні тести (contract tests) — окремий шар, що звіряє мок зі справжньою схемою API, щоб дрейф контракту не проходив непомітно.
- Мокайте на найтоншому потрібному рівні. Якщо мета — показати порожній стан, підмініть лише один ендпоінт, а не всю мережу. Що менше замокано, то ближче тест до реальності.
- Не перевіряйте зайвого в тілі запиту — фіксуйте лише поля, що справді важливі для сценарію.
Правильна модель — не «мок проти реального бекенда», а піраміда: багато швидких мокнутих тестів на логіку інтерфейсу й крайові випадки, тонкий шар контрактних тестів для узгодженості, і кілька дорогих наскрізних тестів на живій системі для найважливіших шляхів. Мокання прибирає флак і прискорює зворотний звʼязок там, де реальний бекенд лише заважає, і чесно поступається місцем живому трафіку там, де перевірити треба саме інтеграцію.
Що таке перехоплення й мокання мережі і навіщо вони в автотестах?
Перехоплення (interception) — це техніка, коли тест вклинюється між браузером і мережею й сам вирішує долю кожного запиту, а мокання (mocking) — коли тест власноруч формує відповідь замість сервера. Механізм такий: фреймворк реєструє правило «для запитів за цим шаблоном URL викликай обробник», і коли браузер збирається зробити відповідний запит, керування передається тесту. Фронтенд при цьому нічого не підозрює — для нього рукотворна відповідь виглядає як звичайний мережевий обмін. Практична цінність: E2E-тест перестає залежати від реального бекенда, який може відповісти повільніше, повернути інший порядок елементів або «моргнути» пʼятисоткою. У Playwright це робиться через page.route() з викликом fulfill, continue або abort усередині обробника.
Навіщо мокати відповіді бекенда, якщо можна ганяти тести на реальному сервері?
Головна причина — детермінізм: замокана відповідь завжди однакова, тож результат тесту залежить лише від коду фронтенда, а не від стану чужої системи. Далі — ізоляція стану: тести, що ходять у спільну базу, заважають один одному в паралельних прогонах, а мок розриває цей звʼязок. Третє — швидкість: локальний мок відповідає майже миттєво замість десятків-сотень мілісекунд на кожен реальний запит. Четверте — крайові випадки: змусити продакшн-API повернути 500 чи порожній список майже неможливо, а мок робить це одним рядком. Пʼяте — незалежність від готовності бекенда: за узгодженим контрактом фронтенд-тести пишуться ще до того, як ендпоінт реалізували. Сильний кандидат одразу назве і зворотний бік: замокавши відповідь, ми свідомо перестаємо перевіряти реальну інтеграцію, тому це проєктне рішення, а не дефолт.
Які рішення може ухвалити обробник перехопленого запиту?
Рішень рівно три, і ця модель однакова в усіх інструментах. Пропустити (continue) — запит іде далі на реальний сервер, але дорогою можна змінити URL, метод, заголовки чи тіло; типове застосування — логування або підміна заголовка авторизації. Підмінити (fulfill) — сервер не викликається взагалі, браузер отримує рукотворну відповідь; це шлях для мокання даних, помилок і порожніх станів. Обірвати (abort) — запит завершується мережевою помилкою, чим імітують втрату зʼєднання чи недоступність сервісу. Важлива властивість: мок вибірковий — запити, що не підійшли під жоден шаблон, ідуть на реальний сервер своїм життям.
Як у тесті показати порожній список, коли в тестовій базі повно даних?
Замість вичищати базу — підмінити відповідь ендпоінта порожнім масивом через fulfill: наприклад, на **/api/notifications віддати body з items: []. Фронтенд отримає валідний JSON і покаже заглушку «Немає даних», яку тест і перевіряє — саме заглушку, а не спінер чи помилку. Той самий прийом працює для інших станів, які важко відтворити в реальній системі: дуже великий список для перевірки пагінації й віртуалізації, профіль з role: admin без реального адмін-акаунта, прикордонні значення на кшталт нуля чи відʼємних чисел, обидві гілки фіч-флага. Ключова цінність: сценарій, який у живій системі трапляється раз на місяць, стає доступним у кожному прогоні й повністю передбачуваним — це прямо зменшує флак.
Як відтворити помилку 500 від сервера і що при цьому перевіряти в UI?
Найпростіше — через fulfill повернути відповідь зі status: 500 і тілом помилки: живий сервер збоїти на вимогу не змусиш, а мок робить це одним рядком. Перевіряти варто не лише появу банера помилки, а й те, що застосунок не втрачає введені користувачем дані й не «зависає» в стані завантаження назавжди. Крім 500, у тестах типово імітують 503 (повідомлення про тимчасову недоступність, ретрай), 429 (обробка обмеження частоти), 502/504 (проблеми на проксі чи шлюзі) та 401/403 (редірект на логін або повідомлення про брак прав). Тут мокання зʼєднується з локаторами й очікуваннями: ми штучно створюємо умову й асертимо, що зʼявився саме той елемент, який має показатися при помилці.
Як протестувати спінер, скелетон чи блокування кнопки на час запиту?
Потрібна штучна затримка (latency): в обробнику маршруту витримати паузу — наприклад, setTimeout на 5 секунд у промісі — і лише потім викликати continue або fulfill. Поки відповідь «їде», тест встигає перевірити, що спінер показався, кнопка заблокована, скелетон на місці. Ця техніка ловить цілий клас багів: спінер, що не зникає; кнопку, яку можна натиснути двічі й відправити дубль запиту; таймаут, що спрацьовує зарано. Є й побічна користь для стабільності: якщо тест жорстко чекає елемент з коротким таймаутом, штучна затримка виявить, що очікування налаштоване занадто оптимістично й у реальній повільній мережі тест сипатиметься.
Як зімітувати обрив зʼєднання чи недоступність сервісу?
Запит обривають замість відповідати на нього: у Playwright — route.abort('failed'). Аргумент — код помилки з фіксованого набору Playwright: за замовчуванням failed, серед інших — timedout, connectionrefused, internetdisconnected, namenotresolved, connectionreset. Назви кодів однакові незалежно від рушія, бо це перелік рівня Playwright, але який саме мережевий стан вони зімітують у конкретному браузері — Chromium, Firefox чи WebKit — може відрізнятися в деталях. Практичний сценарій: обірвати **/api/** і перевірити, що інтерфейс показує зрозумілу помилку, а не білий екран чи вічний спінер.
Тест з моком падає: запит пішов на реальний сервер, хоча маршрут зареєстровано. У чому типова причина?
Найчастіша причина — порядок: правило перехоплення спрацьовує лише для запитів, зроблених після його реєстрації. Якщо спершу викликати перехід на сторінку, а потім реєструвати маршрут, тест «спізниться» — запит уже полетів на живий бекенд. Тому маршрут завжди ставлять до дії, яка тригерить запит: до page.goto(), до кліку. Друга типова причина — шаблон URL не збігається з реальним запитом, і оскільки мок вибірковий, неспійманий запит тихо йде на сервер без жодної помилки. Діагностувати таке зручно, подивившись реальний трафік тесту — які URL справді пішли й чи відповідають вони шаблону.
Чим стаб відрізняється від мока?
Заглушка (stub) просто повертає заздалегідь заготовлену відповідь — її завдання дати системі валідні дані, щоб тест рухався далі, і її не цікавить, скільки разів і з якими аргументами її викликали. Мок (mock) — це заглушка з очікуваннями (expectations): він не лише віддає відповідь, а й перевіряє сам факт і характер взаємодії — чи був викликаний ендпоінт, скільки разів, з яким тілом чи заголовками. Тобто стаб — це «нехай сервер поверне ось це», а мок — «переконаймося, що фронт надіслав саме це». Таксономію тестових двійників (test double) увів Джерард Мезарос у книзі xUnit Test Patterns, а популяризувала її стаття Мартіна Фаулера «Mocks Aren't Stubs». На практиці межа розмита й більшість інструментів дають обидві можливості одним механізмом, але різниця підказує, що саме перевіряє тест: стан інтерфейсу після відповіді — чи коректність вихідного запиту.
Як перевірити, що кнопка «Зберегти» надсилає саме той запит, який очікується?
Перехопити запит і зробити assertion на його вміст: в обробнику маршруту прочитати тіло через route.request().postDataJSON(), зберегти його в змінну, відповісти через fulfill, а після кліку порівняти збережене тіло з очікуваним. Так ми перевіряємо не стан інтерфейсу після відповіді, а коректність того, що фронтенд відправив — метод, тіло, заголовки. Строго за таксономією Мезароса це ближче до шпигуна (spy): стаб, який записує інформацію про те, як його викликали, а перевірка йде після дії, а не через жорсткі очікування наперед. У мережевому тестуванні цю деталь зазвичай не розрізняють, але корисно памʼятати, що assertion на вміст запиту й фіксоване «має бути викликано рівно раз» — різні за строгістю підходи. Важливий практичний нюанс: не фіксуйте в перевірці все тіло до останнього поля — лише поля, важливі для сценарію, інакше тест ламатиметься від кожного доданого аналітичного параметра.
Що станеться, якщо на один шаблон URL зареєструвати кілька обробників?
Спрацьовують вони у зворотному до реєстрації порядку: першим запит бере останній зареєстрований обробник, і він може явно передати керування попередньому — у Playwright це метод fallback. Це не примха, а корисний механізм шаруватості: загальний мок кладуть у налаштування (setup) для всіх тестів, а в окремому тесті перекривають його вужчим правилом, не чіпаючи спільний код. Без розуміння цього порядку легко отримати загадку «мій мок у тесті не працює», коли насправді його перекрив інший обробник. Окремо варто знати про область дії: маршрут буває на рівні сторінки (page) або цілого контексту браузера (context) — контекстний зручний, коли той самий мок потрібен усім вкладкам, наприклад для глушіння аналітики.
Чому мок треба прибирати після тесту і як це виглядає в CodeceptJS?
Залишений маршрут перехоплюватиме запити наступних тестів у тому самому воркері й даватиме плаваючі падіння, які важко діагностувати, — тож прибирання моку це частина ізоляції стану, а не косметика. У CodeceptJS з Playwright-хелпером мок ставлять через I.mockRoute() з обробником, а знімають через I.stopMockingRoute() з тим самим шаблоном. Сам CodeceptJS з браузером напряму не працює — усе йде через хелпер, найчастіше на базі Playwright, тому модель перехоплення та сама, лише синтаксис через обʼєкт I. Історична деталь для повноти: існував окремий хелпер на базі Polly.js (@codeceptjs/mock-request) з методами на кшталт I.mockRequest() — він працює з Puppeteer і WebDriver, але не з Playwright, і корисний саме для режиму запису-відтворення на не-Playwright рушіях. Сигнатури цих методів змінювалися між релізами, тому завжди варто звіритися з документацією версії у своєму проєкті.
Що таке HAR-файл і навіщо він AQA?
HAR (HTTP Archive) — формат запису мережевого обміну: файл з розширенням .har, усередині звичайний JSON зі списком усіх запитів і відповідей — URL, методи, заголовки, тіла, часи, коди статусів. Його вміють експортувати DevTools усіх основних браузерів (вкладка Network) і проксі-інструменти; офіційним стандартом W3C він так і не став — лишається де-факто стандартом спільноти. Для AQA у нього дві ролі. Перша — діагностика: приклавши HAR до звіту про впалий тест, ви даєте розробнику точну картину, які запити пішли й що повернулося. Друга — режим запису-відтворення (record and replay): один раз записуєте реальний обмін, а далі тести відтворюють відповіді з файлу, не торкаючись сервера; Playwright уміє це на рівні сторінки чи контексту, шукаючи збіг за URL і методом, а для POST — ще й за тілом запиту. Це золота середина між живим бекендом і ручними моками: дані реалістичні, бо колись справді прийшли з сервера, але залежності від сервера вже немає.
Які ризики в тому, щоб покласти HAR-файл у репозиторій?
Два принципово різні ризики. Перший — безпековий: HAR містить усе, зокрема заголовки авторизації, токени й куки, тому незачищений файл у репозиторії означає закомічені живі облікові дані; перед комітом .har обовʼязково чистять від секретів. Другий — старіння: HAR застигає в минулому, і якщо API змінив контракт, старий запис починає брехати — тест лишається зеленим на застарілих даних, тоді як реальний застосунок уже зламаний. Це той самий дрейф контракту, що й у ручних моків, просто в іншій обгортці, тому записи треба періодично оновлювати й підстраховуватися перевірками проти справжньої схеми API.
Коли обрати зовнішній проксі на кшталт Charles чи mitmproxy замість перехоплення у фреймворку?
Проксі стоїть між застосунком і мережею й бачить увесь трафік, тому виграє там, куди тестовий route не дістає: мобільні застосунки, десктоп-клієнти, стороннє SDK, трафік повз керований Playwright контекст, а також живе розслідування реальної сесії чи глобальний throttling. Charles — комерційний GUI-проксі з наочними інструментами: Map Local (підмінити відповідь локальним файлом), Map Remote, Rewrite, Breakpoints для ручного редагування на льоту. mitmproxy — безкоштовний і з відкритим кодом, з термінальним і вебінтерфейсом, а головне — з аддонами на Python і неінтерактивним mitmdump, що природно вбудовується в CI. Для HTTPS обом потрібна довіра браузера чи пристрою до кореневого сертифіката проксі — без цього зашифрований трафік лишиться нерозбірливим потоком, бо TLS якраз і задуманий захищати обмін від посередника. Для суто вебового E2E перехоплення в тесті зазвичай зручніше — воно детермінованіше, простіше в CI й живе поряд з перевірками; проксі — інструмент дослідження й нестандартних випадків.
Чим штучна затримка одного запиту відрізняється від throttling?
Затримка в обробнику маршруту стосується конкретного запиту: ви сповільнюєте одну відповідь і дивитеся, як UI переживає її очікування. Throttling — це обмеження пропускної здатності всього мережевого каналу, імітація повільного 3G: впливає на кожен запит, на завантаження ресурсів, на все одразу. Перше робиться прямо в тесті парою рядків, друге — через протокол налагодження браузера (у Chromium — CDP-команди керування мережею) або через зовнішній проксі. Практична різниця: затримка відповідає на питання «що буде, якщо цей ендпоінт гальмує», throttling — «як застосунок загалом поводиться в поганій мережі». Плутати їх не варто: тест зі сповільненим одним запитом нічого не скаже про поведінку сторінки на повільному каналі.
Які ризики надмірного мокання і як тримати баланс?
Головна небезпека одним реченням: замокавши все, ви тестуєте свої припущення про бекенд, а не сам бекенд. Найпідступніший прояв — дрейф контракту (contract drift): бекенд перейменував поле name на fullName, реальний застосунок зламався, а тест і далі зелений, бо мок про зміну не знає. Додаються хибна впевненість (жоден мокнутий тест не перевіряв автентифікацію, CORS, серіалізацію, реальні коди статусів), крихкі перевірки на деталі запиту, що ламаються від доданого заголовка аналітики, і вартість підтримки — сотні моків перетворюються на несинхронізовану «тіньову» копію бекенда. Орієнтири балансу: мокати зовнішнє й некероване (платіжні шлюзи, аналітику, флакі-залежності), але не мокати без потреби власний основний API в наскрізних тестах; тримати кілька справжніх E2E на живому бекенді для критичних шляхів; додати шар контрактних тестів, що звіряє моки зі справжньою схемою API; мокати на найтоншому потрібному рівні — один ендпоінт, а не всю мережу. Правильна модель — піраміда: багато швидких мокнутих тестів на логіку інтерфейсу, тонкий шар контрактних, і кілька дорогих наскрізних на живій системі.
Три кейси, де контроль над мережею вирішує, зелений тест чи флак: підміна даних на порожній стан із головною пасткою «маршрут поставили після дії»; перевірка вихідного запиту через перехоплення тіла (той самий механізм, а по суті вже не стаб, а шпигун); і робота з негативними сценаріями — 500, штучна затримка, обрив зʼєднання. Скрізь — що дивитися і чому.
Кейс 1. Підміна даних: порожній стан і пастка «route після дії»
Найчастіший сценарій — віддати рукотворну відповідь замість того, щоб добувати особливий стан у базі. Перевіряємо, що на порожній масив інтерфейс показує заглушку «Немає даних», а не вічний спінер чи помилку:
import { test, expect } from '@playwright/test';
test('порожній стан списку сповіщень', async ({ page }) => {
// маршрут реєструємо ДО навігації, яка тригерить запит
await page.route('**/api/notifications', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ items: [], total: 0 }),
}));
await page.goto('https://app.example.com/notifications');
await expect(page.getByText('Немає сповіщень')).toBeVisible();
});
Що дивитися і чому:
- Порядок — це не стиль, а причина найтиповішого падіння. Правило перехоплення спрацьовує лише для запитів, зроблених ПІСЛЯ його реєстрації. Постав
page.routeпісляpage.goto— і тест «спізниться»: сторінка вже сходила на реальний бекенд, мок перехопить порожнечу, а асершн упаде на живих даних. Симптом плутають з багом застосунку, хоча винен порядок рядків. - Мок вибірковий. Шаблон
**/api/notificationsперехопив лише цей ендпоінт; аналітика, конфіг, аватарки поїхали на реальний сервер своїм життям. Це нормально: мокаємо рівно те, що потрібно сценарію, і не глушимо всю мережу без потреби. - Перевіряй саме заглушку, а не відсутність рядків. Порожній масив має вести до видимого empty state, а не до спінера, що не зникає. Тест на конкретний текст-заглушку ловить регресію, коли розробник забув гілку «даних немає».
Кейс 2. Перевірка вихідного запиту: коли стаб перетворюється на шпигуна
Попередній кейс перевіряв стан інтерфейсу після відповіді — це стаб. Тут завдання зворотне: переконатися, що кнопка «Зберегти» справді шле PATCH з правильним тілом. Механізм той самий route, але ми ще й читаємо запит і робимо assertion на його вміст — за таксономією це вже ближче до шпигуна (spy), бо факт виклику фіксуємо ПІСЛЯ дії, а не задаємо жорсткі очікування наперед:
import { test, expect } from '@playwright/test';
test('форма профілю шле правильний PATCH', async ({ page }) => {
let sentBody: unknown = null;
await page.route('**/api/profile', async (route) => {
// читаємо, що фронт зібрав у запит
sentBody = route.request().postDataJSON();
// і все одно віддаємо відповідь, щоб сценарій рухався далі
await route.fulfill({ status: 200, body: '{}' });
});
await page.goto('https://app.example.com/profile');
await page.fill('#name', 'Оновлене імʼя');
await page.click('#save');
// фіксуємо лише поле, що справді важливе для сценарію
expect(sentBody).toMatchObject({ name: 'Оновлене імʼя' });
});
Що дивитися і чому:
toMatchObject, а неtoEqualна все тіло. Мок, що звіряє запит до останнього поля, ламається від будь-якої несуттєвої зміни — доданого заголовка аналітики чи нового необовʼязкового параметра. Такий тест дає багато шуму й мало сигналу. Фіксуй лише поля, критичні для сценарію.fulfillтут обовʼязковий, навіть коли відповідь не цікавить. Ми перехопили запит, тож реального сервера вже немає; без відповіді фронт зависне на завантаженні й тест упаде не з тієї причини. Порожній'{}'достатній, щоб UI завершив дію.- Стаб vs мок — це різні цілі перевірки. Кейс 1 перевіряв, що показав інтерфейс; цей — що надіслав фронт. Тримати різницю в голові корисно: вона підказує, який саме бік взаємодії під контролем тесту.
Кейс 3. Негативні сценарії: 500, повільна відповідь і обрив
Щасливий шлях — половина роботи. Друга половина — поведінка, коли бекенд збоїть, тупить або зникає, а змусити живий сервер зробити це на вимогу зазвичай неможливо. Три техніки — три рішення обробника (fulfill, пауза + continue, abort):
// A. Помилка сервера: банер помилки + збережені дані
await page.route('**/api/checkout', (route) =>
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Internal Server Error' }),
}));
await page.click('#pay');
await expect(page.locator('.error-banner')).toBeVisible();
// B. Повільна відповідь: перевіряємо стани завантаження
await page.route('**/api/report', async (route) => {
await new Promise((resolve) => setTimeout(resolve, 5000));
await route.continue(); // після паузи пускаємо реальний запит
});
// C. Обрив зʼєднання: код помилки з фіксованого набору Playwright
await page.route('**/api/**', (route) => route.abort('failed'));
Що дивитися і чому:
- Після 500 перевіряй не лише банер. Важливо, що застосунок не втратив уже введені дані й не завис у стані завантаження назавжди. Банер зʼявився, а форма очистилася — це теж баг, просто менш очевидний.
- Штучна затримка ловить цілий клас багів. Спінер, що не зникає; кнопку, яку можна натиснути двічі й відправити дубль; таймаут, що спрацьовує зарано. Якщо тест жорстко чекає елемент з коротким таймаутом, пауза в 5 секунд оголить, що очікування налаштоване занадто оптимістично й у реальній повільній мережі тест сипатиметься.
abortбере код лише з набору Playwright. За замовчуванням'failed'; є ще'timedout','connectionrefused','internetdisconnected','namenotresolved','connectionreset'. Це імітація обриву одного запиту, а не глобальне сповільнення каналу: throttling усієї мережі (повільний 3G) роблять через CDP або зовнішній проксі, а не черезroute.- Прибирай моки після тесту. Залишений маршрут перехоплюватиме запити наступних тестів у тому самому воркері й дасть плаваючі падіння, які важко діагностувати. У CodeceptJS цю ж роль грає
I.stopMockingRoute(...)— це частина ізоляції стану, а не косметика.
Навіщо мокати мережу
- Можу пояснити, чому мок робить тест детермінованим: замокана відповідь однакова щоразу, тож результат залежить лише від коду, а не від стану чужої бази.
- Розумію, як мокання розриває спільний стан між тестами й прибирає гонки (race conditions) та «брудні» дані від попереднього прогону.
- Знаю ще три вигоди: швидкість (локальний мок відповідає майже миттєво), доступ до крайових випадків (edge cases) на кшталт 500 чи порожнього списку, і незалежність від готовності бекенда за узгодженим контрактом.
- Розумію зворотний бік: замокавши відповідь, я навмисно перестаю перевіряти реальну інтеграцію.
Модель перехоплення
- Можу пояснити єдину модель незалежно від інструмента: «шаблон URL → обробник → одне з трьох рішень».
- Знаю ці три рішення: пропустити (
continue), підмінити (fulfill), обірвати (abort) — і типове застосування кожного. - Розумію, чому маршрут треба реєструвати ДО дії, що тригерить запит: правило спрацьовує лише для запитів після його реєстрації, інакше тест «спізниться».
- Знаю, що мок вибірковий: запити, які не підійшли під шаблон, ідуть на реальний сервер.
- Розумію, чому кілька обробників на один шаблон спрацьовують у зворотному порядку реєстрації і як через
fallbackперекрити загальний мок вужчим у конкретному тесті. - Можу показати перехоплення в Playwright (
page.route/context.route+fulfill/continue/abort) та його аналог у CodeceptJS (I.mockRoute/I.stopMockingRouteчерез Playwright-хелпер). - Розумію, чому прибирання моку (
stopMockingRoute) — частина ізоляції: залишений маршрут перехоплюватиме запити наступних тестів у тому самому воркері.
Stub vs mock
- Можу пояснити різницю: заглушка (stub) лише віддає заготовлену відповідь, а мок (mock) — це заглушка з очікуваннями, що перевіряє сам факт і характер виклику.
- Знаю, коли який потрібен: stub — «нехай сервер поверне ось це» (перевіряю стан UI); mock — «переконаймося, що фронт надіслав саме це» (перевіряю вихідний запит через assertion на тіло).
- Розумію нюанс таксономії: assertion на вміст запиту після дії ближчий до шпигуна (spy), ніж до «чистого» мока з жорсткими очікуваннями наперед.
Підміна даних і негативні сценарії
- Знаю типові сценарії підміни даних: порожній стан, дуже великий список, особлива роль (
role: admin), локалізація, прикордонні значення, фіч-флаги — і що саме перевіряю в кожному. - Можу відтворити помилку сервера, повернувши потрібний код статусу (500, 503, 429, 502/504, 401/403), і знаю, що перевіряти в UI для кожного.
- Розумію, що при помилці перевіряю не лише банер, а й збереження введених даних і що застосунок не «зависає» в завантаженні.
- Можу зімітувати повільну відповідь через паузу в обробнику й пояснити, який клас багів це ловить: спінер, що не зникає, дубль запиту по подвійному кліку, зарано спрацьований таймаут.
- Знаю, як обірвати зʼєднання через
route.abort('failed'), і памʼятаю про інші коди (timedout,connectionrefused,internetdisconnectedтощо) з набору Playwright. - Розумію різницю між затримкою одного запиту й обмеженням каналу (throttling), яке роблять через CDP або зовнішній проксі.
HAR і проксі
- Можу пояснити, що HAR (HTTP Archive) — це JSON-запис усього обміну (URL, методи, заголовки, тіла, коди), де-факто стандарт спільноти, а не RFC/W3C.
- Знаю дві ролі HAR для AQA: артефакт діагностики впалого тесту й основа режиму запису-відтворення (record and replay), де збіг шукається за URL і методом, а для POST — ще й за тілом.
- Розумію дві засторони HAR: він старіє разом із контрактом API і містить секрети (токени, куки), тож перед комітом його треба чистити.
- Можу пояснити, коли обрати зовнішній проксі (Charles / mitmproxy) замість перехоплення в тесті: нативні застосунки, стороннє SDK, розслідування наживо, глобальний throttling.
- Знаю різницю Charles vs mitmproxy (GUI-комерція проти відкритого CLI зі скриптами-аддонами на Python) і чому для HTTPS проксі потрібна довіра до його кореневого сертифіката.
Ризики надмірного мокання
- Розумію головну небезпеку: замокавши все, я тестую свої припущення про бекенд, а не сам бекенд.
- Можу пояснити дрейф контракту (contract drift): бекенд перейменував поле, застосунок зламався, а мок (чи застарілий HAR) лишає тест зеленим.
- Знаю інші ризики: хибна впевненість (мок не перевіряє автентифікацію, CORS, серіалізацію), крихкі перевірки на точне тіло запиту, вартість підтримки «тіньової» копії бекенда.
- Можу сформулювати баланс: мокати зовнішнє й некероване, не мокати власний основний API в E2E, тримати кілька живих E2E на критичні шляхи, звіряти моки контрактними тестами й мокати на найтоншому потрібному рівні.
Тест червоний, бо реальний API відповів на 300 мс повільніше й повернув інший порядок елементів. Що це насправді сигналізує?
Питання
Обробник перехопленого запиту мусить ухвалити одне з трьох рішень. Яких?