Рейтинг@Mail.ru

Юнит-тесты Delphi или разработка через тестирование

Автор: Alex. Опубликовано в Программирование . просмотров: 31522

Рейтинг:  5 / 5

Звезда активнаЗвезда активнаЗвезда активнаЗвезда активнаЗвезда активна
 

Разработка через тестирование в Delphi производится с помощью встроенного инструмента DUnit. В статье мы рассмотрим, как создаются тестовые проекты Delphi, как создавать юнит тесты и как тестировать.

Итак, сначала поговорим о том, что такое DUnit. DUnit – это инструмент тестирования с открытыми исходными кодами, основанный на JUnit. Доступен он как для Delphi, так и для C++.

Вообще, в состав Delphi этот инструмент включён начиная с Delphi 2005. Для Delphi и C++ Builder DUnit устанавливается автоматически установщиком RAD Studio. В папке \source\DUnit (внутри папки, куда установлен Delphi) вы можете найти много ресурсов, в том числе исходные файлы, документацию и примеры тестов. Поставляется DUnit под лицензией Mozilla Public License 1.1 (MPL).

В статье я не буду углубляться в теорию, а лишь покажу, как пользоваться инструментом DUnit в Delphi. Будем считать, что читатель знает, что такое разработка через тестирование.

Создание тестового проекта

Тестовый проект содержит один или несколько тестовых случаев, которые представляют из себя обычные .pas файлы и будут доступны в IDE на панели Project Manager. Также RAD Studio предоставляет в ваше распоряжение мастер создания тестового проекта «Test Project Wizard». Рекомендуется создавать два отдельных проекта: один тестируемый, а второй тестирующий. Так вам не придётся в будущем удалять ваши тесты из готового приложения.

Давайте для начала создадим проект, который мы будем тестировать. Допустим, это будет оконное VCL приложение. Выберите пункт меню «File -> New -> VCL Forms Application - Delphi». Созданный проект сохраните.

После создания тестируемого проекта, создадим тестовый проект. Для этого выберите пункт меню «File -> New -> Other...», затем в диалоге «New Items» выберите «Unit Test -> Test Project» и нажмите «OK».

Создание тестового проекта в Delphi

На первом шаге мастера «Test Project Wizard» в поле «Source Project» можно указать тестируемый проект, если их несколько. В полях «Project Name» и «Location» указывается название и расположение тестового проекта. В поле «Personality» выбирается язык программирования (в нашем случае – это Delphi). Все перечисленные поля заполнились автоматически, что нам подходит. Галочку «Add to project group» оставьте, чтобы проект добавился в текущую группу проектов. Нажмите «Next >».

Создание тестового проекта: Шаг 1

На следующем шаге можно выбрать, как будет выполняться тест (поле «Test Runner»): в окне («GUI») или в консоли («Console»). Оставим здесь предложенный по умолчанию вариант – «GUI». В поле «Test Framework» указываются инструменты тестирования. Поменять в этом поле ничего нельзя, т.к. для Delphi и C++ поддерживается только инструмент DUnit. Нажмите «Finish» и вы увидите, что в группе проектов появился новый пустой тестовый проект.

Создание тестового проекта: Шаг 2

Тестовые случаи и тестирование

В типичном тестовом проекте, для каждого тестируемого класса есть тестирующий класс, но это не обязательно. Тестирующий класс также привязан к тестовому случаю. Как правило, тестирующий класс имеет набор из одного или нескольких методов, которые соответствуют одному или нескольким методам тестируемого класса. В один тестовый проект могут быть включены несколько тестовых случаев. Запуск каждого тестового случая и тестового проекта может быть автоматизирован с помощью bat-файлов или скриптов сборки проекта.

В основном рекомендуется создавать тесты в отдельном проекте (отдельно от тестируемого проекта). Так вам не нужно будет удалять тесты из проекта перед финальной сборкой проекта.

RAD Studio предоставляет вам мастер «Test Case Wizard» для помощи в создании тестовых случаев, которые вы сможете настроить на своё усмотрение.

Прежде чем сделать тестовый случай, давайте определимся, что мы будем тестировать. В тестируемом проекте Project1, который мы создали, есть форма. Давайте её и будем тестировать. Допустим, наша форма будет построчно сравнивать два текстовых файла и показывать строки с различиями. Сравнивать будем двумя способами: учитывая регистр и без учёта регистра. Код для теста сделаем элементарный, будем сравнивать строки с одинаковыми индексами.

