Flaky tests в API: как сделать тесты стабильными на 100%

← Вернуться к статьям

Введение

Представьте ситуацию: ваш CI/CD пайплайн падает. Вы перезапускаете тесты — они проходят. Через час снова падение на том же тесте. Вы не меняли код. Что происходит?

Это Flaky tests — нестабильные тесты, которые иногда проходят, иногда падают без явных причин. По статистике Google, в крупных проектах до 16% тестов являются Flaky. Это тормозит разработку, подрывает доверие к тестам и стоит огромных денег.

💸 Цена нестабильных тестов

Реальная статистика:

В этой статье вы узнаете:

📋 Содержание

Что такое Flaky tests

📖 Определение

Flaky test (нестабильный тест) — это тест, который иногда проходит, иногда падает, при этом код приложения не менялся. Тест дает разные результаты при одинаковых условиях.

Признаки Flaky теста

Признак Описание Как проявляется
🔄 Нестабильность Тест падает случайно Сегодня ✅, завтра ❌, послезавтра ✅
🎲 Непредсказуемость Невозможно предсказать результат Локально ✅, в CI ❌
🔁 Повторный запуск помогает Тест проходит после перезапуска "Перезапусти — должно пройти"
🤷 Нет явной причины Код не менялся "Почему он упал? Не знаю"

Типичный сценарий

$ npm test

✓ User login test
✓ Create order test
✗ Get order details test  // Упал!

$ npm test  // Перезапуск без изменений

✓ User login test
✓ Create order test
✓ Get order details test  // Прошел! 🤔

// Через час
✗ Get order details test  // Снова упал! 😡

🎯 Масштаб проблемы

Исследование Google (2016):

Исследование Microsoft (2020):

5 причин нестабильности API тестов

1. Зависимость от внешних API

Проблема: Тесты зависят от реальных внешних API (платежи, уведомления, погода, карты и т.д.)

⚠️ Что может пойти не так:

Пример Flaky теста:

// ❌ Нестабильный тест
test('Should get weather data', async () => {
  // Зависит от внешнего API
  const response = await fetch('https://api.weather.com/forecast');
  const data = await response.json();

  expect(data.temperature).toBeGreaterThan(0);
  // ⚠️ API может:
  // - Не ответить (timeout)
  // - Вернуть ошибку 429 (rate limit)
  // - Быть недоступным
});

2. Race Conditions и асинхронность

Проблема: Тесты не учитывают асинхронную природу API запросов.

// ❌ Race condition
test('Should create and retrieve order', async () => {
  // Создаем заказ
  await fetch('/api/orders', {
    method: 'POST',
    body: JSON.stringify({ item: 'Laptop' })
  });

  // Сразу пытаемся получить
  const response = await fetch('/api/orders/latest');
  const order = await response.json();

  expect(order.item).toBe('Laptop');
  // ⚠️ Может упасть, если:
  // - База не успела обновиться
  // - Есть задержка репликации
  // - Кэш не инвалидировался
});

3. Случайные и динамические данные

Проблема: API возвращает разные данные при каждом запросе.

// ❌ Нестабильные данные
test('Should get user profile', async () => {
  const response = await fetch('/api/users/123');
  const user = await response.json();

  // ⚠️ Поля могут меняться:
  expect(user.id).toBe('123');
  expect(user.lastLogin).toBe('2025-10-20T10:00:00Z'); // Разное каждый раз!
  expect(user.notifications).toHaveLength(5); // Может быть 4, 5, 6...
  expect(user.sessionToken).toBe('abc123'); // Новый каждый раз!
});

4. Таймауты и медленные API

Проблема: API отвечает медленно, тесты падают по таймауту.

// ❌ Таймауты
test('Should process payment', async () => {
  const response = await fetch('/api/payments', {
    method: 'POST',
    body: JSON.stringify({ amount: 100 })
  });

  // ⚠️ Может упасть если:
  // - API отвечает медленно (> 5 сек)
  // - База данных под нагрузкой
  // - Очередь обработки заполнена
}, 5000); // timeout 5 секунд

5. Нестабильное окружение (Staging/Dev)

Проблема: Тесты зависят от staging/dev окружения, которое нестабильно.

⚠️ Проблемы staging окружения:

Как Mock API решает проблему

Mock API предоставляет полный контроль над API ответами, устраняя все 5 причин нестабильности.

✅ Преимущества Mock API для стабильных тестов

  1. Детерминированные ответы: Одинаковые данные при каждом запуске
  2. Нет внешних зависимостей: Не зависит от интернета, сторонних сервисов
  3. Мгновенные ответы: Нет таймаутов, всегда быстро
  4. Контроль над данными: Точно знаете, что вернется
  5. Изоляция: Каждый тест работает независимо
  6. 100% доступность: Работает всегда, без сбоев

Сравнение: Real API vs Mock API

Аспект Real API Mock API
Доступность ❌ 95-99% ✅ 100%
Скорость ответа ❌ 100-2000ms ✅ 10-50ms
Детерминированность ❌ Разные данные ✅ Одинаковые всегда
Rate limiting ❌ Да (100-1000 req/min) ✅ Нет ограничений
Зависимость от сети ❌ Требует интернет ✅ Работает офлайн
Стабильность ❌ 70-85% ✅ 100%

