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 буде не фоном, а реальною страховкою.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