Java單元測試技術1


摘要本文針對當前業軟開發現狀,先分析了WEB開發的技術特點和單元測試要解決的問題,然后分別闡述了解決這些問題的單元測試技術,內容包括:JUnit、測試樁構建、訪問數據庫的Java代碼測試、Struts框架測試、服務器布署環境下的組件測試、Spring下的單元測試,以及覆蓋率檢查技術,最后還談到了測試自動化技術以及希望在業軟推廣的自動化測試框架和它帶來的好處。另外,隨本文還附有例子代碼供大家參考。

關鍵詞:JavaWeb開發、單元測試、工具、JUnitEasyMockDBUnitStrutsStrutsTestCaseCactusSpringCobertura、覆蓋率檢查、自動化測試、例子代碼。

 

單元測試與開發技術密切相關,業軟基於Java的開發一般是WEB應用開發,涉及的開發技術繁多,尤其是現在的開源軟件盛行,更給Java增添了無窮的活力和生機,同時也給單元測試增加了復雜度,我們推行單元測試面臨着前所未有的挑戰,難怪項目組抱怨單元測試難測或測不起來。本文試圖給出基於業軟開發現狀的Java單元測試完整解決方案,以便更加有效地在業軟推行單元測試。下面我們從分析開發技術特點以及對單元測試的影響說起。

1      基於Java開發的技術特點

u  容器管理的組件開發

開發WEB應用程序,實質上就是在開發一系列組件。組件的類型有很多,JavaBeanServletFilterJSP TaglibEJBSpring Bean,等等。這些組件一般是不能獨立運行的,需要將它們布署到WEB服務器,通過與WEB容器或EJB容器交互才能實現一定的業務邏輯,也就是說,組件依賴的許多對象是運行時由容器創建的,如HttpServletRequestHttpServletResponseServletContextSessionContextFilterChainPageContext,等等,這就面臨着單元測試時這些容器對象如何生成的問題。對於這些組件的測試一般我們有兩種單元測試方法:一種是對被測組件進行隔離測試,組件依賴的服務器環境對象用樁取代,它的缺點是構建太麻煩;另一種是將組件運行在真實服務器環境下,有別於系統測試,被測對象是我們主動在測試代碼中創建的,它的優點是更接近於真實環境、免除了構建的工作量。

u  頁面顯示的視圖開發

有很多技術用於將視圖與業務邏輯分開,如StrutsJSFWebWorkSpring等,頁面開發的技術也有很多,如HtmlXMLJSPJavaScriptVelocity,等等,對於這些頁面文件,技術上很難進行編譯靜態檢查,對它們的測試雖然有一些工具支持,但效果均不理想,而且代碼Review大家反映也很難發現實質性的頁面問題,如何確保頁面文件的質量一直是單元測試要解決的問題。

u  框架復雜

WEB開發使用的框架很少有自己獨立設計的,一般都會使用現成的架構,如StrutsJSFWebWorkEJBSpringHibernateiBATIS等等,不同的框架,能夠支持單元測試的程度也是不一樣的。

u  數據庫訪問技術多

業軟的WEB應用開發很少有不訪問數據庫的,因為多是面向業務的開發,業務離不開數據存儲。項目組可能會選擇不同的數據庫訪問技術,如JDBCEntityBeanSpringHibernateiBATIS等,對於涉及數據庫訪問的代碼如何做單元測試也是我們需要解決的問題。

2      單元測試技術需要解決的問題

有許多的單元測試技術和工具,綜合起來,無非是為了解決以下問題。

u  驅動(Driver)—驅動被測單元

單元不能獨立運行,必須實現調用它們的代碼,我們稱其為驅動代碼,其實最簡單的驅動就是實現main方法,大家常用的驅動典型工具就是JUnit

u  構建樁(Stub)—模擬被測單元依賴的對象

被測的孤立單元通常會對其它對象有依賴,這種依賴通常表現在:依賴對象通過被測方法參數傳入或者被測對象保存有依賴的對象引用,然后在被測方法中調用了依賴對象的方法。構造依賴對象,一方面我們可以直接將開發完成的並且之前已經過單元測試的代碼直接拿過來用,另一方面,也是更常用的方法,就是自己構建模擬對象,我們稱其為樁,但自己寫樁很麻煩,工作量大,現成構建樁的理想工具是EasyMock

