Delphi的测试工具DUnit

时间:2021-04-17 11:35:41

DUnit
Delphi
的终极测试工具

by Will Watts
edited by Juanco Añez
Copyright © 1999 Will Watts. All rights reserved.
Later versions are © 2000-2001 The DUnit Group. All rights reserved.
This text may be distributed freely as long as it's reproduced in its entirety.

There's an English version of this document here

翻译:蔡焕麟

内容

采用 DUnit 进行单元测试

档案内容
起步
你的第一个测试项目
SetUp 与 TearDown
测试套件
逐步建立测试套件

其它功能

在主控台模式下执行测试
扩充功能

参考数据

采用 DUnit 进行单元测试 

DUnit 是一个类别框架,目的是要支持 XP 的软件测试方法。它支持 Delphi 4 以后的版本。

其概念为,当你在开发或修改程序代码时,你就要同时开发出相称的测试程序,而不是把它们延后到测试阶段。若能随时更新测试程序并且经常反复地执行它们,你就能够更轻易地产生可靠的程序代码,而且在进行修改与重整(refactorings)时更有把握不会破坏原有的程序代码,于是,应用程序等于有了自我测试的能力。

DUnit 提供了一些类别以便组织与执行这些测试。DUnit 提供两种执行测试的方式:

  • 图形使用者接口的应用程序,让你方便地选择个别的以及一整组的测试。
  • 主控台(console)应用程序。

DUnit 的灵感源自 JUnit 框架,该框架是由 Kent Beck 与 Erich Gamma 为 Java 程序语言所设计的,但是 DUnit 已经逐渐发展成威力更强的 Delphi 专属工具。最早是由 Juanco Añez 设计成 Delphi 的版本,目前则是由 SourceForgeDUnit Group 所维护。

档案内容

随着 DUnit 套件所发布的档案应该存放在一个属于自己的目录下,以便保留完整的目录结构:

 

目录名称

说明

DUnit

 

 

framework

事先编译好的框架模块

 

src

函式库原始码

 

doc

辅助说明档,网页与 MPL 授权许可

 

 

images

网页的图形档案

 

 

API

Time2Help 产生的 API 文件

 

Contrib

其它人贡献的模块

 

 

XPGen

一个可以自动产生测试案例(test cases)的工具

 

tests

给这个框架本身所使用的测试案例

 

bin

事先编译好,可以单独执行的 GUI 测试程序

 

examples

 

 

 

cmdline

示范如何在命令列环境下使用 DUnit

 

 

collection

一个类似 Java 的集合(collections)实作以及它的 DUnit 测试案例

 

 

registration

使用测试案例注册系统(registration system)(译注:示范几种注册测试案例的方法)

 

 

structure

组织测试程序代码的方式

 

 

 

diffunit

把测试案例放在独立的单元里面

 

 

 

sameunit

把测试案例和被测试的程序代码放在同一个单元里面

 

 

registry

一步步教你建立一个存取 Registry 的工具及其测试案例

 

 

embeddable

示范如何将 GUITestRunner 嵌入至其它窗口内

 

 

(...)

 

 

 

TListTest

给 Delphi 的 Classes.TList 对象使用的测试案例

 

目录 src 包含下列档案

 

文件名称

说明

TestFramework.pas

框架本身

TestExtensions.pas

可用来扩充测试案例的 Decorator 类别

GUITesting.pas

用来测试使用者接口(窗口与对话盒)的类别

TextTestRunner.pas

在主控台模式下执行测试的函式

GUITestRunner.pas

此框架的图形化使用者接口

GUITestRunner.dfm

GUITestRunner Form

 

framework 目录中包含以上各单元编译过的版本,以及用来连结 .BPL 的 .DCP 档案(对应的 .BPL 档案存在 bin 目录里)(译注1)。

起步

在开始使用 DUnit 之前,Delphi 的单元搜寻路径里必须包含 DUnit 的原始码或编译后的档案路径。你可以在 Delphi IDE 中点选 Tools | Environment Options | Library,然后把 DUnit 路径加到原有的路径清单里:

Delphi的测试工具DUnit

 

另一种做法,是将 DUnit 路径加到预设的项目选项或者特定的项目选项里,在 IDE 中点选 Project | Options:

Delphi的测试工具DUnit

 

你的第一个测试项目

建立一个新的应用程序,然后关闭 Delphi 为你自动产生的 Unit1.pas 并且不要储存。储存这个新的项目(在你想要测试的应用程序的相同目录下的 'real life' 目录)并且命名为 Project1Test.dpr。

