
Как покрыть тестами legacy VB.NET: характеризационные тесты, швы и обёртки
В старом VB.NET-проекте тестов обычно нет, а логика размазана по формам, модулям и Shared-методам. Переписать всё ради тестируемости нельзя, но начать покрывать код тестами можно уже на этой неделе. Показываем порядок действий и минимальные приёмы.
Цель: зафиксировать поведение, а не доказать правильность
Тесты для legacy-кода пишут не так, как для нового. В новом проекте тест описывает требование. В старом требования давно потеряны, а единственный достоверный источник истины это то, как система ведёт себя сейчас. Поэтому первые тесты называют характеризационными: они фиксируют текущее поведение, даже если оно выглядит странно.
Такой подход снимает главный страх. Вы не решаете, баг ли округление в третьем знаке или так задумано. Вы записываете факт и получаете сигнал, если рефакторинг его изменит. Обсуждать, правильно ли поведение, можно потом, отдельной задачей и с заказчиком.
Тестовый проект рядом с основным
Добавьте в решение отдельный проект тестов на VB.NET. Подойдут xUnit, NUnit или MSTest: все три работают с Visual Basic и запускаются из Test Explorer в Visual Studio и командой dotnet test. Если основной проект на .NET Framework 4.x, тестовый проект может быть в новом SDK-формате с целевой платформой net48, это упрощает подключение пакетов.
Писать тесты на VB.NET не обязательно, C#-проект тоже может ссылаться на VB-сборку. Но если команда сопровождения пишет на VB, держите тесты на том же языке, иначе их перестанут читать.
Если нужные методы объявлены как Friend, не делайте их Public ради тестов. Добавьте в основной проект атрибут InternalsVisibleTo с именем тестовой сборки, и тесты увидят внутренние члены.
' В основном проекте
<Assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Billing.Tests")>Где искать первые точки входа
Не начинайте с самого запутанного места. Найдите код, у которого есть понятный вход и выход и нет обращений к базе, файлам и интерфейсу: расчёт скидки, разбор строки из файла обмена, формирование номера документа, конвертация единиц. Такие методы часто живут в модулях (Module) и Shared-классах, и это удача: их можно вызывать из теста напрямую.
Если логика сидит в обработчике кнопки на форме, сделайте одну механическую правку: перенесите тело обработчика в отдельный метод класса без изменения кода, а в обработчике оставьте вызов. Это безопасный рефакторинг, который Visual Studio выполняет командой Extract Method. После него появляется что тестировать.
- методы с ветвлениями по статусам и типам документов;
- всё, что считает деньги, налоги и даты;
- парсеры входящих файлов и форматтеры исходящих;
- код, в котором недавно были инциденты.
Шов для времени, базы и конфигурации
Главное препятствие для тестов это скрытые зависимости: DateTime.Now, прямые запросы к SqlConnection, чтение ConfigurationManager.AppSettings, обращение к файловой системе. Термин «шов» (seam) из книги Майкла Физерса «Эффективная работа с унаследованным кодом» обозначает место, где поведение можно подменить, не меняя сам код вокруг.
Самый дешёвый шов это необязательный параметр или свойство с реализацией по умолчанию. Рабочий код продолжает вызывать метод как раньше, а тест подставляет свою зависимость.
Public Interface IClock
ReadOnly Property Today As Date
End Interface
Public Class SystemClock
Implements IClock
Public ReadOnly Property Today As Date Implements IClock.Today
Get
Return Date.Today
End Get
End Property
End Class
Public Class InvoiceRules
Private ReadOnly _clock As IClock
Public Sub New(Optional clock As IClock = Nothing)
_clock = If(clock, New SystemClock())
End Sub
Public Function IsOverdue(dueDate As Date) As Boolean
' было: Return dueDate < Date.Today
Return dueDate < _clock.Today
End Function
End ClassПервый характеризационный тест
Тест пишется в три шага. Вызовите метод с реальными входными данными, лучше взятыми из продакшен-логов или выгрузок. Поставьте в проверку заведомо неверное ожидание, запустите тест и посмотрите, что метод вернул на самом деле. Перенесите фактическое значение в проверку. Звучит как жульничество, но именно так и фиксируется поведение.
Для методов с большим выходом, например формирования XML для обмена или текста печатной формы, удобнее снапшот-тесты. Библиотека Verify сохраняет результат в файл рядом с тестом при первом запуске и сравнивает с ним при следующих. Изменение поведения превращается в понятный дифф, который можно принять или отклонить.
Imports Xunit
Public Class InvoiceRulesTests
Private Class FixedClock
Implements IClock
Public Property Value As Date
Public ReadOnly Property Today As Date Implements IClock.Today
Get
Return Value
End Get
End Property
End Class
<Theory>
<InlineData("2026-09-01", False)>
<InlineData("2026-08-31", True)>
Public Sub IsOverdue_FixesCurrentBehavior(due As String, expected As Boolean)
Dim clock As New FixedClock With {.Value = #2026-09-01#}
Dim rules As New InvoiceRules(clock)
Assert.Equal(expected, rules.IsOverdue(Date.Parse(due)))
End Sub
End ClassЧто делать с базой данных
В legacy-коде запросы обычно вперемешку с логикой. Не пытайтесь сразу внедрить репозитории и ORM. Сначала разделите метод на две части: загрузка данных в DataTable или список объектов и обработка уже загруженного. Вторая часть тестируется как обычная функция, а DataTable в тесте собирается в несколько строк кода.
Для слоя доступа к данным пишите отдельные интеграционные тесты против настоящей СУБД: локального экземпляра SQL Server Express или контейнера. Их запускают реже, но они ловят то, что не поймает ни один мок, например изменение типа колонки или неявное преобразование дат.
Как встроить тесты в работу команды
Договоритесь о простом правиле: любой исправленный баг сопровождается тестом, который падал до исправления. За полгода такой практики покрываются ровно те места, где система действительно ломается. Добавьте запуск dotnet test или vstest.console в сборку на CI, чтобы красный тест останавливал выпуск версии.
Не гонитесь за процентом покрытия. Для legacy-проекта 20 процентов, сосредоточенные в расчётах и обменах, ценнее 70 процентов, набранных тестами на свойства и конструкторы.
Как это делают сегодня
Вопросы и ответы
Нужна ли Visual Studio Enterprise для тестов legacy-кода?
Нет. Microsoft Fakes, который умеет подменять Shared-методы вроде DateTime.Now, есть только в Enterprise, но швы через интерфейсы и параметры работают в любой редакции и с бесплатными xUnit или NUnit.
Можно ли тестировать код внутри форм WinForms?
Напрямую неудобно. Перенесите логику из обработчиков в отдельные классы и тестируйте их. Сами формы проверяют вручную или UI-автотестами, но это отдельная и дорогая история.
Что делать, если тест фиксирует явный баг?
Оставьте тест как есть и пометьте его комментарием со ссылкой на задачу. Исправление бага будет отдельным изменением, в котором вы осознанно поменяете ожидание.