Итак, на форму ставим два текстовых поля TRichEdit и две кнопки TButton. Затем обрабатываем события от нажатий кнопок и дописываем код следующим образом:

unit Unit1;
 
interface
 
uses
   Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
   System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs,
   Vcl.StdCtrls, Vcl.ComCtrls, System.Generics.Collections;
 
type
   TForm1 = class(TForm)
      RichEdit1: TRichEdit;
      RichEdit2: TRichEdit;
      Button1: TButton;
      Button2: TButton;
      procedure FormCreate(Sender: TObject);
      procedure FormDestroy(Sender: TObject);
      procedure Button1Click(Sender: TObject);
      procedure Button2Click(Sender: TObject);
   private
   public
      //Список индексов разных строк.
      differentStrings: TList<integer>;
      //Процедура сравнения текста.
      procedure Compare(ignoreCase: boolean);
      //Процедура подкрашивания различающихся строк.
      procedure ShowDifferences;
   end;
 
var
   Form1: TForm1;
 
implementation
 
{$R *.dfm}
 
uses Math;
 
procedure TForm1.Button1Click(Sender: TObject);
begin
   //Вызываем процедуру сравнения текста.
   Compare(false);
   //Вызываем процедуру подкрашивания различающихся строк.
   ShowDifferences;
end;
 
procedure TForm1.Button2Click(Sender: TObject);
begin
   //Вызываем процедуру сравнения текста.
   Compare(true);
   //Вызываем процедуру подкрашивания различающихся строк.
   ShowDifferences;
end;
 
procedure TForm1.FormCreate(Sender: TObject);
begin
   differentStrings := TList<integer>.Create;
end;
 
procedure TForm1.FormDestroy(Sender: TObject);
begin
   differentStrings.Free;
end;
 
procedure TForm1.Compare(ignoreCase: boolean);
begin
 
end;
 
procedure TForm1.ShowDifferences;
 
   function GetSelStart(const text: TStrings; const lineIndex: integer): integer;
   var
      i: integer;
   begin
      //Вычисляем индекс первого символа указанной строки текста.
      Result := 0;
      for i := 0 to lineIndex - 1 do
         Result := Result + Length(text[i]) + 1;
   end;
 
   procedure ShowTextLines(const richEdit: TRichEdit);
   var
      index: integer;
   begin
      //Подкрашиваем все строки в чёрный цвет.
      richEdit.SelectAll;
      richEdit.SelAttributes.Color := clBlack;
      //Подкрашиваем разницу.
      for index in differentStrings do
         if index < richEdit.Lines.Count then
         begin
            richEdit.SelStart := GetSelStart(richEdit.Lines, index);
            richEdit.SelLength := Length(richEdit.Lines[index]);
            richEdit.SelAttributes.Color := clRed;
         end;
   end;
 
begin
   //Подкрашиваем разницу в текстовом поле 1.
   ShowTextLines(RichEdit1);
   //Подкрашиваем разницу в текстовом поле 2.
   ShowTextLines(RichEdit2);
 
 
   RichEdit1.PlainText := true;
   ShowMessage(RichEdit1.Lines.Text);
   RichEdit1.PlainText := false;
   RichEdit1.Lines.SaveToFile('c:\temp\tst.txt');
   ShowMessage(RichEdit1.Lines[RichEdit1.Lines.Count - 1]);
end;
 
end.

Как видите, по нажатию на кнопки будут вызываться две процедуры Compare и ShowDifferences. Первая процедура будет сравнивать два текста и сохранять индексы несовпадающих строк в список differentStrings, а вторая процедура будет на основе этого списка подкрашивать несовпадающие строки в красный цвет. В процедуру Compare будет передаваться параметр ignoreCase определяющий способ сравнения строк. Пока реализацию функции Compare делать не будем, а сразу сделаем тест для неё.

Для создания тестов нужно сделать следующие шаги:

        • Откройте юнит, для которого нужно сделать тесты и переключитесь на закладку «Code». Т.е. ваш юнит должен отображаться в редакторе кода.
        • Выберите пункт меню «File -> New -> Other...».
        • В диалоге «New Items» выберите «Unit Tests -> Test Case» и нажмите «OK».

Создание тестового случая в Delphi

        • В открывшемся мастере «Test Case Wizard» на первом шаге нужно указать путь к файлу тестируемого юнита в поле «Source file» (сюда автоматически подставляется путь к файлу для текущего открытого в редакторе юнита) и выбрать, какие классы и методы нам нужно тестировать. В нашем примере будем тестировать только один метод Compare, поэтому поставьте галочку только напротив него. Нажмите «Next >».

