Каждая команда которая бросала автоматизацию рассказывала одну историю. Писали тесты. Работали. Начали падать — то по одному, то пачками. Тратили вечера на разбор. Устали. Удалили. Вывод: "автоматизация не работает в нашем случае".
Нестабильные тесты — это не особенность автоматизации. Это симптом четырёх известных архитектурных проблем. Каждая решается конкретными техниками. Ни одна не требует "купить другой инструмент".
Причина 1: Тест торопится
Самая распространённая причина flaky tests — это тест который кликает раньше чем страница готова.
Выглядит это так: тест открывает страницу, немедленно ищет кнопку, не находит (страница ещё загружается), падает с "element not found". В следующий запуск сервер ответил быстрее — тест прошёл. В следующий раз CDN притормозил — снова упал.
Классическое "решение" которое делают неправильно: добавить sleep(2000) — подождать две секунды. Работает пока сервер отвечает меньше чем за две секунды. Не работает когда нагрузка растёт или сеть медленнее.
ждать состояния, а не времени.
// Неправильно
await page.waitForTimeout(2000)
await page.click('.checkout-btn')
// Правильно
await page.waitForSelector('.checkout-btn', { state: 'visible' })
await page.click('.checkout-btn')
// Ещё правильнее (Playwright делает это автоматически)
await page.click('.checkout-btn') // auto-wait встроенСовременные инструменты включая Playwright делают auto-wait по умолчанию. Если вы всё ещё используете фиксированные таймауты — это сигнал что инструмент или подход устарели.
Причина 2: Тесты зависят друг от друга
Тест A создаёт тестового пользователя. Тест B читает этого пользователя и проверяет его данные. Запускаете B раньше A — B падает. Запускаете параллельно — B иногда падает, иногда нет, в зависимости от порядка.
Это называется shared state между тестами. Это медленная смерть тестового набора.
Признаки проблемы:
каждый тест создаёт всё что ему нужно и убирает за собой.
Используйте beforeEach и afterEach для создания и удаления тестовых данных. Каждый тест — изолированная единица которая оставляет мир в том же состоянии в котором нашла.
Это дороже: каждый тест тратит время на setup. Но тесты становятся предсказуемыми, что стоит дороже.
Причина 3: Хрупкие локаторы
Есть локаторы которые выживают при рефакторинге. И есть локаторы которые не переживают следующего спринта.
div:nth-child(3) > span.text-gray-500
//div[@class='form-group'][2]/input
.container > .row > .col-md-6 > buttonЛюбое изменение структуры HTML или CSS-классов — и они ломаются. Они описывают позицию элемента в дереве, а не его смысл.
[data-testid="submit-order-button"]
[aria-label="Оформить заказ"]
button:has-text("Оформить заказ")Они описывают что элемент делает, а не где он находится. При рефакторинге верстки они остаются валидными.
Лучшее решение — добавить data-testid атрибуты к ключевым элементам в коде приложения. Это небольшая работа для разработчика которая радикально стабилизирует тесты.
Если доступа к коду нет — используйте текстовые локаторы и ARIA-роли. Они устойчивее CSS-классов к большинству UI-изменений.
Причина 4: Внешние зависимости без мокирования
Ваш тест проверяет что пользователь получил email после регистрации. Логика правильная. Но тест использует реальный почтовый сервис. Сервис притормозил — тест упал с таймаутом. Вы получили алерт в час ночи. Полезли разбираться. Оказалось — проблема не в вашем коде.
Это классическая ситуация: тест зависит от системы на которую вы не влияете.
мокировать внешние зависимости.
Вместо реального email-сервиса — тест-дублёр который подтверждает что письмо было запрошено с правильными параметрами. Вместо реального платёжного шлюза — мок который возвращает success. Вместо внешнего API — перехват запроса и возврат фиксированного ответа.
// Playwright: перехват сетевых запросов
await page.route('**/api/payments/**', route => {
route.fulfill({ status: 200, body: JSON.stringify({ status: 'success' }) })
})Тест становится стабильным: он проверяет ваш код а не инфраструктуру третьих сторон.
Системная метрика: flakiness rate
Если вы не измеряете стабильность тестов — вы не управляете ею.
Flakiness rate — это процент тестов которые иногда проходят иногда падают без изменений в коде. Приемлемый уровень: меньше двух процентов от набора. Выше пяти процентов — тестовый набор ненадёжен и скоро будет заброшен.
Хорошие инструменты показывают эту метрику автоматически. Если ваш инструмент не показывает — смотрите на историю запусков и считайте вручную раз в спринт.
FAQ
Почему автотесты часто падают без изменений в коде?
Причин несколько: фиксированные таймауты вместо ожидания состояния, зависимости между тестами через общие данные, хрупкие локаторы по CSS-классам или DOM-позиции, зависимость от внешних сервисов без мокирования. Каждая из этих причин решается конкретными техниками — нестабильные тесты почти никогда не являются проблемой инструмента.
Что такое flaky tests и как с ними бороться?
Flaky tests — это тесты которые иногда проходят, иногда падают, при одинаковом коде. Главные причины: гонка состояний (тест не ждёт загрузки элемента), зависимость от порядка запуска, хрупкие локаторы. Решение: использовать auto-wait вместо setTimeout, изолировать тесты друг от друга, переходить на data-testid локаторы.
Как выбрать правильные локаторы для автотестов чтобы они не ломались?
Используйте data-testid атрибуты — они устойчивы к любому рефакторингу верстки. Следующий по надёжности — ARIA-атрибуты (aria-label, role). Избегайте локаторов по CSS-классам и XPath по DOM-структуре — они ломаются при любом изменении шаблона.
Тесты проходят локально но падают в CI — почему?
Обычные причины: разное время загрузки (CI медленнее, таймауты не соответствуют), разные версии браузера, параллельный запуск который обнажает зависимости между тестами, отсутствие мокирования внешних сервисов. Проверьте каждую причину последовательно.
Как правильно изолировать тесты друг от друга?
Каждый тест должен создавать необходимые данные в beforeEach и удалять их в afterEach. Никаких общих глобальных переменных между тестами. Если тесты работают с базой данных — используйте транзакции которые откатываются, или отдельную тестовую базу которая сбрасывается перед каждым тестом.