TL;DR
Триада контекста и самопроверка для починки тестов — это методика сборки промпта и конвейера для LLM и кодинг-агентов, которая автоматически обновляет упавшие юнит-тесты вслед за изменениями в основном коде проекта (source–test co-evolution, совместная эволюция кода и тестов). Вместо того чтобы скармливать модели только лог ошибки или только git diff, подход объединяет три слоя контекста — симптом падения, изменённый код на уровне целых методов или файлов и неизменённый связующий код — с коротким циклом самопроверки по результатам компиляции и запуска.
Когда разработчик меняет бизнес-логику, старые тесты начинают падать с ложными тревогами — не из-за бага в продукте, а потому что сам тест устарел. Если просто кинуть в чат с LLM стектрейс ошибки или кусок диффа, модель галлюцинирует сигнатуры, чинит не ту строчку или просто удаляет проверки (assert), чтобы тест формально стал «зелёным». Причина в том, что в реальных проектах тест редко ломается от правки одной функции: обычно цепочка вызовов проходит через несколько изменённых методов и вспомогательных классов, которые не видны напрямую из текста теста.
Метод решает эту проблему за счёт подачи в промпт полной причинно-следственной цепочки из трёх блоков: Симптом (лог выполнения и стектрейс упавшего теста), Причина (изменения в тестируемом коде на уровне методов или файлов, а не узких кусков диффа) и Опора (неизменённые методы-посредники, которые связывают тест с местом поломки). Дополнительно запуск одного раунда итеративной доработки с обратной связью от компилятора и тест-раннера и передача этой структуры кодинг-агенту радикально повышают долю успешно починенных сложных тестов при меньших затратах токенов.
Схема метода
ШАГ 1: Сборка контекста в один пакет → [Упавший тест] + [Symptom: лог падения] + [Cause: дифф и полные методы/файлы] + [Support: связующие неизменённые классы]
ШАГ 2: Первичная генерация патча с рассуждением по 4 этапам → [Диагностика симптома → Связка с диффом → Локализация правок в тесте → Патч]
ШАГ 3: Итеративная самопроверка (1 раунд с запуском теста) → [Лог новой ошибки компиляции/проверки → Исправленный тест]
ШАГ 4: Проверка сохранения смысла (LLM-судья / инструкция) → [Контроль, что assert не удалены и проверяют ту же логику]
Шаги 1 и 2 выполняются в одном запросе (или задаются как правило в инструкции кодинг-агенту вроде Codex / Claude Code). Шаг 3 требует запуска теста и второго запроса с логом результата, если первая версия не прошла.
Пример применения
Задача: В финтех-сервисе оплаты подписок (наподобие биллинга в Яндекс Плюсе или Т-Банке) бэкенд-разработчик переписал отправку метрик списания: теперь StubPaymentEmitter.emit() сохраняет ссылку на оригинальный объект события paymentEvent, а не его копию paymentEvent.copy(), чтобы не плодить объекты в памяти. Из-за этого старый юнит-тест testSubscriptionChargeMetrics(), который вызывал emit() до заполнения transactionId("tx-900"), начал падать с ComparisonFailure: expected:<[tx-900]> but was:<[]>. Нужно починить тест через LLM или агента, не сломав проверку.
Промпт:
Обнови устаревший юнит-тест, который начал падать после изменений в основном коде (Source-Test Co-evolution).
Цель — сделать так, чтобы тест снова компилировался и проходил на новой версии кода, полностью сохранив исходный смысл проверок (test intent). Запрещено удалять или ослаблять assert-проверки ради формального прохождения.
@Test
public void testSubscriptionChargeMetrics() {
final StubPaymentEmitter emitter = new StubPaymentEmitter("billing", "ru-central");
ChargeMetrics metrics = new ChargeMetrics();
metrics.charge(order);
metrics.reportChargeTime(0).emit(emitter);
metrics.sqlOrderId("tx-900");
metrics.transactionId("tx-900");
Map actualEvent = emitter.getEvents().get(0).toMap();
Assert.assertEquals("tx-900", actualEvent.get(BillingMetrics.TX_ID));
}
[ERROR] ru.fintech.billing.ChargeMetricsTest.testSubscriptionChargeMetrics -- Time elapsed: 0.003 s <<< FAILURE!
org.junit.ComparisonFailure: expected:<[tx-900]> but was:<[]>
at org.junit.Assert.assertEquals(Assert.java:117)
at ru.fintech.billing.ChargeMetricsTest.testSubscriptionChargeMetrics(ChargeMetricsTest.java:88)
Изменения в тестируемом коде (полные затронутые методы):
В классе StubPaymentEmitter extends PaymentEmitter:
public void emit(Event event) {
if (event instanceof ServiceMetricEvent) {
ServiceMetricEvent metricEvent = (ServiceMetricEvent) event;
metricEvents.computeIfAbsent(metricEvent.getMetric(), name -> new ConcurrentLinkedDeque<>())
- .add(metricEvent.copy());
+ .add(metricEvent);
}
}
(Плюс в коммите изменены ещё 17 методов форматирования метрик в ChargeMetrics, не влияющие на ID транзакции).
Связующий неизменённый код (как вызов из теста доходит до изменённого класса):
В классе PaymentEmitter:
public void emit(ServiceMetricEvent.Builder builder) {
emit(builder.build(service, host));
}
В классе ChargeMetrics:
public PaymentEmitter reportChargeTime(long time) {
// собирает builder из текущих полей metrics на момент вызова
}
Перед написанием кода выполни 4 шага:
1. По определи точную строку падения и какое значение оказалось пустым или неверным.
2. Сопоставь и : отследи непрямой (транзитивный) вызов от теста через связующий метод к изменённому классу и отсей изменения в коде, которые не влияют на падение.
3. Определи, какие строки в нужно переставить, обновить или дополнить, чтобы учесть новое поведение без удаления assert.
4. Выдай исправленный код теста.
Результат:
Модель по шагам отследит, что вызов reportChargeTime(0).emit(emitter) на строке 6 теста через связующий метод PaymentEmitter.emit() триггерит изменённый StubPaymentEmitter, причём раньше событие формировалось иначе, а теперь вызывается до установки sqlOrderId("tx-900") и transactionId("tx-900"). В ответе модель перенесёт вызов metrics.reportChargeTime(0).emit(emitter) ниже строк с установкой "tx-900", оставив Assert.assertEquals("tx-900", ...) нетронутым.
Почему это работает
Главная слабость LLM при поддержке тестов — слепота к непрямым зависимостям. В живых проектах большинство сломанных тестов затрагивают сразу несколько изменённых методов и классов, а настоящая причина падения часто прячется в транзитивном вызове (метод А в тесте вызывает неизменённый метод Б, а тот передаёт управление в изменённый класс В). Когда модели дают только стектрейс (Symptom) или только узкие строки git diff (Hunk-level Cause), она не видит мостика между ними и начинает наугад менять аргументы вызова в самом тесте.
Сильная сторона современных моделей и кодинг-агентов — умение сопоставлять цепочки вызовов и отсеивать шум, когда все звенья цепи лежат перед глазами. Если дать модели изменения на уровне целых методов или файлов (а не обрезанных кусков диффа) и добавить связующий неизменённый код (Support), она восстанавливает логику диспетчеризации и точно отличает те части коммита, которые реально сломали тест, от десятка попутных правок.
Второй важный механизм — экономия на рассуждениях и итерациях. Режим глубокого размышления (reasoning mode) помогает модели связать симптом с причиной, но без структуры она тратит массу токенов на лишние ветки. Если явно задать четырёхэтапную схему действий и добавить ровно один раунд самопроверки с логом компиляции/выполнения после первой попытки, модель снимает основную долю ошибок без бесконечных циклов и без дорогого перебора.
Рычаги управления:
- Масштаб контекста причины (Cause): для простых правок сигнатуры достаточно уровня метода (Method-level), но если в коммите менялись поля классов, конструкторы или наследование — подавай уровень файла (File-level).
- Количество итераций самопроверки: максимальную отдачу даёт первый раунд обратной связи по логу запуска (r=1). Второй и третий раунды дают убывающую отдачу, поэтому при ограничении бюджета останавливайся после первой доработки.
- Защита от «читерства» модели: всегда фиксируй запрет на удаление assert или проверяй патч отдельным запросом-судьёй, иначе модель может починить тест путём удаления проверки.
Шаблон промпта
Ты — инженер по автоматизации тестирования. Твоя задача — починить упавший юнит-тест после обновления основного кода проекта (Source-Test Co-evolution).
{код_упавшего_теста}
{лог_ошибки_и_стектрейс}
{дифф_и_полный_код_изменённых_методов_или_файлов}
{неизменённые_методы_посредники_и_классы_связывающие_тест_с_изменениями}
Выполни диагностику строго по 4 этапам перед генерацией патча:
1. Анализ симптома: зафиксируй строку падения в тесте и тип ошибки (ошибка компиляции или сбой assert/runtime).
2. Трассировка причины: свяжи вызовы внутри через с изменениями в . Отфильтруй изменения из коммита, которые не влияют на этот тест.
3. План адаптации теста: определи минимальные изменения в подготовке данных, порядке вызовов или ожидаемых значениях теста.
4. Проверка целостности (Oracle Check): убедись, что исходное намерение теста сохранено, а проверки (assertions) не удалены и не заменены на пустышки.
Сначала кратко выпиши выводы по 4 этапам , затем выдай готовый исправленный код метода теста.
Что подставлять в плейсхолдеры:
- {код_упавшего_теста} — полный текст тестового метода (и setUp/@BeforeEach, если он есть в классе).
- {лог_ошибки_и_стектрейс} — вывод тест-раннера (Maven/Gradle/pytest/vitest) с текстом ошибки и номерами строк.
- {дифф_и_полный_код_изменённых_методов_или_файлов} — изменения в основном коде из коммита, развёрнутые до полных методов или файлов.
- {неизменённые_методы_посредники_и_классы_связывающие_тест_с_изменениями} — определения промежуточных методов/интерфейсов, которые вызываются из теста и передают управление в изменённый код.
🚀 Быстрый старт — вставь в чат или в инструкцию кодинг-агенту (CLAUDE.md / AGENTS.md):
Вот шаблон починки упавших тестов после изменений в коде (Триада контекста Symptom + Cause + Support). Адаптируй его под мою задачу: [опиши свой стек и какой тест упал после рефакторинга].
Задавай вопросы или сам собери нужные файлы через поиск по репозиторию, чтобы заполнить поля.
[вставить шаблон выше]
LLM (или кодинг-агент) запросит лог падения теста, сам найдёт затронутые в коммите методы и промежуточные классы-посредники, после чего предложит патч без ослабления assert.
Ограничения
⚠️ Деградация на гигантских коммитах: Когда один коммит затрагивает десятки и сотни файлов одновременно, а сам тест требует многострочной перестройки в нескольких местах, точность одиночных запросов к LLM заметно падает — для таких случаев нужен автономный кодинг-агент с доступом к репозиторию и запуску тестов.
⚠️ Риск ложного «зелёного» теста (ослабление оракула): Без явного контроля качества модель иногда добивается прохождения теста за счёт удаления падающего
assertили подстановки тривиальной проверки. Авторы специально отсекали такие патчи через LLM-судью с высоким уровнем рассуждений.
⚠️ Слепота одного только лога ошибки: Подача только лога падения (
Symptom) без кода изменений решает лишь малую часть задач — модель лечит симптом вслепую, не понимая новой логики приложения.
Как исследовали
Исследователи сначала изучили более 2 500 реальных случаев совместного обновления кода и тестов в популярных открытых Java-проектах за период с мая 2024 по апрель 2026 года (чтобы исключить утечку ответов в обучающие выборки современных моделей) и обнаружили, что старые бенчмарки оторваны от жизни: в реальности 70,9% сломанных тестов вызывают сразу несколько изменённых методов, 77,8% сопровождаются правками вызываемых классов, а 38,4% требуют правок теста сразу в нескольких местах. Из динамически проверенных коммитов (где старый тест проходил на старом коде, падал на новом, а патч разработчика снова проходил) команда отобрала эталонный бенчмарк на 500 задач из 15 крупных проектов (включая Apache Druid, ShardingSphere, IoTDB, Spring AI, Quarkus, Netty, Dubbo) и прогнала на нём три современные модели — GPT-5.4, DeepSeek-V3.2 и Qwen3-Coder-480B-Instruct, а также автономного агента Codex.
Эксперименты показали наглядную картину: если дать моделям только лог падения (Symptom), тесты проходят лишь в 17,6%–31,4% случаев (88 из 500 у DeepSeek-V3.2, 115 у Qwen3-Coder и 157 у GPT-5.4). Зато расширение контекста причины (Cause) с узких кусков диффа до полных методов и файлов резко поднимает успешность (например, у DeepSeek-V3.2 прохождение тестов вырастает со 162 (32,4%) на уровне кусков диффа до 262 (52,4%) на уровне файлов, а у GPT-5.4 — до 276 (55,2%) уже на уровне методов), а добавление связующего контекста (Support) и первого раунда самопроверки с обратной связью даёт дополнительный прирост. Разложив цепочки рассуждений DeepSeek-V3.2 на 14 типовых действий в 4 этапах, авторы смогли направить модели без дорогого режима размышлений по нужной траектории, а передача найденных принципов сборки контекста в кодинг-агент Codex повысила успешность починки самых сложных случаев (где обычные промпты пасовали) на 66,7%.
Адаптации и экстраполяции
💡 Адаптация для инструкции кодинг-агенту (
CLAUDE.md/AGENTS.md): Вместо ручного копирования файлов пропиши правило сборки «Триады контекста» прямо в системную инструкцию своего агента (Claude Code, Cursor, Codex).## Правило починки упавших тестов (Source-Test Co-evolution) Если после изменений в коде упал существующий тест: 1. Не правь тест сразу по тексту ошибки (Symptom). 2. Собери три слоя контекста: - Symptom: полный стектрейс и строка падения теста. - Cause: git diff И полный текст всех изменённых методов/классов, которые вызываются прямо или косвенно. - Support: неизменённые методы-диспетчеры и родительские классы, связывающие вызов в тесте с изменённым кодом. 3. Сделай максимум 1–2 итерации правки с прогоном тест-раннера. 4. Запрещено удалять, комментировать или ослаблять assert-проверки ради прохождения теста.
🔧 Техника: Проверка патча через промпт LLM-судьи → защита от удаления
assertЕсли автоматизируешь починку тестов в CI/CD конвейере, добавь после генерации патча короткий проверочный промпт (как сделали авторы исследования с помощью GPT-5.4):
Сравни исходный тест T и исправленный тест T' с учётом изменений в коде V -> V'. Проверь: не прошёл ли тест T' только потому, что из него удалили проверки (assertions), загрубили ожидаемые значения или пропустили вызов проверяемого метода? Ответь PASS только если T' полностью сохраняет проверочный смысл исходного теста.
Ресурсы
- Исследование: Finding the Pivots to Repair Broken Source–Test Evolution Using LLMs (arXiv:2611.03197v1, ноябрь 2026)
- Авторы: Xiao Chen, Songqiang Chen, Jialun Cao, Shing-Chi Cheung (The Hong Kong University of Science and Technology), Yun Peng (Fudan University), Chun Yong Chong (Monash University Malaysia)
