Sung in Blog

                     一些技術文章 & 一些生活雜碎
          一、引子



          JUnit源碼是我仔細閱讀過的第一個開源項目源碼。閱讀高手寫的代碼能學到一些好的編程風格和實現思路,這是提高自己編程水平行之有效的方法,因此早就想看看這些赫赫有名的框架是怎么回事了。今天就拿最簡單的JUnit下手,也算開始自己的源碼分析之路。

          JUnit作為最著名的單元測試框架,由兩位業界有名人士協力完成,已經經歷了多次版本升級(了解JUnit基礎、JUnit實踐)。JUnit總體來說短小而精悍,有不少值得我們借鑒的經驗在里面;但是也有一些不足存在,當然這對于任何程序來說都是難免的。


          下面我們將從整體(宏觀)和細節(微觀)兩方面來分析JUnit源碼,以下分析基于3.8.1版。


          二、宏觀——架構與模式



          打開源碼文件,你會發現JUnit源碼被分配到6個包中:junit.awtui、junit.swingui、junit.textui、junit.extensions、junit.framework、junit.runner。其中前三個包中包含了JUnit運行時的入口程序以及運行結果顯示界面,它們對于JUnit使用者來說基本是透明的。junit.runner包中包含了支持單元測試運行的一些基礎類以及自己的類加載器,它對于JUnit使用者來說是完全透明的。



          剩下的兩個包是和使用JUnit進行單元測試緊密聯系在一起的。其中junit.framework包含有編寫一般JUnit單元測試類必須是用到的JUnit類;而junit.extensions則是對framework包在功能上的一些必要擴展以及為更多的功能擴展留下的接口。



          JUnit提倡單元測試的簡單化和自動化。這就要求JUnit的使用要簡單化,而且要很容易的實現自動化測試。整個JUnit的設計大概也是遵循這個前提吧。整個框架的骨干僅有三個類組成(下圖所示)。
          image

          ?????? 如果你掌握了TestCase、TestSuite、BaseTestRunner的工作方式,那么你就可以隨心所欲的編寫測試代碼了。



          ?????? 下面我們來看看junit.framework中類之間的關系,下圖是我根據源代碼分析出來的,大部分關系都表示了出來。

          image

          先來看看各個類的職責。Assert類提供了JUnit使用的一整套的斷言,這套斷言都被TestCase繼承下來,Assert也就變成了透明的。Test接口是為了統一TestCase和TestSuite的類型;而TestCase里面提供了運行單元測試類的方法;在TestSuite中則提供了加載單元測試類,檢驗測試類格式等等的方法。TestResult故名思意就是提供存放測試結果的地方,但是在JUnit中它還帶有一點控制器的功能。


          在這里指出其中我認為有些不妥的地方。圖上TestCase和TestResult之間是雙向的依賴關系,而在UML類圖的關系中指出:依賴關系總是單向的。就讓我們來看看這這個可疑的地方。


          TestCase中的代碼:

          /**

          * Runs the test case and collects the results in TestResult.
          */

          public void run(TestResult result) {
          //調用了result中的run方法,
          //TestResult按照名稱來看應該是一個記錄測試結果的類,怎么還能run?
          ?????? result.run(this);

          }

          相應得TestResult中的代碼:

          /**
          * Runs a TestCase.
          */
          protected void run(final TestCase test) {

          ?????? //開始測試
          ?????? startTest(test);

          ?????? //這個匿名內類的使用一會再講

          ?????? Protectable p= new Protectable() {
          ??????????????public void protect() throws Throwable {

          ????????????????????//天那,這里又調用了TestCase里面的runBare方法

          ????????????????????test.runBare();

          ???????????? }

          ??????};

          ?????? runProtected(test, p); //這個方法就是要執行上面制定的匿名內類
          ?????? 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添加出錯記錄



          ?????? }



          }




          為什么JUnit里面會出現這樣奇怪的依賴關系,還有違反單一職責原則的TestResult?當我看到junit.extentions包中的TestSetup時,也許我猜到了作者的用意。我們來看下TestSetup中有關的代碼:



          public void run(final TestResult result) {



          ?????? //又看到了上面類似的匿名內部類



          ?????? Protectable p= new Protectable() {



          ??????????????public void protect() throws Exception {



          ???????????????????? //不過這個內部類里面的實現有所不同



          setUp();



          ???????????????????? basicRun(result);



          ???????????????????? tearDown();



          ??????????????}



          ?????? };



          ?????? //調用了TestResult中的runProtected方法來執行上面的實現



          ?????? result.runProtected(this, p);



          }




          這個類的產生是為了彌補TestCase類的一個小小的缺陷(具體請見下部分)。注意到在這個類里面也有和TestResult類似的匿名內部類。這種匿名內部類全是Protected接口的無名實現,這里的目的我認為有兩點:



          1)????????由于內部類可以在接下來的情景中完全不可見,而且不被任何人使用,因此也就隱藏了接口的實現細節。



          2)????????為了提高可重用性,而使用內部類比較快捷。這樣不管你protect方法里面具體執行什么,對它錯誤、失敗、異常捕捉的代碼(TestResult中的runProtected方法)就可以重用了。



          這也正是為什么會出現上面那樣奇怪的依賴關系:為了復用,就要讓runProtected方法放在一個TestCase和TestSetup都能調用的地方。



          不過我認為為了復用而破壞了系統良好的結構和可讀性,是需要仔細斟酌的。JUnit這樣的設計估計是為了以后框架多次擴展后的重用考慮的。



          說完了讓我費解的問題。談談我覺得JUnit框架中最讓我感嘆的地方,那就是小小的框架里面使用了很多設計模式在里面。而這些模式的使用也正是為了體現出整個框架結構的簡潔、可擴展。我將粗略的分析如下(模式應用的詳細內容請關注我關于設計模式的文章)。先看看在junit.framework里面使用的設計模式。



          ?????? 命令模式:作為輔助單元測試的框架,開發人員在使用它的時候,應該僅僅關心測試用例的編寫,JUnit只是一個測試用例的執行器和結果查看器,不應該關心太多關于這個框架的細節。而對于JUnit來說,它并不需要知道請求TestCase的操作信息,僅把它當作一種命令來執行,然后把執行測試結果發給開發人員。命令模式正是為了達到這種送耦合的目的。



          ?????? 組合模式:當系統的測試用例慢慢變得多起來,挨個運行測試用例就成了一個棘手的問題。作為一個方便使用的單元測試框架,這一點是必須解決的。因此JUnit里面提供了TestSuite的功能,它允許將多個測試用例放到一個TestSuite里面來一次執行;而且要進一步的支持TestSuite里面套TestSuite的功能。使用組合模式能夠很好的解決這個問題。
          在上面我們已經提到了junit.extentions包中的內容TestSetup。來看看整個包的結構吧。image


          先簡要的介紹下包中各個類的功能。ActiveTestSuite對TestSuite進行了改進,使得每個test運行在一個單獨的線程里面,并且只到所有的線程都結束了才會結束整個測試。ExceptionTestCase是對TestCase進行的改進,可以方便的判斷測試類是否拋出了期望的異常。而剩下的三個類,大概你看的出來是使用了裝飾模式來設計的。其中TestDecorator為具體裝飾類制定好了使用規則,RepeatedTest和TestSetup則是具體實現的裝飾類。



          那為什么extentions包中ActiveTestSuite和ExceptionTestCase沒有使用裝飾模式呢?原因在于裝飾模式在結構上要求存在類似于組合模式的遞歸。而對于已有的TestCase和TestSuite來說,直接繼承它們要比構建一個新的遞歸結構要來得快得多而且簡單;并且這些增強功能都只是針對TestCase或者TestSuite。使用了裝飾模式來擴展的類與以上不同的是,它們功能的增強是針對任何Test實現的。如果不采用裝飾模式同樣的功能要為TestCase、TestSuite以及以后的其他Test實現分別寫出子類。因此使用裝飾模式能夠很巧妙的解決這個問題。

          下面來介紹下junit.runner包。上面已經提到,對于JUnit使用者來說,它可說是完全透明的,這個包里面提供了JUnit自己的測試類加載。下面就是包中所有類的關系圖。


          image

          沒有什么好講的,都是使用反射機制來將測試類加載進來,還有讀取properties文件的操作。如果想學習下反射機制的應用可以閱讀這部分的源碼。

          剩下的三個包這里也不作介紹,大部分的內容都是GUI的繪制(當然junit.textui包除外)。

          JUnit中還使用了觀察者模式來完成單元測試結果的自動更新(詳細內容請見我關于觀察者模式的文章)。

          這樣,對JUnit的整體框架有了全面的認識。總體來說各個包分工明確,設計上采用了必要的設計模式來增強了擴展性和重用性,很值得學習和借鑒。
          posted on 2005-10-16 13:06 Sung 閱讀(709) 評論(0)  編輯  收藏 所屬分類: Java
          主站蜘蛛池模板: 湟源县| 宽城| 阿鲁科尔沁旗| 潼关县| 城固县| 石首市| 淳安县| 临夏县| 贵南县| 滦平县| 锡林郭勒盟| 格尔木市| 克东县| 永济市| 米脂县| 平舆县| 鄂伦春自治旗| 孙吴县| 海阳市| 屏边| 皮山县| 城固县| 大丰市| 沙坪坝区| 于都县| 秦安县| 新田县| 金塔县| 彰化市| 垦利县| 大足县| 嘉禾县| 舟曲县| 城固县| 汶川县| 天等县| 新兴县| 自治县| 鄱阳县| 容城县| 柯坪县|