Flaky Tests: причины и решения
Flaky test (нестабильный тест) — тест, который проходит или падает случайно при одном и том же коде, без каких-либо изменений в логике — главный источник недоверия к тест-suite.
Зачем нужно
Нестабильные тесты разрушают доверие к CI: команда начинает перезапускать упавшие тесты не разбираясь в причине. Это маскирует реальные баги. Устранение flakiness — важнейшая задача поддержки тестовой инфраструктуры.
Где используется
- E2E тесты на Playwright/Cypress (чаще всего)
- Integration тесты с реальной БД или сетью
- Unit тесты с Date.now(), Math.random, таймерами
Основной контент
Основные причины flakiness
1. Гонки (race conditions) и таймауты
// ПЛОХО — элемент может не появиться за 100ms
await page.waitForTimeout(100);
await page.click('#submit');
// ХОРОШО — ждём конкретного состояния
await page.waitForSelector('#submit:not([disabled])');
await page.click('#submit');
// Playwright — авто-ожидание встроено в click/fill
await page.click('#submit'); // уже ждёт элемент
2. Зависимость от порядка тестов
// ПЛОХО — тест 2 зависит от данных теста 1
let userId: number;
test('создаёт пользователя', async () => {
userId = await createUser; // userId нужен тесту 2
});
test('удаляет пользователя', async () => {
await deleteUser(userId); // падает если тест 1 не прошёл
});
// ХОРОШО — каждый тест самодостаточен
test.beforeEach(async () => {
testUser = await createUser;
});
test.afterEach(async () => {
await deleteUser(testUser.id);
});
3. Нестабильные данные (Date, random, id)
// ПЛОХО
expect(new Date.toISOString()).toBe('2024-01-01T00:00:00.000Z');
// ХОРОШО — фиксируем время
jest.useFakeTimers;
jest.setSystemTime(new Date('2024-01-01'));
expect(formatToday).toBe('01.01.2024');
4. Утечки состояния между тестами
// ПЛОХО — глобальная переменная загрязняет тесты
let cache = {};
test('кэширует результат', () => { cache['key'] = 'value'; });
test('пустой кэш', () => { expect(cache).toEqual({}); }); // падает!
// ХОРОШО
beforeEach(() => { cache = {}; });
5. Сетевая нестабильность в E2E
// Playwright — retry встроен в assertions
await expect(page.locator('.data-loaded')).toBeVisible({ timeout: 10000 });
// playwright.config.ts — глобальный retry для CI
export default defineConfig({
retries: process.env.CI ? 2 : 0,
});
Диагностика flaky tests
# Playwright — повторить тест 10 раз чтобы воспроизвести flakiness
npx playwright test --repeat-each=10 tests/checkout.spec.ts
# Jest — запустить в случайном порядке
npx jest --randomize
Частые ошибки
- Перезапуск вместо фикса —
--retries 3маскирует проблему; используй retry только как страховку, не как решение - Игнорирование flaky теста —
.skipна нестабильный тест означает что критичный сценарий не покрыт - Один тест покрывает много — длинный E2E сценарий имеет больше точек отказа; дели на более короткие независимые тесты
Связанные темы
- _MOC Тестирование
- E2E -- паттерны и антипаттерны
- E2E тестирование
- Playwright -- обзор
- Cypress -- установка и основы