艾智杰
同濟大學電子信息與工程學院計算機應用技術系,上海 201804
測試驅動開發(TDD)是一種基于循環開發的軟件開發過程。遵循TDD的編程人員,在正式進行開發之前,通常先要確定在本階段需要實現的改進或者新功能,然后通過編寫一系列的測試代碼來檢驗這些改進和功能。一般情況下,這些測試代碼都會運行失敗。接下去的任務便是編寫能夠使得這些測試通過的代碼,并且在完全通過測試后,重構代碼,以達到生產標準。這個過程將會一直循環下去,直到所有的改進或者功能完成。下圖展示了這一過程。

圖1 基于TDD的開發循環
CxxTest是專門為C++語言所開發的TDD框架。它具有不需要RTTI,可以承載外部庫,處理異常等優點。作為一種輕量級框架,CxxTest將所有的代碼都僅包含在一個頭文件(tdd.h)中。也就是說,CxxTest框架僅需要一個現代C++編譯器就可以運行測試程序,甚至在必要時,可以通過它捕獲異常和使用GUI展示。
CxxTest作為一種輕量級的測試驅動開發框架,其優點在于使用簡單。我們通常使用已有的控制臺測試啟動程序來調用我們自己編寫的測試用DLL。之后,該測試程序就會對此DLL的各個注冊方法進行測試,并且最終輸出結果。
整個測試的過程大致可以分成兩個部分,第一部分是測試類的選取,而第二部分則是具體的對我們所定義的方法的測試。圖1表示的是在測試類級別上的選擇,而圖2則是圖1中帶有“*”標記步驟的具體拓展,表現了CxxTest測試驅動開發框架如何逐個調用測試類中的各個測試方法。為了讓示意圖盡可能簡介,這里沒有顯示出異常處理。筆者將會另辟一節敘述。

圖2 類的選取過程

圖3 方法的測試過程
測試類和方法的包裝注冊是整個測試開始前的準備工作。這一步的注冊將會告訴CxxTest框架,有哪些類、其中的哪些方法需要進行測試。
整個注冊過程的第一階段是在編譯階段通過CxxTest框架自定義的宏將所有的類對象定義為全局變量。然后當系統載入我們編寫的帶有測試類和方法的DLL時,首先會對全局變量進行初始化,將所有這些經過特殊處理的測試類對象加入到隊列中,以供后續使用。
測試類的包裝注冊是通過TESTCLASS(CSomeClass)宏實現的。該宏最關鍵的代碼如下所示:

該宏首先定義了函數CSomeClass _TddNamespaceResolv er::GetNameSpace() (未在上面的代碼中展示該函數細節),用于獲取CSomeClass的帶有命名空間的全稱,隨后,通過將TDD::ClassRegistrar< CSomeClass >類的匿名對象地址加入到全局智能指針中予以保留。
這里起到關鍵作用的是TDD::ClassRegistrar
最后,必須指出的是,我們真正添加進全局隊列的并不是CSomeClass類對象,而是經過包裝的TDD::ClassRegistrar
第二階段則是對測試類方法的注冊。這項功能是通過TESTMETHOD(MethodName)宏實現的。其核心代碼如下(略去次要部分)。

這里著重解釋真正做測試類注冊工作的__m_ MethodName _variable,該變量在類對象的初始化過程中、類的構造函數被觸發前先被初始化。
仔細觀察該變量,他屬于TDD::MethodRegistrar


可見,其構造函數僅僅是將測試方法加入隊列,而當調用MethodRegistrar::RunTest()時,便會真正開始進行測試。
在初始化之后,程序便進入了入口點函數TDD::UnitTestBase::RunTests()。該函數其實異常簡單,只是從隊列中找到測試類,然后再對每一測試類找到需要測試的方法,調用多態方法MethodRegistrar:: RunClassTests ()進行測試,然后尋找下一個測試類,循環如此過程。
MethodRegistrar:: RunClassTests ()的主要經過正如“測試過程”一節中的圖2所示,具體對應的函數也可以通過描述簡單匹配,這里就不再贅述了。至于如何由此函數調用方法測試的執行者MethodRegistrar::RunTest(),再由此函數調用TESTMETHOD()宏所定義的包裝函數,最后再回到我們自己的函數的過程,筆者將會在下一節展示。
CxxTest的設計初衷就是為程序員提供測試框架,以檢查可能的錯誤。為了一方面檢查錯誤,另一方面在檢查到錯誤之后讓程序繼續執行以運行更多測試來檢查其他可能的錯誤,CxxTest的設計者對經典的C++異常機制進行了包裝。
CxxTest使用了“模板方法”設計模式,將所有的異常機制都封裝在TryCatch類中,該類的模板方法便是TryCatch::Execute(),在基類中,設計者將其設計為純虛函數,以后每當需要進行測試時,都會重新定義一個類(比如說用于做方法測試的TryCatchTest類),該類繼承自TryCatch類,并且重新實現Execute()函數。最終在測試時,框架則會調用
TryCatch:: TryCatchAndReport()函數,該函數的代碼如下所示(略去次要代碼)。

那么CxxTest又是如何重定義Execute()函數呢?其實,做法很簡單,他只是簡單地將Execute()函數定義為對MethodRegistrar::RunTest()的調用,該函數內部又調用了在方法注冊時使用的那個測試方法的包裝函數,然后由該包裝函數直接調用我們所定義的測試函數(就是在TESTMETHOD()宏后面的代碼)。
再深一步,根據前面的分析,框架設計者認為,應該在Execute()函數中可能會拋出異常,而該函數實際上最終調用的是我們自己所定義的代碼,那我們自己的代碼一定需要定義異常嘛?其實不然,我們完全可以利用CxxTest框架所提供的驗證宏。這里我們僅針對最為常用的TDD_VERIFY(expression)宏進行展開分析,其他類似。該宏的關鍵如下所示:

其實他就是先判斷expression的真假,然后直接調用TDD::Verifier::Verify()函數,此函數的功能非常簡單,就是判斷__tdd_b是否為假,如果為假,則拋出異常。關鍵代碼如下:

CxxTest作為一款輕量級的TDD框架,在設計的時候充分利用了C++的各種特性,使得其運作機制看似復雜卻條例清晰。本文理出了整個CxxTest框架的運行主線,并且對其中較為重要的部分做出了詳細的解釋。
[1]Robert C.Martin著.敏捷軟件開發:原則,模式與實踐[M].鄧輝,等譯.清華大學出版社,2003,9.
[2]Test-driven development.http://en.wikipedia.org/wiki/Test-driven_development.14 January 2010.
[3]李瑛,彭軍.測試驅動開發在系統中的設計實現及效能分析[J].計算機與數字工程,2007,35(1).