u  驗證(Verify

用例執行是否成功,需要在測試中添加驗證點,需要將預期結果與測試執行獲得的實際結果進行比較,為此JUnit為我們提供了驗證的基本邏輯框架,其它工具可以基於它實現更復雜的驗證邏輯,如DBUnit實現的對數據庫表數據的驗證。

u  用例管理

常有同事提到用main方法也能實現對被測單元的驅動,但我覺得最大的不足是無法實現對用例的有效管理,為此JUnit為我們提供了用例管理的基礎框架,通過引入測試套的概念將用例有效地組織起來。

u  結果輸出(Report

測試結束后要能夠將本次運行的結果情況形成報告,並以圖形化直觀的形式報告給用戶。JUnit也為我們做到了,尤其是IDEJUnit的集成,使我們在開發過程中做單元測試變得更加方便。

u  覆蓋率檢查

公司要求被測代碼要求達到語句的100%覆蓋,是否覆蓋到了,我們可以借助覆蓋率檢查工具,做測試執行的同時進行覆蓋率檢查,對未覆蓋到的代碼可能會發現兩類問題:不可達代碼,這樣的代碼需要優化;用例設計不充分,這時就要及時補充用例。常用的覆蓋率檢查工具有PureCoverageCobertura

u  測試自動化

實現測試自動化的好處大家都很明白,方便回歸測試,節省了工作量;另一個好處是便於對測試的監控,這一點我們在后面會談到。我們模索系統測試自動化已有多年,但效果都不理想,主要原因我覺得和系統測試本身的特點有關,因為系統測試是站在用戶角度看系統功能的整體表現(這其中最討厭的是我們經常還有需求變更,如何做到以不變應萬變,我們曾經嘗試過,但效果均不理想)。但單元測試不同於系統測試,單元不能獨立運行,需要我們實現驅動代碼,它的這個特點決定了實現單元測試自動化是非常容易也是順理成章的事。

3      Java單元測試技術總覽

基於Java的開發一般分為Java應用程序開發Web應用程序開發,目前我們已經有了針對Java開發的較為全面的單元測試解決方案,可以說,每個開發領域都已經有了相應的單元測試技術支持,以下是一個簡單示意圖,后面的章節我們針對每項技術分別闡述。

 

4      單元測試的基礎框架(JUnit

JUnitJava單元測試的經典之作,它的功能包括:提供實現測試用例的框架,並驅動測試執行;提供驗證邏輯;用例執行結果統計與報告;測試套、測試用例管理。JUnitJava單元測試的基礎框架,Java領域的其它測試工具和技術一般都是基於它的擴展,如我們后面提到的:EasyMockStrutsTestCaseDBUnit等,另外Spring中的單元測試支持也是基於它的。

使用JUnit只需要將junit.jar加入CLASSPATH路徑即可。因為Java編譯后不需要鏈接(不同於C++),加上流行的IDE,如EclipseJBuilderNetBeans等,都集成有JUnit,我們可以在IDE中邊寫代碼、邊做測試、邊做重構。所以實際上,Java的單元測試比起C++來更方便。

1)        基本概念

測試用例(TestCase:同我們公司的測試用例概念,但用例是以測試方法(測試代碼)的形式體現的,一個測試方法對應一個測試用例,JUnit框架提供了抽象類TestCase,我們要做的就是繼承該類,增加測試方法,在測試方法中實現對被測試代碼的調用,並增加驗證點。同一個類可以有很多個測試方法,你只要向測試框架提供這個實現類就可以了,框架負責生成測試用例對象(就是實例化測試用例),一個測試方法生成一個測試用例對象,用例有用例名稱,取的就是測試方法名。測試方法的原型(函數定義)必須滿足以下要求:

ü  當然是公共方法

ü  方法名以“test”開頭

ü  方法無參數

ü  方法的返回值類型為void

框架在裝載你的測試用例類的時候,使用JAVA的反射技術,基於以上條件找到你的測試方法,並創建用例對象。

測試套(TestSuite:若干個測試用例對象組成一個測試套,測試套與測試用例的關系就如同文件夾與文件的關系一樣,測試套包含測試用例,當然也可以包含子測試套。之所以這樣,是便於測試用例的組織和管理。JUnit提供了可直接實例化的TestSuite類,實現了很多功能,例如:分析一個測試用例類,對每一個測試方法生成一個測試用例對象,將這個測試用例類的所有測試用例對象作為一個測試套;實現對測試用例的調用以及子測試套的遞歸調用(實際上是子測試套包括的用例)。

2)        測試用例類的基本結構

以下是測試用例類的基本結構:

public class HelloWorldTest extends TestCase

{

 

    /*

     * 每次用例執行前要執行的初始化方法

     */

    protected void setUp( ) throws Exception

    {

        super.setUp();

    }

 

    /*

     * 每次用例執行后要執行的清除功能

     */

    protected void tearDown( ) throws Exception

    {

        super.tearDown();

    }

 

    /*

     * 一個測試方法,在其中實現對被測單元的調用,並驗證

     */

    public final void testCalculate( )

    {

        //TODO 實現 calculate()

    }

 

}

其中TestCase基類由JUnit框架提供。

3)        框架常見類介紹

JUnit包含6個包(package):junit.awtuijunit.swinguijunit.textuijunit.extensionsjunit.frameworkjunit.runner。其中前三個包中包含了JUnit運行時的入口程序以及運行結果顯示界面,它們對於JUnit使用者來說基本是透明的。junit.runner包中包含了支持單元測試運行的一些基礎類以及自己的類加載器,它對於JUnit使用者來說是完全透明的。剩下的兩個包是和使用JUnit進行單元測試緊密聯系在一起的。其中junit.framework包含有編寫一般JUnit單元測試類必須要用到的JUnit類;而junit.extensions則是對framework包在功能上的一些必要擴展以及為更多的功能擴展留下的接口。我們常用的還是junit.framework包,所以下面我就將這個包下的類作一介紹。

 

以上是這個包下的類圖,為方便理解,我將Throwable類和Error類也放了進來,注意它們是JDK提供的類,非本包的類。

u  接口Test、抽象類TestCase、類TestSuite之間的關系

Test是接口(interface),定義有方法runTestCaseTestSuite都實現了該接口,前面我們提到TestSuite包含TestCase TestSuite有一個Vector成員,Vector的元素就是它包含的TestCase,當然也可以是子TestSuite,所以為便於管理,TestSuite只認Test接口,在它看來TestSuite包含的是Test對象,而不用細分為TestCase和子TestSuite。對測試套run方法的調用,相應地就會遞歸調用它所包含的用例run方法和子測試套run方法。這里使用到了Composite設計模式,另外,從命令調用(run命令)與命令實現分離的角度看又能看到Command設計模式的影子。

u  抽象類TestCase

抽象類TestCase實現了接口Test,以下是接口Test的一個方法原型:

public abstract void run(TestResult result);

TestCase對這個方法的實現如下:

public void run(TestResult result) {

     result.run(this);

}

對這個方法的調用又將主動權交給TestResult,由TestResult來執行對被測方法的調用,這樣便於TestResult跟蹤用例執行結果並記錄下來,以便作最后的統計分析。

轉到TestResult中這個方法的實現:

protected void run(final TestCase test) {

     startTest(test);

     Protectable p= new Protectable() {

        public void protect() throws Throwable {

            test.runBare();

        }

     };

     runProtected(test, p);

 

     endTest(test);

     //這個方法和上面的startTest用於調用TestListener接口對象,實現監聽機制。

}

在這個方法中調用了TestCaserunBare方法:

public void runBare() throws Throwable {

     setUp();

     try {

        runTest();

     }

     finally {

        tearDown();

     }

}

可以看到先調用了setUp,之后不管是否產生了異常,都一律調用了tearDown,這兩個方法在TestCase中空實現,我們的用例子類可以覆蓋(overload)這兩個方法,用於用例執行前的初始環境建立和用例執行后的環境清除。接下來我們關注TestCaserunTest方法,看它如何調用到我們的測試方法的。前面提到,TestCase維護了一個用例名稱,這個用例名稱在TestCase實例化時傳入,用例名稱就是測試方法名,知道了測試方法名,也就知道了測試方法原型,使用JAVA的反射機制實現了對測試方法的調用。注意這個測試方法正是TestCase子類需要實現的。

protected void runTest() throws Throwable {

        ......

     Method runMethod= null;

     try {

        runMethod= getClass().getMethod(fName, null);// fName是測試方法名

     } catch (NoSuchMethodException e) {

        ......

     }

     ……

     try {

        runMethod.invoke(this, new Class[0]);

     }

        ......

}

u  TestSuite

TestSuite不需要我們再子類化了,它維護了對一組測試用例對象的引用(包括它所包含的嵌套TestSuite對象),它也實現了Test的接口,run方法的實現就是遞歸調用它所引用的對象的run方法,下面我們談一下給定一個TestCase子類(指類定義),TestSuite是如何針對其中定義的每個測試方法生成對應的TestCase對象的。以下是TestSuite的兩個相關方法,一個是構造方法(給定用例類,生成該測試套下的用例對象)和成員方法(給定用例類,生成子測試套下的用例對象)。

public TestSuite(final Class theClass){ }

public void addTestSuite(Class testClass) { }

這兩個方法的實現相似,基本實現思路是根據JAVA的反射技術,找出用例定義的滿足以下條件的成員方法(測試方法)

條件1:當然是公共方法

條件2:方法名以“test”開頭

條件3:方法無參數

條件4:方法的返回值類型為void

對應每一個這樣的方法生成該用例類的一個對象,並將用例名初始化為該方法名。測試套下的用例對象就是這樣生成。

u  Assert

AssertTestCase的基類,其中定義了可供TestCase使用的大量靜態方法,用於實際返回值與預期值的比較,這些方法名都是以assert開頭,在我們自己寫的測試方法中可以使用這些方法用於測試結果的驗證。要注意的是JUnit對測試失敗(實際值與預期值不符)是以拋出AssertionFailedError異常的形式體現的。我們看一個assert方法的典型實現:

static public void assertTrue(String message, boolean condition) {

     if (!condition)

        fail(message);

}

static public void fail(String message) {

     throw new AssertionFailedError(message);

}

一旦驗證邏輯驗證失敗,該用例就會拋出AssertionFailedError異常,TestResult記錄下測試失敗的該用例對象引用以及它所拋出的異常,這兩者的對應關系是TestFailure類體現的,它用於維護用例對象與異常(注意異常的引用類型是所有異常的基類Throwable)的對應關系。TestResult有一個Vector類型成員變量fFailures,它的元素就是TestFailure對象。

用例執行中除了可能會捕獲到AssertionFailedError異常,被測代碼有可能會拋出其它異常,包括RuntimeException運行期異常、JAVA虛擬機拋出的Error異常,拋出了這樣的異常,也意味着用例執行失敗,為此TestResult還有另一個Vector類型成員變量fErrors,它的元素也是TestFailure對象,不過它維護的是用例對象與非AssertionFailedError異常的對應關系。

在用例執行結果中分別顯示出因拋出AssertionFailedError異常而失敗的用例個數(故障,Failure),以及因拋出非AssertionFailedError異常而失敗的用例個數(錯誤,Error)。

以下是在Eclipse中的的執行結果圖示:

 

4)        JUnit4.X的用例設計

JUnit4.0之后的版本在用例設計上有較大的改變,主要使用了JDK5.0Annotation新特性,但業軟有相當多的版本還未升級到JDK5.0,另外,部分基於JUnit的單元測試工具還未升級到JUnit4.X,所以短期內就會出現JUnit新老版本共存的局面,但不管如何,JUnit中設計用例都很方便。下面我們簡單看一下JUnit4.0以后的用例設計。

u  用例類不再需要繼承自TestCase基類

u  測試方法名也不需要必須以test開頭

u  幾個Annotation標識

Ø  @Test—標識一個測試方法(對應一個用例)

l  expected屬性可以標識該方法執行期望會拋出什么異常

l  timeout屬性可以指定該方法必須在指定的時間內執行結束

l  驗證失敗拋出JDK自帶的AssertionError異常

Ø  @Before —每個測試方法運行前要執行的方法,相關於以前的setUp()

Ø  @After —每個測試方法運行后要執行的方法,相關於以前的tearDown()

Ø  @Ignore —暫時不運行該測試方法

另外,新版本的EclipseANT已經全面支持JUnit4.X


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM