程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 用JUnit框架實現Java單元測試

用JUnit框架實現Java單元測試

編輯:關於JAVA

隨著軟件項目的逐漸增大,軟件測試在軟件開發中的地位顯得越來越重要。如果軟件項目沒有良好的測試流程,隨著系統的增大,無論項目管理人員還是軟件開發人員都會對項目的前景失去信心,甚至會對項目的目標產生分歧,因為長期以來沒有對程序代碼和系統設計進行有效的控制,很多問題都被暫時掩蓋或逐漸演化成其他的問題。軟件開發周期越長,就會使得問題進化的版本越多,最後造成的結果是“剪不斷,理還亂”。

單元測試是整個測試流程中最基礎的部分,它們要求程序員盡可能早地發現問題,並給予控制,這是其一。另外,如果集成測試出現問題,它們可以幫助診斷。這樣就為在軟件開發流程中建立高效的事件反應機制打下了堅實基礎。

JUnit就是為Java程序開發者實現單元測試提供一種框架,使得Java單元測試更規范有效,並且更有利於測試的集成。

JUnit的內部結構

JUnit的軟件結構

JUnit 共有七個包,核心的包就是junit.framework 和junit.runner。Framework包負責整個測試對象的構架,Runner負責測試驅動。

JUnit的類結構

JUnit有四個重要的類:TestSuite、TestCase、TestResult、TestRunner。前三個類屬於Framework包,後一個類在不同的環境下是不同的。這裡使用的是文本測試環境,所以用的是 junit.textui.TestRunner。各個類的職責如下:

1.TestResult,負責收集TestCase所執行的結果,它將結果分為兩類,客戶可預測的Failure和沒有預測的Error。同時負責將測試結果轉發到TestListener(該接口由TestRunner繼承)處理;

2.TestRunner,客戶對象調用的起點,負責對整個測試流程的跟蹤。能夠顯示返回的測試結果,並且報告測試的進度。

3.TestSuite, 負責包裝和運行所有的TestCase。

4.TestCase, 客戶測試類所要繼承的類,負責測試時對客戶類進行初始化,以及測試方法調用。

另外還有兩個重要的接口:Test和TestListener。

1.Test, 包含兩個方法:run() 和countTestCases(),它是對測試動作特征的提取。

2.TestListener, 包含四個方法:addError()、addFailure()、startTest()和endTest(),它是對測試結果的處理以及測試驅動過程的動作特征的提取。

下面給出的兩個類圖(篇幅有限,只顯示主要部分)很好地闡明了類之間的關系,以及junit的設計目標(如圖1)。測試案例的類采用Composite模式。這樣,客戶的測試對象就轉變成一個“部分—整體”的層次結構。客戶代碼僅需要繼承類TestCase,就可以輕松的與已有的其他對象組合使用,從而使得單元測試的集成更加方便。

圖1 測試結構圖

圖2是測試跟蹤類圖。圖2左邊TestSuite包含了測試對象集合,右邊包含了測試結果集。具體如何處理結果,以及包含哪些測試對象,並沒有立即得出結論,而是盡量地延遲到具體實現的時候。例如,實現接口TestListener的JUnit中就含有:junit.awtui.TestRunner、junit.swingui. TestRunner、junit.ui.TestRunner等,甚至客戶用自己的類實現TestListener,從而達到多樣化的目的。

圖2 測試跟蹤圖

從以上兩個類圖,可以了解JUnit對單元測試的基本思路,這個框架的核心就是結果集和案例集。

JUnit的實現流程

典型的使用JUnit的方法就是繼承TestCase類,然後重載它的一些重要方法:setUp()、teardown()、runTest()(這些都是可選的),最後將這些客戶對象組裝到一個TestSuite對象中,交由 junit.textui.TestRunner.run (案例集) 驅動。下面分析案例集是如何運轉的。

圖3基本上闡述JUnit的測試流程架構。我們將從不同的角度來詳細分析這個圖。

圖3 測試序列圖

首先,從對象的創建上來分析。客戶類負責創建Suite和aTestRunner。注意,類TestRunner含有一個靜態函數Run(Test),它自創建本身,然後調用doRun()。客戶類調用的一般是該函數,其代碼如下:

static public void run(Test suite){
    TestRunner aTestRunner= new TestRunner();//新建測試驅動
    aTestRunner.doRun(suite, false);//用測試驅動運行測試集
}