Детерминированные тесты

📖 Что такое детерминированный тест?

Детерминированный тест — это тест, который при одинаковых входных данных всегда возвращает один и тот же результат. Нет случайности, нет зависимости от внешних факторов.

Принципы детерминированных тестов

  1. Изолированность: Тест не зависит от других тестов
  2. Воспроизводимость: Можно запустить 100 раз — 100 раз одинаковый результат
  3. Предсказуемость: Известно, что вернет API
  4. Независимость: Не зависит от времени, сети, внешних API

Примеры до/после

Пример 1: Тестирование создания заказа

❌ ДО (Flaky test)

test('Create order', async () => {
  // Реальный API
  const res = await fetch(
    'https://staging.myapi.com/orders',
    {
      method: 'POST',
      body: JSON.stringify({
        userId: 123,
        item: 'Laptop'
      })
    }
  );

  const order = await res.json();

  // ⚠️ Проблемы:
  // - Staging может быть недоступен
  // - userId 123 может не существовать
  // - Таймаут если сервер медленный
  // - Конфликт если другой тест
  //   уже создал заказ

  expect(order.id).toBeDefined();
  expect(order.status).toBe('pending');
});

// 🎲 Стабильность: 60-70%

✅ ПОСЛЕ (Stable test)

test('Create order', async () => {
  // Mock API
  const res = await fetch(
    'https://yourdomain.lightboxapi.ru/orders',
    {
      method: 'POST',
      body: JSON.stringify({
        userId: 123,
        item: 'Laptop'
      })
    }
  );

  const order = await res.json();

  // ✅ Всегда возвращает:
  // {
  //   "id": "order_123",
  //   "userId": 123,
  //   "item": "Laptop",
  //   "status": "pending",
  //   "createdAt": "2025-10-20T10:00:00Z"
  // }

  expect(order.id).toBe('order_123');
  expect(order.status).toBe('pending');
});

// ✅ Стабильность: 100%

Пример 2: Интеграционный тест с платежами

❌ ДО (Flaky test)

test('Process payment', async () => {
  // Реальный Stripe API
  const payment = await stripe.charges.create({
    amount: 2000,
    currency: 'usd',
    source: 'tok_visa'
  });

  // ⚠️ Проблемы:
  // - Stripe может быть недоступен
  // - Rate limiting (100 req/sec)
  // - Таймауты при нагрузке
  // - Случайные ошибки сети
  // - Стоимость: $0.01 за тест

  expect(payment.status).toBe('succeeded');
});

// 🎲 Стабильность: 75-80%
// 💰 Стоимость: $10/месяц (1000 тестов)

✅ ПОСЛЕ (Stable test)

test('Process payment', async () => {
  // Mock Stripe API
  const payment = await fetch(
    'https://yourdomain.lightboxapi.ru/stripe/charges',
    {
      method: 'POST',
      body: JSON.stringify({
        amount: 2000,
        currency: 'usd',
        source: 'tok_visa'
      })
    }
  ).then(r => r.json());

  // ✅ Детерминированный ответ
  // Мгновенно, бесплатно, стабильно

  expect(payment.status).toBe('succeeded');
  expect(payment.amount).toBe(2000);
});

// ✅ Стабильность: 100%
// 💰 Стоимость: $0

Пример 3: E2E тест с Cypress

❌ ДО (Flaky test)

// cypress/e2e/orders.cy.js
describe('Order flow', () => {
  it('Creates and views order', () => {
    cy.visit('/orders/new');
    cy.get('#item').type('Laptop');
    cy.get('#submit').click();

    // ⚠️ Ждем ответа от реального API
    cy.wait(3000); // Race condition!

    cy.get('.order-id')
      .should('be.visible');

    // ⚠️ Может упасть если:
    // - API медленно отвечает
    // - Staging недоступен
    // - Database под нагрузкой
  });
});

// 🎲 Стабильность: 65-70%

✅ ПОСЛЕ (Stable test)

// cypress/e2e/orders.cy.js
describe('Order flow', () => {
  beforeEach(() => {
    // Перехватываем API с Mock
    cy.intercept('POST', '/api/orders', {
      statusCode: 200,
      body: {
        id: 'order_123',
        status: 'pending'
      },
      delay: 100 // Детерминированная задержка
    }).as('createOrder');
  });

  it('Creates and views order', () => {
    cy.visit('/orders/new');
    cy.get('#item').type('Laptop');
    cy.get('#submit').click();

    // ✅ Ждем Mock ответ
    cy.wait('@createOrder');

    cy.get('.order-id')
      .should('contain', 'order_123');
  });
});

// ✅ Стабильность: 100%

Метрики стабильности тестов

Как измерять Flaky tests

📊 Главные метрики:

  1. Flaky Rate: % тестов, которые нестабильны
  2. Rerun Rate: Сколько раз перезапускаем тесты
  3. Pass Rate: % успешных прогонов
  4. Mean Time to Green: Среднее время до успешного прогона

Формула Flaky Rate

