一、引子
JUnit源碼是我仔細(xì)閱讀過(guò)的第一個(gè)開(kāi)源項(xiàng)目源碼。閱讀高手寫的代碼能學(xué)到一些好的編程風(fēng)格和實(shí)現(xiàn)思路,這是提高自己編程水平行之有效的方法,因此早就想看看這些赫赫有名的框架是怎么回事了。今天就拿最簡(jiǎn)單的JUnit下手,也算開(kāi)始自己的源碼分析之路。
JUnit作為最著名的單元測(cè)試框架,由兩位業(yè)界有名人士協(xié)力完成,已經(jīng)經(jīng)歷了多次版本升級(jí)(了解JUnit基礎(chǔ)、JUnit實(shí)踐)。JUnit總體來(lái)說(shuō)短小而精悍,有不少值得我們借鑒的經(jīng)驗(yàn)在里面;但是也有一些不足存在,當(dāng)然這對(duì)于任何程序來(lái)說(shuō)都是難免的。
下面我們將從整體(宏觀)和細(xì)節(jié)(微觀)兩方面來(lái)分析JUnit源碼,以下分析基于3.8.1版。
二、宏觀——架構(gòu)與模式
打開(kāi)源碼文件,你會(huì)發(fā)現(xiàn)JUnit源碼被分配到6個(gè)包中:junit.awtui、junit.swingui、junit.textui、junit.extensions、junit.framework、junit.runner。其中前三個(gè)包中包含了JUnit運(yùn)行時(shí)的入口程序以及運(yùn)行結(jié)果顯示界面,它們對(duì)于JUnit使用者來(lái)說(shuō)基本是透明的。junit.runner包中包含了支持單元測(cè)試運(yùn)行的一些基礎(chǔ)類以及自己的類加載器,它對(duì)于JUnit使用者來(lái)說(shuō)是完全透明的。
剩下的兩個(gè)包是和使用JUnit進(jìn)行單元測(cè)試緊密聯(lián)系在一起的。其中junit.framework包含有編寫一般JUnit單元測(cè)試類必須是用到的JUnit類;而junit.extensions則是對(duì)framework包在功能上的一些必要擴(kuò)展以及為更多的功能擴(kuò)展留下的接口。
JUnit提倡單元測(cè)試的簡(jiǎn)單化和自動(dòng)化。這就要求JUnit的使用要簡(jiǎn)單化,而且要很容易的實(shí)現(xiàn)自動(dòng)化測(cè)試。整個(gè)JUnit的設(shè)計(jì)大概也是遵循這個(gè)前提吧。整個(gè)框架的骨干僅有三個(gè)類組成(下圖所示)。

?????? 如果你掌握了TestCase、TestSuite、BaseTestRunner的工作方式,那么你就可以隨心所欲的編寫測(cè)試代碼了。
?????? 下面我們來(lái)看看junit.framework中類之間的關(guān)系,下圖是我根據(jù)源代碼分析出來(lái)的,大部分關(guān)系都表示了出來(lái)。

