JavaRush /Курси /Claude code /Safety baseline для модернізації модуля

Safety baseline для модернізації модуля

Claude code
Рівень 27 , Лекція 0
Відкрита

1. Модернізація без baseline — це лотерея

У legacy-модулі на кшталт mrr-engine найнебезпечніше починати з привабливої думки: «Давайте спочатку трохи почистимо код, а потім подивимося, що зламалося». Це ремонт квартири без замірів і фотографій: стіну знесли, проводка стирчить, а довести, що розетка була не тут, уже нічим. Не зафіксували поведінку до правки — після рефакторингу не відрізните спрощення від поломки.

У CashFlow Dashboard це особливо болісно, а ціна пряма: MRR впливає на гроші й звіти. Винесли «нешкідливий» метод — MRR за червень поїхав на два відсотки. Це не інженерна суперечка, це лист від фінансового менеджера в дуже сумному тоні. Legacy-код не кричить «Ви зламали сумісність» — він мовчки змінює результат, а дізнаєтеся ви пізніше.

Тому перед модернізацією нам потрібен не просто набір тестів, а захисний baseline — система перевірок: «Ось так модуль поводиться в цих сценаріях. Поки триває серія структурних змін, поведінка лишається стабільною там, де ми обіцяли її зберегти».

Інвентаризація поведінки
    → обираємо критичні сценарії
    → фіксуємо спостережувану поведінку
    → збираємо tests + snapshots + smoke-checks
    → описуємо це в CHARACTERIZATION_TESTS.md
    → проганяємо baseline перед кожним значущим diff

Тут важлива одна тонкість: baseline потрібен не заради документації, а щоб кожен наступний рефакторинг ішов під вимірюваним захистом, а не «на око».

2. Baseline — це система перевірок, а не один тест

Коли вперше чуєте про safety baseline, легко подумати: «Треба просто написати побільше тестів». Ні. Звичайний тест перевіряє, як система має працювати. Baseline — як система реально працює зараз, навіть якщо поведінка вам не подобається: спершу чесно зафіксувати дивність, щоб потім випадково не «виправити» її всередині рефакторингу. У CashFlow Dashboard це кілька шарів перевірок, а не один тип.

Вид перевірки Що фіксує Коли особливо корисний Приклад у CashFlow
Characterization test Поточну спостережувану поведінку сценарію Коли потрібно зберегти поведінку під час рефакторингу Повернення в періоді впливає на наступний місяць
Golden master Повний знімок складного результату Коли багато полів і формула розгалужена JSON-звіт по MRR за травень
Smoke check Життєздатність критичного шляху Перед кожним невеликим diff Перерахунок місячного звіту проходить без помилки
Contract/API check Сумісність зовнішнього формату Якщо модуль живить інші частини системи Схема вивантаження звіту не змінюється
Manual verification Перевірку очима власника процесу Якщо важливий бізнес-контекст або зовнішній еталон Звірка зі звітом фінансиста за травень 2024 року

Ось чому baseline — саме система, а не один файл із тестом. Одного characterization test замало: прикриє важливу гілку, але пропустить зміну формату звіту. Одного golden master теж замало: грубий, падає через дрібниці поза змістом.

Тут корисно згадати модуль про безпечний рефакторинг: там ми робили function-level characterization — одна функція, її входи та виходи, маленька зміна. Зараз масштаб інший: захищаємо не одну функцію, а цілий модуль і ланцюжок майбутніх рефакторингів. Baseline має витримати серію змін, а не один commit.

3. Сценарії baseline обираємо за ризиком і цінністю

Behavior inventory і RISK_MAP.md у вас уже на руках — і це чудова новина. Погана новина в тому, що туди легко накидати стільки сценаріїв, що baseline стане музеєм усього, що взагалі будь-коли відбувалося в модулі — повільним, крихким, марним. Завдання не «покрити все», а обрати сценарії з максимальним ризиком і максимальною цінністю: важливі для бізнесу, такі, що часто ламаються під час структурних змін і вже позначені в RISK_MAP як чутливі — повне повернення в першому періоді, часткове повернення посеред періоду, зміна плану в день білінгу, pause/resume підписки. Baseline спочатку захищає ядро, а не охоплює весь світ із першого заходу.

Подивитися на відбір можна так:

Сценарій Чому потрапляє в baseline Чим перевіряємо
Часткове повернення посеред періоду Високий бізнес-ризик і складна формула Characterization + golden master
Зміна плану в день білінгу Часто ламає розрахунок меж періоду Characterization
Пауза / відновлення підписки Легко втратити сумісність за датами Characterization + smoke
Щомісячний експорт MRR Важливий зовнішній формат для downstream-систем Golden master + contract check

Claude Code тут корисний не як суддя, а як акуратний аналітик. Віддайте йому inventory і risk map у режимі inspect-only — нехай запропонує кандидатів, але рішення за вами:

