一、简介
DUnit是xUnit家族成员之一,源于著名的回归测试框架JUnit,由Juanco A?ez设计成Delphi版本,可以从dunit.sourceforge.net网站免费获得,最新版本9.2.1。获得dunit-9.2.1.zip文件后,解压缩到指定文件夹,我直接放到了本机的F:下。
二、配置类库
开发工具我使用的是Borland的Delphi 2006(正确的叫法应该是Borland Developer Studio 2006,以下简称BDS)。虽然BDS自带了DUnit,且通过“New Items”对话框中“Unit Test”下的各项就可以建立测试工程和测试用例,但这里将直接使用解压后的文件。此外本文将同时介绍如何在C++Builder和Delphi中使用DUnit,其中C++Builder部分源于猛禽的《在BCB中使用DUnit》。
打开BDS后,点击菜单“File”->“New”->“Other”,打开“New Items”对话框:
在该对话框中,选择“C++Builder Projects”或“Delphi Projects”,然后选择其中的“VCL Forms Application”,点击“OK”按钮。项目创建后先关闭Form1,由于此时项目尚未保存,BDS会提示是否保存,选择“No”,不保存,因为这里不需要Form1,需要的只是GUI运行环境。下面点击菜单“File”->“Save All”,在弹出的对话框中设置项目文件名称及保存位置,项目文件名称即项目名称,我的项目文件名称分别为NUnitCB.bdsproj和NUnitOP.bdsproj,并保存在本机的G:/YPJCCK/DUnit/Delphi/DUnitCB和G:/YPJCCK/DUnit/Delphi/DUnitOP文件夹中。
项目创建后,点击菜单“Project”->“Options”,弹出对话框:
在对话框的树型菜单中选择“Paths and Define”,然后在窗口右边选择与“Include search path”对应的“Edit”按钮,打开“Path”对话框:
请使用“…”按钮选择DUnit源码存放路径F:/dunit-9.2.1/src,再使用“Add”按钮将该路径添加到列表中。添加成功后点击“OK”按钮返回之前的窗口,再点击“OK”按钮返回项目。这是在C++Builder中。如果是在Delphi中,点击菜单“Project”->“Options”后弹出对话框如下:
在对话框的树型菜单中选择“Directories/Conditionals”,然后在窗口右边选择与“Search path”对应的“…”按钮,打开“Directories”对话框:
接下来的操作与之前一样。
设置好后,请点击菜单“Project”->“Add to Project”,将F:/dunit-9.2.1/src下的GUITestRunner.pas和TestFramework.pas文件添加到项目中。添加的时候,BDS可能会提示错误,相信我,不要管它。此外,如果是在C++Builder中,添加完成后请按下F9键,使程序运行一遍,以生成GUITestRunner.hpp和TestFramework.hpp文件,这两个文件将在后边用到。
三、编写用于测试的类
用于测试的类很简单,名为TBook,只有id和name两个属性,这两个属性将分别用于两个用例当中。
下面开始编写,请点击菜单“File”->“New”->“Other”,打开“New Items”对话框:
在该对话框中选择“C++Builder Projects” 下的“C++Builder Files”或“Delphi Projects”下的“Delphi Files”,然后选中“Unit”,点击“OK”按钮。此时Unit文件虽然在工程中已生成,但尚未保存在硬盘上,所以请先按下快捷键Ctrl + S,我将文件命名为Book.cpp和Book.pas。
文件创建后,需要修改代码,下边是C++Builder代码:
Book.h文件:
//---------------------------------------------------------------------------
#ifndef BookH
#define BookH
//---------------------------------------------------------------------------
#include <VCL.h>
class TBook
{
private:
AnsiString pid;
AnsiString pname;
AnsiString GetId();
void SetId(AnsiString value);
AnsiString GetName();
void SetName(AnsiString value);
public:
__property AnsiString id = {read = GetId, write = SetId};
__property AnsiString name = {read = GetName, write = SetName};
};
#endif
Book.cpp文件:
//---------------------------------------------------------------------------
#pragma hdrstop
#include "Book.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
AnsiString TBook::GetId()
{
return pid;
}
void TBook::SetId(AnsiString value)
{
pid = value;
}
AnsiString TBook::GetName()
{
return pname;
}
void TBook::SetName(AnsiString value)
{
pname = value;
}
这里可能引起费解的是属性,属性是Borland对C++做的扩展,在C++Builder中定义属性要先为这个属性声明一个作为read访问器的函数和一个作为write访问器的过程,并实现之,然后使用__property关键字来定义属性,并将read访问器和write访问器与这两个函数和过程相关联。注意,这里建议将函数和过程声明在private部分,这样在调用时就会只看到属性,而不会看到这两个函数和过程了。
下边是Delphi代码:
unit Book;
interface
type
TBook = class
private
pid : string;
pname : string;
function GetId():string;
procedure SetId(value: string);
function GetName():string;
procedure SetName(value: string);
public
constructor Create;
property id : string read GetId write SetId;
property name : string read GetName write SetName;
end;
implementation
constructor TBook.Create;
begin
inherited Create;
end;
function TBook.GetId():string;
begin
GetId := pid;
end;
procedure TBook.SetId(value: string);
begin
pid := value;
end;
function TBook.GetName():string;
begin
GetName := pname;
end;
procedure TBook.SetName(value: string);
begin
pname := value;
end;
end.
这里可能引起费解的还是属性,这在前边已经介绍过,Delphi与之类似,只不过关键字是property。另外,别忘了定义一个构造函数,Delphi是不会默认提供的。
至此,用于测试的类编写完成。
四、编写测试用例
这里只用了一个类进行测试,名为TBookTest,该类继承自TTestCase类。TBookTest类包含两个用例,分别对应该类的testId和testName方法,即每个方法实现了一个测试用例。注意,在DUnit中,对于测试方法的名称并没有特殊要求,但要求其访问符必须为__published(C++Builder)和published(Delphi),所有以__published和published关键字修饰的方法都将被视为测试方法。此外,TBookTest还包括SetUp和TearDown这两个方法,前者在每个测试方法开始之前执行,多用来做初始化;后者在每个测试方法完成之后执行,多用来清理资源。下面开始编写TBookTest。
点击菜单“File”->“New”->“Other”,打开“New Items”对话框,在该对话框中选择“C++Builder Projects” 下的“C++Builder Files”或“Delphi Projects”下的“Delphi Files”,然后选中“Unit”,点击“OK”按钮。此时请按下快捷键Ctrl + S,保存文件,我将文件命名为TBookTest.cpp和TBookTest.pas。
下面修改代码,C++Builder代码如下:
BookTest.h文件:
//---------------------------------------------------------------------------
#ifndef BookTestH
#define BookTestH
//---------------------------------------------------------------------------
#include "Book.h"
#include "testframework.hpp"
class TBookTest : TTestCase
{
TBook *book;
protected:
void __fastcall SetUp();
void __fastcall TearDown();
__published:
void __fastcall testId();
void __fastcall testName();
};
#endif
BookTest.cpp文件:
//---------------------------------------------------------------------------
#pragma hdrstop
#include "BookTest.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
void __fastcall TBookTest::SetUp()
{
book = new TBook();
}
void __fastcall TBookTest::TearDown()
{
book = NULL;
}
void __fastcall TBookTest::testId()
{
book->id = "001"; //设置id属性的值为001
//使用Check查看id属性的值是否为001
Check(book->id == "001", "id属性被测试!");
}
void __fastcall TBookTest::testName()
{
book->name = "ASP"; //设置name属性的值为ASP
//使用Check查看name属性的值是否为JSP,这是个必然出现错误的测试
Check(book->name == "JSP", "name属性被测试!");
}
这里SetUp和TearDown方法没什么好说的,就是执行了对book对象的初始化和清理,不过testId和testName需要说明一下。前者是在对book的id属性进行测试,首先赋值为”001”,然后使用Check方法查看id属性与期待值的比较结果,由于我的期待值也是”001”,所以比较结果为true,执行后这个用例是成功的;后者则是对book的name属性进行测试,也是先赋值为”ASP”,然后使用Check方法查看其值与期待值的比较结果,由于我特意将期待值设定为根本不可能的”JSP”,因此比较结果为false,这个用例执行后会出现一个错误。但请注意,由于我是特意要让测试出现错误,所以将期待值设定成了不可能的值,如果你是测试人员,请千万不要这么做,否则如果别的地方导致了错误,很容易给自己造成不必要的麻烦。
与JUnit、NUnit不同,DUnit中没有提供Assert类,相关的方法都被定义到了TTestCase类中,由于测试类都要继承TTestCase,因此在测试类中可以直接调用这些方法。方法有26个:
1.Check()方法,最常用的方法,用来查看表达式是否为true,为true则测试成功,反之失败。
2.CheckTrue()和 CheckFalse()方法,用来查看变量是否为false或true,如果CheckFalse ()查看的变量的值是false则测试成功,如果是true则失败,CheckTrue ()与之相反。
3.CheckEquals()和CheckNotEquals()方法,用来查看两个对象的值是否相等或不等。
4.CheckEqualsBin()、CheckNotEqualsBin()方法,用来比较两个无符号32位整数,并在输出信息时以二进制格式输出数值。
5.CheckEqualsHex()和CheckNotEqualsHex()方法,用来比较两个无符号32位整数,并在输出信息时以十六进制格式输出数值。
6.CheckEqualsMem()和CheckNotEqualsMem()方法,按指定范围Length比较两个指针所指向的内存中所保存的内容是否相同或不同,可以参考Delphi中的CompareMem函数。
7.CheckEqualsString()和CheckNotEqualsString()方法,比较两个字符串是否相同。
8.CheckEqualsWideString()和CheckNotEqualsWideString()方法,比较两个字符串是否相同,该字符串以Unicode格式编码。
9.CheckNull()和CheckNotNull()方法,查看对象是否为空和不为空。
10.CheckSame()方法,用来比较两个对象是否指向同一内存地址。
11.CheckException()方法,用来查看指定测试方法返回的异常与指定的异常类型是否相同。
12.CheckInherits()方法,查看指定的前一个类型是否是后一个类型的基类。例如:
CheckInherits(TObject, TTestCase);
13.CheckIs()方法,查看对象的类型是否兼容于指定类型,例如TObject是VCL中所有类型的基类,因此所有类型的对象都兼容于TObject。
14.CheckMethodIsNotEmpty()方法,查看方法是否非空,即方法中是否有代码。
15.Fail()方法,意为失败,用来抛出错误。我个人认为有两个用途:首先是在测试驱动开发中,由于测试用例都是在被测试的类之前编写,而写成时又不清楚其正确与否,此时就可以使用Fail方法抛出错误进行模拟;其次是抛出意外的错误,比如要测试的内容是从数据库中读取的数据是否正确,而导致错误的原因却是数据库连接失败。
16.FailEquals()、FailNotEquals()和FailNotSame()方法,功能与Fail方法一样,但带格式,很多Check()方法都直接使用了这些方法。
下面是Delphi代码:
unit BookTest;
interface
uses
Book,
TestFramework;
type
TBookTest = class(TTestCase)
book : TBook;
protected
procedure SetUp; override;
procedure TearDown; override;
published
procedure testId;
procedure testName;
end;
implementation
procedure TBookTest.SetUp;
begin
book := TBook.Create();
end;
procedure TBookTest.TearDown;
begin
book.Free;
end;
procedure TBookTest.testId;
begin
book.id := '001'; //设置id属性的值为001
//使用Check查看id属性的值是否为001
Check(book.id = '001', 'id属性被测试!');
end;
procedure TBookTest.testName;
begin
book.name := 'ASP'; //设置name属性的值为ASP
//使用Check查看name属性的值是否为JSP,这是个必然出现错误的测试
Check(book.name = 'JSP', 'name属性被测试!');
end;
end.
至此,测试类创建完成。
五、运行DUnit
要运行DUnit,在程序中写代码就可以达成。请在BookTest.h文件的最后一行代码#endif之前增加代码“_di_ITest __fastcall GetSuite(TMetaClass * aClass);”,然后在BookTest.cpp中追加代码:
//用于替代TTestCase类的Suite方法,因为该方法使用了C++不支持的特性
_di_ITest __fastcall GetSuite(TMetaClass *aClass)
{
_di_ITest Result;
if (!Supports(new TTestSuite(aClass), __uuidof(ITest), &Result))
throw Exception("Interface ITest not supported");
return Result;
}
//模拟Delphi中的initialization
class Initialization
{
public :
Initialization()
{
RegisterTest(GetSuite(__classid(TBookTest)));
}
};
Initialization initialization;
最后修改DUnitCB.cpp文件如下:
//---------------------------------------------------------------------------
#include <vcl.h>
#include "TestFramework.hpp"
#include "GUITestRunner.hpp"
#pragma hdrstop
//---------------------------------------------------------------------------
WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
try
{
Application->Initialize();
//Application->Run();
Guitestrunner::RunRegisteredTests();
}
catch (Exception &exception)
{
Application->ShowException(&exception);
}
catch (...)
{
try
{
throw Exception("");
}
catch (Exception &exception)
{
Application->ShowException(&exception);
}
}
return 0;
}
//---------------------------------------------------------------------------
这是在C++Builder中。如果是在Delphi中,请将BookTest.pas的最后一行代码“end.”修改为:
initialization
RegisterTest(TBookTest.Suite);
end.
然后修改DUnitOP.dpr文件如下:
program DUnitOP;
uses
Forms,
TestFramework in 'F:/dunit-9.2.1/src/TestFramework.pas',
GUITestRunner in 'F:/dunit-9.2.1/src/GUITestRunner.pas',
Book in 'Book.pas',
BookTest in 'BookTest.pas';
{$R *.res}
begin
Application.Initialize;
//Application.Run;
GUITestRunner.RunRegisteredTests;
end.
注意,如果看不到C++Builder中的DUnitCB.cpp或Delphi中的DUnitOP.dpr文件,请点击菜单“Project”->“View Source”。改好后,点击菜单“Run”->“Run”或按F9键运行程序,就可以使用看到DUnit界面了:
此时就可以使用TBookTest类对TBook类进行测试了。点击“Run”按钮,运行结果如下图:
testId前的点是绿色,而testName前的点是红色,且进度条也显示为红条,这表明testName中存在错误。不过这个错误是预计之内的,如果不想看到,可以在BDS中将testName()方法中的”JSP”改成”ASP”,然后重新运行,此时进度条已不是红色,而是绿色了。
六、测试套件
当有多个测试类需要按结构分组进行测试时,可以使用测试套件来完成这项工作。DUnit的测试套件就是返回类型为ITestSuite的函数,如下:
//使用suite的AddTest函数添加测试
function Suite1: ITestSuite;
var
suite: TTestSuite;
begin
suite := TTestSuite.Create('TestUnit Suite');
suite.AddTest(TBookTest.Suite);
Result := suite;
end;
//在创建的同时添加测试
function Suite2: ITestSuite;
begin
Result := TTestSuite.Create('TestUnit Suite', [TBookTest.Suite]);
end;
以上是Delphi代码,C++Builder代码如下:
//使用suite的AddTest函数添加测试
_di_ITest __fastcall Suite1()
{
TTestSuite* suite;
suite = new TTestSuite("TestUnit Suite");
suite->AddTest(GetSuite(__classid(TBookTest)));
return *suite;
}
//在创建的同时添加测试
_di_ITest __fastcall Suite2()
{
_di_ITest ts[] = {GetSuite(__classid(TBookTest))};
return *(new TTestSuite("TestUnit Suite", ts, 1));
}
七、更多运行方法
要运行DUnit测试,还可以更灵活,例如将“RegisterTest(TBookTest.Suite);”这行代码直接写到DUnitOP.dpr文件中、“GUITestRunner.RunRegisteredTests;”之前(此为Delphi代码,C++Builder代码为“RegisterTest(GetSuite(__classid(TTestSF)));”,应写到DUnitCB.cpp文件中、Guitestrunner::RunRegisteredTests();之前),或使用GUITestRunner的RunTest方法。
此外,DUnit也提供有命令行测试环境,要调用该环境则需建立“Console Application”,并引用F:/dunit-9.2.1/src文件夹下的TextTestRunner.pas文件,具体使用方法可以参考GUITestRunner。