— История о том, почему половина ваших тестов падает «сама по себе», и как я научился находить виновников по исходному коду.
Лид: Час на перезапуск билда — это норма?
Представь: пятница, вечер. Ты запускаешь CI для последнего пулл-реквеста, идёшь наливать кофе, возвращаешься… а билд упал. Один тест. Ты перезапускаешь проходит. «Флаки», — вздыхаешь ты и ставишь лейбл flaky. На следующей неделе история повторяется. Потом ещё раз.
Мы привыкли, что нестабильные тесты — это неизбежное зло. Их ловят повторными прогонами, а если повезёт вырезают. Но знаешь, что реально бесит? В 80% случаев корень проблемы можно найти, просто посмотрев на код теста.
Я написал инструмент, который это делает автоматически. Без логов CI, без истории прогонов — только AST и машинное обучение.
Назвал его FlakyDetector. Первая версия была исследовательским прототипом (про него у меня выходила статья на Хабре). А теперь — это полноценный продукт: CLI, веб-дашборд, CI-интеграция и даже React-фронтенд. И да, он open source.
Давай разберёмся, как это устроено.
Контекст: Что такое flaky test и почему их не лечат логгерами?
Flaky-тест — это тест, который без изменения кода может и упасть, и пройти. Классика:
time.sleep(0.5)— а на медленной машине нужно 0.7.datetime.now()— а завтра упадёт из-за перехода на летнее время.Глобальная переменная
counter, которую мутируют тесты параллельно.Запрос к реальному API без мока то доступ есть, то нет.
Большинство проектов реагируют просто: перезапускают билд. Некоторые используют pytest-rerunfailures. Но это лечение симптомов, а не причины.
Можно, конечно, написать линтер, который ищет time.sleep. Но что делать с «запахом» цикломатической сложности в тесте? Или с тем, что тест одновременно зависит от времени и от глобального состояния? Простые правила не работают.
Нужно что-то умнее. Например, статический анализ + ML.