Проаналізуй behavior inventory та RISK_MAP для cashflow/mrr-engine.
Не змінюй код.

Для кожного кандидата на safety baseline поверни:
- сценарій,
- чому він критичний,
- який тип перевірки підходить,
- які вхідні дані потрібно зафіксувати,
- які спостережувані виходи потрібно порівнювати,
- на які файли та докази ти спираєшся.

Зверніть увагу на формулювання: тут немає «напиши тести». Спочатку аналіз, потім вибір. Claude упевнено видасть десяток кейсів, але якщо серед них немає вашого найболючішого refund-сценарію — він не дочитав контекст або не зрозумів downstream-залежність. Baseline починається не з генерації тестів, а з узгодження списку сценаріїв.

4. Characterization test фіксує реальність

Найчастіша помилка в модернізації трапляється дуже рано. Людина сідає писати characterization test — і непомітно пише bugfix test: не на поточну поведінку, а на бажану. Психологічно це зрозуміло: хочеться нарешті зробити «як правильно». Але так ви підміняєте модернізацію зміною бізнес-логіки — інша задача, з іншим ризиком. Щоб не плутатися, жорстко розділяйте чотири категорії поведінки.

Категорія Що це означає Що робити в baseline
Current behavior Так система працює зараз Зафіксувати
Desired behavior Так хотілося б, щоб працювала Не включати в refactor baseline
Known bug Ми розуміємо, що це баг, але він існує Явно позначити й тимчасово зберегти
Compatibility behavior Поведінка дивна, але на неї вже зав’язані інші частини Зафіксувати особливо уважно

Візьмімо типовий приклад із CashFlow: повернення в поточному місяці зменшує MRR наступного місяця, а не поточного. Для бізнесу сумнівно. Але якщо на це вже зав’язана legacy-вивантаження або звіт у сусідньому модулі, «акуратно виправити» логіку дорогою ви не маєте права. Спочатку фіксуєте як є, потім окремою задачею змінюєте поведінку й готуєте новий план перевірки.

Невеликий characterization test може виглядати так:

import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;

@Test
void refund_в_периоде_пока_влияет_на_следующий_месяц() {
    Report may = mrrEngine.calculate("fixtures/refund-same-period.json", "2024-05");
    Report june = mrrEngine.calculate("fixtures/refund-same-period.json", "2024-06");

    assertEquals("100.00", may.getMrrTotal());   // травень не змінюється
    assertEquals("80.00", june.getMrrTotal());   // червень зменшується на 20
}

Це не тест у жанрі «ура, логіка правильна», а «поки система поводиться ось так, і рефакторинг цього не зламає». Такий тест дисциплінує і вас, і Claude Code: якщо AI запропонує «покращити формулу» разом із винесенням методу — у вас уже є причина сказати ні, це інша задача.

5. Golden master доповнює тести знімком результату

Іноді модуль улаштований так, що розкласти його на десяток маленьких assertions важко, довго або невигідно. mrr-engine будує підсумковий звіт з агрегатами, розбивкою за планами, churn delta та парою службових полів. Тут виручає golden master — еталонний знімок результату на фіксованому вході.

Аналогія проста: characterization test — перевірка окремих показників на приладовій панелі, golden master — фото всієї панелі цілком. Вхід зафіксований — отже, після рефакторингу фотографія має збігтися. Не збіглася — ви щось змінили. Не обов’язково погано, але точно помітно.

Найпростіший варіант може виглядати так:

import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;

@Test
void майский_MRR_отчет_совпадает_со_снимком() {
    String actual = snapshotService.buildNormalizedSnapshot("fixtures/may-2024.json");
    String expected = TestFiles.read("snapshots/may-2024.snapshot.json");

    assertEquals(expected, actual); // знімок збігається цілком
}

Тут ключове слово — Normalized. Timestamp, випадковий порядок полів або технічний ID прогону — і snapshot падає не по суті. Тому добрий golden master майже завжди нормалізується: прибирає поля без бізнес-сенсу, стабілізує порядок елементів, приводить округлення до єдиного вигляду.

Саме тому golden master не замінює characterization tests, а доповнює їх: швидко ловить масові зміни, але погано пояснює, що саме зламалося. Characterization точніше показує зміст відхилення. Разом — здорова пара.

6. CHARACTERIZATION_TESTS.md — карта baseline

Коли baseline починає рости, у ньому легко загубитися. Один сценарій пішов у JUnit-тест, інший — у знімок, третій перевіряє бухгалтер вручну, четвертий позначений як дивина заради сумісності. Не звести це в явний артефакт — і за тиждень не згадаєте, навіщо цей snapshot. Потрібен CHARACTERIZATION_TESTS.md — не замість тестів, а як карта: які сценарії входять у baseline, чим перевіряються, на які evidence спираються, де залишаються ручні кроки. У legacy це особливо цінно — частина знання живе не в коді, а в головах і старих звітах.