点选 File | New | Unit 以建立一个新的(没有 form 的)单元,由于我们会把测试案例写在这个档案里面,所以储存的时候就取  Project1TestCases 之类的文件名称,接着在 interface 的 uses 子句里加入 TestFramework

宣告一个 TTestCaseFirst 类别,该类别继承自 TTestCase,然后实作一个如下所示的 TestFirst 方法(显然地,这个小范例只是为了让你顺利起步),注意最后的 initialization 区段,TTestCaseFirst 类别就是在这里完成注册的。

unit Project1TestCases;

 

interface

 

uses

  TestFrameWork;

 

type

  TTestCaseFirst = class(TTestCase)

 published

   procedure TestFirst;

 end;

 

implementation

 

procedure TTestCaseFirst.TestFirst;

begin

  Check(1 + 1 = 2, 'Catastrophic arithmetic failure!');

end;

 

initialization

  TestFramework.RegisterTest(TTestCaseFirst.Suite);

end.

测试的结果是置于所呼叫的 Check 方法里面,这里我很无聊地想要确认 1 + 1 是否等于 2。TestFramework.RegisterTest 程序会把传入的测试案例对象注册到此框架的注册系统里。

在执行这个项目以前,点选主选单的 Project | View Source 以开启项目的原始码,把 TestFrameWork 以及 GUITestRunner 加到 uses 子句里,然后移除预设的 Application 程序代码,并以下面的程序代码取代:

program Project1Test;

 

uses

 Forms,

 TestFrameWork,

 GUITestRunner,

 Project1TestCases in 'Project1TestCases.pas';

 

{$R *.RES}

 

begin

 Application.Initialize;

 GUITestRunner.RunRegisteredTests;

end.

现在试着执行程序,如果一切正常,你应该会看到 DUnit 的 GUITestRunner 窗口,里面有一个树状元件显示可用的测试(目前只有 TestFirst),点一下 Run 按钮即可执行测试。画面上的复选框可以让你以阶层的方式选择欲测试的项目,还有额外的按钮以便切换测试项目或整个分支的选取状态。

若要加入更多的测试,只需简单地在 TTestCaseFirst 里加入新的测试方法,TTestCase.Suite 类别方法会透过 RTTI(RunTime Type Information,执行时期型态信息)自动地寻找并且呼叫它们,这些测试方法必须符合两个条件:

  • 测试方法必须是不带参数的程序。
  • 测试方法必须宣告为 published。

注意 DUnit 会为它所找到的每个方法各自建立一个类别的实体(instance),所以测试方法之间不可共享实体的数据。

现在要再加入两个测试方法:TestSecond 与 TestThird,其宣告如下:

TTestCaseFirst = class(TTestCase)

published

 procedure TestFirst;

 procedure TestSecond;

 procedure TestThird;

 end;

 

...

 

procedure TTestCaseFirst.TestSecond;

begin

  Check(1 + 1 = 3, 'Deliberate failure');

end;

procedure TTestCaseFirst.TestThird;

var

  i: Integer;

begin

  i := 0;

  Check(1 div i = i, 'Deliberate exception');

end;

如果你重新执行这个程序,你就会看到 TestSecond 测试失败了(旁边有一个小的紫红色方框),而 TestThird 会丢出一个异常(旁边的方框是红色的),通过测试的方框会是绿色的,而没有执行的测试则是灰色的。失败的测试清单会被列在下方的面板上,当你去点选它们就可以在底部的面板上看到它们的详细资料。

如果你在 IDE 里面执行程序,你会发现每当程序发生错误时就会暂停,当你用 DUnit 进行测试时,这样的行为可能不是你想要的,你可以照下面的步骤将 IDE 的这项功能关掉:点选 Tools | Debugger Options,然后把 Language Exceptions 页夹的 Stop on Delphi Exceptions 项目取消。

Setup 与 TearDown

我们通常会在执行一组测试之前进行一般的准备工作,并在事后进行清理。比如说,在测试一个类别的时候,你也许会想要建立该类别的实体,然后对它施行一些检查,最后再将它释放,如果测试项目很多的话,你将免不了在每一个测试方法里面撰写重复的程序代码。DUnit 对此提出的解决方案是,在每一个测试方法被执行之前和之后分别去呼叫 TTestCase 的虚拟方法 SetupTearDown,以终极测试的行话来说,由这两个方法来提供测试前的必要处理就称为一个 fixture(译注 2)。

以下范例扩充了 TTestCaseFirst 并增加几个测试 Delphi 集合类别 TStringList 的方法:

interface

 

uses

 TestFrameWork,

 Classes;  // needed for TStringList

 

type

 TTestCaseFirst = class(TTestCase)

 private

   Fsl: TStringList;

 protected

  procedure SetUp; override;

  procedure TearDown; override;

 published

  procedure TestFirst;

  procedure TestSecond;

  procedure TestThird;

  procedure TestPopulateStringList;

  procedure TestSortStringList;

 end;

 

...

 

procedure TTestCaseFirst.SetUp;

begin

 Fsl := TStringList.Create;

end;

 

procedure TTestCaseFirst.TearDown;

begin

  Fsl.Free;

end;

 

procedure TTestCaseFirst.TestPopulateStringList;

var

 i: Integer;

begin

 Check(Fsl.Count = 0);

 for i := 1 to 50 do    // Iterate

   Fsl.Add('i');

 Check(Fsl.Count = 50);

end;

 

procedure TTestCaseFirst.TestSortStringList;

begin

  Check(Fsl.Sorted = False);

  Check(Fsl.Count = 0);

  Fsl.Add('You');

  Fsl.Add('Love');

  Fsl.Add('I');

  Fsl.Sorted := True;

  Check(Fsl[2] = 'You');

  Check(Fsl[1] = 'Love');

  Check(Fsl[0] = 'I');

end;

测试套件(Test suites)

当你在测试一个真正有用的(non-trivial)应用程序时,你会想要建立一个以上的 TTestCase 衍生类别,欲将这些类别加到上层节点,你只需在 initialization 子句里面注册它们就行了,写法跟上面的范例一样。有时候,你可能想要更清楚地定义测试案例之间的结构关系,为此 DUnit 提供了建立测试套件的功能,它可以让你在测试案例中包含其它的测试案例或测试套件(使用 Composite 样式)。

如同在 TTestCaseFirst 测试案例中所显示的,当算术运算的测试方法执行时,SetUp 和 TearDown 方法虽然有被呼叫但完全没做任何事。其中有两个处理字符串串行的方法,最好能将它们分离成独立的测试套件,做法是先把 TTestCaseFirst 拆成两个类别,分别是 TTestArithmetic 与 TTestStringList:

type

  TTestArithmetic = class(TTestCase)

 published

   procedure TestFirst;

   procedure TestSecond;

   procedure TestThird;

 end;

 

 TTestStringlist = class(TTestCase)

 private

   Fsl: TStringList;

 protected

  procedure SetUp; override;

  procedure TearDown; override;

 published

  procedure TestPopulateStringList;

  procedure TestSortStringList;

 end;

(当然啦,你也得更新这些方法的实作才行)

然后把 inistailization 的程序代码改成这样:

RegisterTest('Simple suite', TTestArithmetic.Suite);

RegisterTest('Simple suite', TTestStringList.Suite);

逐步建立测试套件

TestFramework 单元的 TTestSuite 类别实作了测试套件,所以你可以用更明显的方式建立测试阶层:

下面的 UnitTests 函式会建立一个测试套件,并且在其中加入两个测试类别:

function UnitTests: ITestSuite;

var

  ATestSuite: TTestSuite;

begin

  ATestSuite := TTestSuite.Create('Some trivial tests');

  ATestSuite.AddTests(TTestArithmetic.Suite);

  ATestSuite.AddTests(TTestStringlist.Suite);

  Result := ATestSuite;

end;

还有另一种写法,跟上面的作用也是完全相同的:

function UnitTests: ITestSuite;

begin

  Result := TTestSuite.Create('Some trivial tests',

                         [                

                          TTestArithmetic.Suite,

                          TTestStringlist.Suite

                          ]);

end;

上面的范例是在呼叫 TTestSuite 的建构元时,把要加入的测试一并透过数组传递过去。

使用上述任一种方式建立的测试套件,其注册方式跟你之前注册个别测试案例的方式是相同的:

initialization

  RegisterTest('Simple Test', UnitTests);

end.

当测试程序执行时,你就会在 GUITestRunner 窗口上看到新的树状阶层。

 

其它功能

在主控台模式下执行测试

有时候,我们会想要在主控台模式下执行测试套件,比如说当你想要用一个 Makefile 执行整批的测试,这时候主控台模式就很有用。如要在主控台模式下执行测试,之前在 DPR 档案里面的 uses 子句中的 GUITestRunner 就要改成 TextTestRunner,并且加入条件编译 {$APPTYPE CONSOLE} 或者在 IDE 里点选 Project | Options | Linker | Generate console application 选项。

