FlakyDetector 2.0: Один комментарий, который перевернул моё представление о нестабильных тестах

от автора

Как мы с вашей помощью добавили анализ фикстур, подняли размерность признаков до 42 и научились видеть order dependency до того, как она рушит CI

🔥 Лид: «А давайте просто пометим тест flaky и забудем»

Полгода назад я написал статью про FlakyDetector — инструмент, который ищет нестабильные тесты по одному лишь исходному коду, Потом была статья FlakyDetector 2.0 . AST + CatBoost, 37 признаков, вроде бы всё круто.

Но один комментарий меня добил.

Пользователь Ariless рассказал реальный кейс: в их проекте тест падал с SLOT_OVERLAP — не потому, что в коде теста было что-то плохое, а потому что фикстура была общая на несколько тестов (shared scope). Предыдущий тест не успел почистить слот — следующий упал.

«Решение оказалось архитектурным, не тестовым. Код самого теста не трогали, а только перевели fixture из shared scope в function scope. SLOT_OVERLAP исчез полностью».

И главный вывод, который он сделал:

«Flakiness as a signal» — тест падал не потому, что был плохо написан, а потому что подсвечивал реальный дефект в дизайне окружения. Если просто пометить его flaky и заглушить, то теряется единственный индикатор проблемы.

Я тогда посмотрел на свой анализатор. И понял: мой FlakyDetector 2.0 был слеп к этому кейсу.

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

Как FlakyDetector 2.0 пропустил SLOT_OVERLAP

Модель flaky_v1_37d.cbm была обучена на 37 признаках: счётчики time.sleepdatetime.now(), глобальные мутации, цикломатическая сложность и так далее.

В кейсе Ariless тест выглядел идеально:

def test_slot_creation(slot_fixture):    slot = slot_fixture.create()   # чисто, нет антипаттернов    assert slot.is_empty()

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

А проблема была в фикстуре, которая объявлена где-то в conftest.py:

@pytest.fixture(scope="module")  # ← вот он, главный злодейdef slot_fixture():    return SharedSlot()           # ← нет yield, нет teardown

И в том, что SharedSlot — мутабельный объект. Тест A что-то в нём поменял, тест B прочитал изменённое состояние. Классическая order dependency.

Без анализа фикстур статический метод никогда бы это не нашёл. Спасибо Ariless — он не просто пожаловался, а дал готовую архитектурную идею.

Что я сделал: Sprint 1 — анализ фикстур (расширение до 42D)

За один спринт я добавил в анализатор 5 новых признаков, которые парсят pytest-фикстуры через AST:

Признак

Что значит

Почему важно

has_session_or_module_fixture

Есть фикстура с scope="module" или "session"

Шанс пересечения состояния между тестами

has_yield_in_fixture

Есть ли yield в теле фикстуры

Без yield — нет teardown-логики

fixture_returns_mutable

Фикстура возвращает [] или {}

Мутабельные объекты в shared scope — яд

fixture_has_autouse

Флаг autouse=True

Неявное внедрение, сложно трассировать

test_uses_fixtures

Доля «рисковых» фикстур в модуле

Количественная оценка заражения модуля

Теперь вектор признаков вырос до 42 измерений.

Что показало обучение.

Я переобучил CatBoost на синтетическом датасете, где перемешал безопасные и рискованные фикстуры. Результат меня поразил:

Признак

Важность

has_yield_in_fixture

66.00%

fixture_returns_mutable

26.81%

test_uses_fixtures

6.15%

has_session_or_module_fixture

1.04%

fixture_has_autouse

0.00% (в синтетике не было вариации)

Отсутствие yield оказалось главным маркером флакинесса. Модель сама выучила то, что Ariless эмпирически обнаружил на своём проекте.

Теперь FlakyDetector не просто говорит «тест подозрительный», а может показать пальцем:

⚠️ В модуле используется фикстура slot_fixture с scope="module", которая не содержит yield и возвращает мутабельный объект. Это может приводить к утечке состояния между тестами.

А что дальше? RAG и динамический анализ Sprint 2–3

Комментарий Ariless также содержал важное пожелание:

«Особенно если инструмент сможет отвечать не только «есть order dependency», а конкретно: «тест B падает, потому что тест A не очистил состояние X».»

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

  1. Плагин pytest-flaky-trail — записывает в SQLite порядок выполнения и результаты тестов, трейсбэки.

  2. LLM + RAG — берёт контекст упавшего теста (код + метрики), отправляет в локальную модель (Qwen 2.5 Coder 7B) и получает гипотезу о причине и исправлении.

  3. ChromaDB — векторизует эти отчёты, чтобы можно было искать семантически похожие проблемы: «покажи все тесты, которые падают из-за незакрытого слота».

Пока это работает в режиме исследования, но уже даёт ответы в стиле:

«Тест test_create_slot упал, потому что тест test_use_slot (выполненный ранее) оставил занятым слот slot_123. Рекомендуем перевести фикстуру на scope="function" или добавить yield с очисткой.»

 Как это выглядит в коде (пример)

Рисковая фикстура, которую теперь находит FlakyDetector 2.0:

import pytest@pytest.fixture(scope="module")   # ← признак 37: Truedef shared_state():    return []                     # ← признак 39: True, и нет yield → признак 38: Falsedef test_a(shared_state):    shared_state.append("a")      # мутацияdef test_b(shared_state):    assert len(shared_state) == 0 # может упасть, если тест A был раньше

Запускаем сканер:

uv run python scripts/scan_folder.py ./tests/

Вывод:

⚠️ CRITICAL: Фикстура shared_state (test_example.py:5)   - scope="module" (shared across tests)   - отсутствует yield (нет teardown)   - возвращает мутабельный список   Рекомендация: добавь yield с очисткой или смени scope на "function"

Огромное спасибо Ariless

Твой комментарий стал триггером для целого направления развития. Если читаешь это — отпишись в комментариях, я хочу публично сказать спасибо. Ты не просто указал на проблему, а дал готовое архитектурное решение и философский принцип «flakiness as a signal», который теперь вшит в ДНК проекта.

Кстати, проект открыт.

GitHub: Artem7898/flakydetector
Документация и research paper — в репозитории, там же лежит обученная модель flaky_v2_42d.cbm.

Установка через uv или Docker — всё как в первой статье, но теперь с новой мощью.

Буду рад лайку и комментарию — это помогает продвигать материалы и подсказывает, что разбирать дальше.

Пиши в комментариях, сталкивался ли ты с нестабильными тестами из-за фикстур? Как ты их ловил? Может, у тебя есть пример, который мы ещё не умеем детектировать?

А если интересна тема анализа кода и ML для тестирования — вот предыдущий разбор про FlakyDetector 1.0 👇 FlakyDetector 2.0 👇
Ссылка на первую статью Ссылка на вторую статью

Сделано с любовью к стабильному CI и к тем, кто не затыкает flaky-тесты костылями.

ссылка на оригинал статьи https://habr.com/ru/articles/1043792/