DUnit logoDUnit
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.
US flag
英文版請點這裡

翻譯:蔡煥麟 (Jul-31-2002)

內容

採用 DUnit 進行單元測試

檔案內容
起步
你的第一個測試專案
SetUp 與 TearDown
測試套件
逐步建立測試套件

其他功能

在主控台模式下執行測試
擴充功能

參考資料

採用 DUnit 進行單元測試 

DUnit 是一個類別框架,目的是要支援 XP 的軟體測試方法。它支援 Delphi 4 以後的版本。

其概念為,當你在開發或修改程式碼時,你就要同時開發出相稱的測試程式,而不是把它們延後到測試階段。若能隨時更新測試程式並且經常反覆地執行它們,你就能夠更輕易地產生可靠的程式碼,而且在進行修改與重整(refactorings)時更有把握不會破壞原有的程式碼,於是,應用程式等於有了自我測試的能力。

DUnit 提供了一些類別以便組織與執行這些測試。DUnit 提供兩種執行測試的方式:

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's environment options dialog

另一種做法,是將 DUnit 路徑加到預設的專案選項或者特定的專案選項裡,在 IDE 中點選 Project | Options:

Delphi's project options dialog

你的第一個測試專案

建立一個新的應用程式,然後關閉 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,執行時期型態資訊)自動地尋找並且呼叫它們,這些測試方法必須符合兩個條件:

注意 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 的虛擬方法 Setup TearDown,以終極測試的行話來說,由這兩個方法來提供測試前的必要處理就稱為一個 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.AddTest(TTestArithmetic.Suite);
  ATestSuite.AddTest(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 TestExtensions 單元中的類別是用來擴充 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,故應以官方釋出的最新版的目錄結構為準。此外,有些檔案和目錄的超連結,在網站上瀏覽時可能會找不到,此乃網站上未存放 DUnit 框架的完整目錄之故,若你已經下載 DUnit 套件,可在本機閱讀其說明文件,就不會發生這種情況了。
  2. Fixture 是一種固定機制,它的用途是:讓一個 test-case 裡面的多項測試能夠共享物件(變數、或任何資源)的初始化及清理動作。所以,fixture 包含三種元素:物件(一個或多個),初始化物件的動作,以及清除物件的動作。欲建立 fixture,你通常會知道哪些物件(或任何變數)是要被測試方法共用的,然後你必須改寫 TTestCase 的 Setup 與 TearDown 方法,把物件的初始化和清除的動作分別寫在 Setup 與 TearDown 裡面。在某些具有自動資源回收(garbage collection)功能的程式語言(例如:Java 的 JUnit),你也可以不用在 TearDown 裡面撰寫釋放資源的程式碼,但有借有還仍不失為一種好的習慣,而且有些收尾動作像是關閉檔案、資料庫連結...等,還是會用到 TearDown。除了管理物件資源,fixture 也有其他用途,例如:管理資料庫交易。如果你正在測試資料庫應用程式,並且希望測試的過程不會改變資料庫的內容,也可以在 TearDown 裡面將所有的交易撤回,或者撰寫交易補償的程式碼。