Как тестировать Delphi-код с помощью DUnit и DUnitX
Автоматические тесты помогают проверять Delphi-приложение после каждого изменения и быстрее находить ошибки в бизнес-логике. Вместо ручного запуска программы разработчик описывает ожидаемое поведение в виде процедур, которые фреймворк выполняет по команде. Такой подход особенно полезен для расчётов, преобразований данных, работы с датами и правил валидации.
В экосистеме Delphi наиболее известны два инструмента: классический DUnit и более современный DUnitX. Оба предназначены для модульного тестирования, однако отличаются синтаксисом, способом обнаружения тестов и удобством интеграции с современными проектами. Выбор зависит от версии Delphi, требований команды и уже существующей кодовой базы.
Хороший тест проверяет одну понятную закономерность и не зависит от интерфейса, файловой системы или конкретной базы данных. Чем меньше внешних условий требуется для запуска, тем стабильнее тестовый набор. В результате тестирование становится частью разработки, а не отдельной ручной процедурой перед выпуском приложения.
Зачем нужен модульный тест
Модульный тест проверяет небольшой участок кода в изоляции: функцию, метод класса или отдельный сервис. Например, для метода расчёта скидки можно задать сумму заказа, категорию клиента и ожидаемый результат. Если после рефакторинга формула изменится, тест сразу покажет расхождение.
Тесты также фиксируют контракт кода. По ним легко понять, какие входные данные допустимы, что происходит при пустой строке и как обрабатываются исключения. При этом тест не должен повторять реализацию проверяемого метода: важно сравнивать результат с независимым ожидаемым значением.
Для разработчика, который работает сразу с Android, iOS, Windows, macOS и Linux, единые проверки особенно ценны. Общая бизнес-логика может использоваться в разных целях, а тесты помогают убедиться, что платформенные адаптеры не изменили её поведение. Примеры практического подхода к разработке доступны в портфолио разработчика.
Подготовка проекта в Delphi
DUnit поставляется как набор исходных модулей и пакетов, а DUnitX обычно подключается через менеджер зависимостей или исходный код проекта. Перед началом следует определить тип приложения: тестовый проект может быть консольным, графическим или библиотекой, которую запускает внешний тестовый раннер.
Для отдельного проекта удобно создать каталог Tests, добавить туда тестовые модули и настроить пути поиска исходников. Тестируемый код лучше вынести в самостоятельные модули без прямой зависимости от форм. Если класс создаёт TForm, открывает соединение или обращается к глобальному состоянию прямо в конструкторе, его будет сложнее проверять изолированно.
В Delphi проект обычно содержит секцию uses, где подключаются тестовый фреймворк и проверяемый модуль. После сборки IDE может запускать тесты через встроенный эксперт, а консольный раннер подходит для автоматической проверки на сервере. Важно включить отладочную информацию и сохранить понятные имена тестов: они становятся основным источником сведений об ошибке.
Первый тест на DUnit
Классический DUnit строится вокруг класса-наследника TTestCase. Методы, начинающиеся с Test, автоматически воспринимаются как тестовые процедуры. Проверки выполняются с помощью утверждений вроде CheckEquals, CheckTrue и CheckFalse.
uses
TestFramework, MathUtils;
type
TDiscountTests = class(TTestCase)
published
procedure CalculatesDiscountForRegularClient;
end;
procedure TDiscountTests.CalculatesDiscountForRegularClient;
begin
CheckEquals(900, CalculatePrice(1000, 10));
end;
initialization
RegisterTest(TDiscountTests.Suite);
end.
В этом примере тест вызывает функцию и сравнивает фактическую цену с ожидаемой. Если значения различаются, DUnit сообщает имя теста, место сбоя и текст утверждения. Процедура регистрации в секции initialization нужна, чтобы раннер обнаружил набор проверок.
Для исключений используется CheckException. Такой тест должен подтверждать конкретный тип ошибки и, при необходимости, проверять её сообщение. Отдельная проверка на неверные параметры делает контракт функции очевидным и защищает от случайного удаления валидации.
procedure TDiscountTests.RejectsNegativeAmount;
begin
CheckException(
EArgumentOutOfRangeException,
procedure
begin
CalculatePrice(-1, 10);
end
);
end;
Современный синтаксис DUnitX
DUnitX использует атрибуты, поэтому тесты не требуется регистрировать вручную. Класс помечается атрибутом TestFixture, а методы — атрибутом Test. Для проверок применяется объект Assert, у которого есть методы AreEqual, IsTrue, IsFalse, IsNull и другие.
uses
DUnitX.TestFramework, MathUtils;
type
[TestFixture]
TDiscountTests = class
public
[Test]
procedure CalculatesDiscount;
end;
procedure TDiscountTests.CalculatesDiscount;
begin
Assert.AreEqual(900, CalculatePrice(1000, 10));
end;
DUnitX поддерживает дополнительные атрибуты для подготовки данных. Setup выполняется перед каждым тестом, TearDown — после него, а SetupFixture и TearDownFixture работают один раз для всего класса. Это удобно для создания общих объектов, но состояние между тестами нужно очищать, иначе порядок запуска может повлиять на результат.
Для повторяющихся наборов входных значений можно использовать параметризованные тесты или отдельные тестовые данные. Такой вариант полезен для граничных случаев: нулевой суммы, максимальной скидки, пустой строки и значения за допустимым диапазоном. Название теста должно отражать проверяемое правило, а не внутренний вызов метода.
Проверка классов, коллекций и исключений
При тестировании класса сначала проверьте его публичное поведение. Внутренние поля и приватные методы не должны становиться целью теста: если приходится обращаться к ним через специальные хаки, это часто указывает на слишком сильную связанность. Создайте объект, вызовите публичный метод и проверьте наблюдаемый результат.
Коллекции требуют проверки количества элементов, порядка и содержимого. Для строк полезно отдельно тестировать регистр, пробелы и Unicode-символы. Для дат следует явно задавать часовой пояс и не полагаться на текущую дату компьютера, иначе одинаковый тест может вести себя по-разному на разных машинах.
Исключения следует проверять там, где они являются частью контракта. Например, отрицательный идентификатор может приводить к EArgumentException, а отсутствие записи — к специализированному исключению приложения. Не стоит ловить любое Exception: такая проверка пропустит ошибку другого типа и даст ложное ощущение корректности.
Для функций, работающих с базой данных, обычно выделяют слой репозитория и подменяют его тестовой реализацией. Внешние API, файловую систему и сетевые сервисы лучше изолировать интерфейсами. Тогда модульный тест проверяет бизнес-правила, а интеграционные тесты отдельно контролируют реальное подключение.
Управление тестовыми данными и зависимостями
Фикстура должна создавать минимальный набор данных, достаточный для проверки сценария. Если каждый тест зависит от большой общей базы объектов, причина сбоя становится неясной. Локальные переменные и фабричные методы помогают сделать подготовку короткой и читаемой.
Для зависимостей применяются заглушки, стабы и моки. Заглушка возвращает заранее заданный результат, а мок дополнительно проверяет факт вызова, параметры и количество обращений. В Delphi это можно реализовать вручную через интерфейсы или подключить библиотеку для mock-объектов, если проект допускает внешние зависимости.
Надёжный тест не использует случайные значения без фиксированного генератора и не зависит от времени выполнения. Если код обращается к текущей дате, передайте поставщик времени через интерфейс. Аналогично, путь к файлу, конфигурацию и параметры подключения следует задавать через тестовое окружение.
Тесты можно запускать из IDE, командной строки и системы непрерывной интеграции. Консольный режим позволяет завершать сборку с ошибкой при провале проверки, сохранять XML-отчёты и подключать их к серверу сборки. Небольшие демонстрационные проекты и эксперименты с программированием можно найти на площадке для экспериментов.
Работа с ошибками и покрытием
Когда тест завершился неудачно, сначала нужно определить тип сбоя. Ошибка утверждения означает, что фактическое значение отличается от ожидаемого. Исключение внутри теста может указывать на дефект подготовительных данных, неверную конфигурацию или ошибку самого приложения. Понятное имя теста и короткое сообщение существенно ускоряют диагностику.
Покрытие кода показывает, какие строки или ветви были затронуты тестами, но высокий процент сам по себе не гарантирует качества. Можно выполнить каждую строку и при этом не проверить важный результат. Гораздо полезнее охватить обычный сценарий, граничные значения, некорректный ввод и ожидаемые исключения.
DUnit и DUnitX хорошо подходят для разных этапов развития проекта. DUnit проще оставить в старом приложении, где уже есть регистрация наборов и устоявшийся раннер. DUnitX удобнее для новых модулей, параметризованных проверок и кода, который должен выглядеть ближе к современным тестовым практикам.
| Возможность | DUnit | DUnitX |
|---|---|---|
| Обнаружение тестов | Регистрация наборов и методов | Атрибуты TestFixture и Test |
| Основной класс | TTestCase |
Класс с атрибутом тестовой фикстуры |
| Проверки | CheckEquals, CheckTrue |
Assert.AreEqual, Assert.IsTrue |
| Подготовка окружения | Методы жизненного цикла базового класса | Атрибуты Setup и TearDown |
| Совместимость | Удобен для старых проектов | Предпочтителен для новых проектов |
| Запуск в CI | Консольный раннер и отчёты | Консольный раннер и расширенные настройки |
Выбор фреймворка не отменяет главного принципа: тест должен быть быстрым, независимым и понятным. Начните с чистых функций и бизнес-правил, затем добавляйте проверки сервисов, преобразований и обработки ошибок. Такой набор постепенно превращается в защитный слой, который помогает безопасно развивать Delphi-приложение.