vyvchy
    Теми розділу

    03 · Веб і мережі для AQA

    Перехоплення й мокання мережі

    Зміст

    Автотест, що відкриває сторінку в браузері, майже ніколи не працює ізольовано. Сторінка робить десятки мережевих запитів (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)Запит завершується мережевою помилкоюІмітація втрати зʼєднання, недоступності сервісу

    Ні

    Так

    Браузер робить запит

    Підходить під шаблон URL?

    Запит іде на реальний сервер

    Викликається обробник маршруту

    Пропустити continue

    Підмінити fulfill

    Обірвати abort

    Реальний сервер,
    можна змінити запит

    Рукотворна відповідь
    без сервера

    Мережева помилка

    Ні

    Так

    Браузер робить запит

    Підходить під шаблон URL?

    Запит іде на реальний сервер

    Викликається обробник маршруту

    Пропустити continue

    Підмінити 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
    500Internal Server ErrorЗагальне повідомлення про помилку, збереження введених даних
    503Service UnavailableПовідомлення «сервіс тимчасово недоступний», ретрай
    429Too Many RequestsОбробку обмеження частоти, повідомлення про ліміт
    502 / 504Bad Gateway / Gateway TimeoutПоведінку при проблемах на проксі/шлюзі
    401 / 403Unauthorized / 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 — ще й за тілом запиту.

    1. запис обміну

    2. відтворення відповідей

    Реальний сервер

    HAR-файл

    Тест без сервера

    1. запис обміну

    2. відтворення відповідей

    Реальний сервер

    HAR-файл

    Тест без сервера

    Такий підхід — золота середина між живим бекендом і ручними моками: дані реалістичні, бо колись справді прийшли з сервера, але тест від сервера вже не залежить. Дві сторони застереження. Перша: HAR старіє — якщо API змінив контракт, старий запис почне брехати, і тест лишатиметься зеленим на застарілих даних (про це нижче в ризиках). Друга, суто безпекова: HAR містить усе, зокрема заголовки авторизації, токени й куки (cookies). Перед тим як покласти .har у репозиторій, його треба почистити від секретів — інакше ви закомітите живі облікові дані.

    Проксі-інструменти: Charles і mitmproxy

    Перехоплення в тесті вклинюється всередині фреймворка. Але є й зовнішній підхід — проксі (proxy), який стоїть між браузером (чи будь-яким застосунком) і мережею й бачить увесь трафік. Це корисно там, куди тестовий route не дістає: мобільні застосунки, десктоп-клієнти, трафік, що йде повз керований Playwright контекст.

    Застосунок або браузер

    Проксі:
    Map Local, Rewrite, throttling

    Сервер / Мережа

    Застосунок або браузер

    Проксі:
    Map Local, Rewrite, throttling

    Сервер / Мережа

    Charles — комерційний GUI-проксі (написаний на Java, працює на macOS, Windows, Linux). Дає наочну картину трафіку й потужні інструменти маніпуляції: Map Local (підмінити відповідь локальним файлом), Map Remote (перенаправити запит на інший хост), Rewrite (переписати заголовки чи тіло за правилами), Breakpoints (зупинити запит і відредагувати вручну на льоту) та throttling для імітації повільної мережі.

    mitmproxy — безкоштовний інструмент з відкритим кодом. Має інтерактивний термінальний інтерфейс (mitmproxy), вебінтерфейс (mitmweb) і неінтерактивний режим для скриптів (mitmdump). Головна його сила для автоматизації — аддони (addons) на Python: ви пишете скрипт, що програмно змінює будь-який запит чи відповідь, і легко вбудовуєте його в CI.

    ОзнакаCharlesmitmproxy
    ЛіцензіяКомерційна (платна)Відкритий код, безкоштовний
    ІнтерфейсГрафічнийТермінальний + веб + CLI
    СкриптованістьОбмежена, через правилаПовна, аддони на Python
    Поріг входуНижчий, наочнийВищий, зате гнучкіший
    Автоматизація в CIНезручноПриродно (mitmdump)

    Обидва працюють і з HTTPS, але для цього браузер чи пристрій має довіряти кореневому сертифікату проксі (його встановлюють вручну). Без цього зашифрований трафік проксі побачить лише як нерозбірливий потік — це наслідок того, що TLS і задуманий захищати обмін від посередника.

    Коли обирати проксі замість перехоплення в тесті. Проксі виграє, якщо трафік іде повз ваш тестовий фреймворк (нативний застосунок, стороннє SDK), якщо потрібно розслідувати наживо реальну сесію або якщо треба глобальний throttling. Перехоплення в тесті виграє за детермінізмом, простотою в CI та тим, що живе прямо в коді тесту поряд з перевірками. Для суто вебового E2E перехоплення зазвичай зручніше; проксі — інструмент дослідження й нестандартних випадків.

    Ризики надмірного мокання

    Мокання — гострий інструмент, і ним легко порізатися. Головна небезпека формулюється одним реченням: замокавши все, ви тестуєте свої припущення про бекенд, а не сам бекенд. Тест стає зеленим і водночас відірваним від реальності.

    Дрейф контракту (contract drift). Ви замокали /api/user, що повертає поле name. За півроку бекенд перейменував його на fullName. Реальний застосунок зламався, а ваш тест і далі зелений, бо мок нічого не знає про зміну. Це найпідступніший клас проблем: що більше моків, то більша ймовірність, що якийсь із них тихо розійшовся з дійсністю. HAR-записи страждають від цього так само — вони теж застигають у минулому.

    Хибна впевненість. Зелений набір мокнутих тестів створює відчуття, що інтеграція працює. Але жоден з них не перевіряв справжній мережевий шлях: автентифікацію, CORS, серіалізацію, реальні коди статусів. Інтеграційні баги проскакують крізь таку сітку непоміченими.

    Крихкі перевірки на деталі запиту. Мок, що перевіряє точне тіло запиту до останнього поля, ламається від будь-якої несуттєвої зміни — доданого заголовка аналітики, нового необовʼязкового параметра. Такий тест дає багато шуму й мало сигналу.

    Вартість підтримки. Кожен мок — це код, який хтось має оновлювати разом зі зміною API. Сотні моків легко перетворюються на паралельну «тіньову» копію бекенда, яку ніхто не синхронізує.

    Практичні орієнтири, як тримати баланс:

    • Мокайте зовнішнє й некероване: сторонні платіжні шлюзи, аналітику, флакі-залежності. Не мокайте без потреби власний основний API в наскрізних тестах — саме його інтеграцію ви й хочете перевірити.
    • Тримайте кілька справжніх E2E-тестів на живому бекенді для критичних шляхів. Хай вони повільніші й трохи флакіші — вони ловлять те, чого мок не побачить ніколи.
    • Для перевірки узгодженості з бекендом використовуйте контрактні тести (contract tests) — окремий шар, що звіряє мок зі справжньою схемою API, щоб дрейф контракту не проходив непомітно.
    • Мокайте на найтоншому потрібному рівні. Якщо мета — показати порожній стан, підмініть лише один ендпоінт, а не всю мережу. Що менше замокано, то ближче тест до реальності.
    • Не перевіряйте зайвого в тілі запиту — фіксуйте лише поля, що справді важливі для сценарію.

    Правильна модель — не «мок проти реального бекенда», а піраміда: багато швидких мокнутих тестів на логіку інтерфейсу й крайові випадки, тонкий шар контрактних тестів для узгодженості, і кілька дорогих наскрізних тестів на живій системі для найважливіших шляхів. Мокання прибирає флак і прискорює зворотний звʼязок там, де реальний бекенд лише заважає, і чесно поступається місцем живому трафіку там, де перевірити треба саме інтеграцію.