Зачем это спрашивают
Нестабильные тесты есть в любом живом проекте автоматизации. Пока их мало, их терпят. Когда их становится много, команда перестаёт смотреть на красные прогоны, и автотесты теряют смысл. Интервьюер хочет понять, как вы поведёте себя в этой ситуации: будете разбираться в причине или нажмёте «перезапустить» и добавите sleep.
Ещё этот вопрос проверяет, понимаете ли вы, что нестабильность бывает не только в тесте. Иногда плавает сам продукт, и тест честно это показывает.
Как построить сильный ответ
Определение
Flaky-тест — тест, который на одном и том же коде и окружении то проходит, то падает. Ключевое — «без изменений»: если упал после коммита, это обычное падение, а не нестабильность.
Основные причины
- Общие данные и порядок. Тесты используют один и тот же объект, и результат зависит от того, кто пришёл первым. Особенно заметно при параллельном запуске.
- Время и ожидания. Жёсткие паузы, слишком короткие таймауты, проверка элемента до того, как он отрисовался.
- Асинхронность. Результат появляется не сразу: фоновые задачи, очереди, обновление интерфейса после ответа сервера.
- Окружение. Нестабильный стенд, медленная сеть, внешние сервисы, которые иногда отвечают ошибкой.
- Дата и случайность. Тест падает в полночь, в конце месяца или на определённом случайном значении.
- Настоящая гонка в продукте. Например, двойной клик создаёт два заказа, но не всегда.
Как бороться
- Разобрать конкретное падение. Сообщение assert, трейсбек, скриншот, видео, трейс. В Playwright для этого есть trace viewer; в pytest-playwright трейс можно сохранять только для упавших тестов флагом
--tracing retain-on-failure. - Изолировать данные. Каждый тест создаёт свои данные и убирает за собой — через фикстуры с
yield, подробнее в статье про фикстуры pytest. В браузерных тестах — новый контекст на каждый тест, чтобы не тащить куки и localStorage. - Ждать состояние, а не время. Вместо
time.sleep(3)илиpage.wait_for_timeout(3000)— проверки, которые сами повторяются до таймаута:
from playwright.sync_api import Page, expect
def test_login_shows_username(page: Page):
page.goto("/login")
page.get_by_label("Имя пользователя").fill("qa_user")
page.get_by_label("Пароль").fill("qa_password")
page.get_by_role("button", name="Войти").click()
expect(page.get_by_test_id("username")).to_have_text("qa_user")
expect ждёт, пока текст станет нужным, и падает только по таймауту. Локаторы Playwright сами ждут, пока элемент станет видимым и доступным для клика.
- Стабилизировать внешнее. Подменять нестабильные внешние сервисы, фиксировать время, задавать seed для случайных данных.
- Воспроизвести локально. Запустить тест много раз подряд или в другом порядке, чтобы поймать частоту падения.
- Карантин с задачей, если быстро починить нельзя. Не удалять и не отключать молча.
Пример ответа
Flaky-тест — это тест, который на том же коде то проходит, то падает. Чаще всего причина в общих данных между тестами, в ожиданиях по времени, в асинхронности или в нестабильном стенде. Иногда это настоящая гонка в продукте, поэтому я не списываю падение на тест сразу. Сначала смотрю сообщение, трейсбек и трейс, повторяю шаг руками. Дальше лечу причину: каждому тесту — свои данные с уборкой через фикстуры, свой контекст браузера; вместо sleep — ожидание нужного состояния через expect; внешние сервисы — подменяю. Если быстро не починить, переношу тест в карантин с задачей и сроком. Перезапуск без разбора и отключение теста решением не считаю: так команда перестаёт доверять прогонам.
Частые ошибки
- «Добавлю sleep». Пауза либо слишком короткая и тест всё равно падает, либо слишком длинная и прогон замедляется. Причина остаётся.
- «Перезапущу, и всё пройдёт». Повтор прячет проблему, а иногда и настоящий баг продукта.
- Молча отключить тест. Проверка исчезает, а риск остаётся.
- Путать нестабильный тест с упавшим после изменений. Если тест стабильно падает после коммита, это сигнал о регрессии, а не flaky.
- Не различать баг теста и баг продукта. Без ручного воспроизведения это гадание. Похожий разбор нужен, когда разработчик не может воспроизвести баг.
Пример на MarketCove
Представьте набор UI-тестов на учебный магазин https://shop.tomiddle.com, запущенный параллельно. Где можно ожидать нестабильности и как её предупредить:
- Корзина хранится в localStorage. Если тесты делят один контекст браузера, товар из одного теста окажется в корзине другого. Решение — новый контекст на каждый тест.
- Заказы и аккаунты живут в памяти сервера. Тест «история заказов в профиле» не должен рассчитывать на точное число заказов, если параллельно другие тесты оформляют заказы под тем же пользователем. Решение — отдельный пользователь на тест или проверка конкретного созданного заказа.
- Вход. Тест нажимает «Войти» и сразу открывает профиль. Токен попадает в localStorage только после ответа сервера, поэтому переход раньше времени даёт то профиль, то форму входа. Нужно дождаться признака входа через
expect, а не полагаться на скорость стенда. - Перезапуск сервера. Данные в памяти пропадают, поэтому тест не должен опираться на заранее созданные заказы.
Задание для практики
Напишите один UI-тест на вход в аккаунт и сделайте его специально нестабильным: прочитайте имя пользователя на странице через inner_text() сразу после клика «Войти», без ожидания. Запустите тест 20 раз подряд и посчитайте, сколько раз он упал. Затем замените чтение на expect(...).to_have_text(...), повторите запуск и сравните. Запишите короткий разбор: причина, как нашли, как починили. Проверьте себя по вопросам в разделе про Playwright и автотесты.
