JavaRush /Курси /Claude code /Легка характеризація

Легка характеризація

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

1. Перед refactor потрібні окремі перевірки

На папері все виглядає просто: у вас уже є тести, отже можна рефакторити. На практиці все веселіше. «Є тести — рефакторити безпечно» часто бреше. Тести бувають надто широкими, крихкими або дивляться не туди: ганяють увесь сценарій checkout, а вам потрібно вийняти один шматок логіки з OrderService.finalizeOrder. Формально страховка є. По факту — дрібним шрифтом.

Особливо це помітно в Commerce OS на сервісних методах, які давно живуть і поступово обростають умовами. finalizeOrder перевіряє оплату, наявність товару, формує результат і роками обростає умовами. Хочеться почистити: прибрати вкладені if, винести валідацію в private helper, перейменувати нечитаєме. Але повний integration-тест надто грубий — скаже «зелене» або «впало» й не дасть упевненості, що поведінка вузької гілки не зсунулась на пів сантиметра вбік.

Саме тут з’являється lightweight characterization. Не заміна harness, не заміна test strategy. Прицільна перевірка: «Ось цю поведінку функція демонструє зараз. Давайте спочатку зафіксуємо її чесно, а форму коду поліпшимо потім». Скромно — і прибирає історії на кшталт «я лише виніс перевірку в helper, а чому відмова через stock тепер обробляється інакше — дізнаємося завтра вранці».

2. Що фіксує characterization-тест

Слово characterization звучить так, ніби ми збираємося писати психологічний портрет методу — на ділі все значно простіше. Characterization test фіксує, що система робить зараз у конкретному спостережуваному сценарії. Не «поведінка ідеальна та схвалена всіма на планеті», а «на поточному коді за таких вхідних даних — такий результат».

Зручна аналогія тут — фотографія. Regression test — охоронець: не пускає відомий баг назад. Contract/unit-тест — специфікація: перевіряє поведінку за домовленістю. Characterization — знімок перед ремонтом. Ви ще не сперечаєтесь, чи красиві шпалери. Ви фіксуєте, як кімната виглядала, доки не почали рухати меблі.

Ось компактне порівняння трьох видів перевірок:

Вид перевірки Головне запитання Що дає Чого не доводить
Contract / звичайний unit-тест «Як функція має працювати?» Перевірку узгодженої поведінки Що поточна legacy-реалізація взагалі цьому відповідає
Regression test «Чи не повернувся відомий баг?» Захист від повторення конкретної помилки Що решта поведінки не змінилася
Lightweight characterization «Що код робить зараз у цьому вузькому сценарії?» Локальну страховку перед refactor Що поточна поведінка правильна з погляду бізнесу

Із цього випливає дуже важлива річ. Якщо під час refactor ви розумієте, що поточна поведінка неправильна, — це не привід «заодно виправити». Це вже не refactor. Зупиніться й заведiть окрему bugfix- або feature-задачу. Менш романтично, ніж «я все одно вже тут був». Зате безпечніше.

3. Спочатку спостережувана поведінка, потім check

Найчастіша помилка в цій темі — попросити Claude Code «напиши characterization tests», не визначивши, що саме ви захищаєте. Прилетить набір пристойних тестів, половина з яких перевіряє внутрішності. А потрібна externally visible behavior — те, що бачить зовнішній споживач методу. Для локального refactor його зручно записати коротким планом:

Behavior X: оплачене замовлення за наявності товару отримує FINALIZED
Behavior Y: оплачена замовлення при stock = 0 отримує REJECTED / OUT_OF_STOCK
Перевірка: два unit-тести в OrdersUnitTest
Поза межами: змінювати контракт методу, тексти помилок, правила stock

Тут важливий сам принцип. Спочатку словами називаєте поведінку, потім добираєте check. Інакше тест диктує, що важливо, а вирішувати має людина. Claude допомагає — але не призначається начальником сенсу.

Щоб не з’їхати у внутрішні деталі, тримайте перед очима просте розрізнення:

