QA Interview Hub · База знань · Тести
System Design
Стабільність і flaky-тести
Звідки беруться flaky-тести
Flaky-тест - це тест, який при тому самому коді іноді проходить, а іноді падає. Головна шкода не в одному падінні, а в тому, що команда перестає довіряти результатам.
Основні причини, від найчастіших:
- Синхронізація. Тест перевіряє стан до того, як UI або бекенд його оновив. Ручні
waitForTimeout замість очікування умови. - Спільні дані. Паралельні тести змінюють один акаунт.
- Залежність від порядку. Тест працює тільки після іншого тесту.
- Асинхронний бекенд. Переказ обробляється через чергу, а тест одразу перевіряє баланс.
- Нестабільне середовище. Перевантажений staging, повільні зовнішні sandbox, мережа.
- Час і дати. Тест падає опівночі, в останній день місяця або в іншій часовій зоні.
- Анімації і нестабільні локатори. Залежність від CSS-класів чи порядку елементів.
Як відрізнити баг продукту від flaky-тесту. Дивимося trace: якщо UI справді показав неправильну суму - це може бути race condition у продукті, і це баг, а не "flaky". Частина flaky-тестів насправді ловить реальні проблеми конкурентності. Тому "просто додати retry" небезпечно.
У Playwright більшість проблем синхронізації вирішують web-first assertions: await expect(locator).toHaveText('70.00') чекає і перевіряє повторно до таймауту. Для бекенду - expect.poll.
На співбесіді: Найчастіші причини flaky - синхронізація, спільні дані, залежність від порядку і асинхронний бекенд. Перш ніж назвати тест flaky, я дивлюся trace, бо іноді нестабільний тест ловить реальний race condition у продукті.
Процес боротьби з flaky: виявити, ізолювати, виправити
Один інженер, що виправляє тести, не масштабується. Потрібен процес.
Виявлення -> Карантин -> Аналіз -> Виправлення -> Повернення
Виявлення. Збираємо історію результатів кожного тесту між прогонами. Тест, що пройшов після retry, Playwright позначає як flaky. Якщо тест має кілька таких результатів за тиждень - він кандидат. Додатково можна раз на ніч проганяти набір з --repeat-each=5, щоб знайти нестабільні тести до того, як вони заважають командам.
Карантин. Нестабільний тест виводиться з блокуючого gate: тег @quarantine, окремий неблокуючий job. На нього обов'язково створюється тікет з власником і терміном. Без власника карантин перетворюється на кладовище.
Аналіз. Trace, відео, логи бекенду за correlation id. Класифікуємо причину: тест, дані, середовище чи продукт.
Виправлення і повернення. Після фіксу тест проганяється багато разів (наприклад, --repeat-each=20) і тільки потім повертається в gate.
Правила, які я б встановив:
- Ліміт карантину, наприклад 2 тижні. Далі тест або виправляють, або видаляють.
- Flaky rate як метрика команди, видима на дашборді.
- Новий тест перед merge проганяється кілька разів.
На співбесіді: Я будую процес: автоматичне виявлення за історією, карантин з власником і терміном, аналіз через trace і логи, повернення в gate тільки після багаторазового стабільного прогону. Ключове - у кожного flaky-тесту є власник, інакше карантин росте вічно.
Retries: коли допомагають, а коли шкодять
Retries в Playwright налаштовуються через retries у конфігу. Тест, що впав і пройшов на повторі, отримує статус flaky.
Коли retry корисний:
- Як тимчасовий захист від інфраструктурних збоїв у CI.
- Для збору діагностики:
trace: 'on-first-retry' записує trace саме тоді, коли тест упав. - Як джерело даних для виявлення flaky.
Коли retry шкодить:
- Коли він ховає баг продукту. Race condition у списанні коштів може проявлятися в 1 з 10 запусків. Retry зробить пайплайн зеленим, а баг піде в прод.
- Коли retries великі (3-5). Час прогону росте, а реальна стабільність не покращується.
- Коли retry на рівні кроку повторює неідемпотентну дію. Повторний POST переказу може створити другий переказ.
Різниця між retry тесту і очікуванням умови. Очікування - це нормальна частина тесту: expect.poll чекає, поки статус переказу стане COMPLETED. Retry тесту - це перезапуск усього тесту з нуля. Перше - правильна синхронізація, друге - запасний механізм.
await expect.poll(async () => {
const res = await api.getTransfer(id);
return res.status;
}, { timeout: 15_000, intervals: [500, 1000, 2000] }).toBe('COMPLETED');
На співбесіді: Я ставлю в CI максимум 1-2 retry і обов'язково відстежую тести, які проходять тільки з повтору. Retry - це страховка і джерело діагностики, а не рішення: у фінтеху він може приховати race condition при списанні коштів.