QA Interview Hub · База знань · Тести

System Design

Стабільність і flaky-тести

Звідки беруться flaky-тести

Flaky-тест - це тест, який при тому самому коді іноді проходить, а іноді падає. Головна шкода не в одному падінні, а в тому, що команда перестає довіряти результатам.

Основні причини, від найчастіших:

  1. Синхронізація. Тест перевіряє стан до того, як UI або бекенд його оновив. Ручні waitForTimeout замість очікування умови.
  2. Спільні дані. Паралельні тести змінюють один акаунт.
  3. Залежність від порядку. Тест працює тільки після іншого тесту.
  4. Асинхронний бекенд. Переказ обробляється через чергу, а тест одразу перевіряє баланс.
  5. Нестабільне середовище. Перевантажений staging, повільні зовнішні sandbox, мережа.
  6. Час і дати. Тест падає опівночі, в останній день місяця або в іншій часовій зоні.
  7. Анімації і нестабільні локатори. Залежність від 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 при списанні коштів.