集成测试工具-dUnit
很好的dUnit介绍的文章,格式化一下转过来
集成测试工具-dUnit 作者 陈省(hubdog)
创建测试案例
unit Utility; interface function Factorial(N:Word):Integer; implementation function Factorial(N:Word):Integer; begin if N=0 then Result:=1 else Result:=N*Factorial(N-1); end; end.
你的任务是测试这个函数是否真的能够给出阶乘运算结果。首先新建一个测试工程,取名为ProjectTests,
然后将默认创建的窗体从项目中删除,添加DUnit的测试框架代码,代码示意如下:
program ProjectTests; uses TestFramework, Forms, GUITestRunner; {$R *.RES} begin Application.Initialize; GUITestRunner.RunRegisteredTests; end.
然后是创建要测试的案例,每个测试案例都要定义为TTestCase的子类,TTestCase类定义在TestFramework.pas
单元中,现在新建一个单元,名为TestUnit,然后在Uses部分添加TestFramework,定义一个TTestCase的子类
TFirstTestCase类,同时我们还要添加测试方法TestFatorial(注意:通常定义测试方法的前缀为Test),代码
示意如下:
unit TestUnit; interface uses TestFrameWork, Utility; type TFirstTestCase = class(TTestCase) private protected published procedure TestFactorial; end; implementation { TFirstTestCase } procedure TFirstTestCase.TestFactorial; begin Assert(Factorial(3)=6, '阶乘运算错误!'); end; initialization TestFramework.RegisterTest('TestUnit Suite',TFirstTestCase.Suite); end.
注意,每增加一个TestCase,我们都要将其注册到测试框架中,这需要在initialization部分增加测试案例注册代码:
TestFramework.RegisterTest('TestUnit Suite', TFirstTestCase.Suite);
另外,可以注意到测试方法TestFactorial必须是Published存取级别的方法,同时该方法必须没有任何的参数定义,
这是由于Delphi的RTTI(运行时类型信息)机制的限制。TestFactorial方法使用Assert断言来检查阶乘函数的结果是否正确。
下面我们就可以运行一下这个测试程序了,运行后会显示一个测试框架的GUI界面,如下图所示:
可以看到最上边的面板按层次关系列出了当前的测试案例,首先是TestSuite,然后是TestCase,最下面是测试方法。
每个节点前都有一个CheckBox,我们可以通过在测试案例前打勾来确定是否测试案例的范围。
点击界面上的 按钮来运行测试,运行后的结果如下:
我们会发现,每个节点前有一个带颜色的方块,当没有运行时,所有节点的方块都为灰色,表示没有被测试。运行测试后,
如果该测试通过的话,方块变为绿色表示成功。下面的面板会显示所有的失败和异常,同时每个错误都会有相应的描述信息。
在上下两个面板之间的进度条表示测试的进展和成功的比率。
下面我们增加一个新的测试,故意让阶乘函数计算10000的阶乘,这时函数会因为溢出而抛出异常,来看DUnit如何检查这种
错误:
procedure TFirstTestCase.TestFactorialLimit; begin //Check函数会检查表达式是否为真 Check(Factorial(10000)>1, '阶乘越界'); end;
运行结果示意图如下:
可以看到如果测试失败,方块就变为粉色,这时测试通过率降到了50%。
复合测试
前面的例子相对比较简单一些,下面我们考虑一个更复杂的场景。假设你觉得Delphi虽然提供了很多的字符串处理函数,
但是参数比较多,调用起来不是很方便,所以想创建一个TStringClass类,将常用的字符串函数封装起来。下面就是
TStringClass类的代码示意:
unit StringClass; interface uses Sysutils; type TStringClass=class(TObject) private FStr:string; public constructor Create(AStr:String); //返回字符串的大写形式 function UpperCase:string; //返回字符串的小写形式 function LowerCase:string; //返回前后加引号的字符串形式 function QuotedStr:string; end; implementation { TStringClass } constructor TStringClass.Create(AStr: String); begin FStr:=AStr; end; function TStringClass.LowerCase: string; begin Result:=SysUtils.LowerCase(FStr); end; function TStringClass.QuotedStr: string; begin Result:=SysUtils.QuotedStr(FStr); end; function TStringClass.UpperCase: string; begin Result:=SysUtils.UpperCase(FStr); end; end.
接下来我们写一个TStringTestCase类来进行测试,下面是类的代码:
unit TestStringUnit; interface uses TestFrameWork, StringClass; type TStringTestCase = class(TTestCase) private FStringClass:TStringClass; protected procedure SetUp; override; procedure TearDown; override; published procedure TestUpperCase; procedure TestLowerCase; procedure TestQuotedStr; end; implementation { TStringTestCase } procedure TStringTestCase.SetUp; begin inherited; FStringClass:=TStringClass.Create('Test'); end; procedure TStringTestCase.TearDown; begin inherited; FStringClass.Free; end; procedure TStringTestCase.TestLowerCase; begin Self.CheckEquals('test', FStringClass.LowerCase, 'LowerCase error!'); end; procedure TStringTestCase.TestQuotedStr; begin Self.CheckEquals('''Test''', FStringClass.QuotedStr, 'QuotedStr error!'); end; procedure TStringTestCase.TestUpperCase; begin Self.CheckEquals('TEST', FStringClass.UpperCase, 'UpperCase error'); end; initialization TestFramework.RegisterTest('TestStringUnit Suite', TStringTestCase.Suite); end.
可以看到这个测试类同前面的TFirstTestCase类最大的不同之处在于TStringTestCase重载了TTestCase
基类的两个重要方法SetUp和TearDown,这两个方法类似于Contructor和Destructor,每次执行测试过程
前,SetUp会被调用,在执行完测试过程后,TearDown方法会被调用。我们可以在SetUp过程创建用于测
试的TStringClass类的实例,并在TearDown方法中释放TStringClass的实例,以避免在测试中反复构造
和释放TStringClass类所造成的不必要的系统开销。
另外可以看到在TStringTestCase的测试方法中,我使用了CheckEquals来检查测试是否通过,TTestCase
类本身还提供了很多类似于Check和CheckEquals的检查方法,具体使用可以参考API文档,这里就不赘述了。
重复测试
在实际开发过程中,我们经常会碰到一些资源泄漏的Bug(画刷,字体,句柄和内存没有释放),它们在程序
最开始的几次运行的时候可能不会暴露问题,而是在几十次、上百次运行之后才会把相应的资源消耗光,那
么如何使用DUnit来测试这类的问题呢?
如果是只需要运行5,6次就会暴露的问题,我们可以在运行测试程序时,多点几次运行按钮来重复测试,但是
如果需要几十遍甚至更多遍测试才能发现的问题,显然靠人手去点是不切实际的。DUnit考虑到了这点,因此
在TestExtensions.pas单元中提供了一个特殊的TRepeatedTest类来完成这项工作。下面是一个内存泄漏的例
子,在我的机器上,执行10次以上就会无法通过测试,代码示意如下:
unit TestLeakUnit; interface uses TestFrameWork, TestExtensions, SysUtils; type TLeakTestCase = class(TTestCase) private protected procedure AllocateMem; published procedure TestLeak; end; implementation { TLeakTestCase } procedure TLeakTestCase.AllocateMem; var Handle: Pointer; begin GetMem(Handle, 100000000); end; procedure TLeakTestCase.TestLeak; begin Self.CheckException(AllocateMem, EOutOfMemory, 'Memory leak'); end; initialization TestFramework.RegisterTest('TestLeakUnit Suite', TRepeatedTest.Create(TLeakTestCase.Suite, 100)); end.
从代码可以看到,使用TRepeatedTest类很简单,只需要将需要重复运行的TestCase的Suite传递给
TRepeatedTest类的构造函数,同时指定要重复执行的次数就可以了,这是一个装饰(Decorator)模式的典型应用。
TestFramework.RegisterTest('TestLeakUnit Suite', TRepeatedTest.Create(TLeakTestCase.Suite, 100));
调整层次关系
从前面例子中我们可以看到测试案例的组织是有层次的,但是我们现在生成的默认的TestSuite都是只包含一个
TestCase,那么有时很可能需要将内容相近的测试放在同一个TestSuite下面,TestSuite下面既包含多个TestCase,
也包含子TestSuite,那么如何来做到呢?
下面修改TestUnit单元的Initialization部分的代码,将TStringTestCase注册在TFirstTestCase的TestSuite下面,
同时将TLeakTestCase的TestSuite也注册在TFirstTestCase的TestSuite下面,代码示意如下:
initialization // TestFramework.RegisterTest('TestUnit Suite', // TFirstTestCase.Suite); begin ParentSuite := TTestSuite.Create('Parent Suite'); ChildSuite := TTestSuite.Create(' Child Suite'); ParentSuite.AddTests(TFirstTestCase); ParentSuite.AddTests(TStringTestCase);
ChildSuite.AddTest(TRepeatedTest.Create(TLeakTestCase.Suite, 100)); ParentSuite.AddSuite(ChildSuite); TestFramework.RegisterTest(ParentSuite); end;
另外注意要将其他几个测试单元的Initialization部分代码注释掉,运行后的示意图如下:
控制台模式的测试
现代软件工程方法,包括极限编程,都非常强调每日构建,持续集成,而如果每次构建系统都是由人手工完成的话,
成本比较高,效率比较低,同时非常容易出错,因此人们普遍采用自动批处理、Make文件、或者类似于Java Ant
的构建工具来创建运行版本。而创建一个可靠的程序运行版本的前提条件就是在创建前,程序应该能够通过所有
的测试。
我们前面讲的测试程序都是基于GUI的,需要手工去运行测试用例,人工判断程序是否通过了所有的测试项目,
这显然无法做到完全的自动化。为此DUnit提供了另外一种运行方式,那就是在控制台模式下运行测试用例。
虽然在控制台模式下,测试案例程序不再有美观的界面和灵活的测试范围控制选项,但它可以无需人的参与而
自动运行全部的测试案例,如果有任何一个测试用例在测试过程中出现错误,DUnit将会返回一个非0的错误码
(数值为所有失败的错误数加上发生的异常数),这使得控制台测试程序很适合放在一个make文件或者应用于
自动测试过程中。将控制台的测试程序加入到批处理后,后续的程序可以根据退出码判断前面的测试是否通过,
从而决定是否继续执行后续的版本创建的工作了,这样就可以做到构建过程的完全自动化了。
要想将我们前面写的测试程序改为在控制台下运行是非常简单的,只要在ProjectTests.dpr文件中加入
{$APPTYPE CONSOLE}编译指令告诉Delphi生成控制台程序,同时添加一些判断代码就可以了,修改后的代码示意如下:
{$APPTYPE CONSOLE} program ProjectTests; uses TestFramework {$IFDEF LINUX}, QForms, QGUITestRunner {$ELSE}, Forms, GUITestRunner {$ENDIF}, TextTestRunner, TestUnit in 'TestUnit.pas', StringClass in 'StringClass.pas', TestStringUnit in 'TestStringUnit.pas', TestLeakUnit in 'TestLeakUnit.pas'; {$R *.RES} begin Application.Initialize; if System.IsConsole then TextTestRunner.RunRegisteredTests else GUITestRunner.RunRegisteredTests; end.
在控制台模式下,DUnit会使用TextTestRunner来运行注册的测试,在GUI方式下会使用GUITextRunner来运行测试,
如果我们想要切换回GUI模式,只要将{$APPTYPE CONSOLE}指令注释掉就可以了。在控制台测试程序运行时,DUnit
会在每个测试运行时在控制台上输出一个点,如果在测试中发生了错误,它会输出字母F表示测试失败,输出字母E
表示测试中发生了一个预期外的异常。
另外DUnit的控制台程序也可以在控制台下启动GUI方式的测试,DUnit的Tests目录下的UnitTests.dpr中提供了这样
一个例子,这个程序通过判断控制台程序运行的选项决定运行文本的测试还是GUI的测试,代码示意如下:
begin if FindCmdLineSwitch('text-mode', ['-','/'], true) then TextTestRunner.RunRegisteredTests(rxbHaltOnFailures) else begin Application.Initialize; Application.Title := 'DUnit Tests'; // RunRegisteredTests class methods are recommended TGUITestRunner.RunRegisteredTests; end; end.
对GUI界面控制的测试
前面我们讲到的例子都是对非可视化的类进行测试,而Delphi最重要的功能之一就是能够通过拖放的方式快速生成非常
专业的界面,在国内外Delphi也被广泛的应用于前台界面程序的开发上。因此对于界面的控制操作,如TabOrder的布局、
界面的输入框是否正确地初始化了、界面是否能够响应某些快捷方式等等也就成为我们测试中很重要的一部分内容。
对于GUI的测试,我们要建立的TestCase需要从GUITesting.pas单元中的TGUITestCase基类继承,而不应该是从TTestCase
类继承了。假设现在有这样系统登录对话框,它有两个标签,两个输入框,和两个按钮,见下图示意:
对于登录界面的测试要求是:当显示时,界面会先将焦点定位于用户名前的输入框,同时界面组件的TabOrder
顺序是先从上向下,然后从左到右,也就是说,用户名的TabOrder应该为0,密码输入框的TabOrder为1,确定
按钮的TabOrder为2,取消按钮的TabOrder为3。同时按下快捷键Enter后,将关闭对话框。
根据上面的测试要求实现的TestCase的代码示意如下:
unit TestDialogUnit; interface uses TestFrameWork, TestForm, Forms, Windows, Controls, GUITesting; type TDialogTestCase = class(TGUITestCase) private FDialog:TFormTest; protected procedure SetUp; override; procedure TearDown; override; published procedure TestTabOrder; procedure TestKey; end; implementation { TDialogTestCase } procedure TDialogTestCase.SetUp; begin inherited; FDialog:=TFormTest.Create(nil); GUI:=FDialog; FDialog.Show; end; procedure TDialogTestCase.TearDown; begin GUI:=nil; FDialog.Free; inherited; end; procedure TDialogTestCase.TestKey; begin FDialog.Edit1.SetFocus; Self.EnterKey(VK_RETURN); Check(FDialog.Visible=false, 'Form Closed?') end; procedure TDialogTestCase.TestTabOrder; begin CheckFocused('Edit1'); Tab; CheckFocused('Edit2'); Tab; CheckFocused('Button1'); Tab; CheckFocused('Button2'); end; initialization TestFramework.RegisterTest('TestDialogUnit Suite', TDialogTestCase.Suite); end.
对代码进行一下说明,为了能对登录界面进行测试,我们首先在类的SetUp方法中建立起对话框的实例,并将实例赋值
给类的GUI属性。运行测试时,DUnit会按照类方法定义的顺序先执行TestTabOrder方法来判断TabOrder是否正确设定
了,首先调用基类TGUITestCase的CheckFocused方法检查Edit1是否在界面显示后最先获得了焦点,如果测试通过就
调用基类的Tab方法来执行焦点遍历,来检查TabOrder的顺序是否正确。接下来是执行TestKey方法,这里我先把焦点
定在Edit1上,然后调用基类的EnterKey输入回车键,如果窗体正确关闭的话,最后检查窗体是否可见。
辅助工具
为了简化编写测试案例的工作量,这里我将介绍一下如何使用Paul Spain编写的DUnit plug-in for Delphi向导来
简化工作量。向导可以在随书的光盘上找到,向导的安装程序会自动在Delphi IDE的主菜单上添加DUnit菜单。
DUnit菜单有两个创建测试案例的菜单:
- New Project... 命令将会创建一个新的DUnit的项目的模板代码。
- New TestCase... 命令将会创建一个新的DUnit的测试案例的模板代码。
新建项目
执行New Project...命令后,向导将会显示下面的对话框:
如果当前IDE中打开的项目名为Msg.dpr,向导会建议测试项目名称为Msg+Tests.dpr,同时我们可以修改Project Path
中建议的项目保存路径为我们指定的任意路径。设定好选项后,按下Enter键后,向导将会自动生成代码模版,
代码示意如下:
// Uncomment the following directive to create a console application // or leave commented to create a GUI application... // {$APPTYPE CONSOLE} program MsgTests; uses TestFramework {$IFDEF LINUX}, QForms, QGUITestRunner {$ELSE}, Forms, GUITestRunner {$ENDIF}, TextTestRunner; {$R *.RES} begin Application.Initialize; {$IFDEF LINUX} QGUITestRunner.RunRegisteredTests; {$ELSE} if System.IsConsole then TextTestRunner.RunRegisteredTests else GUITestRunner.RunRegisteredTests; {$ENDIF} end.
光有测试项目还不够,我们还要添加测试案例(Test Case)。选中DUnit | New TestCase...菜单,会显示创建TestCase
的对话框,示意如下:
注意,该向导默认建议的UnitPath是项目的路径再加一个dunit(见图中的Unit Path输入框)。这里我们删除结尾的
”dunit"”字符串,同时禁止Add to TestCase “uses” clause选项。按下Enter键后会生成如下的Test Case模版代码:
unit MsgTestsTests; interface uses TestFrameWork; type TMsgTestsTests = class(TTestCase) private protected // procedure SetUp; override; // procedure TearDown; override; published // Add test methods here... end; implementation initialization TestFramework.RegisterTest('MsgTestsTests Suite', TMsgTestsTests.Suite); end.
注意在生成代码前,会显示下面的对话框(这是这个向导的一个bug),我们直接选择Cancel就可以了。
除了DUnit Plugin之外,DUnit本身提供了一个XPGen的工具,XPGen通过使用递归下降的代码解析器分析Delphi单元文件
中的类定义,自动生成测试用例骨架代码。
XP的使用非常简单,先用Delphi打开.."Contrib"XPGen目录下的XPGen.dpr,编译运行后,将显示下面界面:
执行菜单命令File | Open,打开前面我们写的StringClass.pas单元,XPGen会自动生成下面的代码:
unit Test_StringClass; interface uses TestFramework, SysUtils, StringClass; type CRACK_TStringClass = class(TStringClass); Check_TStringClass = class(TTestCase) public procedure setUp; override; procedure tearDown; override; published procedure VerifyUpperCase; procedure VerifyLowerCase; procedure VerifyQuotedStr; end; function Suite: ITestSuite; implementation function Suite: ITestSuite; begin result := TTestSuite.Create('StringClass Tests'); result.addTest(testSuiteOf(Check_TStringClass)); end; procedure Check_TStringClass.setUp; begin end; procedure Check_TStringClass.tearDown; begin end; procedure Check_TStringClass.VerifyUpperCase; begin fail('Test Not Implemented Yet'); end; procedure Check_TStringClass.VerifyLowerCase; begin fail('Test Not Implemented Yet'); end; procedure Check_TStringClass.VerifyQuotedStr; begin fail('Test Not Implemented Yet'); end; end.
之后我们就可以将生成的代码复制到Delphi中,然后将其保存起来。如果有很多要测试的类的话,XPGen无疑将极大的
减轻我们的工作量。
总结
在本文中我们探讨了如何使用DUnit来实现持续测试,持续集成。DUnit虽然不一定是保证软件质量的万灵丹,但是它确
实是一个很好的工具。相信经过大量实践,我们一定能够从中受益的
浙公网安备 33010602011771号