Flaky Rate = (Количество Flaky тестов / Общее количество тестов) × 100%

Пример:
- Всего тестов: 500
- Flaky тестов: 50
- Flaky Rate = (50 / 500) × 100% = 10%

Бенчмарки стабильности

Flaky Rate Оценка Действия
< 5% ✅ Отлично Поддерживать уровень
5-10% ⚠️ Приемлемо Планировать улучшения
10-20% ❌ Проблема Срочно стабилизировать
> 20% 🔥 Критично Приостановить новые фичи, фокус на стабилизации

Реальные результаты с Mock API

Flaky Rate
98%

Снижение (с 30% до 0.6%)

Время тестов
-75%

с 12 мин до 3 мин

Reruns
-95%

с 50/день до 2/день

Доверие команды
100%

Команда верит тестам

Кейс: Команда из 8 разработчиков

📈 ДО внедрения Mock API:

📈 ПОСЛЕ внедрения Mock API:

Best Practices

1. Изолируйте тесты от внешних зависимостей

✅ Правило:

Каждый тест должен быть самодостаточным. Не зависеть от:

2. Используйте фиксированные данные

// ❌ Плохо: Случайные данные
const userId = Math.random().toString();
const timestamp = new Date().toISOString();

// ✅ Хорошо: Фиксированные данные
const userId = 'user_123';
const timestamp = '2025-10-20T10:00:00Z';

3. Контролируйте время

// ❌ Плохо: Зависит от реального времени
expect(user.createdAt).toBe(new Date().toISOString());

// ✅ Хорошо: Mock времени
jest.useFakeTimers();
jest.setSystemTime(new Date('2025-10-20T10:00:00Z'));
expect(user.createdAt).toBe('2025-10-20T10:00:00Z');

4. Логируйте и мониторьте Flaky tests

# .github/workflows/tests.yml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Run tests
        run: npm test

      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: test-results
          path: test-results.xml

      # Отслеживание Flaky tests
      - name: Detect Flaky Tests
        uses: test-analytics/action@v1
        with:
          results: test-results.xml

5. Автоматически помечайте Flaky тесты

// jest.config.js
module.exports = {
  testRunner: 'jest-circus/runner',
  reporters: [
    'default',
    ['jest-flaky-test-reporter', {
      threshold: 2, // Помечать после 2 падений
      markAsFlaky: true
    }]
  ]
};

FAQ

❓ Как быстро найти все Flaky тесты?

Ответ: Запустите все тесты 10 раз подряд:

# Bash
for i in {1..10}; do npm test; done

# Или с Jest
npm test -- --repeat=10

Тесты, которые хотя бы раз упали — Flaky. Или используйте инструменты:

  • Flaky Test Detection (GitHub Actions)
  • BuildPulse (CI/CD аналитика)
  • Datadog CI (мониторинг тестов)

❓ Можно ли использовать Mock API только для Flaky тестов?

Ответ: Да! Гибридный подход:

  • Unit тесты: Всегда Mock API (изоляция)
  • Integration тесты: Mock API для нестабильных зависимостей
  • E2E тесты: 90% Mock API, 10% real API (smoke tests)

❓ Не потеряем ли мы покрытие, используя Mock API?

Ответ: Нет, если правильно спроектировать тесты:

  • Mock API тесты: Проверяют логику вашего приложения (95% тестов)
  • Contract Tests: Проверяют, что Mock соответствует реальному API
  • Smoke Tests: 5-10 критических сценариев с real API (раз в день)

Результат: Покрытие увеличивается, т.к. тесты стабильны и команда им доверяет.

❓ Как убедить команду перейти на Mock API?

Ответ: Покажите цифры:

  1. Измерьте текущий Flaky Rate
  2. Посчитайте стоимость (время × ставка)
  3. Сделайте pilot с 10 самыми проблемными тестами
  4. Покажите результаты: -95% Flaky, -70% времени

Заключение

🎯 Главные выводы

  1. Flaky tests — серьезная проблема: Стоят $100,000+ в год для команды из 10 человек
  2. 5 причин нестабильности: Внешние API, race conditions, случайные данные, таймауты, нестабильное окружение
  3. Mock API — решение: 100% стабильные, детерминированные тесты
  4. Детерминированность: Одинаковые входные данные → одинаковый результат
  5. Измеряйте метрики: Flaky Rate < 5% — отлично
  6. Best Practices: Изолируйте тесты, фиксируйте данные, мониторьте стабильность

🚀 План действий

  1. Неделя 1: Измерьте Flaky Rate (запустите тесты 10 раз)
  2. Неделя 2: Выберите 10 самых проблемных тестов
  3. Неделя 3-4: Настройте Mock API для этих тестов
  4. Неделя 5: Измерьте результаты, покажите команде
  5. Неделя 6+: Мигрируйте остальные тесты

Цель: Flaky Rate < 5% за 2 месяца

🎯 Попробуйте LightBox API

Хотите избавиться от Flaky tests за неделю?

LightBox API — это Mock API платформа для стабильных тестов.

Начать бесплатно →

Статья обновлена: 20 октября 2025
Автор: LightBox API Team