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 是一個類別框架,目的是要支援 XP 的軟體測試方法。它支援 Delphi 4 以後的版本。
其概念為,當你在開發或修改程式碼時,你就要同時開發出相稱的測試程式,而不是把它們延後到測試階段。若能隨時更新測試程式並且經常反覆地執行它們,你就能夠更輕易地產生可靠的程式碼,而且在進行修改與重整(refactorings)時更有把握不會破壞原有的程式碼,於是,應用程式等於有了自我測試的能力。
DUnit 提供了一些類別以便組織與執行這些測試。DUnit 提供兩種執行測試的方式:
DUnit 的靈感源自 JUnit 框架,該框架是由 Kent Beck 與 Erich Gamma 為 Java 程式語言所設計的,但是 DUnit 已經逐漸發展成威力更強的 Delphi 專屬工具。最早是由 Juanco Añez 設計成 Delphi 的版本,目前則是由 SourceForge 的 DUnit Group 所維護。
隨著 DUnit 套件所發布的檔案應該存放在一個屬於自己的目錄下,以便保留完整的目錄結構:
目錄名稱 | 說明 | |||
---|---|---|---|---|
DUnit |
|
|||
|
framework |
事先編譯好的框架模組 |
||
|
src |
函式庫原始碼 |
||
|
doc |
輔助說明檔,網頁與 MPL 授權許可 |
||
|
|
images |
網頁的圖形檔案 |
|
Time2Help 產生的 API 文件 |
||||
|
Contrib |
其他人貢獻的模組 |
||
|
|
一個可以自動產生測試案例(test cases)的工具 |
||
|
tests |
給這個框架本身所使用的測試案例 |
||
|
bin |
事先編譯好,可以單獨執行的 GUI 測試程式 |
||
|
examples |
|
||
cmdline | 示範如何在命令列環境下使用 DUnit | |||
|
|
collection |
一個類似 Java 的集合(collections)實作以及它的 DUnit 測試案例 |
|
|
|
registration |
使用測試案例註冊系統(registration system)(譯註:示範幾種註冊測試案例的方法) |
|
|
|
組織測試程式碼的方式 |
||
|
|
|
diffunit |
把測試案例放在獨立的單元裡面 |
|
|
|
sameunit |
把測試案例和被測試的程式碼放在同一個單元裡面 |
一步步教你建立一個存取 Registry 的工具及其測試案例 |
||||
embeddable | 示範如何將 GUITestRunner 嵌入至其他視窗內 | |||
|
|
(...) |
|
|
|
|
TListTest |
給 Delphi 的 Classes.TList 物件使用的測試案例 |
目錄 src 包含下列檔案
檔案名稱 | 說明 |
---|---|
框架本身 |
|
可用來擴充測試案例的 Decorator 類別 |
|
用來測試使用者介面(視窗與對話盒)的類別 |
|
在主控台模式下執行測試的函式 |
|
此框架的圖形化使用者介面 |
|
GUITestRunner Form |
framework 目錄中包含以上各單元編譯過的版本,以及用來連結 .BPL 的 .DCP 檔案(對應的 .BPL 檔案存在 bin 目錄裡)(譯註1)。
在開始使用 DUnit 之前,Delphi 的單元搜尋路徑裡必須包含 DUnit 的原始碼或編譯後的檔案路徑 。你可以在 Delphi IDE 中點選 Tools | Environment Options | Library,然後把 DUnit 路徑加到原有的路徑清單裡:
另一種做法,是將 DUnit 路徑加到預設的專案選項或者特定的專案選項裡,在 IDE 中點選 Project | Options:
建立一個新的應用程式,然後關閉 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 項目取消。
我們通常會在執行一組測試之前進行一般的準備工作,並在事後進行清理。比如說,在測試一個類別的時候,你也許會想要建立該類別的實體,然後對它施行一些檢查,最後再將它釋放,如果測試項目很多的話,你將免不了在每一個測試方法裡面撰寫重複的程式碼。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;
當你在測試一個真正有用的(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 類別允選你重複裝飾的測試許多次,例如,重複執行 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 類別可以讓你為一個測試案例類別進行唯一一次的初始化設定(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~