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

    06 · Автоматизація: стратегія

    Флакі-тести: причини, діагностика, лікування

    Зміст

    Уяви: ти нічого не змінив у коді, запускаєш той самий тест удруге — і замість червоного він раптом зелений. Або навпаки: локально сотня прогонів поспіль без єдиного збою, а в CI він падає раз на десять запусків. Це і є флакі-тест (flaky test) — тест, результат якого недетермінований (non-determinism): на одному й тому ж коді, на одному й тому ж коміті він то проходить, то падає. Найгірше в ньому не сам збій, а те, що він отруює довіру до всієї сьюти: коли команда звикає, що червоне «саме мине після рестарту», вона перестає читати падіння — і разом із флаком проґавлює справжню регресію. Флакі-тест — це не дрібна незручність, а повільна корозія цінності автоматизації.

    Тема має високу вагу на співбесідах: питання «що таке флакі-тест і як ти з ним борешся» — майже обов'язкове для AQA будь-якого рівня, бо воно одразу показує, чи людина реально жила з CI, чи лише писала тести локально. Ця глава — канонічний виклад флаку на сайті: інші глави (браузерна механіка очікувань у розділі «Веб», ретраї засобами інструмента, «локально проходить — у CI падає» в CI/CD, флак в API-тестах) дають свої дельти й посилаються сюди. Якщо читаєш перед співбесідою за 15 хвилин — обов'язковий мінімум це таксономія причин, діагностика і теза «фейл тесту ≠ баг продукту»; метрики й командну політику можна залишити на другий прохід.

    Що таке флакі-тест і чому він отруює сьюту

    Формально флакі-тест — це тест із ненульовою ймовірністю різного результату за фіксованих вхідних умов: той самий код застосунку, той самий код тесту, те саме оточення (хоча б номінально), а вердикт стрибає. Ключове слово — недетермінованість. Детермінований тест на незмінних входах завжди дає той самий вихід; флакі — ні.

    Чому один нестабільний тест — це непропорційно велика проблема:

    • Ерозія довіри. Щойно команда бачить, що червоне буває «несправжнім», спрацьовує ефект хлопчика, який кричав «вовки»: на кожне падіння реакція «та це знову флак, перезапусти». У цій атмосфері справжній баг проходить непоміченим.
    • CI-гейт стає театром. Якщо merge розблоковують перезапуском джоба до першого зеленого, гейт більше нічого не гарантує — він лише імітує контроль.
    • Дорогий шум. Кожен «фальшивий» червоний — це чийсь час на розбір: відкрити звіт, подивитися трейс, зрозуміти, що знову те саме. Помножити на розмір сьюти й частоту прогонів — виходять людино-години на порожньому місці.

    Тому флак лікують, а не терплять. Але щоб лікувати, треба спершу знати причину — а причин рівно п'ять сімейств.

    Таксономія причин

    Будь-який флак зводиться до одного з п'яти джерел. Ця класифікація — не академічна: вона і є алгоритм діагностики, бо кожне сімейство має свій характерний симптом і свій метод лікування.

    Час, момент готовності UI/даних

    Стан, який лишили інші тести/прогони

    Залежність від порядку виконання

    Ресурси, мережа, сторонні сервіси

    Сам застосунок недетермінований

    Тест недетермінований

    Що змінюється між прогонами?

    Синхронізація

    Дані

    Порядок і взаємозалежність

    Оточення й інфраструктура

    Баг продукту

    Час, момент готовності UI/даних

    Стан, який лишили інші тести/прогони

    Залежність від порядку виконання

    Ресурси, мережа, сторонні сервіси

    Сам застосунок недетермінований

    Тест недетермінований

    Що змінюється між прогонами?

    Синхронізація

    Дані

    Порядок і взаємозалежність

    Оточення й інфраструктура

    Баг продукту

    Синхронізація

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

    Класичний антипатерн — жорстке очікування (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), коли читання одразу після запису іноді бачить старі дані, стан, що протікає між запитами користувача, — усе це нестабільність продукту, яку тест чесно ловить.

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

    Діагностика: відтворити, зібрати артефакти, локалізувати

    Діагностика флаку — це відповідь на два питання: чи це взагалі флак (а не стабільно відтворюваний баг) і до якого з п'яти сімейств він належить. Порядок дій:

    Так, завжди

    Ні, плаває

    Так

    Ні, лише в наборі

    Тест упав

    Відтворюється
    стабільно?

    Це не флак:
    баг тесту або продукту

    Зібрати артефакти:
    трейс, відео, логи, скрин

    Запустити локально
    в циклі: repeat-each

    Падає
    ізольовано?

    Синхронізація / дані /
    оточення / баг продукту

    Порядок і
    взаємозалежність

    Так, завжди

    Ні, плаває

    Так

    Ні, лише в наборі

    Тест упав

    Відтворюється
    стабільно?

    Це не флак:
    баг тесту або продукту

    Зібрати артефакти:
    трейс, відео, логи, скрин

    Запустити локально
    в циклі: repeat-each

    Падає
    ізольовано?

    Синхронізація / дані /
    оточення / баг продукту

    Порядок і
    взаємозалежність

    Артефакти — головне джерело правди. Флак майже неможливо зловити «дивлячись очима», бо він рідкісний. Тому інфраструктура має ловити його за тебе: трейс (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 без тікета й відповідального — це просто спосіб назавжди перестати дивитися на тест. Жорсткіша, але чесна альтернатива для зовсім занедбаних тестів: краще видалити тест, ніж тримати вічно зелений через ретраї, — мертвий тест, що нічого не гарантує, гірший за його відсутність, бо створює ілюзію покриття.

    Так

    Ні

    Так

    Ні

    Тест флакає
    й блокує merge

    Причину видно
    швидко?

    Полагодити зараз

    Карантин: тег @flaky,
    прибрати з гейта

    Тікет: власник + дедлайн

    Полагоджено
    до дедлайну?

    Повернути в гейт

    Ескалація або видалення

    Так

    Ні

    Так

    Ні

    Тест флакає
    й блокує merge

    Причину видно
    швидко?

    Полагодити зараз

    Карантин: тег @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-досвідом.

    Джерела