以下范例 Project1TestConsole.dpr 的项目原始码:

{$APPTYPE CONSOLE}

 

program Project1TestConsole;

uses

 TestFrameWork,

 TextTestRunner,

 Project1TestCases in 'Project1TestCases.pas';

 

{$R *.RES}

 

begin

  TextTestRunner.RunRegisteredTests;

end.

程序执行的输出结果会像这样:

--

DUnit: Testing.

..F.E..

Time: 0.20

FAILURES!!!

Test Results:

Run: 5

Failures: 1

Errors: 1

 

There was 1 error:

1) TestThird: EDivByZero: Division by zero

 

There was 1 failure:

1) TestSecond

注意第三行的 '..F.E..' 字符串,其中每一个句点(.)代表一项执行无误的测试,'F' 表示测试失败(failed),而 'E' 表示发生异常(exception)。

如果你希望当测试失败时,让 TextTestRunner 停止执行并且传回一个非零的结束码,你可以传入一个rxbHaltOnFailures 参数值,像这样:

TextTestRunner.RunRegisteredTests(rxbHaltOnFailures);

当你使用 Makefile 来执行测试套件的时候,这些回传的结束码会很有用处。

扩充功能

The TextExtensions 单元中的类别是用来扩充 DUnit 框架的功能,大部分的类别使用了「*」(GoF, Gang of Four)的 "Design Patterns" 书中所定义的 decorator 样式。

TRepeatedTest

TRepeatedTest 类别允选你重复装饰的测试许多次,例如,重复执行 TestFirst 测试案例中的 TTestArithmetic 10 次,你的程序可以这么写:

uses

 TestFrameWork,

 TestExtensions, // needed for TRepeatedTest

 Classes;        // needed for TStringList

 

...

 

function UnitTests: ITest;

var

  ATestArithmetic : TTestArithmetic;

begin

  ATestArithmetic := TTestArithmetic.Create('TestFirst');

  Result := TRepeatedTest.Create(ATestArithmetic, 10);

end;

请注意 TTestArithmetic 的建构元:

ATestArithmetic := TTestArithmetic.Create('TestFirst');

这里我把要重复执行的测试方法的名称传递给建构元,当然这个名称一定不能写错,否则随后执行时只能得到令人失望的结果。

如果你想要重复测试 TTestArithmetic 的全部方法,你可以把它们放在一个套件里:

function UnitTests: ITest;

begin

  Result := TRepeatedTest.Create(ATestArithmetic.Suite, 10);

end;

TTestSetup

TTestSetup 类别可以让你为一个测试案例类别进行唯一一次的初始化设定(Setup 与 TearDown 方法是每次执行测试方法时就会被呼叫)。例如,如果你正在撰写一组测试以验证某些存取数据库的程序代码,你可能会从 TTestSetup 衍生一个类别,并且利用它来开启和关闭数据库。

 

参考数据

位于 SourceForge 的 DUnit 首页(https://sourceforge.net/projects/dunit/),有最新的原始码,邮件论坛,问答集...等。

Delphi 的终极测试工具 ( http://www.suigeneris.org/juanca/writings/1999-11-29.html),Juancarlo Añez 在这篇文章里介绍了他设计的 DUnit 类别,此文最初公布于 Borland 开发人员社群网站。

JUnit Test Infected: Programmers Love Writing Tests (http://www.junit.org/junit/doc/testinfected/testing.htm),这是一篇介绍 JUnit 的好文章, DUnit 就是以此框架为基础而发展出来的。

Simple Smalltalk Testing: With Patterns(http://www.xprogramming.com/testfram.htm),Kent Beck 最早的文件,比较适合熟悉 Smalltalk 的人阅读。

~o~

译注

  1. 部分档案目录在新版本里面已经不存在了,例如:framework,故应以官方释出的最新版的目录结构为准。
  2. Fixture 是 XP 术语。在一个 Test Case 里面,负责初始化及清理的动作,对应到实作上就是 SetUp 与 TearDown 这两个虚拟方法。每次执行测试时,会事先呼叫所有的 fixture 的 Setup 方法,并且在测试结束前呼叫 TearDown,我想也就因为这些是固定要执行的动作,所以取其名为 fixture,若要译成中文,也许可以说成「固定装置」或「固定机制」。 fixture 的典型用法是在 Setup 方法里面配置资源,并且在 TearDown 里面释放资源,如果你正在测试存取数据库的程序,并且希望测试的过程不会改变数据库的内容,也可以在 TearDown 里面将所有的交易撤回,或者撰写交易补偿的程序代码。