Suite對象負責創建眾多的測試案例,並將它們包容到本身。客戶測試案例繼承TestCase類,它將類,而不是對象傳給Suite對象。Suite對象負責解析這些類、提取構造函數和待測試方法。以待測試方法為單位構造測試案例,測試案例的fName就是待測試方法名。測試結果集由aTestRunner創建。這似乎同先前闡述的類圖有些矛盾,那裡闡述了一個測試集可以包含很多個不同的測試驅動,似乎先創建結果集比較理想。顯然,這裡對測試結果的處理只采用了一種方式,所以這樣做同樣可行。

其次,從測試動作的執行上來分析,測試真正是從suite.run(result) 開始的。其代碼如下:

public void run(TestResult result){
   //從案例集中獲得所有測試案例,分別執行
   for (Enumeration e= tests(); e.hasMoreElements(); )
   {
    if (result.shouldStop() )
    break;
    Test test= (Test)e.nextElement();
    runTest(test, result);
   }
}

一旦測試案例開始執行,首先使用一個回調策略將自身交由Result。這樣做的每一步測試,測試驅動aTest Runner都可以跟蹤處理。這無形中建立了一個龐大的監視系統,隨時都可以對所發生的事件給予不同等級的關注。

我們分析一下涉及到的動作行為的設計模式:

1. Template Method (模板方法)類行為模式,它的實質就是首先建立方法的骨架,而盡可能地將方法的具體實現向後推移。TestCase.runBare()就采用了這種模式,客戶類均可以重載它的三個方法,這樣使得測試的可伸縮性得到提高。

public void runBare() throws Throwable{
   setUp();
   try {runTest();}
   finally {tearDown();}
}

2. Command (命令)對象行為模式,其實質就是將動作封裝為一個對象,而不關心動作的接收者。這樣動作的接收者可以一直到動作具體執行時才需確定。接口Test就是一個Command集,使得不同類的不同測試方法可以通過同一種接口Test構造其框架結構。這樣對測試的集成帶來了很多方便。

JUnit的Exception的拋出機制

JUnit的異常層次分為三層:1.Failure,客戶預知的測試失敗,可以被Assert方法檢測到;2. Error,客戶測試的意外造成的;3.Systemerror, JUnit的線程死亡級異常,這種情況一般很少發生。JUnit的這三種異常在TestResult類的RunProtected()方法得到很好體現。這裡用Protectable接口封裝了Test的執行方法,其實p.protect執行的就是test.runBare()。

public void runProtected(final Test test, Protectable p){
   try {p.protect();}
   catch (AssertionFailedError e)
   {addFailure(test, e);}
   catch (ThreadDeath e)
   {rethrow e;}
   catch (Throwable e)
   {addError(test, e);}
}

代碼首先檢查是否是Assertion FailedError,然後判斷是否是嚴重的ThreadDeath。這種異常必須Rethrow,才能保證線程真正的死亡,如果不是,說明它是一種意外。

前兩種異常均保存在測試結果集中,等到整個測試完成,依次打印出來供客戶參考。

實施JUnit的幾點建議

從以上的分析中,可以了解JUnit的結構和流程,但是在實際應用JUnit時,有幾點建議還需要說明,如下:

1. 客戶類可以重載runTest(),它的缺省實現是調用方法名為fName的測試方法。如果客戶不是使用TestSuite加載TestCase,就尤其需要對其重載,當然這種方式並不贊成使用,不利於集成。另外,setUp()和tearDown()的功能似乎與構造函數雷同,但如果測試案例之間具有類繼承關系,采用構造函數初始化一些參數就會造成數據的混亂,不利於判定測試結果的有效性。

2. 待測試函數的調用順序是不確定的,采用的數據結構是Vector()。如果需要有順序關系,可以將它們組合到一起,然後用同一個測試方法。

3. 為了使測試結果清晰明了,程序中最好不要有打印輸出,要麼程序的打印輸出與JUnit測試的打印輸出不要用同一個數據源System.out。其實這是兩種測試習慣,直接打印輸出是較傳統的,從測試動機上考慮它也是較隨意的,並且結果需要人工觀察。如果直接打印輸出較多的話,觀察者可能無法獲得滿意的結果。

此外,如何擴展這個測試框架呢? junit.extensions包給出了幾點提示。我們可以使用junit.extensions. ActiveTest在不同的線程中運行一個測試實例。 對於要對測試案例添加新的功能可以采用Decorator模式,可以參考junit.extensions.TestDecorator以及它的子類junit.extensions.TestSetup、junit.extensions.RepeatedTest。這些僅僅提供了一些拓寬的思路,涉及到具體測試目標,還需進一步地挖掘。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved