← назад к разделу

Представьте: тест падает. Вы запускаете снова — проходит. Код не менялся. Это нестабильный тест, и он опаснее, чем его отсутствие.

Команда быстро привыкает перезапускать «красное» и не воспринимать его всерьёз. В какой-то момент среди ложных срабатываний теряется настоящая поломка. Один такой тест подрывает доверие ко всему набору — если нельзя доверять красному, зачем вообще смотреть на результаты?

Поэтому бороться с нестабильностью — не наведение порядка, а условие, при котором e2e вообще имеет смысл.

Тайминги — причина номер один

Тест проверил элемент раньше, чем тот появился на экране. Или нажал кнопку до того, как страница завершила загрузку. Результат — иногда успевает, иногда нет.

Это самая частая причина нестабильности, и она почти полностью снимается одним приёмом: вместо фиксированных задержек (sleep) используют ожидание условия. В Playwright это называется web-first assertions — фреймворк сам ждёт, пока элемент появится, прежде чем проверять.

// Плохо: ждём две секунды и надеемся
await page.waitForTimeout(2000);
await expect(page.locator('.result')).toBeVisible();

// Хорошо: Playwright сам ждёт, пока элемент не появится
await expect(page.locator('.result')).toBeVisible();

Подробнее об ожиданиях — в статье Assertions и ожидания.

Гонки — действие и ожидание разъехались

Тест кликает кнопку и сразу проверяет результат. Но запрос к серверу ещё не завершился. Иногда сервер отвечает быстро и тест проходит, иногда чуть медленнее — и уже нет.

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

// Плохо: клик и ожидание независимы — может проскочить мимо
await page.click('#save');
await page.waitForResponse('/api/orders');

// Хорошо: ожидание «обёрнуто» вокруг клика
const [response] = await Promise.all([
  page.waitForResponse('/api/orders'),
  page.click('#save'),
]);

О перехвате сети и ожидании ответов — в статье Контроль сети.

Общее состояние — тесты мешают друг другу

Тест оставил в базе запись. Следующий тест наткнулся на неё и упал — хотя должен был работать с чистыми данными. При параллельном запуске это случается постоянно.

Решение — изоляция: каждый тест создаёт своё, а после чистит. Не «до» или «после всего набора», а именно каждый — своё.

Если тест зависит от данных, он должен сам их создать. Если он что-то изменяет — откатить. Тогда порядок запуска и параллельность перестают иметь значение.

Подробнее о стратегии данных — в статье Тестовые данные и авторизация.

Внешние зависимости — сеть и сторонние сервисы

Тест обращается к стороннему API. Тот медленно отвечает или вовсе недоступен. Тест падает — не из-за вашего кода, а из-за чужого.

Внешние зависимости мокируют: Playwright умеет перехватывать запросы и возвращать заготовленный ответ, не обращаясь к реальному серверу. Это делает тест быстрым и стабильным независимо от состояния внешнего мира.

await page.route('https://api.external.com/data', route =>
  route.fulfill({ json: { status: 'ok' } })
);

Retries — обезболивающее, а не лечение

Playwright умеет перезапускать упавший тест (retries). В CI это полезно: иногда инфраструктура подводит сама по себе — нестабильная сеть в контейнере, перегруженный раннер. Один лишний запуск отсеивает такой случайный сбой.

// playwright.config.ts
export default defineConfig({
  retries: process.env.CI ? 2 : 0,
});

Но retries не устраняет нестабильность — он её скрывает. Тест «зелёный со второго раза» выглядит рабочим, а внутри по-прежнему проблема. Если тест регулярно проходит только с повтора — это сигнал: нужно разобраться в причине, а не радоваться зелёному.

Ноль повторов локально помогает быстрее замечать нестабильные тесты и чинить их до того, как они попадут в CI.

Детерминизм — один код, один результат

Тест должен давать одинаковый результат каждый раз при одном и том же коде. Это и называется детерминизм.

Враги детерминизма — всё «плавающее»:

  • Текущее время и даты. Тест, проверяющий «создан сегодня», даст разный результат в разные дни. Подставляйте фиксированное время.
  • Случайные данные. Если тест создаёт пользователя с Math.random() в имени, имя каждый раз разное. Используйте контролируемые уникальные ключи.
  • Порядок элементов. Если список отсортирован по полю, которое может совпасть, порядок непредсказуем. Не опирайтесь на сортировку по умолчанию, если в ней нет жёсткой гарантии.
  • Часовой пояс. Запуск на разных машинах может давать разное «местное время» — явно указывайте зону.

Детерминированный тест либо всегда зелёный, либо всегда красный. Только тогда красному можно верить.

Карантин — не игнор

Нашли нестабильный тест. Что делать?

Отключить и забыть — плохой ответ. Проверка пропадёт, и никто не узнает, что она больше не работает.

Правильный ответ — карантин: пометить тест отдельной меткой, зафиксировать задачу на починку и починить причину. Пока тест в карантине, он запускается отдельно и не блокирует CI, но о нём знают и работают над ним.

Молча закомментированный тест — это потерянная проверка, о которой никто не помнит.

Коротко

  • Нестабильный тест опаснее отсутствующего: команда перестаёт доверять результатам и пропускает настоящие поломки.
  • Главные причины: тайминги, гонки, общее состояние между тестами, внешние зависимости.
  • Тайминги лечат web-first assertions — Playwright сам ждёт, а не вы жёстко задаёте паузу.
  • Гонки лечат связыванием действия и ожидания через Promise.all.
  • Общее состояние лечат изоляцией: каждый тест создаёт и чистит своё.
  • Внешние зависимости мокируют через page.route, чтобы не зависеть от чужого сервера.
  • retries в CI снижают шум от случайных сбоев инфраструктуры, но не лечат нестабильность кода.
  • Детерминизм: один код → один результат. Фиксируйте время, зону, ключи — всё «плавающее».
  • Нестабильный тест переводят в карантин и чинят причину, а не замалчивают.

Что почитать дальше

  • Assertions и ожидания — web-first assertions и правильные ожидания в Playwright.
  • Контроль сети — перехват запросов, моки внешних зависимостей.
  • Тестовые данные и авторизация — изоляция данных между тестами.
  • E2E в CI — как запускать тесты в пайплайне и читать отчёты.