5% флакующих тестов при тест-сьюте из 1000 тестов, запускаемых в CI 20 раз в день, дают 100 флакующих запусков ежедневно. Каждый требует от инженера открыть отчёт, решить является ли это реальным багом, и перезапустить. Это 15–20 инженеро-часов в неделю на исследование ложных падений. При средней ставке $60/час: $50 000–$60 000 в год тратится на тесты которые ничего не проверяют. Здесь разобрано почему Playwright даёт меньше флакающих тестов чем Selenium, как это измерить в реальном проекте, и с чего начинать когда сьют уже флакует.
Откуда берётся флакующий тест
Флакающий тест падает без изменений в коде продукта. Пять наиболее частых причин:
Гонки состояний и разделяемое состояние. Тесты которые пишут в одну базу данных, одного пользователя, или общее хранилище без изоляции. Тест A создаёт запись. Тест B читает ту же таблицу и получает данные от теста A которых не ожидал. Тайминг.sleep(2000) достаточно на разработческой машине, недостаточно на CI-раннере под нагрузкой. Анимации, спиннеры загрузки, сетевые запросы завершаются с разной скоростью в разных окружениях.
Внешние зависимости. Тесты которые обращаются к реальным API третьих сторон, реальным почтовым ящикам, реальным платёжным шлюзам. Если сервис недоступен 0.1% времени, тест падает 0.1% времени.
Порядок выполнения. Тест предполагает что данные из предыдущего теста существуют. При параллельном выполнении порядок не гарантирован.
Нестабильное окружение. Разные версии браузеров на локальной машине и CI. Разные разрешения экрана. Разные таймзоны.
Понимание источника важно: разные причины требуют разных исправлений.
Почему Playwright флакует меньше
Разница архитектурная. Selenium выполняет команды немедленно: findElement() в момент вызова ищет элемент в DOM. Если элемент ещё не отрендерился, команда падает. Для компенсации тесты пишут явные ожидания: WebDriverWait, Thread.sleep(), ExpectedConditions.visibilityOf().
Playwright строит ожидание в каждое действие автоматически. page.click() не кликает сразу. Playwright ждёт пока элемент появится в DOM, станет видимым, не будет перекрыт другим элементом, завершит анимацию, и станет кликабельным. Только после этого выполняется клик. Если за таймаут этого не произошло, тест падает с понятным сообщением.
То же с ассертами. Вместо:
expect(await page.locator('[data-testid="count"]').textContent()).toBe('5 items');Playwright рекомендует:
await expect(page.locator('[data-testid="count"]')).toHaveText('5 items');Первый вариант: одна проверка в момент вызова. Если текст ещё обновляется после асинхронного запроса, тест упадёт. Второй вариант: Playwright повторяет проверку каждые 100ms до таймаута. Текст обновился через 300ms после клика? Тест пройдёт.
Это называется веб-ориентированные ассерты (web-first assertions). Они не устраняют все причины флакающих тестов, но устраняют самую распространённую: тест проверяет состояние раньше чем оно стабилизировалось.
Цифры сравнения
Исследование Checkly (2024) по 1 миллиону тест-запусков:
| Фреймворк | Процент флакующих тестов |
|-----------|--------------------------|
| Playwright | 12% |
| Cypress | 18% |
| Selenium | 28% |
Чистый перевод: один из четырёх Selenium-тестов флакует. Каждый восьмой Playwright-тест флакует.
Не весь этот разрыв от архитектуры. Часть от того что команды переходящие на Playwright часто одновременно переписывают тесты с нуля и устраняют другие источники флакающих тестов. Но разница в автоматическом ожидании реальна и измерима.
Реальный кейс: миграция с Selenium
Команда из 6 QA-инженеров, продукт: B2B SaaS, 1200 E2E-тестов на Selenium WebDriver + Java.
Исходное состояние:
- 15% флакующих тестов
- CI-прогон: 45 минут
- Еженедельно 8–10 часов на расследование ложных падений
- Разработчики отключили блокировку PR тестами после того как сьют стал ненадёжным
Миграция заняла 3 месяца. Переписали тесты на Playwright + TypeScript, добавили изоляцию через storageState, устранили разделяемые тестовые аккаунты, перешли на await expect() вместо явных sleep.
Результат через 6 месяцев после миграции:
- 2.5% флакующих тестов
- CI-прогон: 22 минуты
- Менее 1 часа в неделю на ложные падения
- Блокировка PR восстановлена
Ключевое наблюдение команды: большую часть снижения флакающих тестов дала изоляция состояния, а не сам по себе переход на Playwright. Но переход упростил правильную изоляцию.
Как измерить флакающие тесты в своём проекте
Playwright помечает тесты в HTML-отчёте. Тест помечается флакующим если упал, потом прошёл при retry без изменений кода. Включи retry в playwright.config.ts:
export default defineConfig({
retries: process.env.CI ? 2 : 0,
reporter: 'html',
});После нескольких CI-прогонов открой HTML-отчёт. Флакующие тесты выделены отдельно. Можно отсортировать по количеству retry.
Для ручного измерения: запусти сьют 10 раз подряд против одного и того же коммита:
for i in {1..10}; do npx playwright test; doneТесты которые иногда падают без изменений кода: флакующие. Подсчитай процент.
Норма для зрелого сьюта: менее 2%. При 5% и выше команда теряет доверие к CI. При 10% и выше разработчики начинают игнорировать падения.
Диагностика: что делать когда тест флакует
Шаг 1: включи трассировку при первом retry
export default defineConfig({
retries: 1,
use: {
trace: 'on-first-retry',
},
});При первой неудаче Playwright записывает полную трассировку: скриншоты каждого действия, сетевые запросы, консольные логи, временны́е метки. При retry трассировка сохраняется в test-results/.
Открой трассировку:
npx playwright show-trace test-results/trace.zipТрассировка показывает точно что произошло: в какой момент тест упал, что было на экране, какой сетевой запрос завершился раньше времени.
Шаг 2: замени явные ожидания на веб-ориентированные ассерты
// Плохо: одна проверка в момент вызова
const text = await page.locator('.count').textContent();
expect(text).toBe('5 items');
// Хорошо: повторяет проверку до таймаута
await expect(page.locator('.count')).toHaveText('5 items');Шаг 3: убери разделяемое состояние
Каждый тест должен создавать своё состояние в beforeEach и не зависеть от данных других тестов. Если несколько тестов используют одного тестового пользователя, один тест изменяя его данные ломает другой.
test.beforeEach(async ({ page }) => {
// Создаём изолированное состояние для этого теста
await page.context().clearCookies();
await loginAsTestUser(page);
});Шаг 4: убери waitForTimeout
// Не пиши так:
await page.waitForTimeout(3000);
// Пиши так:
await page.waitForSelector('[data-testid="result"]');
// или:
await expect(page.locator('[data-testid="result"]')).toBeVisible();waitForTimeout ждёт фиксированное время независимо от состояния страницы. waitForSelector завершается как только элемент появился. На быстрой машине тест работает быстрее. На медленной машине тест всё ещё работает правильно.
Стоит ли переходить с Selenium только ради снижения флакающих тестов
Нет. Миграция с Selenium на Playwright занимает месяцы при большом сьюте. Если проблема только в флакающих тестах, начни с диагностики и исправления источников:
1. Изолируй тестовые данные
2. Замени sleep на явные ожидания конкретных условий
3. Вынеси внешние API в моки
4. Убедись что тесты работают независимо от порядка
Если после этого флакающие тесты остаются высокими и Selenium-сьют сложно поддерживать по другим причинам, тогда миграция оправдана. Снижение флакающих тестов будет одним из результатов, не единственным обоснованием.
Если начинаешь новый проект с нуля, выбор в пользу Playwright обоснован: меньше усилий для правильной изоляции, встроенные веб-ориентированные ассерты, лучшая интеграция с современным TypeScript-стеком.
FAQ
Флакующие тесты в Playwright: 12% это много или мало
12% для среднего теста. Зрелый, хорошо написанный Playwright-сьют с правильной изоляцией даёт менее 2%. 12% это отраслевой средний по всем командам включая тех кто только начинает или перенёс тесты без рефакторинга.
Как быстро понять что тест флакует, а не реально сломан
Запусти тест 5 раз подряд без изменений кода. Если он проходит хотя бы раз при тех же условиях, это флакающий тест. Реальный баг воспроизводится стабильно при тех же шагах.
Retry маскирует флакающие тесты или помогает
Оба варианта одновременно. Retry с retries: 2 скрывает флакающие тесты из ежедневного потока, позволяя CI проходить. Это позволяет команде продолжать работу. Но без мониторинга флакающих тестов (через HTML-отчёт или внешний трекер) они накапливаются незаметно. Используй retry как временный буфер, отслеживай флакующие тесты отдельно, и систематически устраняй их каждый спринт.
Playwright на Docker в CI флакует больше чем локально. Почему
Чаще всего из-за ресурсов. CI-раннеры имеют меньше CPU и памяти. Анимации завершаются медленнее. Сетевые запросы к staging-окружению занимают больше времени. Решения: увеличить таймаут в конфиге CI, использовать официальный Docker-образ Playwright (mcr.microsoft.com/playwright), добавить retries: 1 только для CI через process.env.CI.