Что такое нестабильные тесты?
Нестабильный тест (flaky test) — тест, который проходит и падает непредсказуемо без изменений кода. Запускаете сюиту — проходит. Запускаете снова на том же коде — тест падает. В третий раз — снова проходит. Это недетерминированное поведение разрушает доверие к тестовой сюите и тратит огромное количество времени разработчиков на расследование ложных падений.
Google сообщал, что 1.5% их тестов были нестабильными, и эти тесты потребляли 2-16% всех вычислительных ресурсов через повторные запуски.
Корневые причины
1. Проблемы таймингов и синхронизации (самая частая)
// ПЛОХО — захардкоженный sleep
await page.click('#submit');
await page.waitForTimeout(3000);
// ХОРОШО — ожидание условия
await page.click('#submit');
await expect(page.locator('.result')).toHaveText('Success', { timeout: 10000 });
2. Зависимости порядка между тестами
Тесты, зависящие от выполнения других тестов ранее.
3. Общее изменяемое состояние
Тесты, модифицирующие глобальное состояние без надлежащей изоляции.
4. Зависимости от внешних сервисов
Тесты, вызывающие реальные внешние сервисы, которые могут быть медленными или недоступными.
5. Конкуренция за ресурсы
Тесты, конкурирующие за ограниченные ресурсы при параллельном выполнении.
6. Логика, зависящая от времени
Тесты, зависящие от текущего времени или часового пояса.
Исправление нестабильных тестов
Обеспечить изоляцию
@BeforeEach
void isolateTest() { database.beginTransaction(); }
@AfterEach
void cleanupTest() { database.rollbackTransaction(); }
Мокировать внешние сервисы
await page.route('**/api/external-service/**', route => {
route.fulfill({ status: 200, body: JSON.stringify({ result: 'mocked' }) });
});
Системы обнаружения
Режим повтора в PR-пайплайне
- name: Запуск новых тестов 20 раз
run: |
for i in {1..20}; do
npx playwright test --grep @new
done
Дашборд отслеживания
Тест: testCheckoutFlow
Последние 100 прогонов: 96 pass, 4 fail (96% надёжность)
Статус: FLAKY (ниже порога 99%)
Последнее падение: TimeoutError на .payment-confirmation
Система карантина
- Пометить: Добавить тег
@Flakyили перенести в карантинный набор - Изолировать: Убрать из блокирующего CI-пайплайна
- Мониторить: Продолжить запуск в отдельном неблокирующем job
- Исправить: Назначить ответственного и установить дедлайн
- Восстановить: После стабильности в N прогонах вернуть в основной набор
@Tag("quarantine")
@Flaky(reason = "Периодический таймаут на медленных CI раннерах", ticket = "BUG-123")
@Test
void testCheckoutWithCoupon() {
// В карантине — выполняется, но не блокирует деплой
}
Лучшие практики предотвращения
- Никогда не использовать захардкоженные ожидания — всегда явные условия
- Запускать тесты в случайном порядке — выявляет зависимости
- Повторять новые тесты — запускать 20-50 раз перед мержем
- Мокировать внешние сервисы — устранить сетевую вариативность
- Использовать уникальные тестовые данные — избежать конфликтов при параллельном запуске
- Еженедельно просматривать метрики нестабильности
Упражнения
Упражнение 1: Диагностика и исправление
Возьмите 3 намеренно нестабильных теста и исправьте каждый. Задокументируйте причину и решение.
Упражнение 2: Система карантина
Создайте механизм тега @Quarantine, настройте CI для раздельного запуска карантинных тестов, постройте скрипт отслеживания.
Упражнение 3: Пайплайн предотвращения
Добавьте тестирование в режиме повтора для новых тестов, настройте случайный порядок, создайте дашборд нестабильности.