艾智杰
同濟(jì)大學(xué)電子信息與工程學(xué)院計(jì)算機(jī)應(yīng)用技術(shù)系,上海 201804
測試驅(qū)動(dòng)開發(fā)(TDD)是一種基于循環(huán)開發(fā)的軟件開發(fā)過程。遵循TDD的編程人員,在正式進(jìn)行開發(fā)之前,通常先要確定在本階段需要實(shí)現(xiàn)的改進(jìn)或者新功能,然后通過編寫一系列的測試代碼來檢驗(yàn)這些改進(jìn)和功能。一般情況下,這些測試代碼都會(huì)運(yùn)行失敗。接下去的任務(wù)便是編寫能夠使得這些測試通過的代碼,并且在完全通過測試后,重構(gòu)代碼,以達(dá)到生產(chǎn)標(biāo)準(zhǔn)。這個(gè)過程將會(huì)一直循環(huán)下去,直到所有的改進(jìn)或者功能完成。下圖展示了這一過程。
圖1 基于TDD的開發(fā)循環(huán)
CxxTest是專門為C++語言所開發(fā)的TDD框架。它具有不需要RTTI,可以承載外部庫,處理異常等優(yōu)點(diǎn)。作為一種輕量級(jí)框架,CxxTest將所有的代碼都僅包含在一個(gè)頭文件(tdd.h)中。也就是說,CxxTest框架僅需要一個(gè)現(xiàn)代C++編譯器就可以運(yùn)行測試程序,甚至在必要時(shí),可以通過它捕獲異常和使用GUI展示。
CxxTest作為一種輕量級(jí)的測試驅(qū)動(dòng)開發(fā)框架,其優(yōu)點(diǎn)在于使用簡單。我們通常使用已有的控制臺(tái)測試啟動(dòng)程序來調(diào)用我們自己編寫的測試用DLL。之后,該測試程序就會(huì)對(duì)此DLL的各個(gè)注冊(cè)方法進(jìn)行測試,并且最終輸出結(jié)果。
整個(gè)測試的過程大致可以分成兩個(gè)部分,第一部分是測試類的選取,而第二部分則是具體的對(duì)我們所定義的方法的測試。圖1表示的是在測試類級(jí)別上的選擇,而圖2則是圖1中帶有“*”標(biāo)記步驟的具體拓展,表現(xiàn)了CxxTest測試驅(qū)動(dòng)開發(fā)框架如何逐個(gè)調(diào)用測試類中的各個(gè)測試方法。為了讓示意圖盡可能簡介,這里沒有顯示出異常處理。筆者將會(huì)另辟一節(jié)敘述。
圖2 類的選取過程
圖3 方法的測試過程
測試類和方法的包裝注冊(cè)是整個(gè)測試開始前的準(zhǔn)備工作。這一步的注冊(cè)將會(huì)告訴CxxTest框架,有哪些類、其中的哪些方法需要進(jìn)行測試。
整個(gè)注冊(cè)過程的第一階段是在編譯階段通過CxxTest框架自定義的宏將所有的類對(duì)象定義為全局變量。然后當(dāng)系統(tǒng)載入我們編寫的帶有測試類和方法的DLL時(shí),首先會(huì)對(duì)全局變量進(jìn)行初始化,將所有這些經(jīng)過特殊處理的測試類對(duì)象加入到隊(duì)列中,以供后續(xù)使用。
測試類的包裝注冊(cè)是通過TESTCLASS(CSomeClass)宏實(shí)現(xiàn)的。該宏最關(guān)鍵的代碼如下所示:
該宏首先定義了函數(shù)CSomeClass _TddNamespaceResolv er::GetNameSpace() (未在上面的代碼中展示該函數(shù)細(xì)節(jié)),用于獲取CSomeClass的帶有命名空間的全稱,隨后,通過將TDD::ClassRegistrar< CSomeClass >類的匿名對(duì)象地址加入到全局智能指針中予以保留。
這里起到關(guān)鍵作用的是TDD::ClassRegistrar
最后,必須指出的是,我們真正添加進(jìn)全局隊(duì)列的并不是CSomeClass類對(duì)象,而是經(jīng)過包裝的TDD::ClassRegistrar
第二階段則是對(duì)測試類方法的注冊(cè)。這項(xiàng)功能是通過TESTMETHOD(MethodName)宏實(shí)現(xiàn)的。其核心代碼如下(略去次要部分)。
這里著重解釋真正做測試類注冊(cè)工作的__m_ MethodName _variable,該變量在類對(duì)象的初始化過程中、類的構(gòu)造函數(shù)被觸發(fā)前先被初始化。
仔細(xì)觀察該變量,他屬于TDD::MethodRegistrar
可見,其構(gòu)造函數(shù)僅僅是將測試方法加入隊(duì)列,而當(dāng)調(diào)用MethodRegistrar::RunTest()時(shí),便會(huì)真正開始進(jìn)行測試。
在初始化之后,程序便進(jìn)入了入口點(diǎn)函數(shù)TDD::UnitTestBase::RunTests()。該函數(shù)其實(shí)異常簡單,只是從隊(duì)列中找到測試類,然后再對(duì)每一測試類找到需要測試的方法,調(diào)用多態(tài)方法MethodRegistrar:: RunClassTests ()進(jìn)行測試,然后尋找下一個(gè)測試類,循環(huán)如此過程。
MethodRegistrar:: RunClassTests ()的主要經(jīng)過正如“測試過程”一節(jié)中的圖2所示,具體對(duì)應(yīng)的函數(shù)也可以通過描述簡單匹配,這里就不再贅述了。至于如何由此函數(shù)調(diào)用方法測試的執(zhí)行者M(jìn)ethodRegistrar::RunTest(),再由此函數(shù)調(diào)用TESTMETHOD()宏所定義的包裝函數(shù),最后再回到我們自己的函數(shù)的過程,筆者將會(huì)在下一節(jié)展示。
CxxTest的設(shè)計(jì)初衷就是為程序員提供測試框架,以檢查可能的錯(cuò)誤。為了一方面檢查錯(cuò)誤,另一方面在檢查到錯(cuò)誤之后讓程序繼續(xù)執(zhí)行以運(yùn)行更多測試來檢查其他可能的錯(cuò)誤,CxxTest的設(shè)計(jì)者對(duì)經(jīng)典的C++異常機(jī)制進(jìn)行了包裝。
CxxTest使用了“模板方法”設(shè)計(jì)模式,將所有的異常機(jī)制都封裝在TryCatch類中,該類的模板方法便是TryCatch::Execute(),在基類中,設(shè)計(jì)者將其設(shè)計(jì)為純虛函數(shù),以后每當(dāng)需要進(jìn)行測試時(shí),都會(huì)重新定義一個(gè)類(比如說用于做方法測試的TryCatchTest類),該類繼承自TryCatch類,并且重新實(shí)現(xiàn)Execute()函數(shù)。最終在測試時(shí),框架則會(huì)調(diào)用
TryCatch:: TryCatchAndReport()函數(shù),該函數(shù)的代碼如下所示(略去次要代碼)。
那么CxxTest又是如何重定義Execute()函數(shù)呢?其實(shí),做法很簡單,他只是簡單地將Execute()函數(shù)定義為對(duì)MethodRegistrar::RunTest()的調(diào)用,該函數(shù)內(nèi)部又調(diào)用了在方法注冊(cè)時(shí)使用的那個(gè)測試方法的包裝函數(shù),然后由該包裝函數(shù)直接調(diào)用我們所定義的測試函數(shù)(就是在TESTMETHOD()宏后面的代碼)。
再深一步,根據(jù)前面的分析,框架設(shè)計(jì)者認(rèn)為,應(yīng)該在Execute()函數(shù)中可能會(huì)拋出異常,而該函數(shù)實(shí)際上最終調(diào)用的是我們自己所定義的代碼,那我們自己的代碼一定需要定義異常嘛?其實(shí)不然,我們完全可以利用CxxTest框架所提供的驗(yàn)證宏。這里我們僅針對(duì)最為常用的TDD_VERIFY(expression)宏進(jìn)行展開分析,其他類似。該宏的關(guān)鍵如下所示:
其實(shí)他就是先判斷expression的真假,然后直接調(diào)用TDD::Verifier::Verify()函數(shù),此函數(shù)的功能非常簡單,就是判斷__tdd_b是否為假,如果為假,則拋出異常。關(guān)鍵代碼如下:
CxxTest作為一款輕量級(jí)的TDD框架,在設(shè)計(jì)的時(shí)候充分利用了C++的各種特性,使得其運(yùn)作機(jī)制看似復(fù)雜卻條例清晰。本文理出了整個(gè)CxxTest框架的運(yùn)行主線,并且對(duì)其中較為重要的部分做出了詳細(xì)的解釋。
[1]Robert C.Martin著.敏捷軟件開發(fā):原則,模式與實(shí)踐[M].鄧輝,等譯.清華大學(xué)出版社,2003,9.
[2]Test-driven development.http://en.wikipedia.org/wiki/Test-driven_development.14 January 2010.
[3]李瑛,彭軍.測試驅(qū)動(dòng)開發(fā)在系統(tǒng)中的設(shè)計(jì)實(shí)現(xiàn)及效能分析[J].計(jì)算機(jī)與數(shù)字工程,2007,35(1).