先來(lái)看看各個(gè)類的職責(zé)。Assert類提供了JUnit使用的一整套的斷言,這套斷言都被TestCase繼承下來(lái),Assert也就變成了透明的。Test接口是為了統(tǒng)一TestCase和TestSuite的類型;而TestCase里面提供了運(yùn)行單元測(cè)試類的方法;在TestSuite中則提供了加載單元測(cè)試類,檢驗(yàn)測(cè)試類格式等等的方法。TestResult故名思意就是提供存放測(cè)試結(jié)果的地方,但是在JUnit中它還帶有一點(diǎn)控制器的功能。
在這里指出其中我認(rèn)為有些不妥的地方。圖上TestCase和TestResult之間是雙向的依賴關(guān)系,而在UML類圖的關(guān)系中指出:依賴關(guān)系總是單向的。就讓我們來(lái)看看這這個(gè)可疑的地方。
TestCase中的代碼:
/**
* Runs the test case and collects the results in TestResult.
*/
public void run(TestResult result) {
//調(diào)用了result中的run方法,
//TestResult按照名稱來(lái)看應(yīng)該是一個(gè)記錄測(cè)試結(jié)果的類,怎么還能run?
?????? result.run(this);
}
相應(yīng)得TestResult中的代碼:
/**
* Runs a TestCase.
*/
protected void run(final TestCase test) {
?????? //開(kāi)始測(cè)試
?????? startTest(test);
?????? //這個(gè)匿名內(nèi)類的使用一會(huì)再講
?????? Protectable p= new Protectable() {
??????????????public void protect() throws Throwable {
????????????????????//天那,這里又調(diào)用了TestCase里面的runBare方法
????????????????????test.runBare();
???????????? }
??????};
?????? runProtected(test, p); //這個(gè)方法就是要執(zhí)行上面制定的匿名內(nèi)類
?????? endTest(test);
}
TestResult中runProtected方法:
public void runProtected(final Test test, Protectable p) {
?????? try {
??????????????p.protect();
?????? }
?????? catch (AssertionFailedError e) {
??????????????addFailure(test, e);??????????????//給TestResult添加失敗記錄
?????? }
?????? catch (ThreadDeath e) { // don't catch ThreadDeath by accident
??????????????throw e;
?????? }
?????? catch (Throwable e) {
??????????????addError(test, e);????????//給TestResult添加出錯(cuò)記錄
?????? }
}
為什么JUnit里面會(huì)出現(xiàn)這樣奇怪的依賴關(guān)系,還有違反單一職責(zé)原則的TestResult?當(dāng)我看到j(luò)unit.extentions包中的TestSetup時(shí),也許我猜到了作者的用意。我們來(lái)看下TestSetup中有關(guān)的代碼:
public void run(final TestResult result) {
?????? //又看到了上面類似的匿名內(nèi)部類
?????? Protectable p= new Protectable() {
??????????????public void protect() throws Exception {
???????????????????? //不過(guò)這個(gè)內(nèi)部類里面的實(shí)現(xiàn)有所不同
setUp();
???????????????????? basicRun(result);
???????????????????? tearDown();
??????????????}
?????? };
?????? //調(diào)用了TestResult中的runProtected方法來(lái)執(zhí)行上面的實(shí)現(xiàn)
?????? result.runProtected(this, p);
}
這個(gè)類的產(chǎn)生是為了彌補(bǔ)TestCase類的一個(gè)小小的缺陷(具體請(qǐng)見(jiàn)下部分)。注意到在這個(gè)類里面也有和TestResult類似的匿名內(nèi)部類。這種匿名內(nèi)部類全是Protected接口的無(wú)名實(shí)現(xiàn),這里的目的我認(rèn)為有兩點(diǎn):
1)????????由于內(nèi)部類可以在接下來(lái)的情景中完全不可見(jiàn),而且不被任何人使用,因此也就隱藏了接口的實(shí)現(xiàn)細(xì)節(jié)。
2)????????為了提高可重用性,而使用內(nèi)部類比較快捷。這樣不管你protect方法里面具體執(zhí)行什么,對(duì)它錯(cuò)誤、失敗、異常捕捉的代碼(TestResult中的runProtected方法)就可以重用了。
這也正是為什么會(huì)出現(xiàn)上面那樣奇怪的依賴關(guān)系:為了復(fù)用,就要讓runProtected方法放在一個(gè)TestCase和TestSetup都能調(diào)用的地方。
不過(guò)我認(rèn)為為了復(fù)用而破壞了系統(tǒng)良好的結(jié)構(gòu)和可讀性,是需要仔細(xì)斟酌的。JUnit這樣的設(shè)計(jì)估計(jì)是為了以后框架多次擴(kuò)展后的重用考慮的。
說(shuō)完了讓我費(fèi)解的問(wèn)題。談?wù)勎矣X(jué)得JUnit框架中最讓我感嘆的地方,那就是小小的框架里面使用了很多設(shè)計(jì)模式在里面。而這些模式的使用也正是為了體現(xiàn)出整個(gè)框架結(jié)構(gòu)的簡(jiǎn)潔、可擴(kuò)展。我將粗略的分析如下(模式應(yīng)用的詳細(xì)內(nèi)容請(qǐng)關(guān)注我關(guān)于設(shè)計(jì)模式的文章)。先看看在junit.framework里面使用的設(shè)計(jì)模式。
?????? 命令模式:作為輔助單元測(cè)試的框架,開(kāi)發(fā)人員在使用它的時(shí)候,應(yīng)該僅僅關(guān)心測(cè)試用例的編寫,JUnit只是一個(gè)測(cè)試用例的執(zhí)行器和結(jié)果查看器,不應(yīng)該關(guān)心太多關(guān)于這個(gè)框架的細(xì)節(jié)。而對(duì)于JUnit來(lái)說(shuō),它并不需要知道請(qǐng)求TestCase的操作信息,僅把它當(dāng)作一種命令來(lái)執(zhí)行,然后把執(zhí)行測(cè)試結(jié)果發(fā)給開(kāi)發(fā)人員。命令模式正是為了達(dá)到這種送耦合的目的。
?????? 組合模式:當(dāng)系統(tǒng)的測(cè)試用例慢慢變得多起來(lái),挨個(gè)運(yùn)行測(cè)試用例就成了一個(gè)棘手的問(wèn)題。作為一個(gè)方便使用的單元測(cè)試框架,這一點(diǎn)是必須解決的。因此JUnit里面提供了TestSuite的功能,它允許將多個(gè)測(cè)試用例放到一個(gè)TestSuite里面來(lái)一次執(zhí)行;而且要進(jìn)一步的支持TestSuite里面套TestSuite的功能。使用組合模式能夠很好的解決這個(gè)問(wèn)題。
在上面我們已經(jīng)提到了junit.extentions包中的內(nèi)容TestSetup。來(lái)看看整個(gè)包的結(jié)構(gòu)吧。

