极限编程的集成测试工具-Dunit 作者 陈省(hubdog)
简介 极限编程(Xtreme Program)的测试理念是,在开发程序的之前或者同时就要编写出相应的测试程序,而不是象通常那样等到测试阶段再来编写测试程序。 当程序完成时,它的测试程序也应该已经完成。从软件工程的普遍规律来看,每修正一个bug的同时会以20%的几率引入新的bug。而XP的测试方式是, 每当修改Bug或者重构代码后都要重复测试所有的测试案例,以免在修改代码时引入新的Bug,只有经过测试的重构才是有效的重构。而且,所有测试案 例的测试应该是全自动的,可以无需操作人员的介入,不论案例是否通过都将自动完成全部的测试。(注意:即使我们不使用XP的开发方式,对系统能够进行集成测试仍然是一个很好的做法。)
XP的测试框架中最重要的组成部分是TestCase(测试案例),TestResult(测试结果)和TestSuite(测试组)。其中TestSuite可以嵌套包含其它的 TestSuite和TestCase,构成一个简单的Composite模式。也就是说XP的测试是结构化的,是分层次的,类似于一颗树,在测试过程中我们可以选择测 试其中某些部分。基于上述的理念,极限编程创始人之一Kent Beck编写了JUnit测试框架用于Java的测试。下面我们将要介绍的DUnit则是一个基于极 限编程(Xtreme Program)理论的Delphi和Kylix集成测试框架。DUnit是一个开放源码的项目,它的官方网址位于http://sourceforge.net/projects/dunit 文档目录结构 下面是DUnit开发包的目录结构:
环境配置 要想使用DUnit,必须将DUnit的路径添加到Delphi的单元搜索路径中。在Delphi IDE中点击菜单Tools | Environment Options, 然后在弹出的对话框中选中Library页面,添加路径到Library Path中。见下图示意:
创建测试案例 现在假设你是一个测试人员,你的研发队伍中有人写了一个计算阶乘的函数,函数采用递归实现,代码如下: 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, program ProjectTests;
uses TestFramework, Forms, GUITestRunner;
{$R *.RES}
begin Application.Initialize; GUITestRunner.RunRegisteredTests; end.
然后是创建要测试的案例,每个测试案例都要定义为TTestCase的子类,TTestCase类定义在TestFramework.pas 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存取级别的方法,同时该方法必须没有任何的参数定义, 下面我们就可以运行一下这个测试程序了,运行后会显示一个测试框架的GUI界面,如下图所示:
可以看到最上边的面板按层次关系列出了当前的测试案例,首先是TestSuite,然后是TestCase,最下面是测试方法。 点击界面上的 按钮来运行测试,运行后的结果如下: 我们会发现,每个节点前有一个带颜色的方块,当没有运行时,所有节点的方块都为灰色,表示没有被测试。运行测试后,
下面我们增加一个新的测试,故意让阶乘函数计算10000的阶乘,这时函数会因为溢出而抛出异常,来看DUnit如何检查这种 procedure TFirstTestCase.TestFactorialLimit; begin //Check函数会检查表达式是否为真 Check(Factorial(10000)>1, '阶乘越界'); end;
运行结果示意图如下: 可以看到如果测试失败,方块就变为粉色,这时测试通过率降到了50%。
复合测试
前面的例子相对比较简单一些,下面我们考虑一个更复杂的场景。假设你觉得Delphi虽然提供了很多的字符串处理函数, 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 另外可以看到在TStringTestCase的测试方法中,我使用了CheckEquals来检查测试是否通过,TTestCase
重复测试
在实际开发过程中,我们经常会碰到一些资源泄漏的Bug(画刷,字体,句柄和内存没有释放),它们在程序
如果是只需要运行5,6次就会暴露的问题,我们可以在运行测试程序时,多点几次运行按钮来重复测试,但是 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传递给 TestFramework.RegisterTest('TestLeakUnit Suite', TRepeatedTest.Create(TLeakTestCase.Suite, 100));
调整层次关系
从前面例子中我们可以看到测试案例的组织是有层次的,但是我们现在生成的默认的TestSuite都是只包含一个
下面修改TestUnit单元的Initialization部分的代码,将TStringTestCase注册在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部分代码注释掉,运行后的示意图如下:
控制台模式的测试
现代软件工程方法,包括极限编程,都非常强调每日构建,持续集成,而如果每次构建系统都是由人手工完成的话, 我们前面讲的测试程序都是基于GUI的,需要手工去运行测试用例,人工判断程序是否通过了所有的测试项目,
要想将我们前面写的测试程序改为在控制台下运行是非常简单的,只要在ProjectTests.dpr文件中加入 {$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来运行测试,
另外DUnit的控制台程序也可以在控制台下启动GUI方式的测试,DUnit的Tests目录下的UnitTests.dpr中提供了这样
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最重要的功能之一就是能够通过拖放的方式快速生成非常
对于登录界面的测试要求是:当显示时,界面会先将焦点定位于用户名前的输入框,同时界面组件的TabOrder
根据上面的测试要求实现的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方法中建立起对话框的实例,并将实例赋值
辅助工具
为了简化编写测试案例的工作量,这里我将介绍一下如何使用Paul Spain编写的DUnit plug-in for Delphi向导来
新建项目 执行New Project...命令后,向导将会显示下面的对话框: 如果当前IDE中打开的项目名为Msg.dpr,向导会建议测试项目名称为Msg+Tests.dpr,同时我们可以修改Project Path // Uncomment the following directive to create a console application program MsgTests; uses {$R *.RES} begin end. 光有测试项目还不够,我们还要添加测试案例(Test Case)。选中DUnit | New TestCase...菜单,会显示创建TestCase 注意,该向导默认建议的UnitPath是项目的路径再加一个dunit(见图中的Unit Path输入框)。这里我们删除结尾的 unit MsgTestsTests; interface uses implementation initialization 注意在生成代码前,会显示下面的对话框(这是这个向导的一个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虽然不一定是保证软件质量的万灵丹,但是它确 |