JavaRush /Курсы /Claude code /Lightweight characterization

Lightweight characterization

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. Остановитесь и заведите отдельную bugfix- или feature-задачу. Менее романтично, чем «я всё равно уже тут был». Зато безопаснее.

3. Сначала наблюдаемое поведение, потом check

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

Behavior X: оплаченный заказ при наличии товара получает FINALIZED
Behavior Y: оплаченный заказ при stock = 0 получает REJECTED / OUT_OF_STOCK
Check: два unit-теста в OrdersUnitTest
Out of scope: менять контракт метода, тексты ошибок, правила 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» — и он пойдёт широко, щедро и местами творчески. А нужно узко и строго.

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

Before refactoring OrderService.finalizeOrder, propose 1-2 lightweight
characterization tests for current observable behavior.
Фокус:
- paid + in-stock -> FINALIZED
- paid + stock=0 -> REJECTED / OUT_OF_STOCK
Do not modify production code yet.
Do not assume this behavior is correct.

Обратите внимание на две фразы, без которых запрос хуже. 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 observable сценария] --> B[Добавили lightweight checks]
    B --> C[Прогнали их на текущем коде]
    C --> D[Сделали один маленький refactor]
    D --> E[Запустили targeted checks и 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 перестаёт быть прыжком в темноту. Он становится обычной инженерной работой: локальной и предсказуемой настолько, чтобы каждое переименование не превращалось в маленький триллер.

1
Задача
Claude code, 20 уровень, 2 лекция
Недоступна
Добавление happy-path characterization test
Добавление happy-path characterization test
1
Задача
Claude code, 20 уровень, 2 лекция
Недоступна
Добавление boundary characterization test
Добавление boundary characterization test
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