Фрагмент може виглядати так:

# CHARACTERIZATION_TESTS.md

## Сценарій: partial_refund_mid_period
- Рівень перевірки: characterization + golden master
- Вхід: fixtures/partial-refund-mid-period.json
- Спостережувані виходи: mrr_total, churn_delta, report_rows
- Відома особливість: refund поки що впливає на наступний місяць
- Автоперевірки: MrrEngineCharacterizationTest, snapshots/may-2024.snapshot.json
- Ручна перевірка: звірка зі звітом фінансиста за травень 2024 року
- Докази: src/main/java/.../MrrEngine.java, reports/may-2024.csv

Зверніть увагу на важливу деталь: manual verification не має губитися серед автоматичних перевірок — ручна звірка має бути видна одразу. Інакше за кілька PR хтось вирішить, що baseline «повний», хоча половина впевненості тримається на неформальному знанні команди.

Добрий CHARACTERIZATION_TESTS.md завжди відповідає на три запитання: що ми захищаємо, чим це перевіряємо, чому сценарій важливий. Не видно з файла — значить, він став декорацією.

7. Чотири шари verification на рівні модуля

Модель verification вам уже траплялася раніше, але тепер вона застосовується не до однієї функції, а до всього modernization-циклу модуля. Це важливий зсув масштабу. Раніше L1 означав: «Як я перевірю ось цю зміну?» Тепер: «Які critical behaviors усього модуля мають залишатися стабільними протягом серії рефакторингів?»

На CashFlow це виглядає так:

Шар Питання Приклад для mrr-engine
L1 — план перевірки Що має зберегтися? Refund, pause/resume, billing-day switch не змінюють спостережуваний вихід
L2 — test design Якими перевірками це ловимо? Characterization, golden master, smoke, contract check
L3 — local harness Що проганяємо локально перед кожним diff? ./gradlew test за characterization suite та snapshot-перевірками
L4 — CI integration Що не дозволяємо злити в PR? Ті самі suite і snapshots у CI-gate

Тут корисно запам’ятати одну думку. Якщо після другого-третього рефакторингу baseline падає на сценарії, який ви «взагалі не чіпали», — не поспішайте оновлювати snapshot. Зупиніться: зміна зачепила ширшу область, ніж здавалося, або baseline зафіксований занадто крихко, або зв’язки всередині модуля сильніші, ніж ви думали.

Саме тому verification на рівні модуля — не «більше тестів». Це дисципліна бачити модуль як систему із залежностями, а не як набір випадкових методів.

8. Claude Code як аналітик, а не як автор baseline

Claude Code на цьому етапі справді дуже корисний: пройде по behavior inventory, знайде файли зі сценаріями, запропонує кандидатів на characterization tests, збере чернетку CHARACTERIZATION_TESTS.md, підкаже output для golden master. За однієї принципової умови: спочатку дослідження, потім пропозиція, потім рішення людини. Перестрибнете порядок — отримаєте гарну автоматизацію самообману.

Добре працює такий запит:

Проаналізуй behavior inventory для cashflow/mrr-engine.
Не змінюй код і не пиши тести одразу.

Потрібно:
- запропонувати 5–7 сценаріїв для safety baseline,
- для кожного вказати спостережувані виходи,
- позначити відомі особливості та поведінку сумісності,
- показати, які сценарії краще покрити characterization test, а які golden master,
- послатися на файли, фікстури та наявні тести.

Після цього корисно зробити другий прохід уже через reviewer-логіку, щоб перевірити сам baseline на здоровість:

Перевір запропонований baseline як reviewer.
Шукай дві речі:
- чи не перетворили ми characterization-тести на bugfix-тести,
- чи не забули ми явно позначити manual verification і known quirks.
Код не змінюй.

Тут особливо доречні ваші попередні артефакти agents/tester.md та agents/reviewer.md: перший допомагає зібрати кандидатів, другий — не повірити в них надто швидко. Human-in-the-loop без пафосу: Claude прискорює підготовку, але що заморожувати як поточну поведінку, вирішує інженер — тільки він тримає бізнес-контекст і межі задачі.

Коли на руках зелений baseline, оформлений у CHARACTERIZATION_TESTS.md, legacy-модуль перестає бути темним лісом, куди страшно заходити навіть із ліхтариком. Він усе ще складний, вередливий, іноді трохи драматичний — як бухгалтерія в останній день місяця, — але тепер у нього є вимірювана поведінка. Отже, до нього можна торкатися не навмання, а справді інженерно. І з цієї точки вже має сенс робити перший маленький крок рефакторингу: baseline буде не фоном, а реальною страховкою.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