Суть простыми словами: Анализатор, который видит грядущие проблемы
FlakyDetector читает твой тест как программист, но без усталости. Он:
Парсит код в абстрактное синтаксическое дерево (AST) — ту самую структуру, которую Python строит перед компиляцией.
Находит антипаттерны —
time.sleep, вызовыdatetime.now(), модификацию глобальных переменных, незамокированные HTTP-запросы и ещё 8 видов.Собирает метрики — количество таких вызовов, цикломатическую сложность, соотношение асинхронных операций к логам.
Кормит всё это в обученную модель CatBoost, которая выдаёт вероятность, что тест флаки.
AST-паттерны: больше чем просто time.sleep
Я добавил детекцию 11 видов антипаттернов, разбитых на категории:
Timing –
sleep,time.time,datetime.now,threading.Timer.State – глобальные переменные, модификация окружения (
os.environ),random.Network – вызовы
requests.get,httpxбез мока.Concurrency –
threading,asyncio.create_taskбез ожидания.
Каждый найденный паттерн получает severity (LOW/MEDIUM/CRITICAL) и confidence (насколько уверен детектор, что это именно проблема).
Feature Engineering: от AST к 37 числам
Просто списка паттернов мало. Модели нужно количество и контекст. Я сформировал 37-мерный вектор:
16 счётчиков конкретных AST-узлов.
9 агрегированных баллов по категориям (Timing, State, Network).
Цикломатическая сложность тестовой функции (если >10 — запах).
Производные признаки:
ast_to_log_ratio(много операций, мало логов — подозрительно),pattern_diversity.8 признаков уверенности: максимальная, средняя, разброс.
Модель: CatBoost вместо чёрного ящика
Я выбрал CatBoost, потому что он:
Работает с категориальными признаками (у нас они есть).
Даёт Feature Importance — можно объяснить, почему тест признан флаки.
Легко сериализуется в
.cbmи грузится в прод.
Модель обучена на синтетическом датасете (смесь реальных проектов из открытых репозиториев + сгенерированные флаки-паттерны). Точность на валидации — 87% (при полноте 82%). Не топ, но для первой версии — достойно. А главное — объяснимо.
Цифры, бенчмарки и сравнение
Чтобы ты понимал масштаб:
Метрика |
Значение |
|---|---|
AST-паттернов |
11+ |
Размерность вектора |
37 |
Модель |
CatBoost (gradient boosting) |
Время анализа одного теста |
~50 мс (на Core i5) |
Языки |
Python 3.12+ |
Интерфейсы |
CLI, REST API, React Dashboard |
Интеграции |
GitHub Actions, Docker, pre-commit |
Конкуренты?
Есть плагины к flake8 (flake8-flaky-tests), но они ищут только time.sleep и pytest.mark.flaky. Есть pytest-repeat, но он не анализирует код. А ML-подхода в open source я не видел вообще. FlakyDetector — уникален.
Практическое применение: Как запустить уже сегодня
Два варианта — для быстрых и для основательных.
CLI-сканер (быстро)
git clone https://github.com/Artem7898/flakydetector cd flakydetector uv venv --python 3.12 source .venv/bin/activate uv pip install -e ".[dev]" python scripts/train_model.py # сгенерировать датасет и обучить модель uv run python scripts/scan_folder.py ./my_project/tests/
Вывод — красивая таблица в терминале (спасибо rich):
? Scanning: ./my_project/tests/ ... Flaky Patterns Detected ┏━━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━┓ ┃ File ┃ Line ┃ Pattern ┃ Severity ┃ Confidence┃ ┡━━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━┩ │ test_api.py │ 42 │ time_sleep │ MEDIUM │ 90% │ │ test_db.py │ 15 │ global_mutate │ CRITICAL │ 95% │ └────────────────┴───────┴───────────────┴──────────┴───────────┘
Docker + веб-дашборд (для менеджеров)
docker-compose up --build # бэкенд на http://localhost:8001 cd dashboard_frontend && npm install && npm run dev # фронт на http://localhost:3000
Открываешь — а там графики распределения severity, список файлов с подсветкой синтаксиса, можно кликнуть на паттерн — увидишь строку кода.
CI/CD: блокируем PR автоматически
Добавь в .github/workflows/flaky_detection.yml:
- name: Run FlakyDetector run: uv run python scripts/scan_folder.py ./tests --fail-on-critical
Если ты дочитал до сюда — спасибо. Ты уже на голову выше тех, кто просто перезапускает билд три раза подряд.
FlakyDetector — это мой эксперимент на стыке статического анализа и ML. Он не идеален, но он уже экономит время. Попробуй просканировать свои тесты. Уверен, найдёшь пару сюрпризов.
Репозиторий: github.com/Artem7898/flakydetector
Предыдущая статья (первая версия): habr.com/ru/articles/969354
Ariless
Интересный подход, особенно комбинация AST + ML. Вопрос: как инструмент обрабатывает случаи, когда flakiness не в самом коде теста, а в shared state между тестами?
У меня был кейс: fixture создавала слот с фиксированными timestamp'ами. Код теста абсолютно чистый, никаких антипаттернов. Но если предыдущий тест не успел удалить свой слот - новый fixture падал с SLOT_OVERLAP. В изоляции тест всегда зелёный, в CI при определённом порядке падал.
37 признаков из AST это не обнаружат - там нет ничего подозрительного. Планируете добавлять анализ взаимодействий между тестами?
Artem7898 Автор
Привет! Ого, вот это кейс — прямо хрестоматийный пример той самой «дьявольской» сложности, с которой мы все сталкиваемся.
Во-первых, огромное спасибо за этот пример — в нём вскрывается настоящее ахиллесово пятно многих статических анализаторов.
Почему мой AST-анализ бессилен в твоём случае
Модель выдаст уверенные 99% с пометкой Чисто, потому что формально код безупречен нет sleep, нет мутаций глобальных переменных, нет сетевых вызовов. Пока не встанет в очередь за другим тестом. Исследования это подтверждают: около 59% нестабильных тестов в Python появляются именно из-за такого порядка выполнения.
Но вот что важно: сама по себе ошибка возникает не в коде, а в данных, которые создаёт фикстура а это уже территория динамического анализа, который требует выполнения, а не просто чтения кода.
· Динамический анализ через логи (как в FlakyXbert). Он сравнивает состояния в момент падения и прохода и может сказать: В момент прогона в слоте было занято, сейчас нет. · Анализ зависимостей данных (как в Peeler). Ищет цепочки: тест А что-то изменил → тест Б это прочитал. · Детерминированные стратегии выполнения. Простой, но мощный метод — инструменты вроде JS-TOD для JS специально прогоняют тесты в случайном порядке (10, 20, 50 раз). Если хоть раз упадёт — значит, есть скрытая связка.
Как это повлияет на мои планы по FlakyDetector
Я сейчас очень плотно изучаю два направления:
· Анализ связок тестов. Хочется не просто сказать У вас проблема в порядке тестов, а указать пальцем: Тест B падает, потому что тест A не почистил за собой конкретный слот. · LLM-классификация из коробки. Современные модели показывают очень достойные результаты в предсказании нестабильности по одному лишь коду, не требуя ручного выделения 37 признаков.
Поскольку я ещё только пилю следующий релиз, вот тебе мои «костыли»:
· pytest-randomly или pytest-xdist --random-order — прогони свои тесты в разнобой, и он тебе со 100% вероятностью упадёт. · Идеальным решением для твоей боли будет transactional fixture. Суть проста: тест запускается → фикстура создаёт слот → тест завершается → фикстура гарантированно всё откатывает. В этом случае все следы пребывания теста в БО уничтожаются, и соседям нечем пачкаться.
А твой кейс по ссылкам на ptvsd, mergify и замоканный datetime я с удовольствием изучу — как раз для полировки следующей версии.
Ещё раз спасибо за такой развёрнутый фидбек! Если будут ещё идеи или примеры — подкидывай. Пиши в комментариях, интересно обсудить.
Ariless
Привет! Именно так и вышло в итоге - transactional fixture. В Playwright это function-scoped fixture: создаёт данные для конкретного теста, убивает их в teardown. Если предыдущий тест по какой-то причине не дочистил, то следующий всё равно начинает с чистого листа.
Интересный момент: решение оказалось архитектурным, не тестовым. Код самого теста не трогали, а только перевели fixture из shared scope в function scope. После этого SLOT_OVERLAP исчез полностью.
И вот здесь как раз проявляется "flakiness as a signal": тест падал не потому, что был плохо написан, а потому что подсвечивал реальный дефект в дизайне окружения. Если просто пометить его flaky и заглушить, то теряется единственный индикатор проблемы.
Про анализ зависимостей между тестами - очень жду. Особенно если инструмент сможет отвечать не только "есть order dependency", а конкретно: "тест B падает, потому что тест A не очистил состояние X". Рандомный порядок выполнения хорошо вскрывает связки, но почти не помогает локализовать источник загрязнения.
Кстати, твой пример с AST vs shared state отлично показывает границу между "code smell detection" и настоящим behavioural analysis. Это уже ближе к tracing/runtime observability, чем к классическому статическому анализу.