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 сценарий имеет больше точек отказа; дели на более короткие независимые тесты

Связанные темы

Ресурсы