理解 JUnit 測試框架實現(xiàn)原理和設(shè)計模式
本文細致地描述了 JUnit 的代碼實現(xiàn),在展示代碼流程 UML 圖的基礎(chǔ)上,詳細分析 JUnit 的內(nèi)部實現(xiàn)代碼的功能與機制,并在涉及相關(guān)設(shè)計模式的地方結(jié)合代碼予以說明。另外,分析過程還涉及 Reflection 等 Java 語言的高級特征。
概述
在測試驅(qū)動的開發(fā)理念深入人心的今天,JUnit 在測試開發(fā)領(lǐng)域的核心地位日漸穩(wěn)定。不僅 Eclipse 將 JUnit 作為默認的 IDE 集成組件,而且基于 JUnit 的各種測試框架也在業(yè)內(nèi)被廣泛應用,并獲得了一致好評。目前介紹 JUnit 書籍文章雖然較多,但大多數(shù)是針對 JUnit 的具體應用實踐,而對于 JUnit 本身的機制原理,只是停留在框架模塊的較淺層次。
本文內(nèi)容完全描述 JUnit 的細致代碼實現(xiàn),在展示代碼流程 UML 圖的基礎(chǔ)上,詳細分析 JUnit 的內(nèi)部實現(xiàn)代碼的功能與機制,并在涉及相關(guān)設(shè)計模式的地方結(jié)合代碼予以說明。另外,分析過程還涉及 Reflection 等 Java 語言的高級特征。
本文的讀者應該對 JUnit 的基本原理及各種設(shè)計模式有所了解,主要是面向從事 Java 相關(guān)技術(shù)的設(shè)計、開發(fā)與測試的人員。對于 C++,C# 程序員也有很好的借鑒作用。
Junit 簡介
JUnit 的概念及用途
JUnit 是由 Erich Gamma 和 Kent Beck 編寫的一個開源的單元測試框架。它屬于白盒測試,只要將待測類繼承 TestCase 類,就可以利用 JUnit 的一系列機制進行便捷的自動測試了。
JUnit 的設(shè)計精簡,易學易用,但是功能卻非常強大,這歸因于它內(nèi)部完善的代碼結(jié)構(gòu)。 Erich Gamma 是著名的 GOF 之一,因此 JUnit 中深深滲透了擴展性優(yōu)良的設(shè)計模式思想。 JUnit 提供的 API 既可以讓您寫出測試結(jié)果明確的可重用單元測試用例,也提供了單元測試用例成批運行的功能。在已經(jīng)實現(xiàn)的框架中,用戶可以選擇三種方式來顯示測試結(jié)果,并且顯示的方式本身也是可擴展的。
JUnit 基本原理
一個 JUnit 測試包含以下元素:
表 1. 測試用例組成
開發(fā)代碼部分 測試代碼部分 測試工具部分待測試類 A |
通過擴展 TestCase 或者構(gòu)造 TestSuit 方法
編寫測試類 B |
一個測試運行器(TestRunner)R,可以選擇圖形界面或文本界面 |
操作步驟:
將 B 通過命令行方式或圖形界面選擇方式傳遞給 R,R 自動運行測試,并顯示結(jié)果。
JUnit 中的設(shè)計模式體現(xiàn)
設(shè)計模式(Design pattern)是一套被反復使用的、為眾人所知的分類編目的代碼設(shè)計經(jīng)驗總結(jié)。使用設(shè)計模式是為了可重用和擴展代碼,增加代碼的邏輯性和可靠性。設(shè)計模式的出現(xiàn)使代碼的編制真正工程化,成為軟件工程的基石。
GoF 的《設(shè)計模式》一書首次將設(shè)計模式提升到理論高度,并將之規(guī)范化。該書提出了 23 種基本設(shè)計模式,其后,在可復用面向?qū)ο筌浖陌l(fā)展過程中,新的設(shè)計模式亦不斷出現(xiàn)。
軟件框架通常定義了應用體系的整體結(jié)構(gòu)類和對象的關(guān)系等等設(shè)計參數(shù),以便于具體應用實現(xiàn)者能集中精力于應用本身的特定細節(jié)。因此,設(shè)計模式有助于對框架結(jié)構(gòu)的理解,成熟的框架通常使用了多種設(shè)計模式,JUnit 就是其中的優(yōu)秀代表。設(shè)計模式是 JUnit 代碼的精髓,沒有設(shè)計模式,JUnit 代碼無法達到在小代碼量下的高擴展性。總體上看,有三種設(shè)計模式在 JUnit 設(shè)計中得到充分體現(xiàn),分別為 Composite 模式、Command 模式以及 Observer 模式。
一個簡單的 JUnit 程序?qū)嵗?
我們首先用一個完整實例來說明 JUnit 的使用。由于本文的分析對象是 JUnit 本身的實現(xiàn)代碼,因此測試類實例的簡化無妨。本部分引入《 JUnit in Action 》中一個 HelloWorld 級別的測試實例,下文的整個分析會以該例子為基點,剖析 JUnit 源代碼的內(nèi)部流程。
待測試類如下:
圖 1. 待測試代碼
該類只有一個 add 方法,即求兩個浮點數(shù)之和返回。
下面介紹測試代碼部分,本文以 JUnit3.8 為實驗對象,JUnit4.0 架構(gòu)類同。筆者對原書中的測試類做了一些修改,添加了一個必然失敗的測試方法 testFail,目的是為了演示測試失敗時的 JUnit 代碼流程。
完整的測試類代碼如下:
圖 2. 測試類代碼
TestCalculator 擴展了 JUnit 的 TestCase 類,其中 testAdd 方法就是對 Calculator.add 方法的測試,它會在測試開始后由 JUnit 框架從類中提取出來運行。在 testAdd 中,Calculator 類被實例化,并輸入測試參數(shù) 10 和 50,最后用 assertEquals 方法(基類 TestCase 提供)判斷測試結(jié)果與預期是否相等。無論測試符合預期或不符合都會在測試工具
TestRunner 中體現(xiàn)出來。
實例運行結(jié)果:
圖 3. 實例運行結(jié)果
從運行結(jié)果中可見:testAdd 測試通過(未顯示),而 testFail 測試失敗。圖形界面結(jié)果如下:
圖 4. 測試圖形結(jié)果
JUnit 源代碼分析
JUnit 的完整生命周期分為 3 個階段:初始化階段、運行階段和結(jié)果捕捉階段。
圖 5. JUnit 的完整生命周期圖( 查看大圖 )
初始化階段(創(chuàng)建 Testcase 及 TestSuite)
圖 6. JUnit 的 main 函數(shù)代碼
初始化階段作一些重要的初始化工作,它的入口點在 junit.textui.TestRunner 的 main 方法。該方法首先創(chuàng)建一個 TestRunner 實例
aTestRunner
。之后 main 函數(shù)中主體工作函數(shù)為?
TestResult r = aTestRunner.start(args) 。
它的函數(shù)構(gòu)造體代碼如下 :
圖 7. junit 的 start(String[]) 函數(shù)
我們可以看出,Junit 首先對命令行參數(shù)進行解析:參數(shù)“ -wait ”(等待模式,測試完畢用戶手動返回)、“ -c ”,“ -v ”(版本顯示)。 -m 參數(shù)用于測試單個方法。這是 JUnit 提供給用戶的一個非常輕便靈巧的測試功能,但是在一般情況下,用戶會像本文前述那樣在類名命令行參數(shù),此時通過語句:
testCase = args[i];
將測試類的全限定名將傳給 String 變量 testcase 。
然后通過:
Test suite = getTest(testCase);
將對 testCase 持有的全限定名進行解析,并構(gòu)造 TestSuite 。
圖 8. getTest() 方法函數(shù)源代碼
TestSuite 的構(gòu)造分兩種情況 ( 如上圖 ):
- A:用戶在測試類中通過聲明 Suite() 方法自定義 TestSuite 。
- B:JUnit 自動判斷并提取測試方法。
JUnit 提供給用戶兩種構(gòu)造測試集合的方法,用戶既可以自行編碼定義結(jié)構(gòu)化的 TestCase 集合,也可以讓 JUnit 框架自動創(chuàng)建測試集合,這種設(shè)計融合其它功能,讓測試的構(gòu)建、運行、反饋三個過程完全無縫一體化。
情況 A:
圖 9. 自定義 TestSuite 流程圖
當 suite 方法在 test case 中定義時,JUnit 創(chuàng)建一個顯式的 test suite,它
利用 Java 語言的 Reflection 機制找出名為
SUITE_METHODNAME
的方法,也即 suite 方法:
suiteMethod = testClass.getMethod(SUITE_METHODNAME, new Class[0]);
Reflection 是 Java 的高級特征之一,借助 Reflection 的 API 能直接在代碼中動態(tài)獲取到類的語言編程層面的信息,如類所包含的所有的成員名、成員屬性、方法名以及方法屬性,而且還可以通過得到的方法對象,直接調(diào)用該方法。 JUnit 源代碼頻繁使用了 Reflection 機制,不僅充分發(fā)揮了 Java 語言在系統(tǒng)編程要求下的超凡能力,也使 JUnit 能在用戶自行編寫的測試類中游刃有余地分析并提取各種屬性及代碼,而其它測試框架需要付出極大的復雜性才能得到等價功能。
若 JUnit 無法找到 siute 方法,則拋出異常,流程進入情況 B 代碼;若找到,則對用戶提供的 suite 方法進行外部特征檢驗,判斷是否為類方法。最后,JUnit 自動調(diào)用該方法,構(gòu)造用戶指定的 TestSuite:
test = (Test)suiteMethod.invoke(null, (Object[]) new Class[0]);
情況 B:
圖 10. 自動判斷并提取 TestSuite 流程圖
當 suite 方法未在 test case 中定義時,JUnit 自動分析創(chuàng)建一個 test suite 。
代碼由 :
return new TestSuite(testClass);
處進入 TestSuite(Class theclass) 方法為 TestSuite 類的構(gòu)造方法,它能自動分析 theclass 所描述的類的內(nèi)部有哪些方法需要測試,并加入到新構(gòu)造的 TestSuite 中。代碼如下:
圖 11. TestSuite 函數(shù)代碼
TestSuite 采用了Composite 設(shè)計模式。在該模式下,可以將 TestSuite 比作一棵樹,樹中可以包含子樹(其它 TestSuite),也可以包含葉子 (TestCase),以此向下遞歸,直到底層全部落實到葉子為止。 JUnit 采用 Composite 模式維護測試集合的內(nèi)部結(jié)構(gòu),使得所有分散的 TestCase 能夠統(tǒng)一集中到一個或若干個 TestSuite 中,同類的 TestCase 在樹中占據(jù)同等的位置,便于統(tǒng)一運行處理。另外,采用這種結(jié)構(gòu)使測試集合獲得了無限的擴充性,不需要重新構(gòu)造測試集合,就能使新的 TestCase 不斷加入到集合中。
在 TestSuite 類的代碼中,可以找到:
private Vector fTests = new Vector(10);
此即為內(nèi)部維護的“子樹或樹葉”的列表。
紅框內(nèi)的代碼完成提取整個類繼承體系上的測試方法的提取。循環(huán)語句由 Class 類型的實例 theClass 開始,逐級向父類的繼承結(jié)構(gòu)追溯,直到頂級 Object 類,并將沿途各級父類中所有合法的 testXXX() 方法都加入到 TestSuite 中。
合法 testXXX 的判斷工作由:
addTestMethod(methods[i], names, theClass)
完成,實際上該方法還把判斷成功的方法轉(zhuǎn)化為 TestCase 對象,并加入到 TestSuite 中。代碼如下圖 :
圖 12. addTestMethod 函數(shù)代碼
首先通過 String name= m.getName(); 利用 Refection API 獲得 Method 對象 m 的方法名,用于特征判斷。然后通過方法
isTestMethod(Method m)
中的
return parameters.length == 0 && name.startsWith("test") && returnType.equals(Void.TYPE);
來判別方法名是不是以字符串“ test ”開始。
而代碼:
if (names.contains(name)) return;
用于在逐級追溯過程中,防止不同級別父類中的 testXXX() 方法重復加入 TestSuite 。
對于符合條件的 testXXX() 方法,addTestMethod 方法中用語句:
addTest(createTest(theClass, name));
將 testXXX 方法轉(zhuǎn)化為 TestCase,并加入到 TestSuite 。其中,addTest 方法接受 Test 接口類型的參數(shù),其內(nèi)部有 countTestCases 方法和 run 方法,該接口被 TestSuite 和 TestCase 同時實現(xiàn)。這是 Command 設(shè)計模式精神的體現(xiàn),
Command 模式將調(diào)用操作的對象與如何實現(xiàn)該操作的對象解耦。在運行時,TestCase 或 TestSuite 被當作 Test 命令對象,可以像一般對象那樣進行操作和擴展,也可以在實現(xiàn) Composite 模式時將多個命令復合成一個命令。另外,增加新的命令十分容易,隔離了現(xiàn)有類的影響,今后,也可以與備忘錄模式結(jié)合,實現(xiàn) undo 等高級功能。
加入 TestSuite 的 TestCase 由 createTest(theClass, name) 方法創(chuàng)建,代碼如下:
圖 13. CreateTest 函數(shù)代碼( 查看大圖 )
TestSuite 和 TestCase 都有一個
fName
實例變量,是在其后的測試運行及結(jié)果返回階段中該 Test 的唯一標識,對 TestCase 來說,一般也是要測試的方法名。在?
createTest
?方法中,測試方法被轉(zhuǎn)化成一個 TestCase 實例,并通過:
((TestCase) test).setName(name);
用該方法名標識 TestCase 。其中,test 對象也是通過 Refection 機制,通過 theClass 構(gòu)建的:
test = constructor.newInstance(new Object[0]);
注意:theClass 是圖 8 中 getTest 方法的 suiteClassName 字符串所構(gòu)造的 Class 類實例,而后者其實是命令行參數(shù)傳入的帶測試類 Calculator,它繼承了 TestCase 方法。因此,theClass 完全具備轉(zhuǎn)化的條件。
至此整個流程的初始化完成。
測試驅(qū)動運行階段(運行所有 TestXXX 型的測試方法)
由圖 7 所示 , 我們可以知道初始化完畢,即 testsuit() 創(chuàng)建好后 , 便進入方法 :
doRun(suite, wait);
代碼如下 :
圖 14. doRun 函數(shù)代碼
該方法為測試的驅(qū)動運行部分,結(jié)構(gòu)如下:
- 創(chuàng)建 TestResult 實例。
- 將 junit.textui.TestRunner 的監(jiān)聽器 fPrinter 加入到 result 的監(jiān)聽器列表中。
其中,fPrinter 是 junit.textui.ResultPrinter 類的實例,該類提供了向控制臺輸出測試結(jié)果的一系列功能接口,輸出的格式在類中定義。 ResultPrinter 類實現(xiàn)了 TestListener 接口,具體實現(xiàn)了 addError、addFailure、endTest 和 startTest 四個重要的方法,這種設(shè)計是 Observer 設(shè)計模式的體現(xiàn),在 addListener 方法的代碼中:
public synchronized void addListener(TestListener listener) { fListeners.addElement(listener); }
將?
ResultPrinter
?對象加入到?
TestResult
?對象的監(jiān)聽器列表中,因此實質(zhì)上 TestResult 對象可以有多個監(jiān)聽器顯示測試結(jié)果。第三部分分析中將會描述對監(jiān)聽器的消息更新。
- 計時開始。
- run(result) 測試運行。
- 計時結(jié)束。
- 統(tǒng)一輸出,包括測試結(jié)果和所用時間。
其中最為重要的步驟為 run(result) 方法,代碼如下。
圖 15. run 函數(shù)代碼
Junit 通過?
for (Enumeration e= tests(); e.hasMoreElements(); ){ …… }
?對 TestSuite 中的整個“樹結(jié)構(gòu)”遞歸遍歷運行其中的節(jié)點和葉子。此處 JUnit 代碼頗具說服力地說明了 Composite 模式的效力,run 接口方法的抽象具有重大意義,它實現(xiàn)了客戶代碼與復雜對象容器結(jié)構(gòu)的解耦,讓對象容器自己來實現(xiàn)自身的復雜結(jié)構(gòu),從而使得客戶代碼就像處理簡單對象一樣來處理復雜的對象容器。每次循環(huán)得到的節(jié)點 test,都同 result 一起傳遞給 runTest 方法,進行下一步更深入的運行。
圖 16. junit.framework.TestResult.run 函數(shù)代碼
這里變量 P 指向一個實現(xiàn)了 Protectable 接口的匿名類的實例,Protectable 接口只有一個 protect 待實現(xiàn)方法。而 junit.framework.TestResult.runProtected(Test, Protectable) 方法的定義為:
public void runProtected(final Test test, Protectable p) { try { p.protect(); } catch (AssertionFailedError e) { addFailure(test, e); } catch (ThreadDeath e) { // don't catch ThreadDeath by accident throw e; } catch (Throwable e) { addError(test, e); } }
可見 runProtected 方法實際上是調(diào)用了剛剛實現(xiàn)的 protect 方法,也就是調(diào)用了 test.runBare() 方法。另外,這里的 startTest 和 endTest 方法也是 Observer 設(shè)計模式中的兩個重要的消息更新方法。
以下分析 junit.framework.TestCase.runBare() 方法:
圖 17. junit.framework.TestCase.runBare() 函數(shù)代碼
在該方法中,最終的測試會傳遞給一個 runTest 方法執(zhí)行,注意此處的 runTest 方法是無參的,注意與之前形似的方法區(qū)別。該方法中也出現(xiàn)了經(jīng)典的 setUp 方法和 tearDown 方法,追溯代碼可知它們的定義為空。用戶可以覆蓋兩者,進行一些 fixture 的自定義和搭建。 ( 注意:tearDown 放在了 finally{} 中,在測試異常拋出后仍會被執(zhí)行到,因此它是被保證運行的。 )
主體工作還是在 junit.framework.TestCase.runTest() 方法中 , 代碼如下 :
圖 18. junit.framework.TestCase.runTest() 函數(shù)代碼
該方法最根本的原理是:利用在圖 13 中設(shè)定的 fName,借助 Reflection 機制,從 TestCase 中提取測試方法:
runMethod = getClass().getMethod(fName, (Class[]) null);
為每一個測試方法,創(chuàng)建一個方法對象 runMethod 并調(diào)用:
runMethod.invoke(this, (Object[]) new Class[0]);
只有在這里,用戶測試方法的代碼才開始被運行。
在測試方法運行時,眾多的 Assert 方法會根據(jù)測試的實際情況,拋出失敗異常或者錯誤。也是在“ runMethod.invoke(this, (Object[]) new Class[0]); ”這里,這些異?;蝈e誤往上逐層拋出,或者被某一層次處理,或者處理后再次拋出,依次遞推,最終顯示給用戶。
流程圖如下 :
圖 19. JUnit 執(zhí)行測試方法,并在測試結(jié)束后將失敗和錯誤信息通知所有 test listener
測試結(jié)果捕捉階段(返回 Fail 或 Error 并顯示)
通過以下代碼,我們可以看出失敗由第一個 catch 子句捕獲,并交由 addFailure 方法處理,而錯誤由第三個 catch 子句捕獲,并交由 addError 方法處理。
圖 20. 失敗處理函數(shù)代碼
圖 21. 失敗處理流程圖
JUnit 執(zhí)行測試方法,并在測試結(jié)束后將失敗和錯誤信息通知給所有的 test listener 。其中 addFailure、addError、endTest、startTest 是 TestListener 接口的四大方法,而 TestListener 涉及到 Observer 設(shè)計模式。
我們嘗試看看 addFailure 方法的代碼:
圖 22. addFailure 方法的代碼
此處代碼將產(chǎn)生的失敗對象加入到了 fFailures,可聯(lián)系 圖 2,此處的結(jié)果在程序退出時作為測試總體成功或失敗的判斷依據(jù)。而在 for 循環(huán)中,TestResult 對象循環(huán)遍歷觀察者(監(jiān)聽器)列表,通過調(diào)用相應的更新方法,更新所有的觀察者信息,這部分代碼也是整個 Observer 設(shè)計模式架構(gòu)的重要部分。
根據(jù)以上描述,JUnit 采用 Observer 設(shè)計模式使得 TestResult 與眾多測試結(jié)果監(jiān)聽器通過接口 TestListenner 達到松耦合,使 JUnit 可以支持不同的使用方式。目標對象(TestResult)不必關(guān)心有多少對象對自身注冊,它只是根據(jù)列表通知所有觀察者。因此,TestResult 不用更改自身代碼,而輕易地支持了類似于 ResultPrinter 這種監(jiān)聽器的無限擴充。目前,已有文本界面、圖形界面和 Eclipse 集成組件三種監(jiān)聽器,用戶完全可以開發(fā)符合接口的更強大的監(jiān)聽器。
出于安全考慮,cloneListeners() 使用克隆機制取出監(jiān)聽器列表:
private synchronized Vector cloneListeners() { return (Vector)fListeners.clone(); }
TestResult 的 addFailure 進一步調(diào)用 ResultPrinter 的 addFailure:
圖 23. ResultPrinter 的 addFailure 函數(shù)代碼
這里并沒有將錯誤信息輸出,而只是輸出了錯誤類型:“ F “。錯誤信息由圖 14 中的:
fPrinter.print(result, runTime);
統(tǒng)一輸出。
這些設(shè)計細節(jié)皆可以由 TestRunner 的實現(xiàn)者自己掌握。
?
總結(jié)
鑒于 JUnit 目前在測試領(lǐng)域的顯赫地位,以及 JUnit 實現(xiàn)代碼本身編寫的簡潔與藝術(shù)性,本文的詳盡分析無論對于測試開發(fā)的實踐運用、基于 JUnit 的高層框架的編寫、以及設(shè)計模式與 Java 語言高級特征的學習都具有多面的重要意義。
更多文章、技術(shù)交流、商務合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯(lián)系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
