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

    07 · Інструменти автоматизації

    Playwright: мережа — перехоплення, мокання, очікування відповідей

    Зміст

    Поки тест лише клікає, він залежить від усього, що стоїть за кнопкою: від бекенда, від сторонньої аналітики, від платіжного , від того, чи є сьогодні в базі замовлення в статусі «скасовано». Частина потрібних сценаріїв на живому оточенні або недосяжна (як віддати 500 з платіжного провайдера?), або нестабільна. Інша частина падає не тому, що застосунок зламався, а тому, що відповідь прийшла на 300 мс пізніше, ніж тест устиг подивитися.

    Мережа — та точка, де AQA перестає бути «клікером» і починає керувати умовами експерименту. Playwright дає API для спостереження й зміни мережевого трафіку браузера: під контроль потрапляють усі запити сторінки, зокрема XHR і fetch. Тому головне в цій темі — не рецепт page.route, а усвідомлена ціна такого вклинювання. Механіка браузерного перехоплення як явища розібрана в главі Перехоплення й мокання мережі; тут — те, що з цієї механіки робить конкретний інструмент.

    Де тест вклинюється в мережу

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

    Окремої інфраструктури для цього не потрібно: достатньо оголосити маршрут (route) для (browser context) через browserContext.route() або звузити його до однієї сторінки через page.route(). Маршрут, заданий на рівні контексту, діє й на попапи та відкриті з них сторінки, а не лише на початкову вкладку — саме тому глушіння аналітики чи зручно вішати на контекст, а не дублювати в кожному тесті.

    Перехоплення вибіркове: запити, що не підійшли під жоден шаблон, спокійно йдуть на реальний сервер. Це важлива інтуїція — маршрут не «вимикає мережу», він накриває лише те, що ви описали.

    Так

    Ні

    Так

    Ні

    Сторінка робить запит

    Є маршрут сторінки?

    Обробник маршруту

    Є маршрут контексту?

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

    fulfill: своя відповідь

    continue: далі зі змінами

    abort: мережева помилка

    Так

    Ні

    Так

    Ні

    Сторінка робить запит

    Є маршрут сторінки?

    Обробник маршруту

    Є маршрут контексту?

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

    fulfill: своя відповідь

    continue: далі зі змінами

    abort: мережева помилка

    Коли правил кілька, порядок не випадковий, і його варто знати дослівно. Маршрути сторінки мають пріоритет над маршрутами контексту. Серед кількох маршрутів, що збіглися з тим самим шаблоном, спрацьовує найпізніше зареєстрований: у доці це сформульовано як «they run in the order opposite to their registration», щоб останній зареєстрований завжди міг перекрити попередні. Практичний наслідок прямий: загальний кладуть у налаштування або в хук, а вужче правило реєструють пізніше — у самому тесті, — і воно переможе.

    Передати керування наступному обробнику в ланцюгу можна лише явно: route.fallback() викликає інші збіги перед відправленням запиту, тоді як route.continue() ланцюг зупиняє — «other matching handlers won't be invoked». Прибирають маршрут разом з обробником окремим викликом page.unroute().

    Три рішення обробника: fulfill, continue, abort

    Коли маршрут спрацював, обробник отримує обʼєкт Route і має рівно три варіанти дій. Це і є весь словник теми — решта деталей нашаровується на ці три рішення.

    fulfill — підмінити. Сервер не викликається взагалі; браузер отримує рукотворну відповідь. Дока формулює це без двозначності: «No requests to the API will be made».

    await page.route('**/api/v1/fruits', async (route) => {
      await route.fulfill({
        status: 200,
        contentType: 'application/json',
        body: JSON.stringify([{ id: 21, name: 'Strawberry' }]),
      });
    });

    Окремий, часто недооцінений режим — виконати справжній запит і пропатчити відповідь. Дока розводить ці два випадки прямо: іноді запит зробити необхідно, але відповідь треба підправити для відтворюваності. Тоді беруть справжню відповідь через APIRequestContext і віддають її в route.fulfill(), перевизначаючи лише потрібні поля:

    await page.route('**/api/v1/fruits', async (route) => {
      const real = await page.request.fetch(route.request());
      const body = await real.json();
      body.push({ id: 100, name: 'Loquat' });
      await route.fulfill({ response: real, body: JSON.stringify(body) });
    });

    Різниця не косметична: у першому режимі ви тестуєте свої уявлення про бекенд, у другому контакт із ним зберігається, а детермінованим робиться лише той фрагмент даних, який заважає.

    continue — пропустити далі, дорогою змінивши URL, метод, заголовки або тіло. Запит іде на реальний сервер.

    abort — обірвати. Це спосіб змоделювати недоступність сервісу. Код помилки тут — не довільний рядок, а значення із закритого набору, повністю переліченого в довіднику класу Route; за замовчуванням failed. Коди не синоніми, і саме тут ховається цінність для тест-дизайну: connectionrefused — «A connection attempt was refused» (сервіс не слухає), namenotresolved — «The host name could not be resolved» (DNS не резолвиться), internetdisconnected — «The Internet connection has been lost» (мережа зникла), timedout — «An operation timed out» (операція не вклалася в час), connectionreset — обрив, що відповідає TCP RST, connectionclosedTCP FIN. Це шість різних негативних сценаріїв, а не один «інтернет відвалився».

    // сервіс рекомендацій лежить — сторінка мусить лишитися робочою
    await page.route('**/api/v1/recommendations', (route) =>
      route.abort('connectionrefused')
    );

    Заголовки: що підміняється, а що мовчки ні

    Підміна заголовків — часта практична потреба: підкинути фіча-флаг, підмінити токен, прибрати заголовок. Робиться вона через continue з опціями:

    await page.route('**/*', async (route) => {
      const headers = { ...route.request().headers(), 'x-feature-flag': 'new-cart' };
      await route.continue({ headers });
    });

    Тут же живуть дві пастки, які довідник називає прямо, і обидві дають хибно зелений тест.

    Перша — редиректи. «The headers option applies to both the routed request and any redirects it initiates. However, url, method, and postData only apply to the original request and are not carried over to redirected requests.» Тобто заголовки переживають ланцюжок редиректів, а підмінені URL, метод і тіло — ні: після першого 3xx поїде оригінал.

    Друга — заборонені заголовки. Частину заголовків (Cookie, Host, Content-Length та інші) підмінити не можна: «If an override is provided for a forbidden header, it will be ignored and the original request header will be used». Жодної помилки при цьому не буде. Тест, який «підмінив» Cookie, щоб перевірити поведінку іншого користувача, насправді перевірив поведінку того самого — і показав зелене.

    Якщо один і той самий заголовок потрібен усім запитам, дешевше не перехоплювати нічого, а задати його конфігурацією: request успадковує з конфігу baseURL, extraHTTPHeaders і налаштування проксі. Конфігурація й фікстури — тема глави Playwright: фікстури, хуки і конфігурація.

    Мокати чи не мокати: ціна детермінізму

    Мережевий (stub) — це інструментальна реалізація (test double): компонента, який замінює реальну залежність «специфічним для тесту еквівалентом» і зобовʼязаний повторити її API, а не поведінку. У класичній таксономії стаб — це точка керування: він заганяє систему в гілку, у яку інакше не потрапити. Мок (mock) — інше: він перевіряє непрямі виходи під час взаємодії, і це не «стаб плюс асерти», а принципово інший спосіб уживання. Повна таксономія дублерів — тема глави Тестові дублери: stub, mock, fake, spy; варто знати, що в силабусі ISTQB CTFL її немає взагалі: стаби, драйвери, симулятори й названі там елементами тестового оточення (§1.4.3), а не видами дублерів.

    Що дає мокання в e2e:

    • детермінізм — відповідь однакова на кожному прогоні;
    • доступ до — порожній список, 500, протермінований токен, платіж із ;
    • швидкість і незалежність — тест не чекає на повільний сервіс і не залежить від готовності бекенда.

    Що воно забирає — і це головна частина відповіді на співбесіді. Замокавши все, ви тестуєте свої припущення про бекенд, а не бекенд. Дублер — знімок чужої поведінки на момент написання, і страховкою від його старіння є звірка дублера зі справжнім застосунком: це поширений спосіб реалізувати контрактні тести — так їх робить Pact, перевіряючи, що всі виклики до тестових дублерів повертають те саме, що повернув би реальний застосунок. Без такої звірки зʼявляється (schema drift) — усталеної галузевої назви явище не має, сама назва тут наша, а механізм простий: бекенд перейменував поле, продакшн зламався, а зелена.

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

    Робочий орієнтир: мокати зовнішнє й некероване (платіжні шлюзи, аналітику, сторонні віджети), не мокати без потреби власний основний API в і мокати на найтоншому потрібному рівні. Мокання на рівні сервісів (WireMock, msw, mock-сервери) і — предмет розділу про API-тестування.

    Очікувати відповідь, а не годинник

    Перехоплення потрібне не лише щоб підміняти. Часто тесту треба просто знати, що обмін відбувся: що після кліку пішов POST, що відповідь має статус 201, що аналітика надіслала подію. Для пасивного спостереження без втручання підписуються на події request і response. Щоб синхронізуватися з конкретною відповіддю після дії, беруть page.waitForResponse() замість фіксованої паузи — URL при цьому збігається за спрощеним glob-патерном, тим самим, що й у page.route().

    Тут є один синтаксичний нюанс, на якому спотикаються часто: очікування створюють до дії, і без await. Дока підписує це прямо в прикладі — «Start waiting for response before clicking. Note no await». Якщо поставити await одразу, тест зупиниться до кліку й дочекається , бо запит ще не почався.

    СерверБраузерТестСерверБраузерТестстворити проміс waitForResponse — без awaitклік по кнопціPOST /api/v1/orders201 Createdawait промісу — відповідь уже в рукахСерверБраузерТестСерверБраузерТестстворити проміс waitForResponse — без awaitклік по кнопціPOST /api/v1/orders201 Createdawait промісу — відповідь уже в руках
    const responsePromise = page.waitForResponse('**/api/v1/orders');
    await page.getByRole('button', { name: 'Оформити' }).click();
    const response = await responsePromise;
    expect(response.status()).toBe(201);

    Той самий порядок стосується й маршрутів: правило спрацьовує лише для запитів, зроблених після його реєстрації, тож маршрут ставлять до дії, яка тригерить запит, — інакше тест перехопить порожнечу.

    І застереження, яке рятує від цілого класу (flakiness): очікування мережі — це не спосіб дочекатися інтерфейсу. Відповідь 200 ще не означає, що список перемалювався. networkidle як сигнал готовності дока прямо позначає як DISCOURAGED і радить замість нього вебасерти. Правильна пара — дочекатися мережі там, де потрібен саме факт запиту, і покластися на та web-first перевірки там, де потрібен стан UI: Playwright: перевірки і автоочікування.

    HAR: записати сесію і відтворити її

    HAR (HTTP Archive) — JSON-формат експорту зафіксованої мережевої сесії, який читають різні інструменти: DevTools вміють вивантажити HAR і завантажити його назад для аналізу. Записаний HAR можна використати і як джерело підмінених відповідей — це режим запису-відтворення: дані реалістичні, бо колись справді прийшли з сервера, але тест від сервера вже не залежить.

    Два рішення тут треба ухвалити свідомо. Перше — безпека. Типовий експорт HAR із DevTools «санітизований»: із нього прибрано Cookie, Set-Cookie й Authorization, щоб не злити секрети в баг-репорт. Повний запис, навпаки, містить усе — токени, кукі, тіла відповідей із персональними даними. Тому .har, який їде в репозиторій, треба чистити руками, а не сподіватися на дефолт того інструмента, яким його записали.

    Друге — старіння. HAR-записи протухають так само, як будь-який інший дублер, і лишають тест зеленим на даних, яких бекенд уже не віддає. HAR добре працює там, де відповідь громіздка й стабільна (довідники, каталоги), і погано там, де контракт активно змінюють.

    APIRequestContext: підготувати стан, а не клікати

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

    Playwright дає для цього APIRequestContext, і дока сама перелічує три його застосування: тестувати серверне API, готувати серверний стан до відкриття застосунку і перевіряти серверні пост-умови після дій у браузері. Тобто «засій через API» — офіційно підтримуваний сценарій, а не обхідний шлях.

    test('нове замовлення видно у списку', async ({ page, request }) => {
      const created = await request.post('/api/v1/orders', {
        data: { sku: 'A-100', qty: 2 },
      });
      expect(created.ok()).toBeTruthy();
      const order = await created.json();
    
      await page.goto(`/orders/${order.id}`);
      await expect(page.getByTestId('order-status')).toHaveText('Нове');
    });

    Далі — чотири деталі, кожна з яких легко дає хибно зелений тест.

    Який саме контекст ви взяли. page.request — це скорочення для page.context().request, той самий екземпляр; він користується тим самим , що й браузерний контекст, і оновлює його з Set-Cookie. Ізольований контекст із власним сховищем кукі створюється лише через apiRequest.newContext(). Для негативних перевірок доступів це критично: якщо ходити через page.request, авторизація «протікає» з браузера, і перевірка «чужий користувач не бачить замовлення» може виявитися хибно зеленою.

    . За замовчуванням обʼєкт відповіді повертається для будь-якого коду — клієнт не кидає ні на 404, ні на 500. Кидання вмикає failOnStatusCode, і межа при цьому — 2xx і 3xx: з увімкненим прапорцем 301 помилкою теж не вважається.

    Редиректи. Вони йдуть автоматично, за замовчуванням до 20. Якщо перевіряти треба сам 3xx, редиректи потрібно вимкнути, інакше тест мовчки пройде весь ланцюжок і побачить 200.

    Таймаут. У запитів є дефолтний таймаут, який не знімається передачею AbortSignal; змінюють його setDefaultTimeout(), а повністю вимикають лише значенням 0. Тобто таймаут діє завжди — навіть якщо тест про нього не згадував.

    І бонус, який змикає цю главу з наступною: стан сховища взаємозамінний між браузерним контекстом і API-контекстом, тож логін можна зробити , а перевіряти — в UI. Механіка — глава Playwright: автентифікація і повторне використання стану. Тестування API як окрема дисципліна (перевірки тіла, схеми, контракти) належить розділу про API-тестування; тут API — лише засіб підготувати стан.

    Типові помилки

    Виглядає як «мок не працює», а насправді маршрут зареєстровано після дії. Правило накриває лише запити, зроблені після його реєстрації. Якщо page.route() стоїть після page.goto(), перехоплювати вже нічого.

    Виглядає як «перевірив поведінку іншого користувача», а насправді підміна Cookie проігнорована. Заборонені заголовки не підміняються, і помилки при цьому не буде — піде оригінальне значення.

    Виглядає як «підмінив URL запиту», а насправді підміна не дожила до редиректу. Заголовки переживають редиректи, url, method і postData — ні.

    Виглядає як очікування, а насправді гонка через await. Проміс waitForResponse створюють до дії й без await; інакше тест починає чекати на запит, який ще не пішов.

    Виглядає як баг , а насправді перехоплення його вимкнуло. Увімкнення маршрутизації вимикає HTTP-кеш («Enabling routing disables http cache»). Тест, який перевіряє 304 або поведінку Cache-Control, під активним маршрутом перевіряє інший застосунок — деталі механіки в главі Кешування.

    Виглядає як «мережеві події зникли», а насправді їх забрав Service Worker. сторінки (зокрема власний воркер інструментів мокання на кшталт MSW) перебирає запити на себе й робить їх невидимими для browserContext.route() і page.route(); дока радить у такому разі вимкнути воркери налаштуванням serviceWorkers: 'block'.

    Виглядає як зелена сюїта, а насправді дрейф схеми. Замоканий власний API робить тести незалежними від бекенда — зокрема від того, що бекенд змінив контракт. Лікує це не мокання, а звірка дублера з реальним застосунком.

    Виглядає як зручний фікстур-файл, а насправді злитий токен. Повний HAR містить Cookie й Authorization; санітизований експорт DevTools — ні. Перед .har треба чистити.

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

    Підсумок

    • Обробник маршруту має рівно три рішення — fulfill (сервер не викликається), continue (далі зі змінами) і abort (мережева помилка); поверх них живе змішаний режим — виконати справжній запит і пропатчити відповідь.
    • Перехоплення вибіркове й упорядковане: незбіглі запити йдуть у мережу, маршрути сторінки перекривають маршрути контексту, а серед збігів перемагає найпізніше зареєстрований.
    • Підміняється не все: заборонені заголовки мовчки ігноруються, а підмінені url, method і postData не переживають редирект — обидві пастки дають зелений тест на непідміненому запиті.
    • Мережу чекають промісом, створеним до дії й без await; мережева відповідь — сигнал про обмін, а не про готовність інтерфейсу.
    • Мок купує детермінізм за ціну довіри до власних припущень: має бути хоча б один тест без дублерів, а знімки (мок, HAR) потребують звірки з реальним застосунком.

    Можливі питання

    «Як замокати API-відповідь у Playwright?» Мінімальна відповідь — page.route() плюс route.fulfill(). Сильна відповідь одразу додає, що маршрут можна поставити на контекст, що незбіглі запити йдуть на реальний сервер і що є режим «зроби справжній запит, але пропатч відповідь». Інтервʼюер слухає, чи ви розрізняєте повний стаб і патч — це різні рівні довіри до бекенда.

    «Чим мок відрізняється від стаба?» Питання має точну відповідь: місцем і моментом перевірки, а не тим, «наскільки розумний обʼєкт». Стаб — точка керування входами, мок перевіряє виходи під час взаємодії й задає очікування на виклики. Помилковий шлях — відповідати «мок розумніший за стаб».

    «Що мокати в e2e, а що ні?» Тут перевіряють інженерну зрілість, а не знання API. Очікують межу: зовнішнє й некероване — так; власний основний API в наскрізному тесті — з великою осторогою; і обовʼязково згадку, що має лишитися хоча б один тест без дублерів, інакше сюїта верифікує ваші припущення.

    «Як дочекатися завершення запиту?» Правильно: page.waitForResponse() з промісом, створеним до дії. Червоний для інтервʼюера — фіксована пауза; жовтий — networkidle як сигнал готовності, бо дока прямо радить замість нього вебасерти.

    «Як змоделювати недоступний сервіс?» Очікують route.abort() — і бонусом розуміння, що коди помилок різні: connectionrefused, namenotresolved, timedout описують різні збої, і сторінка може реагувати на них по-різному.

    «Ваші тести зелені, а на продакшні баг у списку замовлень. Як таке можливо?» Відповідь, яку хочуть почути, називає замокану відповідь, застарілий HAR і розходження дублера з реальним контрактом бекенда — і план: звірити дублер із реальним застосунком або мати рівень тестів, який ходить у справжній бекенд. Назви-терміна тут не чекають: усталеної галузевої назви явище не має, тож описувати варто механізм. Нестабільність через мережу — тема глави Боротьба з флаком засобами інструмента.

    Джерела

    Де тест вклинюється в мережу

    • Playwright — Network — API спостереження й зміни трафіку, маршрут на контекст і на сторінку, дія на попапи, вибірковість перехоплення.
    • Playwright API — class: Route — зворотний до реєстрації порядок обробників, різниця route.continue() і route.fallback().
    • Playwright — class Page — пріоритет маршрутів сторінки над маршрутами контексту, найпізніше зареєстрований маршрут, page.unroute().
    • mitmproxy — How mitmproxy works — та сама модель вклинювання між клієнтом і сервером на рівні проксі.

    Три рішення обробника: fulfill, continue, abort

    • Playwright — Network — три рішення обробника; підміна відповіді на основі справжньої, отриманої через APIRequestContext.
    • Playwright — Mock APIs — повний стаб («запит до API не відбувається взагалі») проти режиму «виконати запит і пропатчити відповідь».
    • Playwright API — class: Route — закритий перелік кодів abort із дефолтом failed і описом кожного класу збою.

    Заголовки: що підміняється, а що мовчки ні

    • Playwright — Network — пропускання запиту зі змінами, зокрема підміна або видалення заголовка.
    • Playwright API — class: Route — заголовки переживають редиректи, а url/method/postData — ні; підміна заборонених заголовків мовчки ігнорується.
    • Playwright — API testing — фікстура request успадковує baseURL і extraHTTPHeaders із конфігурації.

    Мокати чи не мокати: ціна детермінізму

    Очікувати відповідь, а не годинник

    • Playwright — Network — події request і response для пасивного спостереження; page.waitForResponse() замість фіксованої паузи; glob-патерни URL у методах перехоплення.
    • Playwright — class Page — проміс очікування створюють до дії й без await; networkidle позначено як DISCOURAGED.

    HAR: записати сесію і відтворити її

    APIRequestContext: підготувати стан, а не клікати

    • Playwright — API testing — три застосування APIRequestContext, зокрема підготовка серверного стану до відкриття застосунку; взаємозамінність стану сховища між API- і браузерним контекстом.
    • Playwright — class APIRequestContextpage.request як скорочення й спільне сховище cookie, ізоляція через apiRequest.newContext(), failOnStatusCode з межею 2xx/3xx, дефолт 20 редиректів, дефолтний таймаут.
    • ISTQB® Certified Tester Foundation Level Syllabus v4.0.1 — §1.4.3–1.4.4 і §4.1: вимоги до тестових даних визначають на етапі дизайну, а створення тестових фікстур — окрема діяльність реалізації тестів.

    Типові помилки

    Підсумок

    Можливі питання

    • xUnit Test Patterns — Test Double — стаб як точка керування, мок як перевірка непрямих виходів, вимога тесту без дублерів.
    • Martin Fowler — Mocks Aren't Stubs — мок як специфікація очікуваних викликів проти стаба із заготовленими відповідями.
    • Playwright — Networkpage.route() і route.fulfill(), обрив запиту, очікування конкретної відповіді.
    • Playwright API — class: Route — коди abort як різні класи мережевого збою.
    • Playwright — class Pagenetworkidle як DISCOURAGED і рекомендація вебасертів.

    Пояснення

    «Поясни» працює з власним API-ключем Anthropic: запит іде з вашого браузера прямо до Anthropic.

    Свій API-ключ (BYOK)

    Вставте власний ключ Anthropic — пояснення працюватиме на реальній моделі. Ключ зберігається лише у вашому браузері (localStorage), нікуди не надсилається, крім api.anthropic.com, і не логується.