Є сенс фіксувати Краще не фіксувати
статус результату, код причини, спостережуваний побічний ефект, зовнішню відповідь методу ім’я private helper, значення локальної змінної, кількість викликів внутрішнього методу, порядок дрібних кроків усередині реалізації

Якщо ви рефакторите finalizeOrder, то вас цікавить, чи повернув метод FINALIZED або REJECTED, а не те, чи викликався validateRefundEligibility рівно один раз. Helper завтра стане двома або зникне. Observable behavior за доброго refactor не змінюється.

4. Commerce OS: два сценарії finalizeOrder

Тепер переведімо все це зі слів у маленький, але цілком реальний приклад. У Commerce OS ви виносите частину логіки OrderService.finalizeOrder у читабельний helper. Перед цим фіксуєте два сценарії: «оплачене замовлення з товаром проходить» і «оплачене замовлення без залишку відхиляється з причини OUT_OF_STOCK». Малий обсяг принциповий: не енциклопедія на 27 кейсів, а два чесні маяки.

Перший тест може виглядати так:

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

@Test
void finalizeOrderReturnsFinalizedForPaidInStockOrder() {
    OrderResult result = orderService.finalizeOrder(paidInStockOrder());

    assertEquals(OrderStatus.FINALIZED, result.status());
    assertEquals("Замовлення підтверджено", result.message());
}

Тут важливо помітити, що тест перевіряє саме те, що бачить зовнішній код: статус і повідомлення. Не який helper спрацював і як розкладено if. Для характеризації це й потрібно.

Другий тест — на межу, яку ви не хочете випадково втратити під час refactor:

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

@Test
void finalizeOrderRejectsOrderWhenStockIsZero() {
    OrderResult result = orderService.finalizeOrder(paidOrderWithZeroStock());

    assertEquals(OrderStatus.REJECTED, result.status());
    assertEquals("OUT_OF_STOCK", result.reason());
}

Якщо обидва тести зелені на поточному коді, у вас є мінімальний safety net для цієї функції. Вона не доводить, що вся логіка замовлень прекрасна, і не замінює integration-тести на checkout. Завдання одне: не дати refactor тихо змінити два спостережувані результати, які ви обіцяли зберегти.

Якщо у вашому проєкті прийнято інший стиль тестів — наприклад, AssertJ замість assertEquals, — дотримуйтеся стилю проєкту. Ми боремося не за красу синтаксису, а за ясність наміру. Ясність важливіша за «ідеальне» оформлення.

Коли маяки зелені, safety net перестає бути абстракцією: наступний refactor робиться під їхнім захистом і гониться разом із existing harness.

5. Як просити Claude, не віддаючи йому рішення

І ось тут починається та частина, де Claude Code дійсно зручний. Він швидко запропонує 1–2 characterization tests, знайде схожі патерни, підкаже наявні factory-методи й test fixtures. Але йому потрібна рамка. «Напиши тести перед refactor» — і він піде широко, щедро та місцями творчо. А потрібно вузько і строго.

Хороший запит виглядає так:

Перед refactoring OrderService.finalizeOrder запропонуй 1-2 lightweight
characterization tests для поточної спостережуваної поведінки.
Фокус:
- paid + in-stock -> FINALIZED
- paid + stock=0 -> REJECTED / OUT_OF_STOCK
Поки що не змінюй production code.
Не вважай, що ця поведінка правильна.

Зверніть увагу на дві фрази, без яких запит гірший. Do not modify production code yet: поки тільки фіксуємо поведінку. Do not assume this behavior is correct: захист від плутанини characterization з contract test. Claude не «поліпшує сенс» — він фіксує факт.

Далі вступає в гру людський review. Навіть якщо тести запропонував ваш tester-agent із Workflow Kit, ви відкриваєте assertions і читаєте їх очима. Питання просте: тест перевіряє спостережувану поведінку чи внутрішню механіку? «Перевіримо, що викликався private helper», «порівняємо внутрішнє поле» — це не той тест перед refactor.

