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

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

    Практичні сценарії AQA: флак і синхронізація

    Зміст

    Найчастіша реальна задача AQA — не написати тест, а зрозуміти, чому він падає раз на десять прогонів. Відповідь майже завжди та сама: тест і застосунок живуть у різному часі. Тест виконує команди швидко й послідовно, а браузер тим часом добудовує розмітку, виконує JavaScript і ходить у мережу. Розрив між «сторінка ще думає» і «тест уже клікає» — джерело більшості нестабільних падінь.

    Глава підсумкова: вона майже не вводить нової механіки, а звʼязує пройдене розділом. Це канон теми очікувань і синхронізації на сайті — інші глави посилаються сюди. При першому проході розділу її можна пропустити й повернутися тоді, коли почнете писати автотести. , і повна таксономія (flakiness) — тема розділу про автоматизацію; тут браузерного плюс процедура, якою його локалізують.

    Синхронізація: діяти рівно тоді, коли застосунок готовий

    Очікування ((auto-)wait) потрібне не тому, що «браузер повільний», а тому, що застосунок має власний темп. Стоїть синхронізація на двох речах.

    Перша — перевірки придатності (actionability), які інструмент робить сам: перед дією він чекає, поки всі релевантні перевірки пройдуть; не дочекався в межах — падає з TimeoutError. Для кліку перевіряється, що вказує рівно на один елемент і що елемент видимий, стабільний, отримує події й увімкнений; для введення додається «редагований». Означення точні, і саме в них — половина «загадок»:

    • Видимий — непорожній без visibility: hidden; нульовий розмір і display: none невидимі, а opacity: 0видимий.
    • Стабільний — той самий bounding box два послідовні кадри анімації.
    • Отримує події — є ціллю вказівникової події в точці дії; так ловиться оверлей, що перехоплює клік.
    • Увімкнений — не задизейблений disabled, батьківським fieldset чи aria-disabled="true".

    Опція force вимикає необовʼязкові перевірки — зокрема перестає контролювати, чи елемент отримує клік: це спосіб зробити тест зеленим, приховавши дефект.

    Друга річ — явні очікування прикладних сигналів, яких інструмент знати не може: чи навішений обробник, чи завершився запит, чи оновилися дані. (auto-waiting) закриває «елемент зʼявився і став клікабельним», не більше. Явне очікування — коли інженер сам пише умову й таймаут (WebDriverWait у Selenium); неявне реагує лише на «елемент не знайдено», і змішувати їх не варто. (hard wait) програє двічі: замало — тест флакне на CI, забагато — прогін розтягується, а web-first й так повторюють перевірку самі. Орієнтир рівня — те, що бачить користувач; анімації глушать окремо.

    Наша практика (не канон). Дві речі з цього абзацу джерела в такій формі не стверджують. Перша — межі автоочікування: дока задає їх переліком перевірок, а не застереженням «воно не знає про прикладні стани»; формулювання наше. Друга — глушіння анімацій CSS- на час прогону: джерельною тут є лише опція animations: 'disabled' для візуальних порівнянь.

    Деталь, що коштує годин: очікування відповіді створюють до дії, яка тригерить запит, інакше швидка відповідь прослизне непоміченою.

    // проміс створюємо ДО кліку — інакше швидка відповідь прослизне
    const orderCreated = page.waitForResponse(
      (r) => r.url().includes('/api/orders') && r.status() === 201
    );
    await page.getByRole('button', { name: 'Оплатити' }).click();
    await orderCreated;

    Подієвий цикл і гонка станів

    JavaScript у браузері виконується в одному потоці: у кожен момент (JavaScript engine) виконує рівно один фрагмент коду. Однопотоковий саме JS-рушій, а не браузер: мережу й таймери обслуговують окремі механізми поза цим потоком.

    Узгоджує все (event loop) — механізм, яким браузер координує події, дії користувача, скрипти, рендеринг і мережу. Спрощено: виконати весь синхронний код → коли стек порожній, узяти наступну задачу з черги → повторити. асинхронної дії стає задачею в черзі, і кожна задача обробляється повністю (run-to-completion). Черга мікрозадач стоїть окремо й чергою задач не є: мікрозадачі — це реакції промісів, queueMicrotask і MutationObserver, а задачі (task) — setTimeout, події DOM і XHR. Після кожної задачі цикл спорожнює чергу мікрозадач повністю.

    Звідси два наслідки. setTimeout(fn, 0) означає «якнайшвидше, коли звільниться потік», а не «зараз»: специфікація прямо каже, що таймери не гарантують виконання за розкладом. І не витісняється примусово — вона затримує і обробку подій, і кадр рендерингу, тож «клік не спрацював» іноді означає, що потік був зайнятий.

    Гонка станів (race condition) — коли результат залежить від порядку завершення асинхронних операцій, а цей порядок не гарантований. Оскільки задача обробляється повністю, гонка виникає не всередині задачі, а між задачами. Класика — живий пошук.

    СерверБраузерКористувачСерверБраузерКористувачна екрані результати для abвводить abзапит за abвводить abcзапит за abcвідповідь на abcвідповідь на ab, прийшла пізнішеСерверБраузерКористувачСерверБраузерКористувачна екрані результати для abвводить abзапит за abвводить abcзапит за abcвідповідь на abcвідповідь на ab, прийшла пізніше

    Лікують це скасуванням застарілого запиту через AbortController.

    Наша практика (не канон). Тут кілька формулювань наші, і джерела їх не несуть. Саме означення гонки джерело не дає — реф покриває лише відсутність гарантій щодо порядку задач. Приклад живого пошуку з двома запитами, стисла формула «перемагає останній, що відповів, хоча має перемагати останній, що надіслали» і конструкція «наївний тест містить одразу дві гонки» (елемента ще може не бути в DOM, а його вміст оновиться після запиту, що триває) — теж наші.

    Та сама механіка має й безпековий бік: у каталозі слабкостей гонка стоїть власним класом -362, а її найупізнаваніший вид — нащадок CWE-367 , де стан ресурсу змінюється між перевіркою й використанням. Припис OWASP звідси варто запамʼятати: якщо два запити можуть змагатися, напиши тест, який їх зіштовхне.

    «Елемент є в DOM, а клік не проходить»

    Наявність вузла в DOM — найраніша й найслабша гарантія готовності. Між «є в дереві документа» і «по ньому можна клікнути» лежить увесь конвеєр рендерингу (rendering pipeline): DOM і CSSOM → дерево рендерингу → layout → paint → composite.

    У дереві рендерингу лишаються тільки вузли, які реально малюватимуться: елемент із display: none там відсутній — він є в DOM, але геометрії не має, — а visibility: hidden присутній, займає місце й просто не видно. Layout рахує геометрію, і результат залежить від розміру екрана; тому свідомо зафіксований viewport — базова умова відтворюваності. Paint заповнює пікселі (растеризація належить саме сюди), composite складає шари.

    display none

    Так

    Ні, анімація

    Так

    Ні, оверлей зверху

    Так

    Вузол доданий у DOM

    Є в дереві рендерингу?

    Геометрії немає
    для тесту невидимий

    Layout: зʼявилася геометрія

    Стабільний два кадри?

    Дію перечікують

    Отримує події в точці дії?

    Клік перехоплено

    Увімкнений — дія проходить

    display none

    Так

    Ні, анімація

    Так

    Ні, оверлей зверху

    Так

    Вузол доданий у DOM

    Є в дереві рендерингу?

    Геометрії немає
    для тесту невидимий

    Layout: зʼявилася геометрія

    Стабільний два кадри?

    Дію перечікують

    Отримує події в точці дії?

    Клік перехоплено

    Увімкнений — дія проходить

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

    Дерево, події й межі пошуку

    DOM — не текст відповіді сервера, а структура в памʼяті браузера: вихідний HTML видно у View Page Source, а DOM показує панель Elements після виконання всього JavaScript. У SPA різниця драматична — у вихідному HTML може стояти лише порожній <div id="root">, тож інструмент без виконання JS (curl) вмісту не побачить. Дерево живе безперервно, тому локатор варто розуміти як рецепт пошуку, що резолвиться в момент дії, а не як збережене посилання на вузол (DOM, селектори та події).

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

    Другий клас «зникнень» — межі пошуку. Shadow DOM дає елементу власне приховане під-дерево, якого document.querySelectorAll() не бачить. буває open або closed: у першому element.shadowRoot віддає корінь, у другому — null, причому closed є сигналом наміру, а не барʼєром безпеки. Коли подія перетинає , спрацьовує ретаргетинг: зовні ціллю видно host-елемент. Playwright автоматично пробиває відкриті для CSS- і текстових локаторів, XPath — ні.

    iframe — не «ще один шар» сторінки, а інший документ зі своїм деревом: контекст треба перемкнути (frameLocator у Playwright, switchTo().frame() у Selenium). (same-origin policy) забороняє сторінці читати чужий документ, але драйвери працюють на рівні браузера, поза цим обмеженням. Звідси діагностичний порядок: «знаходжу вручну в DevTools, а тест — ні» першим ділом перевіряють на інший контекст.

    Локатор: стабільність починається з нього

    Селектор — шаблон, за яким добирають елементи; для тесту важать не сила правила в каскаді, а унікальність і стійкість збігу. Дві пастки дають не падіння, а гірше — тихо неправильний локатор. Перша: часткові збіги атрибутів (^=, $=, *=) рятують від згенерованих класів на кшталт Button_root__a1b2c, але ~= до трійки не належить — він шукає ціле слово у списку через пробіл. Друга — XPath: contains(text(), 'Оплата') для <button>Оплата <b>картою</b></button> бачить лише перший текстовий вузол, а //x[1] це не «перший x у документі»; ліки — contains(., ...) і (//x)[1]. У XPath ідуть заради осей угору по дереву (parent, ancestor), бо CSS їх не має, а реляційний :has() доступний в усіх браузерах лише з грудня 2023.

    Ієрархія вибору: роль плюс доступне імʼя → видимий текст або лейбл → data-testid → CSS за стабільним атрибутом → XPath. Локатори за роллю не залежать від верстки й побічно ловлять дефекти доступності: якщо «кнопка» насправді клікабельний div без ролі, пошук за роллю її не знайде. Слабке місце локаторів за текстом — локаль, а data-testid — явний контракт із кодом, який легко ненавмисно видалити, бо «він нічого не робить».

    Розмітка є, а обробників ще немає: CSR, SSR і гідратація

    Хто будує HTML — сервер чи браузер, — вирішує, коли сторінка стає придатною до дії. CSR (client-side rendering) будує розмітку в браузері силами JavaScript, тож у перші миті в DOM немає нічого корисного; SSR (server-side rendering) віддає готовий HTML. А гідратація (hydration) — запуск клієнтських скриптів, які додають стан та інтерактивність до відрендереної сервером розмітки, — створює найпідступніший стан: сторінка виглядає завантаженою, але не реагує на введення, доки скрипти не виконаються й обробники не навісяться.

    Звідси незручна правда: перевірка видимості підтверджує наявність у розмітці, а не готовність до взаємодії. Чекати треба на ознаку інтерактивності — зникнення disabled чи узгоджений маркер; стандартного DOM-атрибута «гідратацію завершено» не існує. Універсального «сторінка готова» теж немає: DOMContentLoaded настає, коли розібрано HTML, load чекає на підресурси, але не на AJAX, а очікування «тихої мережі» документація Playwright позначає як небажане.

    Архітектурна пара, з якої це росте, — SPA і MPA. Джерельним тут є один полюс: SPA (single-page application) завантажує один документ і далі перемальовує його вміст скриптом, а History API (pushState/replaceState) міняє адресу без запиту на сервер (Асинхронне завантаження: SPA/MPA, CSR/SSR).

    Наша практика (не канон). Практика домовлятися з командою про явний маркер готовності (data-hydrated, aria-busy="false") канонічного джерела не має — це річ конкретного застосунку й наша конвенція. Другий полюс архітектурної пари й висновки з порівняння — теж наші. Термін MPA (multi-page application) з означенням «кожен екран є окремим документом» канонічного джерела не має; порівняння частоти події load (у MPA при кожному переході, у SPA один раз на старті) — наш вивід; механіка «клієнтський роутер перехоплює клік по посиланню й сам змінює адресу та вміст» у джерелах не описана. Звідси й наше робоче правило: після клієнтської навігації перевіряй дві речі одночасно — адресу й появу контенту, бо сама лише адреса доводить тільки роботу роутера. Наше й застереження про проміжну сторінку в ланцюжку редиректів: очікування ловить URL, який існує лише мить, тож надійніше синхронізуватися на кінцевому стані.

    Дані приїжджають окремо: запит, мережа й затримка

    «Сторінка відкрилась» не означає «дані на місці». AJAX — обмін даними із сервером у фоні без перезавантаження; технічно це або старий XMLHttpRequest, або Fetch API. У fetch є пастка, на якій ловляться і розробники, і тестувальники: відповідь 404 чи 500 дає успішний проміс, бо реджект стається лише на частині помилок — як-от мережевій або поганій схемі. Тому статус перевіряють вручну: response.ok істинний лише для 2xx. Тіло при цьому є потоком і читається один раз. У XMLHttpRequest схоже: readyState === 4 означає «завершилось» — успішно або з помилкою.

    Під кожним запитом лежить повний життєвий цикл: DNS → TCP → TLS → надсилання → очікування сервера → відповідь. Перший запит до нового хоста дорожчий за наступні, бо зʼєднання перевикористовується (keep-alive), а стан pending означає не одне: сервер думає, запит стоїть у черзі через ліміт одночасних зʼєднань до походження, або зʼєднання довге навмисно (Життєвий цикл запиту, DevTools).

    (latency) — час, за який дані долають шлях від джерела до призначення, зазвичай як round-trip; (bandwidth) — скільки інформації проходить крізь зʼєднання за одиницю часу. Величини незалежні, тому десятки дрібних API-викликів і завантаження великого файла ламаються з різних причин. Сюди ж — (head-of-line blocking), де важливо не сплутати рівні: у HTTP/1.x воно на прикладному рівні, тому клієнти відкривають кілька зʼєднань; HTTP/2 прибирає його там, але його специфікація прямо зазначає, що блокування на рівні TCP протокол не усуває; HTTP/3 над QUIC закриває і це (HTTP/2 і HTTP/3).

    І про таймаути: коли тест падає з таймауту, спершу зрозумій, чий саме таймаут спрацював — мережевий, HTTP-клієнта, (клієнт бачить 504) чи тест-фреймворка. Піднімати таймаут замість діагнозу — ховати проблему.

    Як зробити тест детермінованим

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

    // маршрут реєструємо ДО дії, що тригерить запит
    await page.route('**/api/rates', (route) =>
      route.fulfill({ status: 200, body: JSON.stringify({ usd: 41.5 }) })
    );

    Тонкощі, на яких спотикаються. Правило спрацьовує лише для запитів, зроблених після реєстрації, а те, що не підійшло під шаблон, іде на реальний сервер. Заборонені заголовки (Cookie, Host, Content-Length) підмінити не вийде: перевизначення ігнорується, і тест «підмінив» Cookie лише в уяві. Коди обриву не синоніми: connectionrefused, namenotresolved та internetdisconnected моделюють три різні сценарії. Зворотний бік — дрейф контракту: замокавши все, ви тестуєте свої припущення про бекенд (Перехоплення й мокання мережі).

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

    Другий інструмент — (throttling): панель мережі ріже пропускну здатність. Пресети змінювалися між версіями браузера, тому надійніше задати власний профіль; поруч живуть Offline і Disable cache.

    Наша практика (не канон). Роль тротлінгу для AQA і сам приклад — наші, довідник панелі їх не описує: для нас це не «перевірка продуктивності», а спосіб зробити видимими гонки, які на швидкому каналі не встигають проявитися (класика — кнопка лишається активною, поки запит летить, і дає подвійне надсилання). Наше й робоче формулювання: якщо тест падає на повільному профілі, він не «флакне на швидкому» — він просто ще не впав.

    Третє — знати, що можна повторювати. (idempotency) означає, що кілька однакових запитів дають на сервері той самий ефект, що й один; такими є всі , а також PUT і DELETE. POST неідемпотентний: два однакові POST /orders створять два замовлення — звідси дублі від подвійного кліку. Тонкість, на якій легко зробити хибний висновок: ідемпотентність стосується стану сервера, а не відповіді, тож 404 на повторний DELETE — очікувана поведінка. Це й відповідає на питання при мережевому збої: ретрай безпечний чи зіпсує дані.

    Чужий стан: кеш, сховища, сесія й токен

    Останній великий клас флаку — стан, який лишився не від цього тесту. «Стара версія» приходить із чотирьох місць: HTTP-кеш браузера, кеш service worker, CDN або , кеш усередині застосунку (Кешування). Найдешевша ізоляція — свіжий профіль на прогін: у Playwright цю роль грає окремий . Але вимикати кеш у всіх тестах не можна: якщо перевіряється сама поведінка , кеш має бути ввімкнений.

    Сховища протікають так само. localStorage розділений лише за походженням і переживає перезапуск браузера; sessionStorage розділений ще й за вкладкою, тож новий контекст його не успадковує — часта причина, чому «залогінена» в одному місці сесія не працює в іншому. захоплює кукі, локальне сховище та IndexedDB, а sessionStorage не зберігається взагалі — його відновлюють вручну. Симптом протікання впізнаваний: тести зелені поодинці й червоні в наборі. Важлива межа: чистка на клієнті не прибирає серверного стану.

    Класична сесія тримає стан на сервері, а браузеру віддає лише ключ, ідентифікатор сесії в кукі; браузер сам вирішує за збереженими метаданими, чи додати кукі до наступного запиту. Тому «наступний тест бачить користувача розлогіненим» означає рівно одне: не поїхала валідна кукі сесії — стан не підхопився, кукі сесійна й не пережила рестарт, сесія згасла або невірні Domain/Path. Файл зі збереженим станом містить живі секрети, тож його не (Кукі, сесії та сховище браузера).

    Альтернатива серверній сесії — токен, найчастіше JWT (JSON Web Token). У звичайному підписаному JWT payload лише закодований base64url, а не зашифрований (шифрування дає окремий варіант — JWE), тож тест може прочитати claim exp і перевірити свіжість токена до запиту, а не ловити 401 постфактум. Для довгих прогонів це принципово: токен зі старту цілком може дожити своє в середині (Автентифікація та авторизація). Ширша рамка: кеш, кукі й сховища переживають перезавантаження, тож без чистки результат залежить від порядку запуску — а це вже не флак, а прихована залежність між тестами.

    Зелено локально, червоно в CI

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

    Свіжа машина. Кожен прогін іде на щойно розгорнутій віртуальній машині, стан між прогонами не переноситься, а один виконує одну за раз. Звідси перший крок налагодження, який дока інструмента ставить перед усіма іншими: переконатися, що агент узагалі здатний запускати браузери — офіційним на Linux-агентах або встановленням залежностей через CLI. Для помилок «Failed to launch browser» є задокументований важіль DEBUG=pw:browser.

    Ресурси, а не «повільний CI». Кількість воркерів за замовчуванням ставиться за виявленою кількістю ядер, і перевищення цього числа спричиняє непотрібні таймаути й падіння — тобто це не випадковість, яку лікує перезапуск джоби. Рекомендація дослівна: workers: 1 у CI, щоб пріоритетом були стабільність і відтворюваність; причина названа тут же — послідовний прогін дає кожному тесту всі ресурси системи. Ширшу паралельність беруть між джобами або потужною self-hosted системою.

    // playwright.config.ts — найдешевший CI-специфічний фікс
    export default defineConfig({
      workers: process.env.CI ? 1 : undefined,
    });

    Інше середовище рендерингу. За замовчуванням браузери запускаються в headless-режимі, а на Linux-агентах headed узагалі вимагає встановленого Xvfb (в офіційному образі й GitHub Action він уже є). Результат рендерингу залежить від операційної системи, її версії, налаштувань, заліза, джерела живлення й самого headless-режиму — звідси припис ганяти візуальні порівняння в тому самому середовищі, де знято бейзлайн.

    Паралельний прогін як лакмус ізоляції

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

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

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

    в усіх випадках одна: ізоляція знімає питання порядку виконання — саме вона робить паралельність і шардінг можливими. Тому «падає в паралелі» майже завжди читається як «спільна фікстура»: у таксономії xUnit клас можливий тільки за глобально спільної , яку різні тести змінюють. Сусідній клас — Resource Leakage: тести або система зʼїдають кінцеві ресурси, симптом упізнаваний («дедалі повільніше, потім раптово падає, після перезапуску тимчасово гоїться»), а рецепт джерела контрінтуїтивний — зменшити пул ресурсів до одиниці, щоб винний тест проявився швидше.

    Хто винен: застосунок, тест чи середовище

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

    Обидва «прості» виходи джерело називає шкідливими: прибрати тест означає свідомо втратити перевірку, а лишити його червоним — затулити інші падіння, бо перехід «зелене → червоне» помітний, а «одне червоне → два червоних» уже ні.

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

    СпостереженняКлас за таксономієюКуди дивитися
    Наодинці зелений, у сюїті падаєInteracting TestsСпільна фікстура, порядок, дані
    Перший запуск інакший за наступніUnrepeatable TestФікстура, що переживає прогін
    Падає лише в паралеліTest Run WarГлобально спільна фікстура
    Падає випадково навіть на одному ранеріNondeterministic TestРізні значення на вході алгоритму
    Дедалі повільніше, потім раптово падаєResource LeakageКінцеві ресурси; звести пул до одиниці

    Далі — три речі, які цю картину доповнюють. Перша: офіційна дока інструмента прямо називає гонку станів однією з головних причин флакі-тестів — іноді браузер устигає першим, іноді код тесту. Друга: до списку джерел нестабільності інший інструмент додає , доступність сервера й бази, залежності від ресурсів і мережу — тобто «винне середовище» є законною гілкою, а не відмовкою. Третя, найнезручніша: застосунок справді буває недетермінованим за побудовою. Класика — сховища: «the responses may not reflect the results of a recently completed write operation. If you repeat your read request after a short time, the response should eventually return the more recent item». Тут тест не флакне, а чесно ловить документовану поведінку, і детермінізм інколи перемикається опцією запиту, а не виборюється ретраями.

    Числа, якими цю тему часто підпирають, вживають лише з атрибуцією й датою: за вимірюванням Google 2016 року, 1,5 % прогонів дають флакі-результат, близько 16 % тестів мають якийсь рівень флаку, а 84 % переходів «зелений → червоний» — це флак, а не . Там-таки названо механізм шкоди: звикання до хибних сигналів веде до ігнорування справжніх падінь.

    Наша практика (не канон). Робочий порядок кроків — наш, джерела процедуру в такому вигляді не описують. Читаємо помилку буквально (перехоплення вказівникових подій, таймаут очікування селектора й expected 200, got 500 — це вже три різні гілки), далі дивимось артефакти прогону, далі відтворюємо в умовах CI, далі ганяємо тест підряд багато разів і насамкінець порівнюємо «наодинці» проти «в наборі». І висновок формулюємо чесно: доки причина не відтворена, коректне формулювання — «ще не встановлено», а не поспішний «root cause». Якщо винен застосунок, тест має лишатися червоним і фіксувати регресію, а не «зеленитися» через force, підняття таймауту чи паузу.

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

    • Виглядає як поганий локатор, а насправді інший контекст. Елемент видно в DevTools, тест його не знаходить — перша гіпотеза не «переписати селектор», а iframe чи shadow tree.
    • Виглядає як «елемента немає», а насправді він не готовий. Вузол уже в DOM, але без геометрії, ще їде або перекритий оверлеєм.
    • Виглядає як баг застосунку, а насправді гідратація. Кнопка намальована, реакції немає: обробники ще не навісилися.
    • Виглядає як флак, а насправді залежність від порядку запуску. Зелені поодинці й червоні в наборі — стан протік через сховище, кукі чи незнятий маршрут.
    • Виглядає як лікування, а насправді маскування. force, збільшений таймаут і фіксована пауза прибирають червоний колір, а не причину.
    • Виглядає як випадковість CI, а насправді конфігурація. Воркерів більше, ніж ядер на агенті, — це задокументована причина таймаутів і падінь, тож перезапуск джоби на тому самому агенті нічого не змінить.

    Підсумок

    • Флак браузерного походження — це розбіжність темпів: тест діє за годинником, застосунок — за подієвим циклом. Синхронізація означає очікування конкретного стану, а не паузу, а щоб знайти винного, дані збирають систематично — за чотирма осями: середовище, підмножина тестів, повтор поспіль, паралельні ранери.
    • Автоочікування закриває придатність елемента до дії: видимість, стабільність, отримання подій, увімкненість. Прикладну готовність воно не знає.
    • Між «є в DOM» і «дія проходить» лежить конвеєр рендерингу; між «розмітка є» і «сторінка реагує» — гідратація.
    • Детермінізм купують контролем над входами: перехоплення прибирає залежність від сервера, свіжий контекст — від чужого стану, тротлінг робить гонку видимою до CI.
    • Перед будь-яким повтором перевір : ретрай GET/PUT/DELETE безпечний, ретрай POST створює дублі.

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

    • «Тест падає раз на десять прогонів. Ваші дії?» Дивляться на порядок міркування: локалізувати, на чому падає → зрозуміти, якого стану тест не дочекався → правити. Відповідь «додам таймаут» закриває тему не на вашу користь.
    • «Чим явне очікування відрізняється від неявного й від автоочікування?» Перевіряють, хто формулює умову й чому неявне з явним не змішують.
    • «Елемент є в DOM, але клік не проходить. Чому?» Очікують кілька причин: немає геометрії, елемент рухається, перекритий оверлеєм, задизейблений, потік зайнятий.
    • «Що таке гонка станів і як вона виглядає у вебі?» Дивляться, чи є за терміном механізм: черга задач, порядок завершення, «перемагає останній, що відповів».
    • «Навіщо мокати мережу в UI-тесті і коли цього робити не варто?» Перевіряють баланс детермінізму проти дрейфу контракту.
    • «Тести зелені поодинці, але червоні в наборі. Де шукати?» Очікують слово «ізоляція» і конкретику: сховища, кукі, дані спільного акаунта, незняті перехоплення.
    • «Локально зелено, на CI червоно. Що перевірите першим?» Дивляться, чи є в голові модель агента: свіжа машина на кожен прогін, воркери за кількістю ядер, headless за замовчуванням і залежне від середовища відтворення візуальних порівнянь.
    • «Чому не можна просто поставити sleep(3) Сильна відповідь пояснює не «це погана практика», а чому пауза не перевіряє стан і програє в обидва боки.

    Джерела

    Синхронізація: діяти рівно тоді, коли застосунок готовий

    Подієвий цикл і гонка станів

    «Елемент є в DOM, а клік не проходить»

    Дерево, події й межі пошуку

    Локатор: стабільність починається з нього

    Розмітка є, а обробників ще немає: CSR, SSR і гідратація

    Дані приїжджають окремо: запит, мережа й затримка

    Як зробити тест детермінованим

    Чужий стан: кеш, сховища, сесія й токен

    Зелено локально, червоно в CI

    • GitHub Docs — Understanding GitHub Actions — свіжа віртуальна машина на кожен прогін і один ранер на одну джобу.
    • Playwright — Continuous Integration — здатність агента запускати браузери як перший крок, workers: 1 у CI, воркери за кількістю ядер і наслідок перевищення, headless за замовчуванням, Xvfb на Linux, DEBUG=pw:browser.
    • Playwright — Visual comparisons (test snapshots) — залежність рендерингу від ОС, версії, налаштувань, заліза й режиму запуску.

    Паралельний прогін як лакмус ізоляції

    • Playwright — Parallelism — воркер як процес ОС, файл як одиниця паралельності, знищення воркера після падіння, вимкнення паралельності одним воркером, serial як не рекомендований режим.
    • CodeceptJS — Parallel Execution — воркер як потік із незалежним екземпляром інструмента й відсутність покрокового виводу.
    • Playwright — Test Isolation (browser contexts) — ізоляція як передумова, що знімає питання порядку виконання.
    • xUnit Test Patterns — Erratic Test — Test Run War лише за глобально спільної фікстури; Resource Leakage і порада звести пул ресурсів до одиниці.

    Хто винен: застосунок, тест чи середовище

    • xUnit Test Patterns — Erratic Test — означення нестабільного тесту, шкідливість обох «простих» виходів, систематичний збір даних за чотирма осями, класи Interacting Tests, Unrepeatable Test, Test Run War, Nondeterministic Test і Resource Leakage.
    • Google Testing Blog — Flaky Tests at Google and How We Mitigate Them — вимірювальне означення, частки 1,5 %, 16 % і 84 % як вимір 2016 року, механізм шкоди від звикання до хибних сигналів.
    • Selenium — Waiting Strategies — гонка станів як одна з головних причин нестабільних тестів.
    • Cypress — Test Retries — виклики API, доступність сервера й бази, ресурси й мережа як названі джерела нестабільності.
    • AWS — DynamoDB read consistency — кінцева узгодженість: свіжий запис може не потрапити у відповідь, а сильна узгодженість є опцією запиту.

    Пояснення

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

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

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