Флакі-тести: причини, діагностика, лікування
Зміст
Уяви: ти нічого не змінив у коді, запускаєш той самий тест удруге — і замість червоного він раптом зелений. Або навпаки: локально сотня прогонів поспіль без єдиного збою, а в CI він падає раз на десять запусків. Це і є (flaky test) — тест, результат якого недетермінований (non-determinism): на одному й тому ж коді, на одному й тому ж він то проходить, то падає. Найгірше в ньому не сам збій, а те, що він отруює довіру до всієї : коли команда звикає, що червоне «саме мине після рестарту», вона перестає читати падіння — і разом із проґавлює справжню . -тест — це не дрібна незручність, а повільна корозія цінності автоматизації.
Питання «що таке флакі-тест і як ти з ним борешся» одразу показує, чи людина реально жила з CI, чи лише писала тести локально. Ця глава — канонічний виклад флаку на сайті: інші глави (браузерна механіка очікувань у розділі «Веб», засобами інструмента, «локально проходить — у CI падає» в CI/CD, флак в API-тестах) дають свої дельти й посилаються сюди. Якщо читаєш перед співбесідою за 15 хвилин — обов'язковий мінімум це таксономія причин, діагностика і теза «фейл тесту ≠ баг продукту»; метрики й командну політику можна залишити на другий прохід.
Що таке флакі-тест і чому він отруює сюїту
Формально флакі-тест — це тест із ненульовою ймовірністю різного результату за фіксованих вхідних умов: той самий код застосунку, той самий код тесту, те саме оточення (хоча б номінально), а вердикт стрибає. Ключове слово — недетермінованість. Детермінований тест на незмінних входах завжди дає той самий вихід; флакі — ні.
Чому один нестабільний тест — це непропорційно велика проблема:
- Ерозія довіри. Щойно команда бачить, що червоне буває «несправжнім», спрацьовує ефект хлопчика, який кричав «вовки»: на кожне падіння реакція «та це знову флак, ретрай». У цій атмосфері справжній баг проходить непоміченим.
- CI-гейт стає театром. Якщо merge розблоковують ретраєм до першого зеленого, гейт більше нічого не гарантує — він лише імітує контроль.
- Дорогий шум. Кожен «фальшивий» червоний — це чийсь час на розбір: відкрити звіт, подивитися , зрозуміти, що знову те саме. Помножити на розмір сюїти й частоту прогонів — виходять людино-години на порожньому місці.
Тому флак лікують, а не терплять. Але щоб лікувати, треба спершу знати причину — а причини зручно згрупувати в п'ять сімейств.
Таксономія причин
Ми групуємо джерела флаку в п'ять сімейств — це наше педагогічне перегрупування, а не вичерпний поділ. У джерел переліки інші: канонічна таксономія xUnit Patterns розкладає Erratic Test на Interacting Tests, Unrepeatable Test, Test Run War, Nondeterministic Test і Resource Leakage; у Фаулера теж п'ятірка, але своя — ізоляція, асинхронність, віддалені сервіси, час і витік ресурсів (останнього в нашому списку немає взагалі); Google називає причини відкритим списком із «etc.», тобто на повноту не претендує. Цінність нашої класифікації в іншому: вона і є алгоритм діагностики, бо кожне сімейство має свій характерний симптом і свій метод лікування.
Синхронізація
Найчастіша причина — і майже завжди перша підозра. Тест і застосунок працюють асинхронно: тест уже намагається клікнути кнопку, а вона ще не з'явилась; читає текст, який щойно оновлюється запитом; переходить далі, поки триває анімація. Результат залежить від того, хто встиг першим, а це — питання мілісекунд, які плавають від прогону до прогону.
Класичний антипатерн — (hard wait): пауза на фіксовану кількість мілісекунд у надії, що «стільки точно вистачить».
// Крихко: сподіваємось, що 2 секунди вистачить завжди
await page.waitForTimeout(2000);
await expect(page.getByRole('button', { name: 'Save' })).toBeVisible();
// Надійно: web-first assertion сам чекає появи до таймауту
await expect(page.getByRole('button', { name: 'Save' })).toBeVisible();
Фіксована пауза програє в обидва боки: на швидкому прогоні вона просто гальмує сюїту, на повільному (навантажений CI-агент) — не дочікується й падає. Правильний інструмент — очікування за умовою: web-first assertions з , стану, очікування конкретної відповіді мережі. Механіку браузерних очікувань детально розбирає глава «Практичні сценарії AQA: флак і синхронізація»; автоочікування конкретно в Playwright — окрема глава розділу «Інструменти».
Дані
Тест залежить від стану, який він не контролює повністю. Типові прояви: два тести пишуть в одного й того ж користувача; дані з попереднього прогону не прибрані й ламають ; тест створює запис з неунікальним email і падає на дублікаті при повторі; перевірка спирається на «перший рядок таблиці», а порядок недетермінований.
Окрема підступна категорія — час, дати й випадковість: тест, що падає лише опівночі або лише в певній таймзоні; перевірка «створено сьогодні», яка ламається на межі доби; Math.random() у . Лікування — ізоляція й детермінізм: унікальні дані на кожен тест, підготовка стану через API/БД, фіксація часу й . Стратегію тестових даних цілком розкриває канонічна глава про фабрики, фікстури та ізоляцію.
Порядок і взаємозалежність тестів
Тест зелений, коли йде п'ятим, і червоний, коли перший, — класична ознака залежності від порядку. Хтось лишив після себе стан (залогінена сесія, створений запис, змінена глобальна змінна), і наступний тест мовчки на нього спирається. Доки порядок стабільний, проблеми не видно; варто ввімкнути рандомізацію порядку або паралельний запуск — і сюїта розсипається.
Це прямий наслідок порушення незалежності й атомарності, які розбирає глава «Анатомія автотесту». Паралелізм цю категорію загострює: два , що ділять один акаунт чи один рядок БД, влаштовують гонку за спільний ресурс — деталі в главі «Паралелізація». Швидкий тест на цю причину: запусти підозрілий тест ізольовано (--repeat-each) — якщо сам по собі він стабільний, винен не він, а сусіди або спільний стан.
Оточення й інфраструктура
Тест падає не через код, а через середовище, у якому виконується. Джерела: перевантажений CI-агент дає інші таймінги, ніж потужний ноутбук; «моргання» мережі; недоступний або повільний сторонній сервіс (платіжка, пошта, зовнішнє API); різниця headless проти headed; відсутні шрифти в Docker- ламають тести; таймзона чи локаль агента відрізняються від локальних; застаріла версія фронтенду з CDN або service worker (див. главу «Кешування»).
Головний важіль тут — прибрати зовнішні залежності з-під ніг тесту: те, що нестабільне й не є об'єктом перевірки, підмінити дублером. Що і навіщо навіть в e2e — у главі «Тестові дублери». Окремий і дуже поширений підвид цієї категорії — «локально зелено, у CI червоно»; про нього нижче.
Баг продукту (справжня недетермінованість)
Найважливіша й найчастіше проґавлена категорія: тест флакає, бо застосунок справді поводиться недетерміновано. Реальна гонка (race condition) на бекенді, (eventual consistency), стан, що протікає між запитами користувача, — усе це нестабільність продукту, яку тест чесно ловить. Кінцева узгодженість тут варта окремого слова, бо це не крайній випадок, а режим за замовчуванням у реальних сховищах: дока DynamoDB пише, що відповідь на читання «may not reflect the results of a recently completed write operation», і що повторний запит через короткий час поверне свіжіші дані. Звідси корисний практичний хід: сильна узгодженість часто є опцією запиту, тож замість ретраїв іноді достатньо попросити її явно.
Звідси головна теза глави, яку легко перекрутити: фейл тесту ≠ баг продукту — але й флак ≠ завжди провина тесту. Червоне не означає автоматично баг у застосунку (частіше це проблема самого тесту), проте недетермінований тест не можна за замовчуванням списувати на «крихкий тест». Спершу треба виключити, що ти дивишся на справжню гонку в продукті. Це найдорожча помилка — заглушити ретраєм реальний баг, який твій «нестабільний» тест єдиний і ловив.
Діагностика: відтворити, зібрати артефакти, локалізувати
Діагностика флаку — це відповідь на два питання: чи це взагалі флак (а не стабільно відтворюваний баг) і до якого з п'яти сімейств він належить. Порядок дій:
Артефакти — головне джерело істини. Флак майже неможливо зловити «дивлячись очима», бо він рідкісний. Тому інфраструктура має ловити його за тебе: трейс (trace) з покроковими знімками DOM, мережею й , відео падіння, скріншот у момент збою, серверні логи з correlation id. Розбір фейлу від звіту до першопричини — тема глави «Звітність»; тут важливо запам'ятати: без збережених артефактів флак недіагностовний, бо на повторному прогоні він уже зелений.
Відтворення керованим повтором. Ручний повтор — ненадійний : пройшло тричі поспіль нічого не доводить. Треба ганяти тест сотнями ітерацій у циклі (--repeat-each у Playwright; в CodeceptJS чи інших — циклом або скриптом-обгорткою), бажано під навантаженням, що імітує CI (обмежити CPU, паралельні воркери). Якщо в ізоляції тест стабільний, а в повному наборі — ні, ти щойно локалізував причину як «порядок і взаємозалежність».
«Локально зелено — у CI червоно»
Окремий діагностичний сценарій, який плутає новачків: локально тест бездоганний, у CI регулярно падає. Це майже завжди категорія «оточення», і причини типові:
- CI-агент слабший і навантаженіший — усе виконується повільніше, вилазять приховані гонки синхронізації, яких на швидкій машині просто не було часу побачити.
- Headless-режим відрізняється від headed розмірами , доступними шрифтами, поведінкою деяких API.
- Паралельність у CI вища — прокидаються конфлікти за спільні дані.
- Чисте оточення: у CI немає твого залогіненого стану, кешу, локальних змінних середовища, які локально мовчки «допомагали» тесту.
Практичний висновок: не «ретрай джоб, поки не позеленіє», а відтвори CI-умови локально (менше ресурсів, headless, паралель, чистий профіль). Детальніше CI-специфічні причини розбирає глава про стабільність автотестів у CI в розділі «Git і CI/CD».
Ретраї: легітимні проти маскування
Ретрай (retry) — автоматичний повторний прогін упалого тесту. Це найгостріша етична розвилка теми, бо ретрай одночасно й корисний інструмент, і найпопулярніший спосіб замести флак під килим.
// playwright.config.ts — 2 ретраї лише в CI, локально нуль
export default defineConfig({
retries: process.env.CI ? 2 : 0,
});
Коли ретрай легітимний: як пом'якшення справді неусувних, рідкісних збоїв інфраструктури (моргання мережі до стороннього сервісу, який ти не контролюєш), поки команда рухається до кореневого фіксу. Ключова умова — ретрай не мовчить: кожен «passed on retry» логується як сигнал флаку, потрапляє в метрику й у на розбір. Ретрай тут — знеболювальне на час лікування, а не саме лікування.
Коли ретрай — маскування: коли retries виставляють глобально й високо (умовні три-п'ять), щоб сюїта «просто була зеленою», і на цьому заспокоюються. Проблеми такого підходу: справжня недетермінованість продукту (той самий баг гонки) ховається за зеленим; час прогону росте (кожен флакі-тест іноді біжить двічі-тричі); флак накопичується, бо стимул його чинити зник. Правило: ретрай купує час, але не скасовує обов'язку знайти причину. Тест, який стабільно проходить лише з другої спроби, — не «майже зелений», а незафіксований дефект. Механіку ретраїв засобами інструмента (рівні конфігу, per-project) розбирає окрема глава розділу «Інструменти».
Карантин
Що робити з тестом, який флакає прямо зараз і блокує merge всій команді, а причину швидко не знайти? Відповідь — карантин (quarantine): тест позначають спеціальним тегом, виносять із блокувального гейта (він більше не «валить» PR), але продовжують ганяти й відстежувати.
test('оновлення профілю', { tag: '@flaky' }, async ({ page }) => {
// ...
});
Карантин розв'язує гострий конфлікт: один флакі-тест не має права зупиняти доставку всієї команди, але й тихо видаляти його не можна — він щось перевіряє. Критично важливо, щоб карантин не став цвинтарем: правильний карантин має власника, дедлайн і трекінг. Тег @flaky без тікета й відповідального — це просто спосіб назавжди перестати дивитися на тест. Жорсткіший варіант для зовсім занедбаних тестів — видалити тест, а не тримати його вічно зеленим через ретраї: мертвий тест, що нічого не гарантує, створює ілюзію . Але це наша позиція, і вона розходиться з каноном: xUnit Patterns називає тесту одним із двох шкідливих простих виходів, бо це свідома втрата покриття (другий такий вихід — лишити тест червоним, за яким перестають помічати нові падіння). Тож видаляти можна лише тоді, коли команда справді свідомо приймає цю втрату, а не ховає нею проблему.
Метрики стабільності
Флак не можна лікувати наосліп — потрібні цифри, інакше «здається, стало краще» ніхто не спростує. Корисні (та чесні) метрики:
- Flaky rate — частка тестів (або прогонів), що дали різний результат за фіксованого коду. Найпряміший вимір здоров'я сюїти.
- Passed on retry — скільки тестів проходять лише з ретраю. Це прямий детектор прихованого флаку: якщо тест позеленів після червоного без змін коду — він нестабільний за визначенням.
- Розмір і вік карантину — скільки тестів у карантині й наскільки давно. Зростаючий тренд означає, що команда виносить швидше, ніж чинить.
- Час до фіксу — скільки живе флакі-тікет від заведення до закриття.
Свідомо уникай вигаданих «індустріальних порогів»: конкретна прийнятна межа flaky rate залежить від контексту (розмір сюїти, зрілість, критичність продукту) — важливіший напрямок тренду, ніж магічне число. Тренди й дашборди — знову тема глави «Звітність».
Політика команди
Технічні прийоми не працюють без домовленостей. Здорова політика щодо флаку тримається на кількох рішеннях:
- Ownership. У кожного червоного є хтось, хто відповідає за розбір — інакше «нічий» флак живе вічно. Хто чинить червоний і як розподілене володіння автотестами — тема глави «shift-left і ownership».
- Флак як дефект, а не фон. Нестабільний тест заводиться як баг із пріоритетом, а не «колись подивимось». Один терпимий флак нормалізує наступний.
- Правила гейта. Домовленість, що саме блокує merge і за яких умов тест іде в карантин, а не тихо ретраїться. Розкладка прогонів (smoke на PR, повна регресія nightly) і quality gates — канон розділу «Git і CI/CD».
- Бюджет на стабільність. Свідомо виділений час на «прибирання» сюїти, бо флак накопичується сам, а розсмоктується — ні.
Типові помилки
- Виглядає як баг застосунку (тест червоний) — а насправді флак самого тесту: гонка синхронізації, брудні дані, залежність від порядку. Перше питання завжди: тест відтворюється стабільно чи плаває?
- Виглядає як флакі-тест, який «просто крихкий», — а насправді справжня гонка в продукті, яку цей тест єдиний ловить. Заглушити його ретраєм — приховати реальний баг.
- Виглядає як розв'язана проблема (сюїта зелена після
retries: 3) — а насправді флак нікуди не дівся, лише сховався; час прогону виріс, стимул чинити зник. - Виглядає як доказ стабільності («ретраїв тричі — пройшло») — а насправді ручний повтор нічого не доводить: потрібні сотні ітерацій під CI-подібним навантаженням.
- Виглядає як баг у коді тесту («локально ж працює») — а насправді різниця оточення: слабший агент, headless, вища паралель, чистий профіль без твого локального стану.
- Виглядає як акуратне рішення (тег
@flaky) — а насправді вічний карантин без власника й дедлайну, тобто тихе списання перевірки.
Підсумок
- Флакі-тест недетермінований: той самий код — різний вердикт. Небезпечний він не збоєм, а ерозією довіри до всієї сюїти й перетворенням CI-гейта на театр.
- Причини ми групуємо в п'ять сімейств: синхронізація, дані, порядок/взаємозалежність, оточення, баг продукту. Це наше перегрупування, а не вичерпний поділ, зате воно і є алгоритм діагностики.
- Фейл тесту ≠ баг продукту — але флак ≠ автоматично провина тесту. Перш ніж лікувати тест, виключи справжню недетермінованість застосунку.
- Діагностика неможлива без артефактів (трейс/відео/логи) і керованого повтору; ручний «пройшло тричі» — не доказ.
- Ретрай купує час, не скасовує фіксу; карантин має бути з власником і дедлайном, а не цвинтарем. Метрики (flaky rate, passed on retry) роблять прогрес видимим.
Можливі питання
- «Що таке флакі-тест і чому це проблема?» Очікують визначення через недетермінованість плюс головний наслідок — ерозію довіри й знецінення гейта, а не просто «тест іноді падає».
- «Назви причини флаку» — інтерв'юер перевіряє, чи є в голові систематика. Сильна відповідь — п'ять сімейств із прикладом на кожне, а не хаотичний список «мережа, таймінги, ще щось».
- «Тест зелений локально, червоний у CI — твої дії?» Дивляться, чи не скажеш «перезапущу джоб». Правильно: зібрати артефакти, відтворити CI-умови локально (менше ресурсів, headless, паралель, чистий профіль), локалізувати причину.
- «Ретраї — це добре чи погано?» Перевірка на зрілість. Очікувана теза: легітимно як тимчасове знеболювальне з логуванням і беклогом; погано як глобальний спосіб зробити сюїту «зеленою» й заспокоїтись.
- «Куди подіти флакі-тест прямо зараз?» Карантин із власником і дедлайном; згадка, що вічний карантин або мовчазний ретрай — антипатерн.
Структура сильної відповіді (той самий скелет розбору й у реальному дебагу): уточни симптом → відтвори повтором і артефактами → віднеси до одного з п'яти сімейств → перевір, що це не баг продукту → тимчасово карантин/ретрай із трекінгом → кореневий фікс → закрий метрикою й командною домовленістю. Кандидат, який проходить цей ланцюг вголос, одразу читається як людина з реальним CI-досвідом.
Джерела
Що таке флакі-тест і чому він отруює сюїту
- xUnit Test Patterns — Erratic Test — канонічне означення («give different results depending on when they are run and who is running them») і розбір двох «простих» виходів: прибрати тест = свідомо втрачений тест, лишити червоним = він затуляє інші падіння.
- Google Testing Blog — Flaky Tests at Google — вужче, вимірювальне формулювання («a test that exhibits both a passing and a failing result with the same code») і механізм шкоди: звикання до хибних сигналів веде до ігнорування справжніх падінь (
trust: secondary, dec-0710).
- xUnit Test Patterns — Erratic Test — канонічна пʼятірка поіменно:
Interacting Tests,Unrepeatable Test,Test Run War(можлива тільки за глобально спільної ),Nondeterministic TestіResource Leakage. - Martin Fowler — Eradicating Non-Determinism in Tests — інша пʼятірка, названа в главі:
Lack of Isolation,Asynchronous Behavior,Remote Services,Time,Resource Leaks— рубрикація саме цієї статті, а не галузева таксономія (trust: secondary, dec-0311). - Selenium — Waiting Strategies — гонка з браузером названа прямо однією з головних причин флакі-тестів, і жорстка пауза програє двічі: замало — падає, забагато — прогін стає непідйомним.
- Playwright — Best Practices — що ставлять замість паузи: web-first чекає й повторює перевірку, а
expect(await locator.isVisible()).toBe(true)не чекає жодної секунди; там же — вимога повної ізоляції тесту як засіб проти сімейства «дані й порядок». - Cypress — Test Retries — перелік типових джерел нестабільності в іншого інструмента: , доступність сервера й БД, залежності від ресурсів, мережа.
- AWS — DynamoDB read consistency — категорія «застосунок справді недетермінований» із конкретним механізмом: кінцева узгодженість за замовчуванням не гарантує, що запис буде видно одразу.
- Google Testing Blog — Flaky Tests at Google — там причини подано відкритим списком із «etc.», тобто на повноту джерело не претендує (
trust: secondary, dec-0710).
Діагностика: відтворити, зібрати артефакти, локалізувати
- xUnit Test Patterns — Erratic Test — канон вимагає не ретраїв, а систематичного збору даних у часі за чотирма осями (середовище, підмножина тестів, повтор поспіль, паралельні ранери); для витоків ресурсів названо конкретну тактику — звести пул до 1.
- Jest — Setup and Teardown — перше діагностичне питання у формулюванні офіційної доки ранера: чи падає тест, коли він єдиний у прогоні.
- Playwright — Trace viewer — інструмент
post-mortem-розбору саме для падінь у CI: і тривалість кожної дії, DOM-знімки до й після, мережа й консоль. - ISTQB® CTAL-TAE Syllabus v2.0 — §6.1: у разі падіння рішення автоматизації має зберегти все, що потрібно для аналізу, включно з дампами й стеками.
Ретраї: легітимні проти маскування
- Playwright — Test retries — ранер розводить три стани, а не два:
passed,flaky(упав з першого разу, пройшов на ретраї) іfailed; тест ще й бачить, що він на ретраї (testInfo.retry), і може почистити серверний стан перед повтором. - Cypress — Test Retries — позиціювання самих інструментів: ретраї вимкнені за замовчуванням і потрібні, щоб флак виявити, а не сховати.
- Playwright — Test configuration — типова конфігурація доки: ретраї лише в CI, локально нуль; трейс
shouldзаписуватися на першому ретраї, тобто ретрай є ще й способом зібрати діагностику. - Google Testing Blog — Flaky Tests at Google — ціна маскування названа числом: схему «падати лише після трьох разів поспіль» джерело саме називає далеко не ідеальною, бо 15-хвилинний тест покаже поломку через 45 хвилин (
trust: secondary, dec-0710).
- Google Testing Blog — Flaky Tests at Google — карантин як третій вихід у практиці, що описана: інструмент стежить за рівнем флаку, за перевищення порогу саджає тест у карантин автоматично й заводить баг; там же чесно названа ціна — це може замаскувати справжню гонку (
trust: secondary, dec-0710). - xUnit Test Patterns — Erratic Test — чому третій вихід взагалі потрібен: прибрати тест = свідомо втрачений тест, лишити червоним = затулити інші падіння.
- ISTQB® CTAL-TAE Syllabus v2.0 — звідки беруться дані для рішення: за історією прогонів знаходять й вирішують, рефакторити їх чи переписувати.
- Google Testing Blog — Flaky Tests at Google — числа подано як атрибутовану вимірку 2016 року, а не галузеву константу: 1,5 % прогонів дають флакі-результат, ~16 % тестів мають якийсь рівень флаку, 84 % переходів «зелений → червоний» — це флак; там же — темп появи флаку дорівнює темпу лагодження (
trust: secondary, dec-0710). - Allure Report — History and retries — інструментальний бік метрики: історія прогонів і ретраїв, з якої тренд узагалі стає видимим.
- ISTQB® CTAL-TAE Syllabus v2.0 — навіщо історія потрібна крім звіту: за нею знаходять крихкі (fragile) тести.
- Google Testing Blog — Flaky Tests at Google — підстава для політики, а не для гасла: темп появи флаку дорівнює темпу лагодження, тож ним керують, а не «виліковують остаточно»; механізм деградації довіри — звикання до хибних сигналів (
trust: secondary, dec-0710). - ISTQB® CTAL-TAE Syllabus v2.0 — §6.1: окремий стан прогону «дефект не в SUT, а в самому рішенні автоматизації» і вимога дати станам чіткі й консистентні означення — це і є формалізована політика .
Відео до теми
Що таке флакі-тест?
(flaky test) — це тест, який на незмінному коді дає то зелений, то червоний результат, тобто веде себе недетерміновано (non-determinism). Ключова ознака — той самий застосунку, той самий код тесту, номінально те саме оточення, а вердикт стрибає від прогону до прогону. Детермінований тест на однакових входах завжди повертає однаковий вихід; порушує саме цю властивість. На практиці це виглядає як «ретраїв — і раптом пройшло» або «локально сто разів зелено, а в CI падає раз на десять». Для співбесіди важливо не плутати зі стабільно відтворюваним падінням: якщо тест червоніє щоразу, це не флак, а звичайний баг тесту чи продукту.
Чому один нестабільний тест — це непропорційно велика проблема?
Бо він руйнує не себе, а довіру до всієї . Щойно команда переконується, що червоне буває «несправжнім», спрацьовує ефект хлопчика, який кричав «вовки»: кожне падіння списують на флак і ретраять, не читаючи. У цій атмосфері справжня проходить непоміченою — її ховає той самий рефлекс «та це знову моргнуло». Додатково страждає CI-гейт: якщо merge розблоковують до першого зеленого, гейт нічого не гарантує, він лише вдає контроль. І це коштує грошей — кожен фальшивий червоний з'їдає чийсь час на розбір звіту, помножений на розмір сюїти й частоту прогонів. Тому флак лікують, а не терплять: один терпимий флакі-тест нормалізує наступний.
Назви сімейства причин флаку.
Ми групуємо джерела флаку в п'ять сімейств, і ця класифікація сама по собі є алгоритмом діагностики. Одразу застереження: це наше перегрупування, а не вичерпний поділ — у xUnit Patterns таксономія Erratic Test інша, у Фаулера п'ятірка своя (з витоком ресурсів, якого в нас немає), а Google перелічує причини відкритим списком із «etc.». Перше — синхронізація: тест і застосунок працюють асинхронно, тест звертається до елемента чи даних раніше, ніж вони готові. Друге — дані: тест залежить від стану, який не контролює повністю (брудні дані з попереднього прогону, неунікальний email, час і випадковість). Третє — порядок і взаємозалежність: тест спирається на стан, який лишив інший тест, тож ламається при зміні порядку чи паралелі. Четверте — оточення й інфраструктура: слабший CI-агент, моргання мережі, повільний сторонній сервіс, headless проти headed. П'яте — баг продукту: застосунок справді недетермінований (реальна гонка, ). Сильна відповідь — це п'ять сімейств із прикладом на кожне, а не хаотичний перелік «мережа, таймінги, ще щось».
Що таке hard wait і чому це антипатерн?
(hard wait) — це пауза на фіксовану кількість мілісекунд у надії, що «стільки точно вистачить», наприклад page.waitForTimeout(2000) перед . Проблема в тому, що фіксована пауза програє в обидва боки: на швидкому прогоні вона просто гальмує сюїту без користі, а на повільному, навантаженому агенті — не дочікується потрібного стану й падає. Вона прив'язана до часу, а не до події, тоді як момент готовності UI плаває на десятки й сотні мілісекунд. Правильний інструмент — очікування за умовою: web-first assertion з , стану, очікування конкретної відповіді мережі. Такий асерт сам чекає появи елемента до свого й закривається щойно умова виконана, тому він одночасно швидший і стабільніший за будь-яку фіксовану цифру.
Як тести флакають через дані?
Тест залежить від стану, який він не створив і не контролює до кінця. Типові прояви: два тести одночасно правлять одного й того ж користувача; дані з попереднього прогону не прибрані й ламають ; тест створює запис із фіксованим email і на повторі падає на дублікаті; перевірка спирається на «перший рядок таблиці», хоча порядок недетермінований. Окрема підступна гілка — час, дати й випадковість: тест, що падає лише опівночі чи в чужій таймзоні, перевірка «створено сьогодні» на межі доби, Math.random() у . Лікується це ізоляцією й детермінізмом: унікальні дані на кожен тест, підготовка стану через API чи БД замість UI, фіксація часу й генератора. Головний принцип — тест сам володіє своїми даними й не покладається на те, що лишили сусіди.
Тест зелений, коли йде п'ятим, і червоний, коли перший. Про що це?
Це класична ознака залежності від порядку виконання: хтось перед п'ятим тестом лишив по собі стан — залогінену сесію, створений запис, змінену глобальну змінну, — і тест мовчки на нього спирається, хоч сам його не готує. Доки порядок стабільний, проблема невидима; варто ввімкнути рандомізацію порядку чи паралельний запуск — і сюїта розсипається. Це прямий наслідок порушеної незалежності й атомарності тестів. Паралелізм цю категорію ще й загострює: два , що ділять один акаунт чи один рядок БД, влаштовують гонку за спільний ресурс. Швидкий діагностичний тест: запусти підозрілий тест ізольовано в циклі — якщо сам по собі він стабільний, винен не він, а сусіди або спільний стан.
Тест зелений локально, червоний у CI. Твої дії?
Найгірша відповідь тут — «перезапущу джоб, поки не позеленіє»; саме її й перевіряє інтерв'юер. Правильно почати з того, що це майже завжди категорія «оточення», і причини типові: CI-агент слабший і навантаженіший, тому вилазять приховані гонки синхронізації, яких на швидкій машині не було часу побачити; headless відрізняється від headed розміром , шрифтами, поведінкою частини API; паралельність у CI вища, тож прокидаються конфлікти за спільні дані; оточення чисте — немає твого локального залогіненого стану, кешу й змінних середовища, які мовчки «допомагали». Практичний план: зібрати артефакти падіння, відтворити CI-умови локально (обмежити ресурси, headless, більше паралелі, чистий профіль), локалізувати сімейство причини й лагодити корінь. Тобто не маскувати різницю оточення ретраєм, а відтворити її.
Флак через оточення — які джерела й головний важіль лікування?
Тут тест падає не через свій код, а через . Джерела: перевантажений CI-агент дає інші таймінги; моргання мережі; недоступний або повільний сторонній сервіс — платіжка, пошта, зовнішнє API; різниця headless проти headed; відсутні шрифти в Docker ламають тести; чужа таймзона чи локаль агента; застаріла версія фронтенду з CDN або service worker. Головний важіль — прибрати з-під ніг тесту зовнішні залежності, які нестабільні й не є об'єктом перевірки: підмінити їх дублером (/). Якщо мета тесту не в самому платіжному провайдері, його моргання не повинно фарбувати твій тест у червоне. Це не «читерство», а елементарна ізоляція: тест має перевіряти свою поведінку, а не аптайм чужого сервісу.
Чи завжди флак — це провина тесту?
Ні, і це найдорожча помилка діагностики. Найчастіше проґавлена категорія — коли застосунок справді поводиться недетерміновано: реальна гонка (race condition) на бекенді, кінцева узгодженість (eventual consistency), коли читання одразу після запису іноді бачить старі дані, стан, що протікає між запитами. Твій «нестабільний» тест тут чесно ловить справжній дефект. Звідси дві тези, які легко переплутати: фейл тесту не дорівнює автоматично багу продукту (частіше винен сам тест), але й флак не дорівнює автоматично провині тесту. Перш ніж списувати нестабільний тест на «крихкість» і глушити ретраєм, треба виключити, що ти дивишся на справжню гонку в продукті. Заглушити ретраєм реальний баг, який єдиний цей тест і ловив, — найгірший можливий результат .
Як діагностувати флак?
Діагностика відповідає на два питання: чи це взагалі флак (а не стабільно відтворюваний баг) і до якого з п'яти сімейств він належить. Спершу — артефакти: флак рідкісний і його майже неможливо зловити «дивлячись очима», тому інфраструктура має ловити його за тебе — (trace) з покроковими знімками DOM, мережею й , відео падіння, скріншот моменту збою, серверні логи з correlation id. Без збережених артефактів флак недіагностовний, бо на повторному прогоні він уже зелений. Далі — керований повтор: ганяєш тест сотнями ітерацій у циклі (--repeat-each у Playwright або скрипт-обгортка в іншому ), бажано під навантаженням, що імітує CI. Якщо в ізоляції тест стабільний, а в повному наборі — ні, ти щойно локалізував причину як «порядок і взаємозалежність».
Чому «ретраїв тричі — пройшло» не доказ стабільності?
Бо ручний повтор — ненадійний . Флак за визначенням має ненульову, але невелику ймовірність збою, тож три-п'ять успішних прогонів поспіль статистично нічого не спростовують: якщо тест падає раз на двадцять запусків, три зелених — рядова випадковість. Щоб чесно судити про стабільність, треба ганяти тест сотнями ітерацій, ще й бажано під CI-подібним навантаженням — обмежені CPU, паралельні воркери, — бо саме дефіцит ресурсів витягує приховані гонки. Той, хто на співбесіді каже «я ретраїв, воно пройшло, значить полагоджено», одразу показує, що з реальним флаком не жив. Доказ стабільності — це число ітерацій без збою, а не побутове відчуття «начебто працює».
Ретраї — це добре чи погано?
Це залежить від того, чим ретрай (retry) супроводжується, і питання перевіряє зрілість. Ретрай легітимний як тимчасове знеболювальне для справді неусувних рідкісних збоїв інфраструктури — наприклад, моргання мережі до стороннього сервісу, який ти не контролюєш, — поки команда рухається до кореневого фіксу. Але за однієї умови: ретрай не мовчить. Кожен «passed on retry» логується як сигнал флаку, потрапляє в метрику й у на розбір. Ретрай стає маскуванням, коли retries виставляють глобально й високо, щоб сюїта «просто була зеленою», і на цьому заспокоюються: справжня гонка продукту ховається за зеленим, час прогону росте, а стимул чинити флак зникає. Правило коротке: ретрай купує час, але не скасовує обов'язку знайти причину. Тест, що стабільно проходить лише з другої спроби, — не «майже зелений», а незафіксований дефект.
Що таке карантин і як зробити його правильно?
Карантин (quarantine) — це відповідь на ситуацію, коли тест флакає прямо зараз і блокує merge всій команді, а причину швидко не знайти. Тест позначають спеціальним тегом (наприклад @flaky), виносять із блокувального гейта, щоб він більше не валив чужі PR, але продовжують ганяти й відстежувати. Карантин розв'язує конфлікт: один флакі-тест не має права зупиняти доставку всієї команди, але й тихо видаляти його не можна — він щось перевіряє. Критично, щоб карантин не став цвинтарем: у правильного карантину є власник, дедлайн і тікет. Тег без відповідального й строку — це просто спосіб назавжди перестати дивитися на тест. Жорсткіший варіант для зовсім занедбаних тестів — видалити тест, а не тримати вічно зеленим через ретраї, бо мертвий тест створює ілюзію , якого немає. Це наша позиція, і вона розходиться з каноном: xUnit Patterns називає тесту одним із двох шкідливих простих виходів, бо це свідома втрата покриття.
Які метрики стабільності сюїти корисні й чесні?
Флак не лікують наосліп — без цифр «здається, стало краще» ніхто не спростує. Flaky rate — частка тестів чи прогонів, що дали різний результат за фіксованого коду; це найпряміший вимір здоров'я сюїти. Passed on retry — скільки тестів проходять лише з ретраю; прямий детектор прихованого флаку, бо позеленіння після червоного без змін коду означає нестабільність за визначенням. Розмір і вік карантину показують, чи команда виносить тести швидше, ніж чинить: зростаючий тренд — тривожний сигнал. Час до фіксу — скільки живе флакі-тікет від заведення до закриття. Свідомо уникай вигаданих «індустріальних порогів» на кшталт «прийнятно 2%»: конкретна межа залежить від розміру сюїти, зрілості й критичності продукту, тож важливіший напрямок тренду, ніж магічне число.
З чого складається здорова командна політика щодо флаку?
Технічні прийоми не працюють без домовленостей, і політика тримається на кількох рішеннях. Ownership: у кожного червоного є хтось, хто відповідає за розбір, інакше «нічий» флак живе вічно. Флак як дефект, а не фон: нестабільний тест заводиться як баг із пріоритетом, а не «колись подивимось» — бо один терпимий флак нормалізує наступний. Правила гейта: команда домовляється, що саме блокує merge і за яких умов тест іде в карантин, а не тихо ретраїться; сюди ж — розкладка прогонів (smoke на PR, повна регресія nightly). Бюджет на стабільність: свідомо виділений час на прибирання сюїти, бо флак накопичується сам, а розсмоктується — ні. Без цих рамок будь-які web-first assertion і поступово тонуть у загальному «та ретрай».
Побудуй структуру сильної відповіді про боротьбу з флаком.
Той самий скелет працює і на співбесіді, і в реальному дебагу, тож проговорити його вголос — найсильніший хід. Спершу уточни симптом: тест відтворюється стабільно чи плаває — бо стабільне падіння це не флак. Далі відтвори проблему повтором і артефактами, а не покладайся на «пройшло тричі». Потім віднеси її до одного з наших п'яти сімейств: синхронізація, дані, порядок, оточення, баг продукту. Обов'язково перевір, що це не справжня недетермінованість застосунку, перш ніж лікувати тест. Якщо треба розблокувати команду негайно — карантин або ретрай, але з трекінгом і власником, не мовчки. Тоді роби кореневий фікс і закривай історію метрикою (passed on retry падає) та командною домовленістю. Кандидат, який проходить цей ланцюг послідовно, одразу читається як людина з реальним CI-досвідом, а не з локальними прогонами на своєму ноутбуці.
Три робочі ситуації, у яких видно різницю між «полагодив флак» і «сховав флак»: таблиця, що перекладає симптом у сімейство причини й перевірку; розбір керованого повтору, який локалізує причину замість здогадів; і чесний із , які не заміняють кореневий фікс, а купують час на нього.
Кейс 1. Симптом → сімейство → перевірка (таблиця рішень)
майже завжди приходить у вигляді симптому, а не діагнозу. Задача — не гадати, а швидко звести симптом до одного з п'яти сімейств і зробити дешеву перевірку, яка підтвердить або спростує гіпотезу. Ось робоча таблиця, з якою зручно падіння:
| Симптом у звіті | Найімовірніше сімейство | Дешева перевірка |
|---|---|---|
Падає на click/expect елемента, який «щойно мав з'явитися» | Синхронізація | Прибрати hard wait, поставити web-first assertion; ганяти --repeat-each |
409/дублікат при повторному прогоні, «email вже зайнятий» | Дані | Генерувати унікальний email на кожен тест; перевірити teardown |
| Зелений ізольовано, червоний у повному наборі | Порядок і взаємозалежність | Запустити один тест у циклі; увімкнути рандомізацію порядку |
| Локально зелено, у CI плаває | Оточення | Відтворити локально: менше CPU, headless, паралель, чистий профіль |
| Читання одразу після запису іноді бачить старі дані | Баг продукту | Відтворити гонку руками через API; підняти питання до бекенду |
Як цим користуватися. По-перше, спершу відповідай на питання «чи це взагалі флак»: якщо тест червоніє щоразу — це не рядок із таблиці, а стабільний баг, і тріаж інший. По-друге, кожен рядок дає перевірку, яку можна зробити за хвилини, а не переписування тесту наосліп. По-третє, останній рядок — найважливіший і найчастіше проґавлений: перш ніж записати тест у «крихкі» й повісити ретрай, треба виключити, що застосунок справді недетермінований. Саме тут ретрай перетворюється з ліків на маскування реального дефекту.
Кейс 2. Локалізація керованим повтором
Тест checkout падає в CI приблизно раз на п'ятнадцять прогонів, локально не відтворюється жодного разу. Спокуса — поставити retries і забути. Правильний хід — спершу зрозуміти, до якого сімейства він належить, а для цього потрібен не ручний ретрай, а керований повтор під навантаженням.
Крок перший — зловити артефакт. Trace в Playwright дає покрокові знімки DOM, мережу й саме того прогону, що впав, — без нього недіагностовний:
// playwright.config.ts — зберігати трейс лише для падінь, щоб не роздувати артефакти
export default defineConfig({
use: { trace: 'retain-on-failure' },
});
Крок другий — відтворити повтором, а не вірою. Ганяємо тест сотнями ітерацій, паралельно, щоб штучно створити дефіцит ресурсів, як у CI:
# 200 повторів у 4 воркери — витягує приховані гонки синхронізації
npx playwright test checkout.spec.ts --repeat-each=200 --workers=4
Крок третій — розвести дві гіпотези. Якщо той самий тест, запущений сам-один у циклі, стабільний, а падає лише в повному наборі — причина не в ньому, а в спільному стані сусідів, тобто це «порядок і взаємозалежність»:
# той самий тест ізольовано: стабільний тут = винні сусіди, не він
npx playwright test checkout.spec.ts --repeat-each=200 --workers=1
Що дивитися і чому. Якщо падіння відтворюється вже під --workers=4, а на одному зникає — ти локалізував гонку за спільний ресурс (спільний акаунт, один рядок БД), і фікс лежить у площині ізоляції даних, а не . Якщо ж тест плаває навіть у повній ізоляції на слабких ресурсах — дивись на конкретний крок: майже завжди це синхронізація, де hard wait не дочекався повільнішого за звичай рендеру. Головне, чого цей кейс уникає: висновку «це просто крихкий тест» без єдиного відтворення. Причину тут не вгадують, її показує повтор.
Кейс 3. Чесний ретрай і карантин, які не ховають флак
Тест profile-update почав плавати перед релізом, причину за годину не знайшли, а він блокує merge усій команді. Тут доречні ретрай і карантин — але так, щоб вони не стали способом більше ніколи на тест не дивитися.
Ретрай вмикаємо лише в CI, локально нуль — щоб розробник бачив справжній результат одразу:
// playwright.config.ts — знеболювальне на час лікування, не заміна фіксу
export default defineConfig({
retries: process.env.CI ? 2 : 0,
reporter: [['list'], ['json', { outputFile: 'results.json' }]],
});
Карантин виносить тест із блокувального гейта тегом, але не глушить його: він і далі ганяється й трекається:
test('оновлення профілю зберігає зміни', { tag: '@flaky' }, async ({ page }) => {
// ... сценарій без змін; тег лише виводить тест із блокувального прогону
});
І найважливіша частина, без якої попередні дві безглузді, — сигнал у метрику. Кожен «passed on retry» треба видобути зі звіту й підняти як дефект, а не проковтнути:
# скільки тестів позеленіли лише з ретраю — прямий детектор прихованого флаку
jq '[.suites[].specs[].tests[] | select(.results | length > 1)] | length' results.json
Що дивитися і чому. По-перше, retries: 2 тут купує команді час до релізу, але тест уже заведений у із власником і дедлайном — тег @flaky без тікета був би тихим списанням. По-друге, підрахунок passed on retry перетворює флак із відчуття на цифру: якщо число росте, ретраї маскують проблему, а не тримають її під контролем. По-третє, коли причину знайдено й полагоджено, тег знімають і повертають тест у гейт — карантин мусить мати вихід, інакше він цвинтар. Різниця між цим кейсом і «поставив retries: 3 і забув» — рівно в тому, що тут ретрай видимий, вимірюваний і тимчасовий.
Суть і небезпека
- Можу дати визначення через недетермінованість: той самий код і — різний вердикт, а не просто «тест іноді падає».
- Розумію, чому головна шкода не збій, а ерозія довіри до всієї й перетворення CI-гейта на театр.
- Знаю різницю між (плаваючий результат) і стабільно відтворюваним падінням (це вже баг тесту або продукту, не флак).
Таксономія причин
- Назву п'ять сімейств причин напам'ять: синхронізація, дані, порядок і взаємозалежність, оточення, баг продукту.
- Розумію, що ця класифікація сама і є алгоритмом діагностики — кожне сімейство має свій симптом і свій метод лікування.
- Знаю, що наша п'ятірка — педагогічне перегрупування, а не вичерпний поділ: у xUnit Patterns своя таксономія
Erratic Test, у Фаулера своя п'ятірка з витоком ресурсів, а Google перелічує причини з «etc.». - Знаю, що порада «краще видалити занедбаний тест» — наша позиція: xUnit Patterns називає тесту одним із двох шкідливих виходів, бо це свідома втрата .
- Можу пояснити, чому hard wait (
waitForTimeout) програє web-first assertion: він прив'язаний до часу, а не до події готовності. - Знаю симптом «даних»: неунікальний email, брудний стан із попереднього прогону, залежність від «першого рядка», час і
Math.random(). - Розпізнаю залежність від порядку: тест зелений п'ятим і червоний першим означає спільний стан, а не власний баг тесту.
- Знаю типові джерела «оточення»: слабший CI-агент, моргання мережі, повільний сторонній сервіс, headless проти headed, шрифти, таймзона.
Баг продукту vs крихкий тест
- Тримаю в голові обидві тези одночасно: фейл тесту ≠ баг продукту, але й флак ≠ автоматично провина тесту.
- Перш ніж глушити нестабільний тест , виключаю справжню гонку в продукті (race condition, eventual consistency).
Діагностика
- Знаю, що без артефактів (, відео, логи, ) флак недіагностовний — на повторі він уже зелений.
- Можу пояснити, чому ручний «ретраїв тричі — пройшло» не доказ: потрібні сотні ітерацій під CI-подібним навантаженням.
- Знаю прийом локалізації: тест стабільний ізольовано, але падає в наборі — значить, причина в порядку й спільному стані.
- Для сценарію «локально зелено — у CI червоно» відтворюю CI-умови локально (менше ресурсів, headless, паралель, чистий профіль), а не ретраю .
Ретраї, карантин, метрики, політика
- Можу розмежувати легітимний ретрай (тимчасове знеболювальне з логуванням і ) і маскування (глобальні
retries, щоб просто позеленіти). - Розумію правило «ретрай купує час, але не скасовує фіксу»: passed on retry — це незафіксований дефект, а не «майже зелений».
- Знаю, що правильний має власника, дедлайн і тікет, інакше тег
@flaky— тихе списання перевірки. - Можу назвати чесні метрики: flaky rate, passed on retry, розмір і вік карантину, час до фіксу — і чому «магічний поріг» гірший за тренд.
- Розумію складові командної політики: ownership червоного, флак як дефект із пріоритетом, правила гейта, бюджет на стабільність.
Квіз
Перед стартом
- Питань: 12
- Поріг «зараховано»: ≥70% правильних відповідей.
- Результат впливає на прогрес; завалені питання підуть у чергу повторення.
- Квіз впливає на компліт теми: тема стає «пройдено», лише коли прочитано теорію І квіз складено на ≥70%.
Питання
Флакі-тест (flaky test) — що це?