Корисно зафіксувати намір прямо в PR description або нотатці поруч із задачею:

Спостережувану поведінку зафіксовано:
- paid + in-stock -> FINALIZED
- paid + stock=0 -> REJECTED / OUT_OF_STOCK

We do not claim this behavior is correct.
Purpose: protect a local refactor in OrderService.finalizeOrder.

Така маленька позначка здається бюрократією рівно до того моменту, поки хтось на рев’ю не спитає: «Чому ми закріпили саме цю поведінку?» Замість філософської розмови — чесна відповідь: «Не тому, що це священна істина. Тому, що ми захищаємо локальний behavior-preserving refactor».

6. Що робити, коли перевірки позеленіли

Коли ці маленькі characterization tests пройшли на поточному коді, починається найприємніша частина: рефакторити можна не наосліп. І тут легко зірватися в «раз усе зелене, давайте заодно ще ось це підправимо». Маленька перевірка дає невеликий кредит довіри, а не іпотеку на переписування половини сервісу. Ви використовуєте ці 1–2 перевірки як вузький safety net усередині того ж incremental loop: маленька зміна, targeted checks, diff.

Робочий цикл тут виглядає так:

flowchart TD
    A[Ви обрали 1-2 спостережувані сценарії] --> B[Додали lightweight checks]
    B --> C[Запустили їх на поточному коді]
    C --> D[Зробили один маленький refactor]
    D --> E[Запустили цільові перевірки та harness]
    E --> F{Поведінка збереглася?}
    F -- Так --> G[Коміт]
    F -- Ні --> H[Стоп, відкат або окрема bug/feature-задача]

Ключове правило на цьому етапі дуже просте: якщо після refactor характеристичний тест упав, дія за замовчуванням — зупинитися, а не «трохи послабити assert». Послаблення тесту — слизька доріжка. П’ять хвилин тому ви самі вирішили зберегти цей observable outcome. Змінився — спочатку визнайте, потім розбирайтеся, чи зміна була допустимою.

Ще одна часта пастка: під час refactor ви раптом натрапляєте на справжній баг. Claude Code тут ще сильніше провокує на героїзм: «О, я ж поруч, зараз виправлю». Не треба. Справжній баг заслуговує на свій regression test і окрему задачу. Інакше behavior-preserving change перетворюється на суміш refactor, bugfix і маленької внутрішньої революції. Такі коктейлі на рев’ю закінчуються погано.

7. Де прийом перестає працювати

На цьому етапі легко закохатися в прийом і почати застосовувати його взагалі до всього підряд. Але межа чітка: він працює на одній функції, одному вузькому flow, одній-двох перевірках. Накриваєте ним великий сервіс з чергами, базою, листами й трьома інтеграціями — і він перестає бути легким і чесним. Починається або лавина тестів, або хибне відчуття безпеки.

Корисно швидко перевіряти себе такою таблицею:

Ситуація Lightweight characterization підходить?
Один метод, два зрозумілі результати, локальний refactor Так
Один сервісний flow, де потрібні один граничний випадок і один основний сценарій Так, якщо межі вузькі
Великий модуль із кількома інтеграціями та побічними ефектами Швидше ні
Refactor зачіпає кілька пакетів і зовнішній контракт Ні, треба звужувати задачу
Ви хочете «заодно» змінити бізнес-правило Ні, це вже не characterization для refactor

Якщо вам страшно чіпати код без п’яти додаткових перевірок, це не привід писати ще п’ять «маленьких» тестів і називати все lightweight. Це сигнал: або refactor надто широкий, або safety net має бути глибшою.

Саме в цьому й цінність прийому. Він не срібна куля. Він чесно закриває дірку між «у нас є загальний harness» і «ми хочемо безпечно почистити ось цей шматок методу». Дві маленькі, зрозумілі, прочитані очима перевірки на поточній поведінці — і refactor перестає бути стрибком у темряву. Він стає звичайною інженерною роботою: локальною та передбачуваною настільки, щоб кожне перейменування не перетворювалося на маленький трилер.

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