"Мой опыт показывает, что создав хорошие тесты, можно значительно увеличить скорость программирования"
(с) Мартин Фаулер.
Delphi 2005 располагает встроенными средствами для организации тестирования работы отдельных модулей программы, основанными на известных open-source проектах DUnit и NUnit (.NET). Среда позволяет создать проект-оболочку для тестов и шаблоны тестирующих модулей. Рассмотрим возможности Delphi 2005 на примере тестирования простого класса, осуществляющего перевод чисел из двоичной формы в символьную по заданному основанию системы счисления и, наоборот, из символьной в двоичную.
Создадим класс, методы которого будут выполнять перевод, а основание системы счисления будет являться свойством класса.
Реализация метода ToString будет содержать ошибки, которые мы будем обнаруживать тестированием. Первая реализация выглядит так:
unit Convertor;
interface
type TNumericConvertor = class private FBase: Integer; public constructor Create (const ABase: Integer); property Base: Integer read FBase; function ToString (const Value: Integer): string; function ToNumber (const Value: string): Integer; end;
implementation
{ TNumericConvertor }
Создадим проект-оболочку для тестов командой File|New|Other выбрав в категории Unit Tests элемент Test Project (см. рис. 1 и 1-1).
Показать скриншоты в отдельном окне: рисунок 1 и рисунок 1-1
После этого группа проектов принимает вид:
Добавим в эту оболочку первый тестирующий модуль командой File|New|Other выбрав в категории Unit Tests элемент Test Case.
Показать скриншоты в отдельном окне:
В результате этих действий в IDE открывается окно с кодом сгенерированного класса для тестирования методов выбранного класса.
unit TestConvertor; { Delphi DUnit Test Case ---------------------- This unit contains a skeleton test case class generated by the Test Case Wizard. Modify the generated code to correctly setup and call the methods from the unit being tested. } interface uses TestFramework, Convertor; type // Test methods for class TNumericConvertor TestTNumericConvertor = class(TTestCase) strict private FNumericConvertor: TNumericConvertor; public procedure SetUp; override; procedure TearDown; override; published procedure TestToString; procedure TestToNumber; end; implementation procedure TestTNumericConvertor.SetUp; begin FNumericConvertor := TNumericConvertor.Create; end; procedure TestTNumericConvertor.TearDown; begin FNumericConvertor.Free; FNumericConvertor := nil; end; procedure TestTNumericConvertor.TestToString; var ReturnValue: string; Value: Integer; begin // TODO: Setup method call parameters ReturnValue := FNumericConvertor.ToString(Value); // TODO: Validate method results end; procedure TestTNumericConvertor.TestToNumber; var ReturnValue: Integer; Value: string; begin // TODO: Setup method call parameters ReturnValue := FNumericConvertor.ToNumber(Value); // TODO: Validate method results end; initialization // Register any test cases with the test runner RegisterTest(TestTNumericConvertor.Suite); end.
В методах тестов заменяем помеченные TODO строки на код, обеспечивающий входные данные для тестируемых методов и сравнивающие результат с ожидаемым.
В методе Setup пишем код для вызова корректного конструктора
procedure TestTNumericConvertor.SetUp; begin FNumericConvertor := TNumericConvertor.Create (10); end;
Метод TestToString принимает вид:
procedure TestTNumericConvertor.TestToString; var ReturnValue: string; Value: Integer; begin Value := 10; ReturnValue := FNumericConvertor.ToString(Value); Assert (ReturnValue = '10', 'Expect ''10'', receive '''+ReturnValue+''''); end;
И последний метод - TestToNumber
procedure TestTNumericConvertor.TestToNumber; var ReturnValue: Integer; Value: string; begin Value := '10'; ReturnValue := FNumericConvertor.ToNumber(Value); Assert (Returnvalue = 10, 'Expect 10, receive '+IntToStr(ReturnValue)); end;
Компилируем и запускаем тестовый проект, его окно выглядит .
После запуска тестов видно, что один из методов исходного класса работает некорректно, так как полученный результат не соответствует ожидаемому (Ожидается '10' получен 'AB')
Показать скриншот в отдельном окнеАнализируя исходный код метода, видно, что при переводе очередного знака числа, условия then и else необходимо поменять местами:
if Rem >= 10 then Result := Result + Char(Rem + Ord('A') - 10) else Result := Result + Char(Rem + Ord('0'));
Перекомпилировав проект после исправления, снова запускаем тесты.
Видно, что ошибка исправлена, но метод все работает не так, как ожидается (Ожидается '10', получено '01').
Показать скриншот в отдельном окнеДальнейший анализ кода метода показывает, что при переводе числа в строку старшие цифры записываются после младших, исправляем эту ошибку, часть кода метода ToString теперь выглядит так:
if Rem >= 10 then Result := Char(Rem + Ord('A') - 10) + Result else Result := Char(Rem + Ord('0')) + Result;
Снова компилируем тестовый проект, после запуска убеждаемся, что исправленный метод теперь работает как ожидалось, при заданных в тесте условиях.
Показать скриншот в отдельном окнеЭто не окончательный показатель гарантии правильной работы методов класса, для полной проверки необходим еще ряд тестов, тем не менее, две ошибки выявлены тестами за короткое время.
Для проверки перевода чисел в другой системе счисления можно создать еще один Test Case, например, тестирующий перевод из двоичного вида в символьный и обратно в двоичной системе счисления.
unit TestConvertor1; { Delphi DUnit Test Case ---------------------- This unit contains a skeleton test case class generated by the Test Case Wizard Modify the generated code to correctly setup and call the methods from the unit being tested. } interface uses TestFramework, Convertor; type // Test methods for class TNumericConvertor TestTNumericConvertor1 = class(TTestCase) strict private FNumericConvertor: TNumericConvertor; public procedure SetUp; override; procedure TearDown; override; published procedure TestToString; procedure TestToNumber; end; implementation uses SysUtils; procedure TestTNumericConvertor1.SetUp; begin FNumericConvertor := TNumericConvertor.Create(2); end; procedure TestTNumericConvertor1.TearDown; begin FNumericConvertor.Free; FNumericConvertor := nil; end; procedure TestTNumericConvertor1.TestToString; var ReturnValue: string; Value: Integer; begin Value := 11; ReturnValue := FNumericConvertor.ToString(Value); Assert(ReturnValue = '1011', 'Expect ''1011'', receive '''+ReturnValue+''''); end; procedure TestTNumericConvertor1.TestToNumber; var ReturnValue: Integer; Value: string; begin Value := '1011'; ReturnValue := FNumericConvertor.ToNumber(Value); Assert(ReturnValue = 11, 'Expect 11, receive '+IntToStr(ReturnValue)); end; initialization // Register any test cases with the test runner RegisterTest(TestTNumericConvertor1.Suite); end.
После компиляции и запуска тестового проекта, видно, что новые тесты добавились к старым, так что любые исправления исходного кода можно протестировать как всеми созданными тестами, так и выбрав конкретные тесты.
Показать скриншот в отдельном окнеВ заключение хочется добавить, что использовать unit-тесты можно было и раньше. Например, полтора года назад, одним из авторов обзора была разработана среда подобного рода для того, чтобы полностью осознать, что же такое unit-тесты. Среда включала в себя главную программу и эксперт для генерации тестирующих модулей. Разумеется, не с таким красивым и удобным интерфейсом, гораздо больше кода приходилось писать вручную, но главный итог такой разработки и использования — осознание необходимости тестирования, в особенности, использования unit-тестов как значительного подспорья для разработки программ.
Спасибо фирме Borland, что такие нужны и удобные средства уже встроены в их новый продукт — Delphi 2005.