先簡(jiǎn)要的介紹下包中各個(gè)類的功能。ActiveTestSuite對(duì)TestSuite進(jìn)行了改進(jìn),使得每個(gè)test運(yùn)行在一個(gè)單獨(dú)的線程里面,并且只到所有的線程都結(jié)束了才會(huì)結(jié)束整個(gè)測(cè)試。ExceptionTestCase是對(duì)TestCase進(jìn)行的改進(jìn),可以方便的判斷測(cè)試類是否拋出了期望的異常。而剩下的三個(gè)類,大概你看的出來(lái)是使用了裝飾模式來(lái)設(shè)計(jì)的。其中TestDecorator為具體裝飾類制定好了使用規(guī)則,RepeatedTest和TestSetup則是具體實(shí)現(xiàn)的裝飾類。
那為什么extentions包中ActiveTestSuite和ExceptionTestCase沒(méi)有使用裝飾模式呢?原因在于裝飾模式在結(jié)構(gòu)上要求存在類似于組合模式的遞歸。而對(duì)于已有的TestCase和TestSuite來(lái)說(shuō),直接繼承它們要比構(gòu)建一個(gè)新的遞歸結(jié)構(gòu)要來(lái)得快得多而且簡(jiǎn)單;并且這些增強(qiáng)功能都只是針對(duì)TestCase或者TestSuite。使用了裝飾模式來(lái)擴(kuò)展的類與以上不同的是,它們功能的增強(qiáng)是針對(duì)任何Test實(shí)現(xiàn)的。如果不采用裝飾模式同樣的功能要為TestCase、TestSuite以及以后的其他Test實(shí)現(xiàn)分別寫出子類。因此使用裝飾模式能夠很巧妙的解決這個(gè)問(wèn)題。
下面來(lái)介紹下junit.runner包。上面已經(jīng)提到,對(duì)于JUnit使用者來(lái)說(shuō),它可說(shuō)是完全透明的,這個(gè)包里面提供了JUnit自己的測(cè)試類加載。下面就是包中所有類的關(guān)系圖。

沒(méi)有什么好講的,都是使用反射機(jī)制來(lái)將測(cè)試類加載進(jìn)來(lái),還有讀取properties文件的操作。如果想學(xué)習(xí)下反射機(jī)制的應(yīng)用可以閱讀這部分的源碼。
剩下的三個(gè)包這里也不作介紹,大部分的內(nèi)容都是GUI的繪制(當(dāng)然junit.textui包除外)。
JUnit中還使用了觀察者模式來(lái)完成單元測(cè)試結(jié)果的自動(dòng)更新(詳細(xì)內(nèi)容請(qǐng)見(jiàn)我關(guān)于觀察者模式的文章)。
這樣,對(duì)JUnit的整體框架有了全面的認(rèn)識(shí)。總體來(lái)說(shuō)各個(gè)包分工明確,設(shè)計(jì)上采用了必要的設(shè)計(jì)模式來(lái)增強(qiáng)了擴(kuò)展性和重用性,很值得學(xué)習(xí)和借鑒。
posted on 2005-10-16 13:06
Sung 閱讀(708)
評(píng)論(0) 編輯 收藏 所屬分類:
Java