© 2002 Войнов Николай
Вы когда-нибудь сталкивались с проблемами при тестировании ваших программ? Если нет – то Вы либо супер-профессионал, либо не писали больших программ, либо не цените Ваше время. Из многочисленных печатных источников по разработке программного обеспечения (ПО), говорится, что фаза тестирования занимает 40% трудозатрат по проекту создания ПО.
Как-то при разработке ПО движения товаров, включающее в себя большое кол-во функций, возникло много проблем: то реакция на событие не работает, то ПО выполняет не то, что от него ожидаешь, … . Всего не упомнишь. ПО было разработано, но затраченные усилия не оправдали себя. Урок состоялся. Поэтому было решено взять небольшой таймаут и подумать о средствах устранения проблем. Самым простым выходом из этой ситуации было включение тестирования в процесс разработки.
Об автоматизированном тестировании программ до этого я знал лишь понаслышке. Буквально следующее: "программа для тестирования InterBase в десятки раз превышает размер самого InterBase".
И был еще "Delphi 6 Campaign CD", приобретенный около полугода назад, на котором присутствовало средство для автоматизированного тестирования – DUnit, библиотека классов, предназначенная для поддержки тестирования программ, принятых в экстремальном программировании и пподдерживающая Delphi4 и выше.
Идея использования DUnit состоит в том, что когда вы пишите или изменяете код, вы сразу пишите и соответствующие этому коду тесты, не откладывая на более позднюю фазу тестирования. При соблюдении этого подхода и пересмотрах через регулярные промежутки времени приложения превращаются в само тестируемые.
DUnit предлагает классы, которые позволяют вам просто организовать и выполнить созданные вами тесты. Предлагаются две опции для запуска ваших тестов:
DUnit была создана на основе библиотеки JUnit, разработанной Кеннетом Беком(Kent Beck) и Эрихом Гамма(Erich Gamma) для языка Java, которая превратилась в мощный инструмент, для программирования на Delphi. Оригинальный порт под Delphi был сделан Juanco Anez и в настоящее время поддерживается DUnit Group на SourceForge.
Кто заинтересовался, может идти сразу на домашнюю страницу DUnit и скачивать библиотеку: DUnit homepage at SourceForge.
Почитал Readme, посмотрел примеры – вдохновляет. Решено было сделать небольшой пилотный проект, уж очень хотелось опробовать автоматизированное тестирование и проверить несколько новых идей в реализации интерфейса.
В основном занимаюсь разработкой ПО, предназначенного для автоматизации складкой деятельности решено было сделать нечто такое, что пригодилось бы мне в повседневной деятельности. Для демонстрации тестирования с библиотекой DUnit была создана программа SalesMgr. Если вы читали Ксавье и Пачеко Delphi Developers Guide и дошли до главы 33 – то однозначно найдете много общего. Есть четыре формы Склад, Клиенты, Новая продажа и архив продаж. Использовались сервер FireBird( +FibPlus). Кроме того, хотелось попробовать контролы из DECOSP Lib.
Программа выполняет следующие действия

и упрощенно имеет следующую структуру

Плотно используются свои и чужие шаблоны форм, поэтому у Вас могут возникнуть вопрос зачем все это. Немного тяжеловесная программа получилась, но уж очень хочется извлечь пользу от этой статьи для повседневной работы.
Посмотрим, чем нам может помочь DUnit. Все мы тестировать не будем, ограничимся основными операциями.

При создании подобных программ, я всегда делаю шаблон проекта, в который входит модуль данных, занимающийся подключением к БД и хранением глобальных переменных. Повторное использование? Тогда можно попробовать повторно использовать и тесты.
Итак, займемся последовательным тестированием модулей:
Чем нам может помочь DUnit? В каталоге \Contrib\XPGen был обнаружен кодогенератор! Всего за пару минут можно сделать шаблон теста для модуля. Простая, но полезная утилитка, экономит массу времени.
Надеюсь, вы уже посмотрели Readme к DUnit, поэтому не будем отвлекаться на основных моментах.
Итак, запускаем XPGen и подаем ему на вход dmFib.pas(TFibDataModule), немного правим и получаем код для тестирования функций dmFib:
unit Test_dmFib;
interface
uses
TestFramework, SysUtils, Forms, dmFib;
type
CRACK_TFibDataModule = class(TFibDataModule);
Check_TFibDataModule = class(TTestCase)
private
DM: TFibDataModule;
public
//это для иннициализации тестов
procedure setUp; override;
procedure tearDown; override;
published
//это непосредственно тесты
procedure VerifyLogin;
procedure VerifyLogout;
end;
function Suite : ITestSuite;
implementation
function Suite : ITestSuite;
begin
result := TTestSuite.Create('dmFib Tests');
// result.addTest(Check_TFibDataModule); почему-то не работает, хотя сгенерировано автомат.
end;
procedure Check_TFibDataModule.setUp;
begin
DM := TFibDataModule.Create(Application);
end;
procedure Check_TFibDataModule.tearDown;
begin
DM.Free;
end;
procedure Check_TFibDataModule.VerifyLogin;
begin
DM.Login;
Check(DM.dbFib.Connected = True, 'Login failure!');
end;
procedure Check_TFibDataModule.VerifyLogout;
begin
DM.Login; DM.Logout;
Check(not DM.dbFib.Connected, 'Logout failure!');
end;
initialization
TestFramework.RegisterTest('dmFib', Check_TFibDataModule.Suite);
end.
Код проекта:
program AutoTestPrj;
uses
Forms,
TestFrameWork,
GUITestRunner,
Test_dmFib in 'Test_dmFib.pas',
dmFib in '..\CommonFiles\dmFib.pas' {FibDataModule: TDataModule},
common in '..\commonfiles\common.pas',
Test_dmSales in 'Test_dmSales.pas';
{$R *.res}
begin
Application.Initialize;
GUITestRunner.RunRegisteredTests;
end.
Запускаем и видим:

Нажимаем на всем знакомую зеленую треугольную кнопочку и получаем первый автоматизированный тест! Думаю, проблем пока не возникло? У меня это получилось после часа изучения демок к библиотеке (у меня был готовый шаблон проекта, поэтому получилось так быстро).
А для тестирования функций TsalesDataModule придется немного повозиться. Это связано с инициализацией входных данных.
unit Test_dmSales;
interface
uses
TestFramework,
SysUtils,
Forms,
dmSales;
type
CRACK_TSalesDataModule = class(TSalesDataModule);
Check_TSalesDataModule = class(TTestCase)
public
procedure setUp; override;
procedure tearDown; override;
published
procedure VerifyNewSale;
end;
function Suite : ITestSuite;
implementation
uses dmAttr, dmFib;
function Suite : ITestSuite;
begin
result := TTestSuite.Create('dmSales Tests');
// result.addTest(testSuiteOf(Check_TSalesDataModule));
end;
procedure Check_TSalesDataModule.setUp;
var ctg: Integer; meas: string;
begin
FibDataModule := TFibDataModule.Create(Application);
AttrDataModule := TAttrDataModule.Create(Application);
SalesDataModule := TSalesDataModule.Create(Application);
FibDataModule.Login;
{ TODO 1 -cDUnit_TEST : добавить тестовые ед.изм. и категории }
AttrDataModule.OpenAttrs;
if AttrDataModule.dtSection.RecordCount = 0 then
AttrDataModule.__InsCatg('Test_Catg');
if AttrDataModule.dtMeasure.RecordCount = 0 then
AttrDataModule.__InsMeas('TESTMEA');
ctg := AttrDataModule.GetCatgId;
meas:= AttrDataModule.GetMeasure;
{ TODO 1 -cDUnit_TEST : добавить тестовое наличие товара на складе }
SalesDataModule.OpenPart;
SalesDataModule.__InsPart(1,ctg,'Part Position 1',50,5,6,meas);
SalesDataModule.__InsPart(1,ctg,'Part Position 2',50,5,6,meas);
SalesDataModule.__InsPart(1,ctg,'Part Position 3',50,5,6,meas);
SalesDataModule.__InsPart(1,ctg,'Part Position 4',50,5,6,meas);
SalesDataModule.__InsPart(1,ctg,'Part Position 5',50,5,6,meas);
{ TODO 1 -cDUnit_TEST : добавить тестовых покупателей }
meas := DateToStr(Now);
SalesDataModule.OpenCust;
SalesDataModule.__InsCust(1,'Customer 1 '+meas,0,'Test Customer 1');
SalesDataModule.__InsCust(1,'Customer 2 '+meas,0,'Test Customer 2');
FibDataModule.CommitRetainingAll;
end;
procedure Check_TSalesDataModule.tearDown;
begin
FibDataModule.Logout;
SalesDataModule.Free;
AttrDataModule.Free;
FibDataModule.Free;
end;
////////////////////////////////////////////////////////////////////////////////
procedure Check_TSalesDataModule.VerifyNewSale;
begin
with SalesDataModule do begin
NewSale; //добавляем по единице каждого товара
Check(dtPrice.RecordCount <> 0, 'нечего продавать');
if dtPrice.RecordCount = 0 then Exit;
dtPrice.First;
while not dtPrice.Eof do begin
dtPrice.Edit;
dtPrice.FBN('QTY').AsInteger := 1;
dtPrice.Next;
end;
SaveNewSale(Now, DateToStr(Now) + ' Test_dmSales');
CloseNewSale;
//ищем нашу новую продажу
OpenSales; dtSales.Last;
Check(dtSales.FBN('DESCR').AsString = DateToStr(Now) + ' Test_dmSales','NewSale failure!');
end;
end;
initialization
TestFramework.RegisterTest('dmSales', Check_TSalesDataModule.Suite);
end.
И, наконец, протестируем интерфейс! Для этого пришлось покопаться в UnitTestGUITesting.pas, так как в примерах этого не было. Все как обычно – запускаем XPGen, даем ему на вход fmNewSale.pas, подправляем немного в соответствии нашими целями.
unit Test_fmNewSale;
interface
uses
TestFramework,
GUITesting,
GUITestRunner,
SysUtils,
Graphics,
Windows,
Classes,
Forms,
fmNewSale;
type
CRACK_TfrmNewSale = class(TfrmNewSale);
Check_TfrmNewSale = class(TGUITestCase)
private
NewSaleFrm: TfrmNewSale;
public
procedure setUp; override;
procedure tearDown; override;
published
procedure VerifyEnabledOp;
procedure VerifysbAcceptClick;
procedure VerifysbCancelClick;
end;
function Suite : ITestSuite;
implementation
uses common, dmAttr, dmFib, dmSales, fmFilter, FrActionFrm;
function Suite : ITestSuite;
begin
result := TTestSuite.Create('fmNewSale Tests');
end;
procedure Check_TfrmNewSale.setUp;
begin
inherited;
FibDataModule := TFibDataModule.Create(nil);
AttrDataModule := TAttrDataModule.Create(nil);
SalesDataModule := TSalesDataModule.Create(nil);
FibDataModule.Login;
SalesDataModule.OpenCust;
NewSaleFrm := TfrmNewSale.Create(nil);
GUI := NewSaleFrm;
end;
procedure Check_TfrmNewSale.tearDown;
begin
GUI := nil;
NewSaleFrm.Free;
inherited;
end;
procedure Check_TfrmNewSale.VerifyEnabledOp;
begin
Show;
Check(NewSaleFrm.sbAccept.Enabled = False, 'можно произвести пустую операцию ?');
Check(NewSaleFrm.sbCancel.Enabled = False, 'можно отменить пустую операцию ?');
SalesDataModule.dtPrice.Edit;
SalesDataModule.dtPrice.FBN('QTY').AsInteger := 0;
SalesDataModule.dtPrice.Post;
Check(NewSaleFrm.sbAccept.Enabled = False, 'можно произвести пустую операцию ?');
Check(NewSaleFrm.sbCancel.Enabled = False, 'можно отменить пустую операцию ?');
end;
procedure Check_TfrmNewSale.VerifysbAcceptClick;
begin
Show;
Check(NewSaleFrm.AddingOp = False, 'пустая операция ?');
SalesDataModule.dtPrice.Edit;
SalesDataModule.dtPrice.FBN('QTY').AsInteger :=1;
SalesDataModule.dtPrice.Post;
Check(NewSaleFrm.AddingOp, 'непустая операция ?');
NewSaleFrm.sbAccept.Click;
Check(NewSaleFrm.AddingOp = False, 'пустая операция ?');
end;
procedure Check_TfrmNewSale.VerifysbCancelClick;
begin
Show;
Check(NewSaleFrm.AddingOp = False, 'пустая операция ?');
SalesDataModule.dtPrice.Edit;
SalesDataModule.dtPrice.FBN('QTY').AsInteger :=1;
SalesDataModule.dtPrice.Post;
Check(NewSaleFrm.AddingOp, 'непустая операция ?');
NewSaleFrm.sbCancel.Click;
Check(NewSaleFrm.AddingOp = False, 'пустая операция ?');
end;
initialization
TestFramework.RegisterTest('NewSale', Check_TfrmNewSale.Suite);
end.
Пришлось немного переделать свою форму-шаблон, так и не смог справиться со стандартными диалогами, которые создаются методом MessageDlg.
Окончательно проект принял такой вид:
program AutoTestPrj;
uses
Forms,
TestFrameWork,
GUITestRunner,
Test_dmFib in 'Test_dmFib.pas',
dmFib in '..\CommonFiles\dmFib.pas' {FibDataModule: TDataModule},
common in '..\commonfiles\common.pas',
dmSales in '..\SalesMgr\dmSales.pas' {SalesDataModule: TDataModule},
dmAttr in '..\commonfiles\dmAttr.pas' {AttrDataModule: TDataModule},
fmFilter in '..\commonfiles\fmFilter.pas' {frmInvFilter},
FrActionFrm in '..\commonfiles\FrActionFrm.pas' {FrActionForm},
fmNewSale in '..\SalesMgr\fmNewSale.pas' {frmNewSale},
OpTemplateFrm in '..\commonfiles\OpTemplateFrm.pas' {OpTemplateForm},
ChildFrm in '..\commonfiles\ChildFrm.pas' {ChildForm},
ItemsFm in '..\commonfiles\ItemsFm.pas' {ItemsFrame: TFrame},
Test_dmSales in 'Test_dmSales.pas',
Test_fmNewSale in 'Test_fmNewSale.pas';
{$R *.res}
begin
Application.Initialize;
GUITestRunner.RunRegisteredTests;
end.

Запускайте тест, смотрите. Если, кто продвинется до тестирования диалогов (клацания кнопок на диалогах) – поделитесь информацией.
Результаты пилотного проекта меня вдохновили. Неплохо за три дня? С удовольствием приму все Ваши замечания и тем более помощь в освоении DUnit.
Не судите строго за SalesMgr, нет времени его доабатывать, Все комментарии присылайте на электронную почту.
Желающие могут скачать:
Copyright© 2002 Войнов Николай Специально для Delphi Plus