Рефакторинг без тестов с безопасным рабочим процессом помощника
Рефакторинг без тестов быстро становится рискованным. Узнайте, как примеры трафика, снапшоты и узкие области охвата помогают менять код с меньшим количеством сюрпризов.

Почему это быстро становится рискованным
Старый код редко ломается там, где это сразу видно. Он даёт сбой в странной ветке, добавленной для одного клиента, в одном формате файла или при таймауте, который проявляется только под нагрузкой. Код может выглядеть коряво, но эта корявая форма часто скрывает правила, которые никто нигде не записывал.
Унаследованные системы за годы набирают привычки. Функция обрезает пробелы, потому что старый импорт присылал битые значения. Поле статуса принимает два написания, потому что другой сервис так и не исправили. Человек, читающий код сейчас, видит бардак. Пользователи и другие системы могут полагаться на этот бардак.
Вот почему рефакторинг без тестов так быстро становится рискованным. Вы не просто приводите в порядок имена и форматирование. Вы трогаете поведение, привязанное к мелким деталям: порядок полей, пустые значения, тайминги ретраев или моменты раннего возврата.
ИИ‑ассистент делает процесс быстрее, и в этом и плюс, и минус одновременно. Он может за несколько минут разбить длинный метод, переименовать неопределённые переменные и убрать дубли. Но он также может «улучшить» условие, объединить два похожих случая или сместить побочный эффект настолько, что результат изменится. Диф выглядит чище. Поведение всё равно смещается.
Старый код часто прячет правила в одноразовых ветках для прошлых клиентов, тихих запасных путях для плохого ввода, побочных эффектах внутри логирования или ретраев и странных особенностях вывода, на которые теперь рассчитывает другой сервис.
Отсутствие тестов лишает вас эталона. Если эндпойнт возвращает странный JSON — это баг или неофициальный контракт? Если пакетная задача пропускает плохие строки без ошибки — это неправильно или нужно? Без проверок каждая правка превращается в гадание.
Крупные рефакторы усугубляют это, потому что они смешивают уборку и изменения логики. Если вы за раз переименуете переменные, вынесете хелперы, переставите условия и замените парсер, вы потеряете след. Когда что‑то ломается, никто не знает, какая правка это вызвала.
На продакшн‑системах, где важна доступность, тихий дрейф поведения стоит дороже, чем неопрятный код. Неряшливый код раздражает. Скрытые изменения правил дорого обходятся.
Начинайте с одного узкого среза
Охват — ваша первая проверка безопасности. Выберите одну вещь, которую люди смогут указать и проверить: один API‑эндпойнт, одну фонную задачу или один экран с одной явной задачей. Маленькое лучше красивого.
Хороший срез имеет чёткий старт и финиш. «Этот эндпойнт принимает customer ID и возвращает текущую сводку счета» — достаточно узко. «Привести в порядок модуль биллинга» — вот как поломки разлетаются в места, до которых вы не собирались дотрагиваться.
Узкий срез также даёт ассистенту жёсткие границы. Скажите ему редактировать только файлы, нужные для этого эндпойнта, сохранять ту же форму ответа и не трогать посторонние вещи. Обычно это означает меньше случайных переименований, меньше переписываний хелперов и меньше неожиданных побочных эффектов.
Хорошие первые срезы: один GET или POST‑эндпойнт, одна планируемая задача импорта/экспорта, одна страница с одной отправкой формы или один генератор отчётов с фиксированным выводом.
После выбора среза зафиксируйте вход и выход до изменений. Сохраните несколько реальных запросов и ответов или запишите точное состояние экрана до и после одного действия. Возьмите один нормальный случай и один «уродливый». Унаследованный код обычно ломается на странном вводе, а не на счастливом пути.
Затем кратко опишите, что должно остаться прежним. Держите это просто: коды статусов и тексты ошибок, имена полей и поведение с null, округления и сортировку, формат даты и побочные эффекты вроде писем, логов или созданных записей.
Эта записка сохраняет честность работы. Она даёт ассистенту контракт, которого нужно придерживаться.
Оставьте всё остальное в покое на пока. Если вы трогаете общий хелпер, а потом исправляете всех вызовов, срез исчезает. Удержитесь от желания переименовать половину папки, переместить файлы или вычистить всё вокруг. Код за пределами среза может оставаться неопрятным ещё один день.
Это сдерживание кажется медленным, но обычно экономит время. Одна безопасная правка, сохраняющая поведение, лучше широкой очистки, которая отправляет вас копаться в логах продакшна в 23:00.
Зафиксируйте реальное поведение до правок
Перед любой чисткой соберите доказательства. Вам нужен небольшой снимок того, что код делает сейчас, даже если часть этого поведения кажется неправильной.
Начните с реальных примеров трафика, а не выдуманных. Продакшн‑запросы, тикеты поддержки, старые логи и сохранённые полезные нагрузки часто раскрывают краевые случаи, которые ручные тесты пропускают. Выберите небольшой набор и держите его читаемым.
Используйте три типа входов: нормальный, пустой и «грязный». Нормальный показывает обычный путь. Пустой — что происходит при пустых или отсутствующих полях. «Грязный» ловит те случаи, которые ломают рефакторинги: лишние пробелы, неправильный регистр, частичные записи, дубли полей, странные форматы дат и неожиданные null.
Для каждого примера зафиксируйте вход, возвращаемое значение или тело ответа, код статуса или тип ошибки и любые побочные эффекты. Последнее важнее, чем многие ожидают. Функция может возвращать тот же JSON после правки, но перестать записывать строку в лог или пропустить биллинговое событие. Если вы не фиксируете побочные эффекты, вы защищаете только часть поведения.
Сохраните снапшот до правок. Храните примеры и текущие ответы в простой папке, фикстуре или лёгкой снапшот‑настройке. Не ждите полного набора тестов. Дешёвого снапшота часто достаточно, чтобы понять, что ассистент изменил что‑то, чего вы не хотели.
Предположим, вы чистите функцию импорта клиентов. Сохраните пять реальных CSV‑строк: одну чистую, одну с пустым email, одну с двумя телефонами, одну с битой разметкой и одну с дублирующимся customer ID. Затем зафиксируйте, что система делает сейчас. Возвращает ли она 200 или 422? Обрезает ли имена? Создаёт одного клиента, двух или ни одного? Пишет ли она строку с ошибкой для проверки?
Эта запись становится вашей страховкой. Вы не доказываете, что код правильный. Вы замораживаете текущее поведение, чтобы менять структуру без гаданий. Когда рефактор станет стабильным, можно будет решать, какое старое поведение сохранить, а какие баги убрать целенаправленно.
Сначала постройте дешёвые проверки
Дешёвые проверки лучше идеальных тестов, когда нужно менять старый код. Если у кода нет страховки, соберите простую из поведения, которое вы уже зафиксировали.
Начните со снапшотов вокруг частей, к которым действительно обращаются пользователи. Сохраните текущий ответ одного эндпойнта, одного отчёта или вывода одной фоновой задачи. Широкого покрытия пока не нужно. Нужен быстрый способ заметить изменение до того, как баг попадёт в прод.
Сырые снапшоты создают шум. ID меняются. Временные метки двигаются. Случайный порядок может сделать идентичные результаты разными. Сначала нормализуйте вывод, затем сравнивайте только важные поля.
Допустим, старый биллинговый эндпойнт возвращает JSON с invoice_id, generated_at, итогами по клиенту, налогом и позициями. Ваша проверка должна сохранять суммы, налоговые правила и форму позиций. Игнорируйте invoice_id и generated_at, если они всегда меняются. Так вы получите стабильный снапшот вместо машины ложных срабатываний.
Стабильные сравнения важнее крутых инструментов. Простой скрипт, нормализующий вывод и сравнивающий его с сохранённым файлом, часто достаточен.
Медленные зависимости могут разрушить рабочий процесс. Если каждый прогон ждёт внешний API, очередь или большой запрос к БД, вы перестанете использовать проверки после пары правок. Заглушите эти части рано. Возвращайте фиксированный ответ от платёжного провайдера. Используйте маленькую фикстуру вместо полного продакшн‑датасета. Делайте прогон настолько коротким, чтобы вы использовали его не задумываясь.
Дешевая проверка должна быть узкой, стабильной, быстрой и простой в обновлении, когда ожидаемый вывод меняется. Если обновление снапшота кажется хирургической операцией, проверка слишком тяжёлая.
Вы не доказываете всю систему. Вы ставите предохранитель. Для рефакторинга с ассистентом это часто разница между аккуратной мелкой правкой и послеобеденными догадками о том, что сломалось.
Используйте скучный цикл рефакторинга
Скучное лучше хитрого. Нужен повторяемый цикл, который поймает дрейф рано, прежде чем одна «маленькая уборка» превратится в баг в проде.
Сначала попросите ассистента объяснить текущий путь выполнения простым языком. Какие входы он читает? Какой вывод возвращает? Какие побочные эффекты вызывает? Какие части выглядят рискованно? Если объяснение туманно или пропускает очевидное, не просите правок.
Затем просите одно небольшое изменение. Хорошие запросы достаточно узкие, чтобы вы могли сравнить поведение до и после за несколько минут. Вынесите один повторяющийся блок в хелпер, не меняя вывода. Переименуйте одну запутанную локальную переменную внутри функции. Разбейте длинное условие на именованные проверки, сохранив ту же логику.
Избегайте промптов вроде «рефакторнуть этот файл» или «очистить модуль». Они приглашают изменения по вкусу, которые вы не сможете верифицировать.
После каждой правки прогоняйте те же примеры трафика или снапшоты, что вы захватили ранее. Проверяйте возвращаемые данные, коды статусов, отрендеренный текст, логи и видимые побочные эффекты. Если один пример изменился и вы этого не ожидали — остановитесь.
Когда поведение дрейфует, сузьте промпт вместо спора с результатом. Ограничьте ассистента одной функцией, одной веткой или одним повторяющимся блоком. Скажите, чтобы он сохранял байт‑в‑байт тот же вывод и не делал переименований за пределами затронутой области.
Маленькие коммиты важнее, чем кажется. Фиксируйте каждый безопасный шаг перед следующим запросом, даже если изменение кажется тривиальным. Стек маленьких коммитов даёт удобные точки отката и существенно упрощает ревью.
Обычно сессия выглядит так: объяснить обработчик, вынести один хелпер, прогнать снапшоты, закоммитить; переименовать две локальные переменные, прогнать снапшоты, закоммитить; убрать одну мёртвую ветку, прогнать снапшоты, закоммитить. Первые десять минут кажется медленно. Потом это начинает экономить часы.
Пять безопасных коммитов обычно лучше одного амбициозного переписывания.
Реалистичный пример
Представьте сервис оформления заказа с одной запутанной функцией calculateCheckout. В ней в одном большом блоке применяются купоны, скидки для участников, налоговые правила и порог бесплатной доставки. Никто ей не доверяет и никто не хочет её трогать, потому что тестов нет.
Такой код манит почистить всё за один проход. Отсюда и начинаются поломки. Меньшее движение обычно безопаснее, чем красивое переписывание.
Начните с вынимания десяти реальных корзин из недавнего трафика. Подберите набор, отражающий нормальное поведение: простая корзина, корзина с купоном, с просроченным купоном, с товарами, освобождёнными от налога, корзина, едва попадающая под бесплатную доставку, и несколько уродливых краевых случаев. Уберите персональные данные и сохраните полезности корзин, чтобы можно было прогонять их снова.
Для каждого примера зафиксируйте, что система делает сейчас. Держите снапшот простым: итоговая сумма, сумма налога, применённые строки скидок и любые сообщения об ошибке, показанные пользователю.
Теперь выберите только одно правило. Возможно, в функции есть ветка «применить 10% скидку, если в корзине есть категория X и подитог больше $100». Попросите ассистента вынести только эту ветку в хелпер вроде applyCategoryDiscount(cart) и не трогать остальное. Не переименовывайте половину файла. Не объединяйте правила пока.
Прогоните те же десять корзин снова. Девять могут совпасть точно. Одна может измениться, потому что в новом хелпере скидка округляется чуть иначе, что меняет налог на несколько центов. Это именно тот сигнал, который вы хотите поймать до продолжения чистки.
Если выводы совпадают — вы заработали следующий шаг. Вынесите ещё одно правило, снова прогоните корзины и продолжайте в коротких циклах. Если выводы не совпадают — отмените правку или исправьте ту ветку, прежде чем трогать что‑то ещё.
Так команды, работающие с критичными для доступности системами, избегают легкомысленных регрессий. Oleg часто использует тот же подход в больших продакшн‑задачах: сначала зафиксировать реальное поведение, затем менять одну узкую часть за раз.
Ошибки, приводящие к предотвратимым поломкам
Самый быстрый путь сломать наследуемый код — попросить ассистента сделать полную уборку и принять гигантский дифф. Когда тесты тонкие, переписывание скрывает множество изменений поведения под красивыми именами и короткими файлами.
Ещё одна распространённая ошибка — смешивать правки стиля с изменениями логики. Переименование переменных, перестановка функций, изменение условий и замена API за один проход сильно усложняют ревью. Если что‑то потом падает, никто не поймёт, откуда пришёл баг — от форматирования или от реального дрейфа поведения.
Красивый код почти ничего не доказывает. Форматтер может успокоить вид файла, в то время как маленькое логическое изменение меняет, кто получает счёт, письмо или блокировку. Читаемые диффы помогают, но проверки по реальному поведению важнее.
Снапшоты помогают только если они остаются чистыми. Если в них попадают временные метки, случайные ID, перемешанный порядок или другие шумные поля, люди перестают им доверять и тыкают «обновить» без проверки. Сначала уберите или зафиксируйте случайные значения, чтобы сбои снапшотов что‑то значили.
Команды также пропускают период сразу после релиза. Они смержили, видят зелёный CI и идут дальше. Потом логи наполняются новыми ошибками, тикеты поддержки упоминают странный вывод, а реальные пользователи попадают в ветку, которой никто не брал примеров раньше.
Если у вас уже есть трекинг ошибок или серверные логи, используйте их специально после первого релиза после рефактора. Следите за всплесками исключений, замедлением запросов и повторяющимися жалобами на один экран или действие. Эти сигналы часто ловят поломки быстрее, чем код‑ревью.
Более безопасная схема проста: просите одно узкое изменение за раз, держите правки стиля отдельно от изменений логики, сравнивайте выводы с сохранёнными примерами трафика или стабильными снапшотами и проверяйте логи и обратную связь пользователей после релиза.
Это кажется медленнее, но обычно экономит время. Один широкий промпт может создать выходные, потраченные на откат. Четыре маленьких промпта, проверенные на реальных входах, проще доверять и легче отменять.
Быстрые проверки перед мерджем
Последнее ревью перед мерджем важнее, чем принято признавать. Чистый дифф всё ещё может менять поведение мелкими, но дорогими способами. Потратьте несколько минут на проверку тех частей, которые ломаются чаще всего.
Начните с доказательств, которые вы собрали до правки. Прогоните те же примеры трафика, те же снапшот‑проверки и те же ручные вводы. Не спрашивайте, стал ли код выглядеть лучше. Спрашивайте, даёт ли каждый пример тот же бизнес‑результат.
Короткая проверка перед мерджем хорошо работает: сравните выводы до и после. Итоги, статус‑поля, отформатированные поля и побочные эффекты должны совпадать там, где должны совпадать. Внимательно прочитайте изменённые пути ошибок. При уборке часто дрейфуют ошибки, особенно вокруг nil‑значений, ретраев, таймаутов и валидации. Просканируйте логи с зоны рефактора. Новые предупреждения, дополнительные ретраи или шумные стектрейсы обычно означают, что вы изменили больше, чем думали. Убедитесь, что вы можете быстро откатить: один маленький коммит, feature‑флаг или отдельный файл легче вернуть, чем широкий переписанный модуль.
Оставьте короткую заметку к мерджу. Укажите, какие примеры вы прогнали, какие пути ошибок проверили и что ещё не покрыто. Прозрачность помогает следующему человеку понять, где быть осторожным, и помогает вам, когда эта зона вернётся на следующей неделе.
Обработка ошибок требует особого внимания, потому что ассистенты часто приводят её в порядок кажущимися безобидными способами. Хелпер теперь может «глотать» ошибку, возвращать другой текст или пропустить ретрай. Пользователи могут по‑прежнему увидеть тот же экран, но биллинг, аудиторские записи или downstream‑задачи изменятся.
Логи помогают это уловить. Если старый код давал одно предупреждение, а новый — двадцать, это не косметика. Ветка теперь срабатывает чаще, или запасной путь больше не работает.
Скорость отката тоже важна. Если вы не можете отменить срез за минуту‑две, срез, вероятно, слишком большой. Маленькие откаты лучше храбрых разбирательств.
Что делать дальше
После одной безопасной очистки не прыгайте сразу в большой перепис. Выберите следующий маленький срез, похожий на тот, что вы только что прошли, и проделайте тот же процесс.
Этот ритм важнее скорости. Стабильные повторения лучше одного амбициозного прохода, который затронул десять файлов и случайно изменил поведение.
Простая рутина хорошо работает: выберите путь с чёткими входами и выходами, соберите несколько реальных примеров перед правкой, держите снапшоты текущего поведения, попросите ассистента изменить только эту узкую область и сравните результаты перед мерджем.
Когда снапшот стабильно проходит через несколько мелких изменений, превращайте его в настоящий тест. Начните с частей, которые дороже всего, когда ломаются: расчёты биллинга, проверки прав, импортные задания или вывод, видимый клиенту.
Не ждите идеального набора тестов. Пара сосредоточенных тестов станет полезной намного быстрее, чем грандиозный план, который так и не реализовали.
Если команда регулярно чистит легаси, полезно иметь человека, который задаёт правила: как используются ассистенты, что проверяется и где остановиться с рефактом. Это практический AI‑первый подход к инженерии и работа Fractional CTO, о которых Oleg пишет на oleg.is, особенно для небольших команд, пытающихся модернизироваться без поломки продакшна.
Цель проста: сначала сохранить поведение, потом улучшать структуру и по ходу добавлять тесты. Так старый код становится безопаснее, а не просто красивее.
Часто задаваемые вопросы
Можно ли безопасно рефакторить старый код при почти полном отсутствии тестов?
Да, но держите охват узким. Начинайте с одного эндпойнта, одной фоновой задачи или одного экрана, зафиксируйте реальные входы и выходы и меняйте только одну узкую часть за раз. Если вы рефакторите весь модуль сразу, каждое изменение превращается в гадание.
С чего лучше начать рефакторинг?
Выбирайте ту часть, которую можно быстро проверить. Один API‑маршрут, одна отправка формы или один отчёт подойдут лучше, чем обширная область вроде биллинга или управления пользователями. Чёткие входы и выходы значительно упрощают обнаружение дрейфа поведения.
Сколько примеров трафика нужно собрать перед началом?
Поначалу много не нужно. Обычно 5–10 реальных примеров дают полезную базу, если включить один нормальный случай, один пустой и несколько «уродливых» случаев из логов или тикетов. Реальный трафик почти всегда лучше выдуманных примеров.
Что считается «грязным» вводом?
«Мессy»‑вход — это то, чему старые системы тихо научились соответствовать: лишние пробелы, смешанный регистр, отсутствующие поля, дубли, странные форматы дат, частичные записи или null там, где его не ждали. Именно эти случаи ломаются первыми при рефакторинге.
Достаточны ли снапшоты, или сначала нужны полные тесты?
Снапшоты отлично подходят как первое сигнальное средство. Сохраняйте то, что должно остаться неизменным: итоговые суммы, названия полей, коды статусов и текст ошибок, а шумные значения вроде временных меток и случайных ID игнорируйте. Позже стабильные снимки можно превратить в полноценные тесты.
Как не допустить, чтобы ассистент изменил поведение при рефакторинге?
Дайте ассистенту жёсткие ограничения. Попросите сначала объяснить текущее поведение, затем запросите одно небольшое изменение внутри одной функции или ветки. Попросите сохранить вывод прежним и избегать переименований, перемещений файлов и переписываний вспомогательных функций вне зоны правок.
Можно ли совмещать правки форматирования и логики?
Нет. Разделяйте правки стиля и изменения логики, чтобы каждую проверять отдельно. Если вы одновременно переименуете переменные, переставите код и поменяете логику, риск пропустить настоящее изменение поведения сильно возрастает.
Что стоит проверить прямо перед мерджем?
Прогоняйте те же примеры трафика и снапшоты, что вы записали до правок, и сравнивайте бизнес‑результат — а не только внешний вид кода. Сверьте возвращаемые данные, коды статусов, пути ошибок, логи и побочные эффекты вроде писем или созданных записей. Убедитесь, что вы сможете быстро откатиться, если что пойдёт не так.
А если новый код немного изменил вывод?
Любое изменение вывода следует считать серьёзным сигналом, пока вы не доказали обратное. Небольшие сдвиги в округлении, порядке полей или обработке ошибок могут сломать биллинг, импорты или другие сервисы. Либо исправьте рефакторинг, чтобы он совпадал с прежним поведением, либо пометьте изменение как намеренное и проверьте его целенаправленно.
Когда превращать снапшоты в полноценные тесты?
Как только один снапшот или пример стабильно проходит через несколько небольших рефакторов, превращайте его в реальный тест. Хорошие первые цели — расчёты биллинга, проверки прав, импортные задания и пользовательский формат вывода. Не нужен идеальный тест‑план — по одному стабильному тесту за раз работает лучше.