Флакі-тести: причини, діагностика, лікування
Зміст
Уяви: ти нічого не змінив у коді, запускаєш той самий тест удруге — і замість червоного він раптом зелений. Або навпаки: локально сотня прогонів поспіль без єдиного збою, а в CI він падає раз на десять запусків. Це і є флакі-тест (flaky test) — тест, результат якого недетермінований (non-determinism): на одному й тому ж коді, на одному й тому ж коміті він то проходить, то падає. Найгірше в ньому не сам збій, а те, що він отруює довіру до всієї сьюти: коли команда звикає, що червоне «саме мине після рестарту», вона перестає читати падіння — і разом із флаком проґавлює справжню регресію. Флакі-тест — це не дрібна незручність, а повільна корозія цінності автоматизації.
Тема має високу вагу на співбесідах: питання «що таке флакі-тест і як ти з ним борешся» — майже обов'язкове для AQA будь-якого рівня, бо воно одразу показує, чи людина реально жила з CI, чи лише писала тести локально. Ця глава — канонічний виклад флаку на сайті: інші глави (браузерна механіка очікувань у розділі «Веб», ретраї засобами інструмента, «локально проходить — у CI падає» в CI/CD, флак в API-тестах) дають свої дельти й посилаються сюди. Якщо читаєш перед співбесідою за 15 хвилин — обов'язковий мінімум це таксономія причин, діагностика і теза «фейл тесту ≠ баг продукту»; метрики й командну політику можна залишити на другий прохід.
Що таке флакі-тест і чому він отруює сьюту
Формально флакі-тест — це тест із ненульовою ймовірністю різного результату за фіксованих вхідних умов: той самий код застосунку, той самий код тесту, те саме оточення (хоча б номінально), а вердикт стрибає. Ключове слово — недетермінованість. Детермінований тест на незмінних входах завжди дає той самий вихід; флакі — ні.
Чому один нестабільний тест — це непропорційно велика проблема:
- Ерозія довіри. Щойно команда бачить, що червоне буває «несправжнім», спрацьовує ефект хлопчика, який кричав «вовки»: на кожне падіння реакція «та це знову флак, перезапусти». У цій атмосфері справжній баг проходить непоміченим.
- CI-гейт стає театром. Якщо merge розблоковують перезапуском джоба до першого зеленого, гейт більше нічого не гарантує — він лише імітує контроль.
- Дорогий шум. Кожен «фальшивий» червоний — це чийсь час на розбір: відкрити звіт, подивитися трейс, зрозуміти, що знову те саме. Помножити на розмір сьюти й частоту прогонів — виходять людино-години на порожньому місці.
Тому флак лікують, а не терплять. Але щоб лікувати, треба спершу знати причину — а причин рівно п'ять сімейств.
Таксономія причин
Будь-який флак зводиться до одного з п'яти джерел. Ця класифікація — не академічна: вона і є алгоритм діагностики, бо кожне сімейство має свій характерний симптом і свій метод лікування.
Синхронізація
Найчастіша причина — і майже завжди перша підозра. Тест і застосунок працюють асинхронно: тест уже намагається клікнути кнопку, а вона ще не з'явилась; читає текст, який щойно оновлюється запитом; переходить далі, поки триває анімація. Результат залежить від того, хто встиг першим, а це — питання мілісекунд, які плавають від прогону до прогону.
Класичний антипатерн — жорстке очікування (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), коли читання одразу після запису іноді бачить старі дані, стан, що протікає між запитами користувача, — усе це нестабільність продукту, яку тест чесно ловить.
Звідси головна теза глави, яку легко перекрутити: фейл тесту ≠ баг продукту — але й флак ≠ завжди провина тесту. Червоне не означає автоматично баг у застосунку (частіше це проблема самого тесту), проте недетермінований тест не можна за замовчуванням списувати на «крихкий тест». Спершу треба виключити, що ти дивишся на справжню гонку в продукті. Це найдорожча помилка карантину — заглушити ретраєм реальний баг, який твій «нестабільний» тест єдиний і ловив.
Діагностика: відтворити, зібрати артефакти, локалізувати
Діагностика флаку — це відповідь на два питання: чи це взагалі флак (а не стабільно відтворюваний баг) і до якого з п'яти сімейств він належить. Порядок дій:
Артефакти — головне джерело правди. Флак майже неможливо зловити «дивлячись очима», бо він рідкісний. Тому інфраструктура має ловити його за тебе: трейс (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 без тікета й відповідального — це просто спосіб назавжди перестати дивитися на тест. Жорсткіша, але чесна альтернатива для зовсім занедбаних тестів: краще видалити тест, ніж тримати вічно зелений через ретраї, — мертвий тест, що нічого не гарантує, гірший за його відсутність, бо створює ілюзію покриття.
Метрики стабільності
Флак не можна лікувати наосліп — потрібні цифри, інакше «здається, стало краще» ніхто не спростує. Корисні (та чесні) метрики:
- 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-досвідом.
Джерела
- Martin Fowler — «Eradicating Non-Determinism in Tests»: https://martinfowler.com/articles/nonDeterminism.html — канонічний розбір причин недетермінованості й підходів до лікування.
- Google Testing Blog — «Flaky Tests at Google and How We Mitigate Them»: https://testing.googleblog.com/2016/05/flaky-tests-at-google-and-how-we.html — практика виявлення й карантину флаку на масштабі.
- Playwright — Retries: https://playwright.dev/docs/test-retries — офіційна механіка ретраїв і виявлення флаку.
- Playwright — Best Practices: https://playwright.dev/docs/best-practices — очікування замість жорстких пауз, детермінізм, стабільні локатори.
Що таке флакі-тест?
Флакі-тест (flaky test) — це тест, який на незмінному коді дає то зелений, то червоний результат, тобто веде себе недетерміновано (non-determinism). Ключова ознака — той самий коміт застосунку, той самий код тесту, номінально те саме оточення, а вердикт стрибає від прогону до прогону. Детермінований тест на однакових входах завжди повертає однаковий вихід; флакі порушує саме цю властивість. На практиці це виглядає як «перезапустив — і раптом пройшло» або «локально сто разів зелено, а в CI падає раз на десять». Для співбесіди важливо не плутати флак зі стабільно відтворюваним падінням: якщо тест червоніє щоразу, це не флак, а звичайний баг тесту чи продукту.
Чому один нестабільний тест — це непропорційно велика проблема?
Бо він руйнує не себе, а довіру до всієї сьюти. Щойно команда переконується, що червоне буває «несправжнім», спрацьовує ефект хлопчика, який кричав «вовки»: кожне падіння списують на флак і перезапускають, не читаючи. У цій атмосфері справжня регресія проходить непоміченою — її ховає той самий рефлекс «та це знову моргнуло». Додатково страждає CI-гейт: якщо merge розблоковують перезапуском джоба до першого зеленого, гейт нічого не гарантує, він лише вдає контроль. І це коштує грошей — кожен фальшивий червоний з'їдає чийсь час на розбір звіту, помножений на розмір сьюти й частоту прогонів. Тому флак лікують, а не терплять: один терпимий флакі-тест нормалізує наступний.
Назви сімейства причин флаку.
Будь-який флак зводиться до одного з п'яти джерел, і ця класифікація сама по собі є алгоритмом діагностики. Перше — синхронізація: тест і застосунок працюють асинхронно, тест звертається до елемента чи даних раніше, ніж вони готові. Друге — дані: тест залежить від стану, який не контролює повністю (брудні дані з попереднього прогону, неунікальний 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, але продовжують ганяти й відстежувати. Карантин розв'язує конфлікт: один флакі-тест не має права зупиняти доставку всієї команди, але й тихо видаляти його не можна — він щось перевіряє. Критично, щоб карантин не став цвинтарем: у правильного карантину є власник, дедлайн і тікет. Тег без відповідального й строку — це просто спосіб назавжди перестати дивитися на тест. Чесна альтернатива для зовсім занедбаних тестів жорсткіша: краще видалити тест, ніж тримати вічно зелений через ретраї, бо мертвий тест створює ілюзію покриття, якого немає.
Які метрики стабільності сьюти корисні й чесні?
Флак не лікують наосліп — без цифр «здається, стало краще» ніхто не спростує. 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-гейта на театр.
- Знаю різницю між флаком (плаваючий результат) і стабільно відтворюваним падінням (це вже баг тесту або продукту, не флак).
Таксономія причин
- Назву п'ять сімейств причин напам'ять: синхронізація, дані, порядок і взаємозалежність, оточення, баг продукту.
- Розумію, що ця класифікація сама і є алгоритмом діагностики — кожне сімейство має свій симптом і свій метод лікування.
- Можу пояснити, чому 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 червоного, флак як дефект із пріоритетом, правила гейта, бюджет на стабільність.
Який тест правильно назвати флакі?
Питання
Флакі-тест (flaky test) — що це?