Модульное тестирование в Delphi

b

Главная иллюзия: «зелёный» прогон — гарантия работоспособности

Первое, что бросается в глаза новичку: тест прошёл, полоса зелёная — код рабочий. На деле это самый коварный самообман. Опытные разработчики знают: тест может быть зелёным, но при этом не проверять ровным счётом ничего. Специалисты всегда смотрят на Arrange-Act-Assert (AAA) структуру: если в секции Assert нет ни одного вызова Check/Assert — это не тест, а имитация бурной деятельности. В Delphi, в отличие от C# или Java, легко забыть добавить проверку после вызова метода, и фреймворк (DUnit, DUnitX) радостно сообщит об успехе.

Неочевидный враг: исключения, которые не должны быть пойманы

Типичная ошибка — оборачивать тестируемый код в try-except «на всякий случай». Этим вы убиваете смысл тестирования. Если в производственном коде вы ожидаете EConvertError, то тест обязан упасть именно с этим исключением, а не проглотить его и вернуть false. Профессионалы используют атрибут ExpectedException (DUnitX) или конструкцию Assert.WillRaise. Тонкость: часто забывают, что исключение в конструкторе объекта — это совершенно другой сценарий, чем исключение в методе. Тестируйте и то, и другое.

Ловушка с зависимостями: mock-объекты и Run-Time Type Information

Delphi — язык с мощной системой RTTI, и это играет злую шутку. Когда вы создаёте mock с помощью TMock (например, в DUnitX через Delphi-Mocks), вы полагаетесь на RTTI. Но RTTI работает только с published-секцией. Если ваш интерфейс или класс имеет методы, объявленные как public без ключевого слова published, mock их «не увидит» и выбросит EProgrammerNotFound. Совет эксперта: всегда проверяйте доступность RTTI для мокируемого интерфейса через TRttiContext в вашем тестовом сетапе. Это сэкономит часы отладки.

Секрет стабильности: изоляция состояния

Многие пишут тесты, которые зависят от глобальных переменных (например, форм, которые уже созданы). Это катастрофа. Тесты в Delphi должны запускаться в чистом контексте. Популярный миф: «достаточно вызвать Application.Initialize в DPR». Нет. В тестах Application не проинициализирован так же, как в рантайме. Специалисты выносят логику в отдельные классы (фабрики, репозитории) без привязки к VCL/FMX. Весь VCL-код должен быть либо замокирован, либо протестирован отдельно в ручном режиме. Автоматические тесты с формами — это боль и страдания.

Профанация: тестирование приватных методов напрямую

Классическая ошибка — использование TPublishedProperty или RTTI для вызова приватного метода класса. Этого делать нельзя. Если вы хотите протестировать внутреннюю логику — вы неправильно спроектировали класс. Рефакторинг: выделите приватную логику в отдельный protected-метод или класс, и тестируйте его. RTTI для тестирования приватных методов — это хрупкая конструкция, которая сломается при смене компилятора (например, с Delphi 10 на 11 или 12).

Неочевидный нюанс: порядок выполнения тестов

В DUnit и DUnitX тесты могут выполняться в произвольном порядке. Но многие ошибочно полагаются на то, что SetUp глобальный. Если вы в одном тесте создали объект и не уничтожили его, а в другом — пытаетесь пересоздать — получите Access Violation. Профессиональный приём: в каждом TearDown (или TearDownOnce) явно обнуляйте ссылки: FreeAndNil(FObject). И никогда не полагайтесь на то, что GC (его нет в классическом Delphi) или автоматическое уничтожение сделают это за вас.

Золотое правило: один Assert на микро-сценарий

Часто вижу в коде: один тест, который проверяет и инициализацию, и расчёт, и результат конвертации. Это интеграционный тест, а не модульный. Специалисты дробят: один метод (или свойство) — один тест. Исключение — проверка той же самой функциональности с разными аргументами (параметризованные тесты). Используйте атрибут [TestCase] (DUnitX) — он создаст отдельный запуск для каждого кейса. Это даст вам точную диагностику: упал не весь набор, а конкретный входной набор.

Скрытая угроза: утечка памяти в тестах

Delphi требует ручного управления памятью. Тест, который создаёт объекты, но не уничтожает их в случае ошибки (например, исключение в середине Arrange), приведёт к утечкам. В большом проекте это убивает производительность. Решение: использовать IInterface с автоматическим подсчётом ссылок или применять паттерн ScopedGuard. Также полезно подключать детектор утечек (ReportMemoryLeaksOnShutdown := True) в тестовом проекте. Если тест зелёный, а в консоли есть сообщение о leak — тест красный, по факту.

Профессиональный приём: проверка граничных условий для типов данных

Специалисты знают: в Delphi Integer (32-битный) и Int64 ведут себя по-разному. Тестировать арифметику только с маленькими числами — ошибка. Обязательно пишите тесты с MinInt, MaxInt, 0, и — для вещественных — с бесконечностью (в Delphi это Infinity для Double). Часто ловушка: StrToFloat('') или StrToFloat(' ') — поведение меняется от версии Delphi. Явное тестирование этого сценария избавит от багов в эксплуатации.

Итог: что на самом деле проверяет профессионал

Эксперт никогда не спрашивает «тесты зелёные?». Он спрашивает: «есть ли тест на Null?», «есть ли тест на пустой интерфейс?», «проверяется ли контракт при исключении?». Качественное модульное тестирование в Delphi — это не про красивый отчёт, а про строгую изоляцию, отсутствие side-эффектов и явные утверждения в каждом сценарии.

Добавлено: 27.04.2026