Как безопасно вносить изменения в legacy-приложение без тестов — характеризационное тестирование и рефакторинг на практике
· Го Комура · Устаревшие технологии, Использование существующих активов, Рефакторинг, Проектирование тестов, Характеризационное тестирование, C#, .NET, Обслуживание, Таблица решений, Техническая консультация
«Я прекрасно понимаю, что нужно исправить. Но стоит представить, что из-за этого сломается что-то ещё, и руки опускаются» — эти слова мы постоянно слышим от тех, кто унаследовал бизнес-приложение без тестов.
У многих бизнес-приложений, написанных на VB6, .NET Framework или Access, нет автоматических тестов вообще. Обновление документации давно остановилось, и фактически «код — это единственная спецификация». При этом бизнес продолжает работать, и запросы на изменение ставки НДС, правку макета отчёта или добавление нового контрагента не будут ждать.
В этом блоге в статье «Сколько ещё будет работать приложение на VB6 — реалистичный путь миграции на .NET» мы рассказывали о подходе, при котором старая система рассматривается как «работающая спецификация», а миграция продвигается через сверку выводов с ней. Эта статья применяет ту же идею к другой ситуации: не к миграции, а к внесению изменений прямо в работающий сейчас код. Центральный инструмент здесь — характеризационный тест (characterization test). Даже в коде без единого теста, если заранее зафиксировать тестом то поведение, которое вы, возможно, вот-вот сломаете, и рефакторинг, и добавление функциональности становятся заметно безопаснее.
1. Сначала вывод
- Не бросайтесь сразу исправлять. Сначала зафиксируйте тестом текущее поведение. Даже без документации сам вывод работающего сейчас кода и есть спецификация.
- Инструмент для этого — характеризационный тест. Он фиксирует не «правильное поведение», а «текущее поведение»: вывод (отчёты, CSV, результаты расчётов и т. п.) сохраняется как есть в качестве ожидаемого значения, а до и после изменения сравнивается разница (метод golden master).
- Для структур, куда тест не воткнуть (логика прямо в обработчике UI-события, прямые обращения к
DateTime.Nowили к пути файла), создайте «шов» (seam) минимальными изменениями — извлечением метода и внедрением интерфейса. Крупная переделка не нужна. - Не смешивайте рефакторинг и добавление функциональности в одном коммите. Условие успеха для рефакторинга — «нулевая разница», для новой функциональности — «только запланированная разница»; смешав их, вы теряете возможность понять, что означает разница вообще.
- Насколько глубоко выстраивать тестовую инфраструктуру, решают три фактора: масштаб доработки × оставшийся срок жизни системы × последствия при сбое. Покрывать всё юнит-тестами — не всегда правильный ответ; иногда верным решением будет «только характеризационные тесты» или «не трогать».
- Начать можно и без CI. Один тестовый проект и папка с файлами ожидаемых значений — уже это заметно повышает безопасность, даже если запускать тесты только вручную.
2. Почему legacy-код «ломается от прикосновения»
Дорабатывать legacy-код страшно не потому, что код старый, а потому, что нет способа проверить, правилен ли результат изменения.
Майкл Физерс в книге «Working Effectively with Legacy Code» определил legacy-код не как «просто старый код», а как «код без тестов».1 Причина в том, что без тестов нет быстрого способа при каждом изменении проверить, стал код лучше или хуже. Если следовать этому определению, даже код, написанный вчера, — legacy, если у него нет тестов.
В коде без тестов раскручивается такой порочный круг:
- Тестов нет, поэтому не видно область влияния изменения, и это страшно
- Раз страшно, существующую структуру не трогают, а обходятся минимальным копипастом и добавлением ещё одного условного ветвления
- Такие временные заплатки накапливаются, и код становится ещё менее читаемым и ещё более хрупким
- Из-за хрупкости становится ещё страшнее (возврат к пункту 1)
Выход из этого круга — не «набраться смелости и сделать масштабный рефакторинг». Порядок обратный: сначала натянуть страховочную сетку (тесты), устранить источник страха и только потом исправлять. Но здесь есть проблема курицы и яйца. Чтобы написать тест, нужна тестируемая структура. А чтобы сделать структуру тестируемой, нужно изменить (отрефакторить) код. Получается, что код без тестов приходится менять без тестов.
Чтобы разрешить это противоречие, доработку legacy-кода ведут в следующем порядке:1
- Ограничиваясь только областью вокруг изменения, зафиксировать текущее поведение снаружи (характеризационный тест)
- Внутри этой страховочной сетки выполнить минимальное изменение с крайне низким риском что-то сломать (например, извлечение метода), создав точку, куда можно воткнуть тест
- Когда структура позволит писать точечные тесты, приступить к нужному изменению (рефакторингу или добавлению функциональности)
В следующих разделах подробно рассмотрим пункты 1 и 2.
3. Характеризационный тест — фиксация «текущего поведения»
3.1 Чем это отличается от обычного теста
Обычный тест проверяет правильное поведение — «по спецификации должно быть так». Характеризационный тест устроен иначе. Он фиксирует как код на самом деле ведёт себя прямо сейчас, сознательно откладывая вопрос о том, правильно это или нет.
Допустим, в документации не указано, как обрабатываются дробные суммы — округлением, отбрасыванием остатка или как-то ещё. Если действующий код отбрасывает остаток, и бизнес так работает уже десять лет, то «отбрасывание остатка» — это, как минимум, фактическая спецификация. Характеризационный тест фиксирует это как есть, в форме «текущий вывод такой-то». Даже если впоследствии окажется, что это баг, сначала его фиксируют. Менять поведение (исправлять баг) нужно отдельно, как намеренное изменение, уже после того, как страховочная сетка натянута.
3.2 Метод golden master по шагам
Для legacy-кода, вывод которого достаточно крупнозернистый, метод golden master — самый выгодный по соотношению затрат и результата вид характеризационного тестирования. Процедура проста:
- Определить, какой вывод формирует изменяемая функциональность (текст отчёта, CSV, список результатов расчёта и т. п.)
- Подготовить представительные входные данные, выполнить действующий код и получить вывод
- Сохранить этот вывод как есть в файл ожидаемых значений (golden master) и закоммитить его в репозиторий
- Впоследствии при каждом изменении кода запускать тест и проверять, что разница между выводом и файлом ожидаемых значений равна нулю
Для реализации на C# не нужна какая-то особая библиотека — достаточно чего-то настолько же простого, как это:
[Fact]
public void MonthlyBillingSummary_GoldenMaster()
{
// 1. Читаем представительные входные данные (например, замаскированные данные из продакшена)
var input = File.ReadAllLines(TestDataPath("billing-input-202606.csv"));
// 2. Вызываем существующую логику как есть и получаем строку вывода
string actual = BillingReport.Generate(input);
// 3. Отсутствие файла ожидаемых значений означает либо «сломано тестовое окружение»,
// либо «это первый запуск». В любом случае молча пропускать нельзя —
// записываем результат и безусловно проваливаем тест
string expectedPath = TestDataPath("billing-expected-202606.txt");
if (!File.Exists(expectedPath))
{
File.WriteAllText(expectedPath + ".candidate", actual);
Assert.Fail("Файл ожидаемых значений отсутствует. Проверьте содержимое файла " +
".candidate, и если оно корректно, закоммитьте его как ожидаемое значение.");
}
// 4. Проверяем точное совпадение с сохранённым поведением
string expected = File.ReadAllText(expectedPath);
Assert.Equal(expected, actual);
}
Избегайте реализации, при которой отсутствующий файл ожидаемых значений на месте сохраняется как текущий вывод, а тест при этом успешно проходит. Если файл ожидаемых значений забыли закоммитить или разместили не там, CI пройдёт зелёным, так и не обнаружив регрессию. Первую запись стоит делать так, как показано выше: выводить файл-кандидат (.candidate) и явно проваливать тест, чтобы поток был строго однонаправленным — сначала человек проверяет результат, и только потом он коммитится как ожидаемое значение.
Поскольку одного сообщения об ошибке Assert.Equal при появлении разницы для анализа маловато, на практике удобнее при провале дополнительно записывать фактический вывод в отдельный файл вроде billing-actual-202606.txt, чтобы можно было сравнить его с ожидаемым значением через инструмент диффа, например WinMerge.
3.3 Выбор входных данных и нормализация вывода
Входные данные выбираются по принципу «типичное плюс граничное». Берём один-два обычных случая и добавляем входные данные, которые проходят через ветвления, найденные при чтении кода: закрытие периода в конце месяца, нулевое количество записей, отрицательные значения, особую обработку для конкретного контрагента и т. п. Если можно использовать замаскированные данные из продакшена, они лучше всего покрывают реальные ветвления.
Недетерминированные значения, попадающие в вывод, перед сравнением нужно нормализовать. Время печати, длительность обработки, GUID, автоинкрементные номера и подобное меняются при каждом запуске, поэтому в исходном виде каждый раз будут давать разницу. После генерации вывода стоит выполнить предобработку — например, заменить регулярным выражением Напечатано: 2026/07/17 16:00 на Напечатано: <DATE> — и только затем сравнивать.
Ориентир того, какой вывод хорошо подходит для golden master:
| Тип вывода | Пригодность | Комментарий |
|---|---|---|
| CSV, файлы фиксированной длины | Отлично | Сохраняются и сравниваются как есть. Первая цель, за которую стоит браться |
| Отчёты (текст или исходные данные предпросмотра печати) | Отлично | Захватывайте строку до того, как она превратится в PDF; сравнение бинарных PDF лучше избегать |
| Список результатов расчёта (суммы, остатки на складе и т. п.) | Отлично | Допустимо добавить тестовый метод, выгружающий результаты в CSV |
| Содержимое, записываемое в БД | Хорошо | После записи выполняем SELECT по таблице, выгружаем в CSV и сравниваем |
| Сам вид экрана | Ограниченно | Подходит, если можно свести к строке. Автоматизация действий на экране требует другого инструментария, о котором рассказано в статье «Автоматизированное UI-тестирование desktop-приложений для Windows» |
| Отправка во внешнюю систему | Ограниченно | Нужен шов (см. следующий раздел), чтобы перехватить данные непосредственно перед отправкой |
4. Как создать «шов» (seam) для внедрения теста
При попытке написать golden master во многих legacy-приложениях натыкаешься на стену: логика прописана прямо в обработчике UI-события, и запустить её без открытия экрана нельзя. Здесь нужно место, куда тестовый код может подставить своё поведение и понаблюдать за результатом, — то, что Физерс называет швом (seam).1
4.1 Отделяем логику от UI извлечением метода
Типичное состояние «до»: расчёт, доступ к БД, зависимость от времени и обновление экрана — всё живёт в одном обработчике события.
// До: всё прописано прямо в обработчике события
private void btnCalc_Click(object sender, EventArgs e)
{
var rows = LoadRowsFromDb(); // Прямой доступ к БД
var now = DateTime.Now; // Зависимость от текущего времени
decimal total = 0;
foreach (var row in rows)
{
if (row.SalesDate.Year == now.Year &&
row.SalesDate.Month == now.Month) // Агрегируем только текущий месяц
{
total += Math.Floor(row.Amount * 1.1m); // Бизнес-правило округления
}
}
lblTotal.Text = total.ToString("N0"); // Прямое обновление экрана
}
В таком виде для тестирования логики агрегации за текущий месяц нужны экран, БД и «сегодняшняя дата». Стандартный ход, делающий это тестируемым с минимальными изменениями, — извлечь только расчёт в отдельный метод, превратив внешние зависимости (результаты из БД и текущее время) в параметры. Рефакторинг Visual Studio «Извлечь метод» (Ctrl+R, M) заодно снижает риск ручных ошибок при переписывании.2
// После: извлечён только расчёт, «результаты из БД» и «текущее время»
// принимаются как параметры
internal static decimal CalcMonthlyTotal(IEnumerable<SalesRow> rows, DateTime now)
{
decimal total = 0;
foreach (var row in rows)
{
if (row.SalesDate.Year == now.Year &&
row.SalesDate.Month == now.Month)
{
total += Math.Floor(row.Amount * 1.1m);
}
}
return total;
}
private void btnCalc_Click(object sender, EventArgs e)
{
var rows = LoadRowsFromDb();
lblTotal.Text = CalcMonthlyTotal(rows, DateTime.Now).ToString("N0");
}
Обработчик события сократился до трёх строк — загрузка, расчёт, отображение, — а извлечённый метод можно тестировать с любыми строками данных и любой датой. Граничные случаи, связанные со временем, — конец месяца, начало месяца, високосный год — тоже воспроизводятся простой передачей даты вроде new DateTime(2028, 2, 29).
4.2 Делаем зависимости заменяемыми через внедрение интерфейса
Для зависимостей, которые не решить одной лишь параметризацией (DateTime.Now используется повсюду, путь к файлу зашит в код и т. п.), зависимость оборачивают в интерфейс и внедряют. В рекомендациях Microsoft Learn по юнит-тестированию для .NET прямая зависимость от DateTime.Now тоже приводится как классический пример того, что нельзя контролировать из теста, и в качестве решения описывается оборачивание в интерфейс для введения шва.3
public interface IClock
{
DateTime Now { get; }
}
public sealed class SystemClock : IClock
{
public DateTime Now => DateTime.Now;
}
// На стороне теста подставляем реализацию, возвращающую фиксированное время
public sealed class FixedClock : IClock
{
private readonly DateTime _fixed;
public FixedClock(DateTime value) => _fixed = value;
public DateTime Now => _fixed;
}
Добавление IClock в конструктор существующего класса заставляет исправлять все места вызова, поэтому на переходный период реалистично добавить рядом конструктор без параметров, использующий по умолчанию SystemClock, и постепенно приводить вызывающий код в порядок. Зашитые в код пути к файлам или строки подключения к БД оборачиваются по тому же принципу — небольшим интерфейсом, предоставляющим только операции «чтения/записи».
Заметим, что в Visual Studio есть встроенный рефакторинг для извлечения интерфейса из существующего класса (Extract Interface), который позволяет проводить такие изменения механически.2
Есть один принцип, которого нужно придерживаться при создании шва: само изменение, создающее шов, не должно менять поведение ни на миллиметр. И извлечение метода, и внедрение интерфейса — механические, в высокой степени сохраняющие поведение операции, которые поддерживают компилятор и IDE. На этом этапе возникнет соблазн «заодно» поправить логику — не поддавайтесь: это работа для этапа после того, как страховочная сетка уже натянута.
5. Таблица решений: насколько глубоко нужно заходить
И характеризационные тесты, и создание швов требуют трудозатрат. Довести весь legacy-код до одного и того же уровня покрытия тестами для небольшой или средней команды нереалистично, да и не нужно. Решение принимается по трём осям:
- Масштаб доработки: исправление бага в несколько строк, добавление функциональности или изменение, затрагивающее структуру
- Оставшийся срок жизни системы: миграция или отказ от неё через год-два, или использование ещё 5 лет и дольше
- Последствия при сбое: косметическая порча вида отчёта или неверная сумма счёта / остаток на складе
| Масштаб доработки | Оставшийся срок жизни | Последствия при сбое | Рекомендуемый уровень |
|---|---|---|---|
| Небольшой (несколько строк, изменение настроечного значения) | Короткий (до ~2 лет) | Малые (косметические искажения отображения) | Только характеризационные тесты. Зафиксировать соответствующий вывод, внести изменение, проверить нулевую разницу — и на этом закончить |
| От небольшого до среднего | Короткий | Большие (затрагивает деньги или склад) | Только характеризационные тесты, но плотнее. Расширить набор входных данных, включая граничные случаи |
| Средний (добавление функциональности, изменение логики) | Долгий (от 5 лет) | От малых до средних | Характеризационные тесты плюс юнит-тесты, ограниченные только областью изменения (создание шва) |
| От среднего до крупного | Долгий | Большие | Характеризационные тесты плюс тестовая инфраструктура, плюс дробление релиза на более мелкие единицы |
| Крупный (нужна структурная переделка) | Короткий | — | Не трогать. Обходиться операционно, не дорабатывая, а трудозатраты направить на миграцию/замену |
| — (запроса на доработку вообще нет) | — | — | Не трогать. Не рефакторить превентивно код, который и так работает |
Две нижние строки с «не трогать» — не пассивный выбор по умолчанию, а активное решение. Инвестиции во внутреннее качество системы с коротким оставшимся сроком жизни не окупаются. Эти трудозатраты лучше направить на решение о миграции, которое разбирается в статье «Таблица решений по продлению жизни и миграции бизнес-приложений VB6 / Access», и на проектирование целевого решения.
Также, если дело доходит до построения тестовой инфраструктуры, нужно провести границу между тем, что относится к юнит-тесту, а что стоит оставить интеграционному тесту (использующему реальную БД или реальные файлы). Эта граница разобрана как таблица решений в статье «Где провести границу между юнит-тестами и интеграционными тестами» — рекомендуем прочитать её вместе с этой статьёй. Поскольку юнит-тесты по своей природе должны быть быстрыми, изолированными и повторяемыми,3 характеризационные тесты, обращающиеся к БД или файловой системе, разумно выносить в отдельный проект или отдельную единицу запуска, отличную от юнит-тестов.
6. Правила эксплуатации — как не сломать страховочную сетку
Характеризационный тест легко превращается в формальность, если неправильно выстроить работу с ним после написания. Ограничимся тремя обязательными правилами.
6.1 Не смешивайте рефакторинг и добавление функциональности в одном коммите
Рефакторинг — это изменение, которое делает код более понятным и удобным для сопровождения, не меняя его поведения.4 Значит, критерий успеха для него — нулевая разница с golden master. У добавления функциональности и исправления бага, напротив, критерий успеха — появление только запланированной разницы. Смешайте эти два вида изменений в одном коммите — и как только появится разница, вы уже не сможете определить, «запланированное это изменение» или «поломка».
| Тип изменения | Обращение с golden master | Критерий успеха |
|---|---|---|
| Рефакторинг (изменение структуры) | Не обновлять | Нулевая разница |
| Исправление бага / добавление функциональности (изменение поведения) | Обновить после ревью разницы | Только запланированная разница |
| Создание шва (извлечение метода, внедрение интерфейса) | Не обновлять | Нулевая разница |
| Изменение правила нормализации ожидаемых значений | Перегенерировать | Причину изменения указать в сообщении коммита |
То же самое верно и на уровне релизов. «Релиз, содержащий только рефакторинг» по определению не должен менять поведение, поэтому при инциденте можно сразу подозревать именно рефакторинг. Смешав их, вы теряете эту возможность разграничения причин.
6.2 Обновляйте ожидаемые значения в порядке «сначала ревью разницы, потом перезапись»
Когда поведение изменено намеренно, обновите и golden master. Закрепите порядок действий:
- Сгенерировать вывод после изменения и визуально просмотреть разницу с текущим ожидаемым значением
- Убедиться, что разница состоит только из запланированного изменения (если изменилась хотя бы одна незапланированная строка — расследовать)
- Перезаписать файл ожидаемых значений новым выводом и включить его в тот же коммит, что и код, чтобы это осталось в истории вместе
Опасный сценарий эксплуатации — «тест стал красным, поэтому перезаписываем ожидаемое значение, чтобы он стал зелёным». Сделав так, вы фиксируете регрессию как «правильное поведение», и страховочная сетка перестаёт быть страховочной сеткой.
6.3 Стройте минимальную конфигурацию, работающую локально даже без CI
Даже в проекте без сервера CI можно начать уже сегодня с такой минимальной конфигурации:
- Добавить в решение один тестовый проект (подойдут MSTest, NUnit и xUnit — даже оставаясь на .NET Framework)
- Хранить файлы ожидаемых значений и входные данные в папке
TestDataи версионировать их вместе с кодом - Сделать командным правилом вручную запускать
dotnet test(или Test Explorer в Visual Studio) перед каждым коммитом - Добавить в инструкцию по релизу одну строку — «запустить тесты и убедиться в нулевой разнице», — чтобы результат не забывали проверить
Чем лучше выстроено логирование, тем быстрее идёт расследование при сверке результатов тестов с реальным выводом приложения. О том, что стоит логировать, рассказано в статье «Минимальные требования к самописному логгеру и чек-лист для интеграционных тестов».
7. Итог
- Legacy-код — это «код без тестов»,1 и настоящая причина, по которой он ломается от прикосновения, в том, что нет способа проверить результат изменения. Прежде чем исправлять, зафиксируйте текущее поведение тестом.
- Характеризационный тест фиксирует не «правильное поведение», а «текущее поведение». С методом golden master — сохранением отчётов, CSV или результатов расчёта как есть в файлы ожидаемых значений и сравнением разницы — можно начать с обычного кода на C#, без специальных библиотек.
- Для структур, куда тест не воткнуть, стройте шов через извлечение метода и внедрение интерфейса. Оборачивание такой зависимости, как
DateTime.Now, — стандартный приём, описанный и в собственных рекомендациях Microsoft по юнит-тестированию.32 - Насколько глубоко выстраивать инфраструктуру, решают масштаб изменения × оставшийся срок жизни × последствия при сбое. «Только характеризационные тесты» или «не трогать» — вполне полноценные решения.
- В эксплуатации не смешивайте рефакторинг (критерий успеха — нулевая разница) с добавлением функциональности (критерий успеха — только запланированная разница),4 и всегда пропускайте обновление ожидаемых значений через ревью разницы. Даже без CI одно лишь командное правило запускать тесты локально сильно меняет уровень безопасности.
Похожие статьи
- Где провести границу между юнит-тестами и интеграционными тестами
- Сколько ещё будет работать приложение на VB6 — статус поддержки рантайма и реалистичный путь миграции на .NET
- Таблица решений по продлению жизни и миграции бизнес-приложений VB6 / Access — оставить, обернуть или заменить
- Минимальные требования к самописному логгеру и чек-лист для интеграционных тестов
- Автоматизированное UI-тестирование desktop-приложений для Windows
Смежные области консультаций
В Komura Software LLC мы занимаемся внедрением характеризационных тестов в существующие бизнес-приложения без тестов, поэтапным рефакторингом к тестируемой структуре, а также помогаем определить, куда стоит инвестировать — в доработку или в миграцию.
- Доработка и обслуживание существующего ПО для Windows
- Использование существующих активов и поддержка миграции
- Техническая консультация и ревью архитектуры
- Контакты
Справочные материалы
-
Michael C. Feathers, “Working Effectively with Legacy Code” (Prentice Hall, 2004). Об определении legacy-кода как «кода без тестов», о процедуре фиксации текущего поведения характеризационным тестом перед началом изменения и о концепции шва (seam) для внедрения теста. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Extract and inline refactorings (Visual Studio). О порядке действий для рефакторингов Visual Studio «Извлечь метод» (Ctrl+R, M) и «Извлечь интерфейс» для C# / Visual Basic. ↩ ↩2 ↩3
-
Microsoft Learn, Unit testing best practices for .NET. О свойствах хорошего юнит-теста (fast / isolated / repeatable / self-checking / timely), о приёме оборачивания неконтролируемой зависимости вроде
DateTime.Nowв интерфейс для введения шва и о том, что зависимости от инфраструктуры не стоит вносить в юнит-тесты, а нужно выносить в интеграционные тесты. ↩ ↩2 ↩3 -
Microsoft Learn, Refactor code (Visual Studio). Об определении рефакторинга как процесса изменения кода с целью упростить его сопровождение, понимание и расширение без изменения поведения. ↩ ↩2
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
До каких пор будут работать приложения VB6 ── состояние поддержки среды выполнения и практичный путь миграции на .NET
До каких пор будут работать приложения VB6? В статье разбирается асимметрия между политикой поддержки среды выполнения VB6 (поддерживаетс...
Обратная совместимость интерфейсов DLL и COM — таблица решений: какие изменения ломают вызывающую сторону
Какие изменения DLL или COM-компонента на самом деле ломают вызывающую сторону? Разбираем три слоя совместимости — бинарную, исходную и п...
Версионирование схемы БД бизнес-приложения — практика миграций, предотвращающая «у каждого клиента своя база»
Практическое руководство по версионированию схемы БД бизнес-приложения, установленного у множества клиентов. Разбираем PRAGMA user_versio...
Если вам досталась система без исходного кода и без документации — практический план, как сопровождать её, не останавливая работу
Разбираем практический план начала эксплуатации и сопровождения бизнес-системы, у которой нет ни исходного кода, ни спецификаций. Охватыв...
Как выбрать межпроцессное взаимодействие в Windows — таблица решений: именованные каналы / TCP / gRPC / разделяемая память / COM
Разбираем, как выбрать способ взаимодействия Windows-приложений друг с другом: сильные стороны и ловушки именованных каналов, локального ...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Услуги по этой теме
Статья напрямую связана со следующими услугами.
Разработка приложений для Windows
Бизнес-приложения, интеграция оборудования и средства связи — от требований до разработки.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- Что такое характеризационный тест (characterization test)?
- Это тест, который фиксирует не «правильное поведение», а «текущее поведение» кода как есть. В legacy-коде, где документация давно не обновляется, часто просто нет способа проверить, что считать правильным, поэтому сначала сохраняют вывод работающего сейчас кода (отчёт, CSV, результат расчёта и т. п.) в качестве ожидаемого значения и затем механически проверяют, что вывод не изменился до и после модификации. Базовый подход — сначала натянуть эту страховочную сетку из зафиксированного поведения, и только потом переходить к рефакторингу или добавлению функциональности.
- С чего начать работу с legacy-кодом, в котором вообще нет тестов?
- Реалистичный путь — писать характеризационные тесты, ограничиваясь только той областью, которую собираетесь менять. Покрыть тестами всю систему целиком почти никогда не окупается по трудозатратам, да и необходимости в этом нет. Сначала определите, какой вывод формирует изменяемая функциональность (отчёт, CSV, содержимое, записываемое в БД, и т. п.), сохраните в файл вывод для представительных входных данных и зафиксируйте его. Внутри этой страховочной сетки выполните небольшие, сохраняющие поведение рефакторинги — например, извлечение метода, — чтобы вычленить логику в тестируемую форму, и только после этого приступайте к нужному изменению.
- Почему нельзя смешивать рефакторинг и добавление функциональности в одном коммите?
- Потому что, когда в выводе появляется разница, теряется возможность понять её причину. Рефакторинг проверяется условием «поведение не изменилось», а добавление функциональности — условием «изменилось только в нужном месте»: критерии успеха прямо противоположны. Если их смешать, невозможно определить, является ли отличие от golden master «намеренным изменением» или «поломкой». Безопаснее разделять их: для коммита с рефакторингом — нулевая разница, для коммита с новой функциональностью — только запланированная разница.
- Когда нужно обновлять golden master (файл ожидаемых значений)?
- Только в момент, когда поведение меняется намеренно — то есть при коммите с добавлением функциональности или исправлением дефекта. При обновлении нужно визуально просмотреть разницу между старым и новым выводом, убедиться, что в ней есть только запланированное изменение, и лишь затем заменить ожидаемое значение новым выводом. Если механически перезаписывать ожидаемое значение просто потому, что тест стал красным, регрессия (непреднамеренное изменение поведения) молча фиксируется как «правильная», и страховочная сетка перестаёт выполнять свою функцию.
Об авторе
Страница с профилем автора статьи.
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.
Публичные ссылки