На примере плохо написанного юнит-теста, который иногда проходит, а иногда нет (flaky test), опишите методику локализации причины нестабильности и набор практических приёмов для устранения флакнов в тестовой базе
Пример плохого (flaky) юнит‑теста и пошаговая методика локализации + практические приёмы исправления. Плохой пример (Python/pytest), причина — гонка по времени (sleep вместо синхронизации): def test_background_job_finishes(): start_background_job() # запускает работу в другом потоке/процессе time.sleep(0.1) # «подождать» — ненадёжно assert job_result_ready() # иногда ещё не готов → тест флакирует Методика локализации причины (шаги) 1) Репродукция и количественная оценка - Запустить тест многократно, чтобы оценить вероятность флейка: запустить тест N=200\text{N}=200N=200 раз локально/в CI. - Собрать долю провалов → например, если флейк появляется в >1%>1\%>1% запусков — приоритет высокий. 2) Изолировать - Запустить только этот тест (без параллельных тестов). - Отключить параллелизм в рантайме (если использовался). - Перемешать / зафиксировать порядок тестов (проверить зависимость от порядка). 3) Собрать артефакты при провале - Логи, стектрейсы, дампы, снимки окружения, вывод тест-раннера. - Включить детализированный лог вокруг момента ожидания/проверки. 4) Инструментировать и повторно запускать - Добавить временные метки, детализированные assertion-ы, трассировку потоков. - Запускать с race-detector / thread sanitizer, если есть (например, TSAN, Go race). - Повторять при разных seed: зафиксируйте seed RNG и попробуйте случайные семена. 5) Бисекция и сравнение окружений - Если флак появился недавно — bisect коммиты. - Сравнить окружения (версии библиотек, CI-образы, лимиты ресурсов). Частые причины флейков и признаки - Тайминги/асинхронность (sleep в тестах, таймауты слишком малые). - Общий/глобальный mutable state между тестами. - Зависимость от внешних сервисов/сети/файловой системы. - Непредсказуемая рандомизация (ненастроенный seed). - Утечки ресурсов (порты, дескрипторы), гонки данных. - Ожидания по локали/часовому поясу/точности плавающей точки. - Параллельное выполнение тестов, несовместимые тесты. Практические приёмы устранения флакнов 1) Заменить sleep на синхронизацию - Использовать join(), Event, Condition, Future/Promise, ожидание конкретного состояния с таймаутом: - Ждать события/сигнала вместо фиксированного sleep. 2) Инъекция детерминизма - Подставлять фиктивные часы (fake clock), фиктивный таймер, фиксированные seed для RNG. 3) Мокать внешние зависимости - Мока/фейк внешних сервисов (HTTP, БД) или запускать локальные стабильные фиксы. 4) Изолировать состояние - Использовать уникальные временные директории/порты/имена для тестов; полное teardown/cleanup. - Возвращать глобальные переменные в исходное состояние в teardown/fixture. 5) Избегать order-dependency - Каждый тест должен создавать и очищать своё окружение; не полагаться на выполнение других тестов. 6) Контроль времени ожидания - Таймауты реальные, но не чрезмерно малы; при увеличении таймаута сначала разобраться в причине, а не просто «поднять» его. 7) Инструменты обнаружения гонок - Включать race detector, valgrind, sanitizers на CI/в локальной проверке для подозрительных модулей. 8) Короткая стратегия приёмов в CI - Автоматический повтор упавшего теста до kkk повторов (например, k=2k=2k=2) — временно, чтобы не ломать фолс‑позитивы в CI, но фиксировать в багтрекере. - Поставить флейков в карантин/маркировать «flaky» с багом и приоритетом на исправление. 9) Метрики и мониторинг - Вести метрику flake‑rate per test, триггерить расследование при flake‑rate >>> например 5%5\%5%. - Периодические «stress» прогоны (nightly) для обнаружения редких флейков: прогон по N=1000\text{N}=1000N=1000 для критичных тестов. Конкретная правка для примера - Плохой: def test_background_job_finishes(): start_background_job() time.sleep(0.1) assert job_result_ready() - Правильно: def test_background_job_finishes(): event = start_background_job_with_signal() # возвращает объект события/joinable assert event.wait(timeout=1.0) # ждем до безопасного таймаута assert job_result_ready() Когда временно не удаётся быстро исправить - Пометить тест как flaky/skip с ссылкой на задачу; добавить мониторинг и не скрывать проблему бесконечным ретраем. Краткий чеклист при расследовании flake - Можно воспроизвести локально? Если да — добавить логи и расшливание. - Зависимость от порядка/параллелизма? - Есть shared mutable state? - Используется external IO/network? - Присутствуют sleep/implicit waits/ненадёжные таймауты? - Пробовали sanitizers/race-detectors? Итог: локализация — это системный процесс: повторное воспроизведение, сбор артефактов, инструментирование, изоляция и устранение корневой причины (тайминги, состояние, внешние ресурсы). Практические приёмы: заменить sleep на синхронизацию, инъекция детерминизма, мокать внешнее, очищать состояние, мониторить flake‑rate и применять карантин до полного исправления.
Плохой пример (Python/pytest), причина — гонка по времени (sleep вместо синхронизации):
def test_background_job_finishes():
start_background_job() # запускает работу в другом потоке/процессе
time.sleep(0.1) # «подождать» — ненадёжно
assert job_result_ready() # иногда ещё не готов → тест флакирует
Методика локализации причины (шаги)
1) Репродукция и количественная оценка
- Запустить тест многократно, чтобы оценить вероятность флейка: запустить тест N=200\text{N}=200N=200 раз локально/в CI.
- Собрать долю провалов → например, если флейк появляется в >1%>1\%>1% запусков — приоритет высокий.
2) Изолировать
- Запустить только этот тест (без параллельных тестов).
- Отключить параллелизм в рантайме (если использовался).
- Перемешать / зафиксировать порядок тестов (проверить зависимость от порядка).
3) Собрать артефакты при провале
- Логи, стектрейсы, дампы, снимки окружения, вывод тест-раннера.
- Включить детализированный лог вокруг момента ожидания/проверки.
4) Инструментировать и повторно запускать
- Добавить временные метки, детализированные assertion-ы, трассировку потоков.
- Запускать с race-detector / thread sanitizer, если есть (например, TSAN, Go race).
- Повторять при разных seed: зафиксируйте seed RNG и попробуйте случайные семена.
5) Бисекция и сравнение окружений
- Если флак появился недавно — bisect коммиты.
- Сравнить окружения (версии библиотек, CI-образы, лимиты ресурсов).
Частые причины флейков и признаки
- Тайминги/асинхронность (sleep в тестах, таймауты слишком малые).
- Общий/глобальный mutable state между тестами.
- Зависимость от внешних сервисов/сети/файловой системы.
- Непредсказуемая рандомизация (ненастроенный seed).
- Утечки ресурсов (порты, дескрипторы), гонки данных.
- Ожидания по локали/часовому поясу/точности плавающей точки.
- Параллельное выполнение тестов, несовместимые тесты.
Практические приёмы устранения флакнов
1) Заменить sleep на синхронизацию
- Использовать join(), Event, Condition, Future/Promise, ожидание конкретного состояния с таймаутом:
- Ждать события/сигнала вместо фиксированного sleep.
2) Инъекция детерминизма
- Подставлять фиктивные часы (fake clock), фиктивный таймер, фиксированные seed для RNG.
3) Мокать внешние зависимости
- Мока/фейк внешних сервисов (HTTP, БД) или запускать локальные стабильные фиксы.
4) Изолировать состояние
- Использовать уникальные временные директории/порты/имена для тестов; полное teardown/cleanup.
- Возвращать глобальные переменные в исходное состояние в teardown/fixture.
5) Избегать order-dependency
- Каждый тест должен создавать и очищать своё окружение; не полагаться на выполнение других тестов.
6) Контроль времени ожидания
- Таймауты реальные, но не чрезмерно малы; при увеличении таймаута сначала разобраться в причине, а не просто «поднять» его.
7) Инструменты обнаружения гонок
- Включать race detector, valgrind, sanitizers на CI/в локальной проверке для подозрительных модулей.
8) Короткая стратегия приёмов в CI
- Автоматический повтор упавшего теста до kkk повторов (например, k=2k=2k=2) — временно, чтобы не ломать фолс‑позитивы в CI, но фиксировать в багтрекере.
- Поставить флейков в карантин/маркировать «flaky» с багом и приоритетом на исправление.
9) Метрики и мониторинг
- Вести метрику flake‑rate per test, триггерить расследование при flake‑rate >>> например 5%5\%5%.
- Периодические «stress» прогоны (nightly) для обнаружения редких флейков: прогон по N=1000\text{N}=1000N=1000 для критичных тестов.
Конкретная правка для примера
- Плохой:
def test_background_job_finishes():
start_background_job()
time.sleep(0.1)
assert job_result_ready()
- Правильно:
def test_background_job_finishes():
event = start_background_job_with_signal() # возвращает объект события/joinable
assert event.wait(timeout=1.0) # ждем до безопасного таймаута
assert job_result_ready()
Когда временно не удаётся быстро исправить
- Пометить тест как flaky/skip с ссылкой на задачу; добавить мониторинг и не скрывать проблему бесконечным ретраем.
Краткий чеклист при расследовании flake
- Можно воспроизвести локально? Если да — добавить логи и расшливание.
- Зависимость от порядка/параллелизма?
- Есть shared mutable state?
- Используется external IO/network?
- Присутствуют sleep/implicit waits/ненадёжные таймауты?
- Пробовали sanitizers/race-detectors?
Итог: локализация — это системный процесс: повторное воспроизведение, сбор артефактов, инструментирование, изоляция и устранение корневой причины (тайминги, состояние, внешние ресурсы). Практические приёмы: заменить sleep на синхронизацию, инъекция детерминизма, мокать внешнее, очищать состояние, мониторить flake‑rate и применять карантин до полного исправления.