Создание тестового случая в Delphi: Выбор тестируемых методов

        • На следующем шаге задаётся тестовый проект в поле «Test Project», имя файла тестового случая в поле «File name», инструменты тестирования в поле «Test Framework» (это поле неактивно, т.к. поддерживается только DUnit) и базовый класс в поле «Base class». Базовый класс TTestCase подходит для большинства случаев, но вы можете использовать и собственный класс. В нашем примере мы не будем здесь ничего менять. Нажмите кнопку «Finish».

Создание тестового случая в Delphi: Шаг 2

После этого вы увидите, что в тестовом проекте появился файл «TestUnit1.pas» и в этот же проект добавлен тестируемый юнит «Unit1.pas».

Тестовый проект в IDE Delphi

А вот что вы увидите в файле TestUnit1.pas:

unit TestUnit1;
{
 
   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, System.SysUtils, Vcl.Graphics, System.Generics.Collections,
   Vcl.StdCtrls, Winapi.Windows, System.Variants, System.Classes, Vcl.ComCtrls,
   Vcl.Dialogs, Vcl.Controls, Vcl.Forms, Winapi.Messages, Unit1;
 
type
   // Test methods for class TForm1
 
   TestTForm1 = class(TTestCase)
   strict private
      FForm1: TForm1;
   public
      procedure SetUp; override;
      procedure TearDown; override;
   published
      procedure TestCompare;
   end;
 
implementation
 
procedure TestTForm1.SetUp;
begin
   FForm1 := TForm1.Create;
end;
 
procedure TestTForm1.TearDown;
begin
   FForm1.Free;
   FForm1 := nil;
end;
 
procedure TestTForm1.TestCompare;
var
   ignoreCase: Boolean;
begin
   // TODO: Setup method call parameters
   FForm1.Compare(ignoreCase);
   // TODO: Validate method results
end;
 
initialization
   // Register any test cases with the test runner
   RegisterTest(TestTForm1.Suite);
end.

Как видите, здесь сделана заготовка для тестирующего класса TestTForm1, унаследованного от класса TTestCase. В секции initialization происходит регистрация этого класса.

В методе SetUp происходит подготовка к тестированию. Здесь нужно создать тестируемый класс и подготовить всё что нужно для тестов, например, подключиться к удалённому серверу или базе данных и т.п. Кстати здесь код сгенерирован с ошибкой: конструктор формы должен принимать входной параметр. Исправим строку кода с созданием формы следующим образом:

FForm1 := TForm1.Create(nil);

Метод TearDown вызывается по окончании тестирования и здесь нужно освободить все ресурсы и удалить все созданные объекты. Здесь сгенерированный код нас устраивает.

Метод TestCompare создан как раз для тестирования нашего метода Compare. Как видите, здесь вызывается наш метод Compare, но нет никаких проверок. Давайте добавим здесь в текстовые поля одинаковый текст, вызовем метод Compare и сделаем проверку результата.

procedure TestTForm1.TestCompare;
var
   ignoreCase: Boolean;
begin
   ignoreCase := true;
   //Добавляем разный текст в текстовые поля.
   FForm1.RichEdit1.Text := 'text1';
   FForm1.RichEdit2.Text := 'text2';
   //Вызываем функцию сравнения.
   FForm1.Compare(ignoreCase);
   //Проверяем результат.
   CheckEquals(1, FForm1.differentStrings.Count,
      'Сравнение не работает. Разница в текстах не определена!');
end;

После этого запустите тестовый проект Project1Tests.exe. Для этого сделайте его активным (дважды щёлкните по проекту в окошке «Project Manager») и затем запустите его выполнение (для этого выберите пункт меню «Run -> Run» или нажмите клавишу F9). После запуска перед вами появится окошко, показанное на картинке ниже.

Окно DUnit для тестирования

Чтобы запустить все тесты выберите пункт меню «Actions -> Run». Если у вас много тестов, а вам нужно выполнить только часть из них, вы можете проставить галочки рядом с нужными тестами и затем вызвать пункт меню «Actions -> Run selected test». После выполнения тестов вы увидите нашу ошибку. Это нормально, ведь пока наша функция Compare не написана и она ничего не сравнивает. Если вы щёлкните на ошибку, то в нижнем поле увидите подробности.

Проведение тестов с помощью DUnit

Теперь давайте исправлять ошибки, чтобы тестирование прошло успешно. Допишите функцию сравнения следующим образом:

procedure TForm1.Compare(ignoreCase: boolean);
var
   i: integer;
begin
   //Очищаем список индексов разных строк.
   differentStrings.Clear;
   //Ищем разные строки.
   for i := 0 to Min(RichEdit1.Lines.Count, RichEdit2.Lines.Count) - 1 do
      if ignoreCase then
      begin
         //Сравнение строк с игнорированием регистра.
         if CompareText(RichEdit1.Lines[i], RichEdit2.Lines[i], TLocaleOptions.loUserLocale) <> 0 then
            differentStrings.Add(i);
      end
      else
      begin
         //Сравнение строк с учётом регистра.
         if CompareStr(RichEdit1.Lines[i], RichEdit2.Lines[i], TLocaleOptions.loUserLocale) <> 0 then
            differentStrings.Add(i);
      end;
   //Если в каком-то списке больше строк, чем в другом,
   //то такие строки тоже считаем разными.
   for i := Min(RichEdit1.Lines.Count, RichEdit2.Lines.Count)
         to Max(RichEdit1.Lines.Count, RichEdit2.Lines.Count) - 1 do
      differentStrings.Add(i);
end;

В методе тестирования добавим тесты для разных вариантов сравнения:

procedure TestTForm1.TestCompare;
var
   ignoreCase: Boolean;
begin
   ignoreCase := true;
   //Добавляем разный текст в текстовые поля.
   FForm1.RichEdit1.Text := 'text1';
   FForm1.RichEdit2.Text := 'text2';
   //Сравниваем тексты с одинаковым количеством разных строк.
   FForm1.Compare(ignoreCase);
   //Проверяем результат.
   CheckEquals(1, FForm1.differentStrings.Count,
      'Сравнение не работает. Разница в текстах не определена!');
 
   //Добавляем в первое текстовое поле ещё одну строку.
   FForm1.RichEdit1.Lines.Add('text3');
   //Сравниваем тексты с разным количеством разных строк.
   FForm1.Compare(ignoreCase);
   //Проверяем результат.
   CheckEquals(2, FForm1.differentStrings.Count,
      'Разница в текстах определена неправильно!');
 
   //Добавляем тексты с разным регистром.
   FForm1.RichEdit1.Lines.CommaText := 'Text1,text2';
   FForm1.RichEdit2.Lines.CommaText := 'text1,text2';
   //Сравниваем тексты с игнорированием регистра.
   FForm1.Compare(ignoreCase);
   //Проверяем количество различающися строк.
   CheckEquals(0, FForm1.differentStrings.Count,
      'Тексты одинаковые, а определены как разные.');
   //Сравниваем тексты с учётом регистра.
   ignoreCase := false;
   FForm1.Compare(ignoreCase);
   //Проверяем количество различающися строк.
   CheckEquals(1, FForm1.differentStrings.Count,
      'Тексты с разным регистром, а определены как одинаковые.');
   //Проверяем индекс различающихся строк.
   CheckEquals(0, FForm1.differentStrings[0],
      'Индекс различающихся строк определён неправильно.');
 
end;

Теперь протестируем нашу функцию Compare. Как видите, всё работает правильно, ошибок нет.

Тестирование DUnit завершилось успешно

Помимо метода CheckEquals в вашем распоряжении есть и другие методы для проверки результатов. Вот основные методы, которые вам могут понадобиться:

        • Check – проверяет выполнение условия и кидает исключение, если условие не выполняется.
        • CheckEquals – проверяет равенство двух значений и кидает исключение, если значения не равны.
        • CheckNotEquals – проверяет неравенство двух значений и кидает исключение, если значения равны.
        • CheckNotNull – проверяет указатель на «не nil» и кидает исключение, если указатель равен nil.
        • CheckNull – проверяет указатель на nil и кидает исключение, если указатель не равен nil.
        • CheckSame – проверяет равенство указателей на объекты и кидает исключение, если указатели не равны.
        • Fail – просто кидает исключение ETestFailure с нужным вам текстом.

Тестирование приватных методов

Отдельно хотелось бы показать, как тестировать приватные методы. Для вызова приватных методов в Delphi мы воспользуемся RTTI (Run-Time Type Information).

Для начала перенесите объявление переменной differentStrings и методов Compare и ShowDifferences в секцию private и добавьте директиву $RTTI, которая даст доступ к приватным методам (к приватным переменным доступ через RTTI разрешён по умолчанию):

{$RTTI EXPLICIT METHODS([vcPrivate..vcPublished])}
TForm1 = class(TForm)
   RichEdit1: TRichEdit;
   RichEdit2: TRichEdit;
   Button1: TButton;
   Button2: TButton;
   procedure FormCreate(Sender: TObject);
   procedure FormDestroy(Sender: TObject);
   procedure Button1Click(Sender: TObject);
   procedure Button2Click(Sender: TObject);
private
   //Список индексов разных строк.
   differentStrings: TList<integer>;
   //Процедура сравнения текста.
   procedure Compare(ignoreCase: boolean);
   //Процедура подкрашивания различающихся строк.
   procedure ShowDifferences;
public
end;

Код тестирования поменяйте следующим образом, не забыв добавить в секцию uses юнит System.Rtti:

procedure TestTForm1.TestCompare;
var
   ignoreCase: Boolean;
   rttiContext: TRttiContext;
begin
   ignoreCase := true;
   //Добавляем разный текст в текстовые поля.
   FForm1.RichEdit1.Text := 'text1';
   FForm1.RichEdit2.Text := 'text2';
   //Инициализируем RTTI-контекст.
   rttiContext := TRttiContext.Create;
   //Сравниваем тексты с одинаковым количеством разных строк.
   rttiContext.GetType(TForm1).GetMethod('Compare').Invoke(FForm1, [ignoreCase]);
   //Проверяем результат.
   CheckEquals(1, TList<integer>(rttiContext.GetType(TForm1).GetField('differentStrings').GetValue(FForm1).AsObject).Count,
      'Сравнение не работает. Разница в текстах не определена!');
end;

Аналогично можно тестировать приватные свойства. Директива $RTTI будет выглядеть тогда следующим образом:

{$RTTI EXPLICIT METHODS([vcPrivate..vcPublished]) PROPERTIES([vcPrivate..vcPublished])}

Итог

Итак, из статьи вы узнали, как реализована поддержка разработки с помощью тестирования в Delphi, а именно: как создавать тестовый проект, как создавать тесты и проводить тестирование, как тестировать приватные методы.

Tags: DUnit Учебники по программированию Delphi

Комментарии   

GunSmoker
+8 #1 GunSmoker 10.02.2016 22:29
Хорошо бы сначала отделить функционал от UI - в отдельный модуль/функции/класс, а уж потом тестировать только функционал, чтобы модульный тест не превратился в интеграционный.
Цитировать
Zapped
+2 #2 Zapped 17.02.2016 01:59
Цитата:
CheckEquals(FForm1.differentStrings.Count, 1,
'Тексты одинаковые, а определены как разные.');
поменяйте местами аргументы:
первым должен быть expected (ожидаемое значение), вторым - actual ("на самом деле"),
а у Вас наоборот...и тогда в сообщении о провале теста пишется неверное "ожидался 0, а было - 1", хотя должно "ожидалась 1, а было 0"
Цитировать
Alex
0 #3 Alex 17.02.2016 15:45
Цитирую Zapped:
Цитата:
CheckEquals(FForm1.differentStrings.Count, 1,
'Тексты одинаковые, а определены как разные.');

поменяйте местами аргументы:
первым должен быть expected (ожидаемое значение), вторым - actual ("на самом деле"),
а у Вас наоборот...и тогда в сообщении о провале теста пишется неверное "ожидался 0, а было - 1", хотя должно "ожидалась 1, а было 0"
Спасибо за замечание. Исправил.
Цитировать
chet
0 #4 chet 18.05.2017 23:55
делал Unit-тест по примеру на сайте
proghouse.ru/programming/56-dunit
дошел до того, что нужно скомпилировать после чего должно появится окошко DUnit: An Xtreme.. его у меня нету. Просто в низу пишет что успешно и все. Больше никаких признаков. Кто может помочь отпишите.
Цитировать
Alex
0 #5 Alex 19.05.2017 15:34
Цитирую chet:
делал Unit-тест по примеру на сайте
proghouse.ru/programming/56-dunit
дошел до того, что нужно скомпилировать после чего должно появится окошко DUnit: An Xtreme.. его у меня нету.

Вам нужно запустить тестовый проект. Вот цитата из статьи:

Цитата:
После этого запустите тестовый проект Project1Tests.exe. Для этого сделайте его активным (дважды щёлкните по проекту в окошке «Project Manager») и затем запустите его выполнение (для этого выберите пункт меню «Run -> Run» или нажмите клавишу F9). После запуска перед вами появится окошко, показанное на картинке ниже.
Цитировать

Добавить комментарий