Могут ли эталонные тесты сделать безопасным рефакторинг с AI-ассистентом?
Эталонные тесты делают рефакторинг с AI-ассистентом безопаснее: они сохраняют утверждённые ответы API, файлы и отчёты, пока ревьюеры проверяют каждое изменение.

Содержание
AI-ассистент для программирования может переписать запутанную функцию быстрее, чем команда успеет решить, с чего начать. Такая скорость полезна лишь тогда, когда у ассистента есть граница, которую нельзя пересекать. Эталонные тесты задают эту границу: они сохраняют результаты, от которых уже зависят пользователи, интеграции и операционные процессы, пока внутреннюю реализацию можно менять настолько смело, насколько требует задача.
Это не разрешение одобрять всё, что legacy-система делает сегодня. Такой подход помогает разделить два решения, которые команды часто смешивают: сохранил ли рефакторинг текущее поведение и нужно ли это поведение вообще сохранять. Если держать решения раздельно, ревью становится точнее. Если смешать их, каждый рефакторинг превращается в спор о требованиях, форматировании, старых ошибках и стиле кода.
Эталон фиксирует поведение, а не истину
Эталонный результат это утверждённая запись ответа системы на известный вход. Тест подаёт тот же вход, сохраняет новый результат и сравнивает его с утверждённым. Если файлы различаются, тест завершается ошибкой, пока ревьюер не решит, было ли изменение намеренным.
В документации ApprovalTests этот подход называется approval testing. Там также отмечается, что его называют snapshot testing или golden master testing. Важно не упустить главное: сгенерированный файл становится спецификацией только после утверждения человеком. Сохранённый результат это свидетельство, а не оракул, возникший сам по себе.
Особенно важно это различие для старого кода. Допустим, endpoint счетов округляет налоговую корректировку вниз, а не вверх. Если сохранить и утвердить его ответ, эталон защитит такое округление во время миграции базы данных. Но он не говорит, что правило законно, выгодно или вообще нужно. Позже владелец продукта может изменить его в отдельном pull request, явно обновив фикстуру.
Перед тем как принять эталон, я хочу получить ответы на три вопроса:
- Какой реальный потребитель заметит изменение этого результата?
- Какие части результата входят в контракт, а какие являются случайным шумом?
- У кого достаточно предметного контекста, чтобы утвердить дифф после падения теста?
Если на первый вопрос никто не может ответить, от эталона лучше отказаться. Скорее всего, вы фотографируете внутреннюю сантехнику. Если нет ответа на третий вопрос, не позволяйте ассистенту самостоятельно рефакторить этот путь. Риск создаёт отсутствие ревьюера, а не отсутствие тестового фреймворка.
Обычные проверки и эталонные тесты решают разные задачи. Точечный тест может сказать, что отклонённая карта возвращает конкретный код ошибки. Эталон может сохранить полную выписку клиента, которую создаёт запутанный конвейер отчётности. Не заменяйте все проверки большим файлом. Используйте сохранённый результат, когда поведение настолько широкое, что ручные проверки были бы неполными, нечитаемыми или настолько дорогими, что команда никогда не стала бы их поддерживать.
Размещайте эталон на границе, от которой зависят люди
Фиксируйте поведение на границах API, файлов и отчётов: именно там изменение становится заметным за пределами переписываемого кода. Лучшая граница обычно находится дальше модульного теста, но ближе, чем вся production-среда.
Для API сохраняйте статус, важные заголовки и каноническое тело ответа. Для генератора файлов сохраняйте байты, если важна точная разметка, или нормализованное представление, если это не так. Для отчёта сохраняйте итоговую таблицу или экспорт, а не десяток приватных объектов, которые случайно его собрали.
У практичной границы есть четыре свойства. Она принимает контролируемый вход. Выдаёт результат, который можно изучить. Работает без вызовов реальных сторонних сервисов. У неё есть стабильный владелец, понимающий, почему изменилась конкретная строка.
Рассмотрим endpoint экспорта клиентов. Слабый вариант выглядит так: попросить ассистента «привести в порядок сервис экспорта» и надеяться, что существующие модульные тесты охватывают детали. Лучше создать репрезентативные входные записи, запустить endpoint через публичный обработчик и сохранить CSV. Такой захват защищает порядок столбцов, обработку пустых полей, экранирование, формат дат и расположение итогов. Именно на этих деталях рефакторинг часто ломает финансовые или операционные процессы.
Не делайте захват настолько широким, чтобы ревьюеры не могли его осмыслить. HTTP-транскрипт с диагностикой маршрутизации, всеми заголовками безопасности, идентификаторами трассировки и мегабайтом не относящейся к делу конфигурации создаёт шум. Контракт ответа не равен каждому байту, который выдаёт работающий процесс.
Полезное правило такое: сохраняйте последнее стабильное представление перед тем, как управление переходит к другому человеку или системе. Для внутреннего алгоритма это может быть объект предметного результата, сериализованный в JSON. Для публичного endpoint это ответ. Для генератора документов это файл. Для ночного бизнес-отчёта это отчёт, который кто-то скачивает и использует.
Стабильные фикстуры требуют контролируемых входов и нормализованных результатов
Эталонный тест падает по полезной причине только тогда, когда фикстура убирает случайные различия. Обычно проблемы создают время, случайные значения, сгенерированные идентификаторы, имена хостов, локаль, пути к файлам, порядок записей и внешние данные. Команды называют это хрупкостью и отказываются от подхода. Чаще всего они просто сохранили движущуюся цель.
Контролируйте изменения как можно ближе к источнику. Передавайте часы через зависимость, вместо того чтобы удалять даты из каждого результата. Задавайте начальное состояние генератора случайных чисел, вместо того чтобы заменять значения после выполнения. Используйте поддельный почтовый провайдер или платёжный шлюз, вместо того чтобы воспроизводить живой ответ. Сортируйте записи там, где контракт не обещает порядок.
Затем сознательно нормализуйте результат. Не используйте широкое регулярное выражение, которое стирает все числа, а потом называйте результат стабильным. Так можно удалить поле, выявляющее регрессию. Правило нормализации должно объяснять, что именно оно удаляет и почему это поле не входит в контракт.
Этот пример на Python сохраняет ответ API после изоляции значений, которые не должны влиять на результат:
import json
from copy import deepcopy
VOLATILE_HEADERS = {"date", "x-request-id", "traceparent"}
def canonical_response(response):
body = deepcopy(response.json())
body.pop("generatedAt", None)
for row in body.get("items", []):
row["id"] = "<generated-id>"
headers = {
name.lower(): value
for name, value in response.headers.items()
if name.lower() not in VOLATILE_HEADERS
}
return json.dumps(
{
"status": response.status_code,
"headers": dict(sorted(headers.items())),
"body": body,
},
indent=2,
sort_keys=True,
) + "\n"
Этот код формулирует утверждение о контракте. Он говорит, что идентификаторы запросов, заголовки трассировки, сгенерированные временные метки и идентификаторы записей не важны для этого теста. Для ответа поиска по каталогу это может быть верно, а для endpoint, где клиент позже использует сгенерированный идентификатор, нет. Автор теста должен понимать разницу.
Для JSON разбирайте данные и сериализуйте их заново со стабильным порядком ключей. Для CSV решите, имеет ли смысл порядок строк, и сортируйте только в обратном случае. Для PDF не начинайте со сравнения сырых байтов. Метаданные, подмножества шрифтов и смещения объектов делают такое сравнение мучительным. Извлекайте текст и количество страниц, если именно это важно пользователям, или тестируйте структурную исходную модель до этапа рендеринга.
Фикстуры должны быть достаточно маленькими, чтобы их можно было прочитать в pull request. Эталон на пятьсот строк может быть оправдан, особенно для отчёта. Эталон на пять тысяч строк без визуальной группировки уже говорит о проблеме поддержки. Разделите его по сценариям или напишите собственный сериализатор, который выводит бизнес-поля в удобном порядке.
Равенство байтов и равенство поведения это разные контракты
Команды тратят время, потому что считают побайтовое равенство единственным честным способом сравнения. Иногда это действительно так. Часто нет.
Побайтовое равенство уместно, когда другая система получает буквальный результат, а правила её парсера или подписи делают значимым каждый символ. Это может быть экспорт фиксированной ширины, шаблон письма, где переносы влияют на отображение, криптографическая полезная нагрузка или канонический файл обмена. В таких случаях нельзя удалять форматирование, от которого зависят потребители.
Поведенческое равенство проверяет смысл после удаления деталей представления, от которых никто не зависит. Очевидный пример это порядок ключей в объекте JSON. Пробелы в читаемом отчёте могут быть неважны, а значения, заголовки и порядок строк важны. Утилита командной строки может выводить длительность выполнения, которая нужна в логах, но не в тесте совместимости.
Опасна промежуточная зона: тест делает вид, что сравнивает поведение, но незаметно убирает значимые поля. Я видел нормализаторы, которые удаляли все идентификаторы, даты и суммы, потому что они мешали читать диффы. Такой тест пропускает ровно те регрессии, на которые жалуются клиенты.
Рядом с каждым захватом оставляйте короткую заметку о контракте. Это может быть комментарий или имя теста, но из них должно быть понятно, что означает равенство. Например:
customer_export_with_closed_accounts_preserves:
- one row per account, ordered by account number
- ISO 8601 dates in the report timezone
- blank closure_date for active accounts
- a final totals row
It ignores:
- export generated timestamp
- request correlation identifier
Это не бюрократия. Такая заметка объясняет ревьюеру, почему новое поле требует обсуждения, а изменившаяся временная метка нет.
То же различие относится к ошибкам. API может обещать статус и стабильный машиночитаемый код, оставляя текст для улучшений. Сохраняйте код и статус. Не фиксируйте плохо написанное сообщение, если только клиенты не разбирают его или сотрудники поддержки не зависят от точной формулировки. Если клиент действительно разбирает текст, исправьте клиента, а не радуйтесь тому, что тестовый набор сохранил плохой контракт.
Первое утверждение требует более тщательной проверки, чем сам рефакторинг
Первый захват это момент, когда команда может случайно сделать ошибку постоянной. Проверяйте его как новый публичный API: с этого момента он будет влиять на то, какие изменения инженеры считают безопасными.
Не поручайте ассистенту создать сотню файлов с фикстурами и не утверждайте их оптом. Так появляется видимость покрытия, хотя никто не проверил содержимое. Начните с небольшого набора входов, которые показывают опасные для вас ветвления. Добавьте обычные и граничные случаи, а также одну-две неудобные записи, из-за которых раньше возникали обращения в поддержку.
Для отчёта по счетам это могут быть активный счёт, закрытый счёт с финальной корректировкой и счёт с пустой необязательной строкой адреса. Для endpoint расчёта цены это обычный заказ, скидка, обнуляющая сумму, и граница округления валюты. Сохраняйте сценарии, которые заставляют код явно показать свои решения.
В документации ApprovalTests описаны полученный и утверждённый результаты. Сохраняйте эту модель из двух файлов, даже если не используете библиотеку. При падении запуска создаётся файл received. Человек изучает дифф и сознательно переносит результат в approved, только если поведение корректно. Обычная команда тестирования не должна перезаписывать утверждённый результат.
Минимальный процесс в shell выглядит так:
./scripts/capture-customer-export --case closed-account
git diff --no-index \
tests/fixtures/customer-export/closed-account.approved.csv \
tests/fixtures/customer-export/closed-account.received.csv
mv tests/fixtures/customer-export/closed-account.received.csv \
tests/fixtures/customer-export/closed-account.approved.csv
Команда mv намеренно проста и заметна. Она требует отдельного действия после проверки. В документации git diff также ясно сказано, что --no-index может сравнивать два пути за пределами индекса репозитория, поэтому он хорошо подходит для пар received и approved. В CI команда захвата должна завершаться ошибкой, если создаёт файл received или если такой файл уже существует.
Не утверждайте результат только потому, что старaя система его выдавала. Проверьте, не раскрывает ли он приватные данные, не выражает ли устаревшую политику и не закрепляет ли ошибку, которую вы уже собирались убрать. Если да, исправьте это до рефакторинга или пометьте фикстуру ссылкой на отслеживаемое требование, а изменение политики вынесите в отдельную проверяемую задачу.
Дайте ассистенту узкую область для изменения кода
Рефакторинг с участием ассистента работает, когда вы задаёте ограниченную цель, исполнимое доказательство и правило остановки. «Модернизируй этот модуль» не содержит ничего из этого. «Замени внутреннюю реализацию сервиса экспорта, сохранив прохождение всех утверждённых фикстур и не меняя публичный обработчик» это настоящая задача.
Когда код достаточно сложен и провоцирует на широкие изменения, я использую такую последовательность:
- Запускаю существующий набор тестов и сохраняю базовые результаты фикстур в чистой ветке.
- Прошу ассистента описать путь вызовов и определить границу, которую покрывает фикстура. Код пока не прошу.
- Даю ограниченную цель рефакторинга: например, выделить форматтер, заменить адаптер доступа к данным или убрать дублирующиеся преобразования.
- Требую запускать точечные тесты после каждого связного изменения и показывать дифф любой фикстуры.
- Останавливаюсь, когда структурное изменение завершено. Нельзя превращать успешный рефакторинг в уборку несвязанных модулей.
У ассистента не должно быть права утверждать полученную фикстуру. Он может объяснить дифф, сгруппировать изменения по причинам и предложить новый ожидаемый результат. Но он не может решить, что допустимы другая сумма налога, пропавшее поле, изменившийся порядок отчёта или новый код ошибки. Это решение принимает ревьюер, отвечающий за контракт.
Такая граница особенно полезна в рабочих процессах с агентами, которые могут изменить много файлов до следующей паузы. Дайте агенту список разрешённых директорий, точную команду, которая должна оставаться зелёной, и инструкцию останавливаться при любом различии фикстур. Агент с широким доступом на запись и автоматическим утверждением способен превратить полезный регрессионный тест в инструмент, который незаметно проводит изменения поведения.
Популярный совет дать ассистенту все тесты и попросить его «сделать их зелёными» здесь ошибочен. Для эталонов это поощряет изменение ожидаемого результата, ослабление нормализатора или изменение настройки теста, пока сравнение не начнёт проходить. Считайте утверждённые фикстуры доступными только для чтения во время реализации. Если нужен новый утверждённый результат, это должно быть отдельное действие человека после ревью кода.
Изменившаяся фикстура это продуктовое решение, которому нужен владелец
Когда эталонный тест падает, сначала классифицируйте дифф, а уже потом трогайте фикстуру. Варианты просты: намеренное изменение продукта, случайная регрессия, исправление старого поведения или тестовый шум. От классификации зависит следующий шаг.
Намеренное изменение продукта требует требования или ясного решения ревьюера, после чего фикстуру можно обновить. При случайной регрессии исправьте код и оставьте фикстуру прежней. Исправление старого поведения требует отдельного объяснения: фикстура теперь описывает и старый контракт, и решение отказаться от него. При тестовом шуме улучшите настройку или нормализацию, чтобы будущие запуски не создавали бессмысленные диффы.
Вот пример, показывающий пользу такой дисциплины. Команда рефакторит сервис сводки заказа, чтобы сократить повторные обращения к базе данных. Эталон API меняется в трёх местах: список товаров получает другой порядок, отсутствующее поле discount превращается в null, а итог меняется с 19.99 на 20.00.
Первое отличие может быть безвредным, если API не обещает порядок товаров. Второе способно сломать мобильный клиент, который различает отсутствие поля и значение null. Третье не относится к форматированию. Оно может показывать ошибку преобразования десятичных чисел, исправленное правило округления или старый дефект, которого никто не замечал. Если ревьюер утвердит всю фикстуру со словами «рефакторинг не должен ничего менять», команда либо сохранит финансовую ошибку, либо скроет регрессию.
Разделите дифф по причинам. Сначала исправьте итоговую сумму. Затем решите, входит ли наличие поля в контракт совместимости. Только после этого решайте, где должен находиться порядок сортировки, в сериализаторе или в нормализаторе фикстуры. Коммитов станет больше, зато каждый из них будет понятен будущему инженеру.
Хорошее описание pull request называет ожидаемые изменения фикстуры простыми словами. Оно не говорит «снапшоты обновлены». Оно говорит: «экспорт теперь выводит неактивные счета после активных, потому что изменилось требование к отчёту» или «ответ больше не раскрывает внутреннее поле источника». Если автор не может написать такое предложение, ревьюерам не стоит утверждать изменение файла.
Большим эталонам нужны удобные способы ревью, а не слепое доверие
Большой результат может быть настоящим контрактом. Но его структура должна помогать человеку проверять содержимое. Тест должен выводить понятные имена, разделять сценарии и направлять дифф к бизнес-смыслу, а не к случайным особенностям сериализации.
Используйте отдельную фикстуру для каждого сценария, а не одну огромную фикстуру для всех вариантов. Называйте сценарии так, чтобы в имени были условие и ожидаемое поведение. invoice_with_tax_exemption лучше, чем case_07. Располагайте стабильные разделы в стабильном порядке. В отчёте группируйте заголовок, строки, итоги и предупреждения, а не выгружайте внутренний граф объектов.
Здесь полезны собственные сериализаторы. В документации ApprovalTests справедливо отмечается, что от расположения данных зависит поддерживаемость файлов утверждений при падениях. Сериализатор, который выводит одно значимое поле в строке, не позволит изменению одного слова превратиться в перестройку ста строк.
Для структурированного результата включайте только поля, относящиеся к границе. Полная выгрузка ORM это плохой эталон: внутренние добавления и рефакторинг создают шум. Вместо этого создайте тестовое представление, предназначенное для ревью:
{
"report": "monthly_revenue",
"period": "2026-06",
"currency": "USD",
"rows": [
{"account": "A-102", "recognized": "1250.00", "deferred": "0.00"},
{"account": "B-451", "recognized": "0.00", "deferred": "400.00"}
],
"totals": {"recognized": "1250.00", "deferred": "400.00"}
}
Это представление не обманывает, если настоящий отчёт выводит ту же бизнес-информацию. Это адаптер для тестирования. Оставьте небольшое число сквозных проверок для итогового CSV, таблицы или PDF, чтобы сам адаптер не создавал ложного чувства безопасности.
Ревьюерам также нужен предел. Если изменение фикстуры слишком велико для чтения, попросите автора разделить его или добавить второе сравнение с разбивкой изменений по полям. Не принимайте фразу «ассистент говорит, что результат эквивалентен». Ассистент может быть прав, но заявление об эквивалентности не является доказательством для ревью.
Эталонные тесты дополняют точечные, а не заменяют их
Эталонные тесты плохо объясняют, почему результат правильный. Они сообщают, что результат отличается. Оставляйте прямые проверки для правил с высоким риском или необходимостью точной диагностики: авторизации, валидации, денежных вычислений, переходов состояний, повторных попыток и классификации ошибок.
Используйте эталоны там, где наблюдаемых деталей много: для читаемого документа, экспорта данных, ответа с большим числом вычисляемых полей или пути совместимости при миграции. Точечные тесты подходят для случаев, когда одно утверждение выражает настоящее правило. Тест с именем applies_state_tax_after_discount понять проще, чем искать это правило в строке 183 фикстуры отчёта.
Такое разделение делает работу ассистента безопаснее. Точечные тесты показывают локальные правила. Эталонные захваты показывают системные последствия. Когда ассистент меняет вычисление, прямой тест указывает на нарушенное правило, а эталон показывает, какой итоговый результат изменился. Ни один тип тестов не должен брать на себя всю нагрузку.
В AppMaster.io практический подход заключается не в том, чтобы абстрактно просить инженеров, использующих AI, быть внимательнее. Им дают воспроизводимые production-границы, быстрые тесты и историю ревью, в которой намеренные изменения очевидны. Команды, которым нужно понять, где таких границ не хватает, могут начать с Team & AI Audit до передачи крупных рефакторингов агентам.
Начните с пути, к которому команда боится прикасаться
Лучший первый объект это не самый чистый сервис. Это поведение, которое все считают опасным, потому что код старый, результаты требовательны к деталям, а никто не хочет отвечать за поломку. Сохраните три-четыре репрезентативных сценария на публичной границе. Прочитайте каждую строку первых утверждённых файлов. Затем позвольте ассистенту внести одно ограниченное внутреннее изменение.
Если результат останется прежним, вы получите уверенность и многоразовый защитный барьер. Если он изменится, дифф даст конкретный вопрос, на который нужно ответить. Это гораздо лучшее место для времени ревью, чем чтение сотни изменённых строк в надежде, что никто не забыл пограничный случай.
Часто задаваемые вопросы
Что такое тест с эталонным результатом?
Это полезная страховка для поведения, которое трудно описать десятками небольших проверок. Такой тест не доказывает, что текущее поведение правильно. Человек должен изучить первый сохранённый результат и решить, что его стоит сохранять.
Эталонные тесты и snapshot-тесты это одно и то же?
Эти подходы пересекаются, но акцент у них разный. Snapshot-тестирование часто проверяет отрисованный объект или компонент, а эталонный тест может фиксировать любую стабильную границу: HTTP-ответ, сгенерированный файл, вывод CLI или бизнес-отчёт.
Когда стоит использовать эталонные тесты вместо модульных?
Используйте их, когда legacy-код имеет широкое наблюдаемое поведение и слабое покрытие, особенно перед структурной переработкой. Они не заменяют точечные проверки решений, связанных с безопасностью, движением денег, правами доступа или недавно обнаруженной ошибкой.
Безопасно ли добавлять сохранённые ответы API в Git?
Нет. Сохраняйте только данные, которые системе разрешено раскрывать, а перед записью фикстуры удаляйте идентификаторы, учётные данные, токены, содержимое клиентов и внутренние пути. Считайте утверждённые фикстуры артефактами, близкими к production: они попадут в репозиторий и pull request.
Как сделать так, чтобы эталонные тесты не падали из-за временных меток и случайных идентификаторов?
Нормализуйте результат до сравнения. Зафиксируйте часы, задайте начальное состояние генератора случайных чисел, замените сгенерированные идентификаторы заполнителями, если их точное значение не важно, отсортируйте неупорядоченные коллекции и удалите изменчивые метаданные, например идентификаторы запросов.
Может ли AI-ассистент для программирования утверждать изменения эталонных результатов?
Нет. Пусть ассистент создаёт каркас захвата, предлагает рефакторинг и объясняет диффы, но решение о каждом изменённом результате должен принимать человек. Ассистент умеет предлагать варианты, однако контракт продукта принадлежит не ему.
Как правильно утвердить изменённую фикстуру?
Нужна явная команда утверждения или осознанное переименование файла received в approved. Обычная команда тестирования не должна переписывать ожидаемый результат: один неосторожный локальный запуск может стереть свидетельство изменения поведения.
Эталонные тесты должны сравнивать байты или разобранные данные?
Строго сравнивайте байты, когда форматирование входит в контракт, например для подписанного экспорта, фиксированного протокольного сообщения или файла, где важна каждая строка. Для обычных JSON и отчётов сначала уберите структурный шум, а затем сохраняйте поля, порядок и форматирование, от которых действительно зависят пользователи или последующие системы.
Почему эталонные тесты становятся хрупкими?
Они часто становятся слишком широкими: команда сохраняет весь ответ сервиса для каждого тестового сценария. Оставьте небольшой набор сквозных эталонных тестов на настоящих границах, добавьте обычные проверки бизнес-правил и используйте небольшие захваты там, где дифф трудно читать.
Какой эталонный тест первым добавить в legacy-приложение?
Начните с критичного для production пути, который команда не хочет менять, потому что его поведение плохо описано. Сохраните несколько типичных сценариев, внимательно проверьте их и разрешите ассистенту работать только после того, как эти фикстуры начнут проходить в CI.


