使用JUnit可以大量減少Java代碼中程序錯誤的個數,JUnit是一種流行的單元測試框架,用于在發布代碼之前對其進行單元測試?,F在讓我們來詳細研究如何使用諸如JUnit、Ant和Oracle9i JDeveloper等工具來編寫和運行單元測試。
為什么使用JUnit?
多數開發人員都同意在發布代碼之前應當對其進行測試,并利用工具進行回歸(regression)測試。做這項工作的一個簡單方法是在所有Java類中以main()方法實施測試。例如,假設使用ISO格式(這意味著有一個以這一格式作為參數的構造器和返回一個格式化的ISO字符串的toString()方法)以及一個GMT時區來編寫一個Date的子類。清單1 就是這個類的一個簡單實現。
不過,這種測試方法并不需要單元測試限定語(qualifier),原因如下:
JUnit框架就是設計用來解決這些問題的。這一框架主要是所有測試實例(稱為"TestCase")的一個父類,并提供工具來運行所編寫的測試、生成報告及定義測試包(test suite)。
讓我們為IsoDate類編寫一個測試:這個IsoDateTest類類似于:
import java.text.ParseException; import junit.framework.TestCase; /** * Test case for <code>IsoDate</code>. */ public class IsoDateTest extends TestCase { public void testIsoDate() throws Exception { IsoDate epoch=new IsoDate( "1970-01-01 00:00:00 GMT"); assertEquals(0,epoch.getTime()); IsoDate eon=new IsoDate( "2001-09-09 01:46:40 GMT"); assertEquals( 1000000000L*1000,eon.getTime()); } public void testToString() throws ParseException { IsoDate epoch=new IsoDate(0); assertEquals("1970-01-01 00:00:00 GMT",epoch.toString()); IsoDate eon=new IsoDate( 1000000000L*1000); assertEquals("2001-09-09 01:46:40 GMT",eon.toString()); } }
本例中要注意的重點是已經編寫了一個用于測試的獨立類,因此可以對這些文件進行過濾,以避免將這一代碼嵌入到將要發布的文檔中。另外,本例還為你希望在你的代碼中測試的每個方法編寫了一個專用測試方法,因此你將確切地知道需要對哪些方法進行測試、哪些方法工作正常以及哪些方法工作不正常。如果在編寫實施文檔之前已經編寫了該測試,你就可以利用它來衡量工作的進展情況。
安裝并運行JUnit
要運行此示例測試實例,必須首先下載并安裝JUnit。JUnit的最新版本可以在JUnit的網站 www.junit.org免費下載。該軟件包很?。s400KB),但其中包括了源代碼和文檔。要安裝此程序,應首先對該軟件包進行解壓縮(junitxxx.zip)。它將創建一個目錄(junitxxx),在此目錄下有文檔(在doc目錄中)、框架的應用編程接口(API)文檔(在javadoc目錄中)、運行程序的庫文件(junit.jar)以及示例測試實例(在junit目錄中)。截至我撰寫本文時,JUnit的最新版本為3.8.1,我是在此版本上對示例進行測試的。
![]() |
要運行此測試實例,將源文件(IsoDate.java和IsoDateTest.java)拷貝到Junit的安裝目錄下,打開終端,進入該目錄,然后輸入以下命令行(如果你正在使用UNIX):
export CLASSPATH=.:./junit.jar javac *.java 或者,如果你正在Windows,輸入以下命令行 set CLASSPATH=.;junit.jar javac *.java
這些命令行對CLASSPATH進行設置,使其包含當前目錄中的類和junit.jar庫,并編譯Java源文件。
要在終端上運行該測試,輸入以下命令行:
java junit.textui.TestRunner IsoDateTest
此命令行將運行該測試,并在圖 1所示的控制臺上顯示測試結果。
才在此工具可以運行類名被傳遞到命令行中的單個測試。注意:只有對命令行的最后測試才在考慮之內,以前的測試都被忽略了。(看起來像一個程序錯誤,是吧?)
JUnit還提供了利用AWT(抽象窗口工具包)或Swing運行測試的圖形界面。為了利用此圖形界面運行測試,在終端上輸入以下命令行:
java junit.awtui.TestRunner IsoDateTest
或者使用Swing界面:
java junit.swingui.TestRunner IsoDateTest
此命令行將顯示圖 2所示的界面。要選擇一個測試并使其運行,點擊帶有三個點的按鈕。這將顯示CLASSPATH(還有測試包,但我們將在后面討論)中所有測試的列表。要運行測試,點擊"Run"按鈕。測試應當正確運行,并在圖 2所示的界面中顯示結果。
在此界面中你應當選中復選框"Reload Classes Every Run",以便運行器在運行測試類之前對它們進行重新加載。這樣就可以方便地編輯、編譯并運行測試,而不需要每次都啟動圖形界面。
在該復選框下面是一個進度條,在運行較大的測試包時,該進度條非常有用。運行的測試、錯誤和失敗的數量都會在進度條下面顯示出來。再下面是一個失敗列表和一個測試層次結構。失敗消息顯示在底部。通過點擊Test Hierarchy(測試層次結構)面板,然后再點擊窗口右上角的"Run"按鈕,即可運行單個測試方法。請記住,使用命令行工具是不可能做到這些的。
注意,當運行工具來啟動測試類時,這些類必須存在于CLASSPATH中。但是如果測試類存儲在jar文件中,那么即使這些jar文件存在于CLASSPATH中,JUnit也不能找到這些測試類。
![]() |
這并不是一種啟動測試的方便方法,但幸運的是,JUnit已經被集成到了其他工具(如Ant和Oracle9i JDeveloper)中,以幫助你開發測試并使測試能夠自動運行。
編寫Junit測試實例
你已經看到了測試類的源代碼對IsoDate實施進行了詢問?,F在讓我們來研究這樣的測試文件的實施。
測試實例由junit.frameword.TestCase繼承而來是為了利用JUnit框架的優點。這個類的名字就是在被測試類的名字上附加"Test"。因為你正在測試一個名為IsoDate的類,所以其測試類的名字就是IsoDateTest。為了訪問除私有方法之外的所有方法,這個類通常與被測類在同一個包中。
注意,你必須為你希望測試的在類中定義的每個方法都編寫一個方法。你要測試構造器或使用了ISO日期格式的方法,因此你將需要為以ISO格式的字符串作為參數的構造器和toString()方法編寫一個測試方法。其命名方式與測試類的命名方式類似:在被測試方法(或構造器)前面附加"test"。
測試方法的主體通過驗證assertion(斷言)對被測方法進行詢問。例如,在toString()實施的測試方法中,你希望確認該方法已經對時間的設定進行了很好的說明(對于UNIX系統來說,最初問世的時間為1970年1月1日的午夜)。要實施assertion,你可以使用Junit框架提供的assertion方法。這些方法在該框架的junit.framework.Assert類中被實施,并且可以在你的測試中被訪問,這是因為Assert是TestCase的父類。這些方法可與Java中的關鍵字assert(是在J2EE 1.4中新出現的)相比。一些assertion方法可以檢查原始類型(如布爾型、整型等)之間或對象之間是否相等(利用equals()方法檢查兩個對象是否相等)。其他assertion方法檢查兩個對象是否相同、一個對象是否為"空"或"非空",以及一個布爾值(通常由一個表達式生成)是"真"還是"假"。在表 1中對這些方法進行了總結。
對于那些采用浮點類型或雙精度類型參數的assertion,存在一個第三種方法,即采用一個delta值作為參數進行比較。另外還要注意,assertEquals()和assertSame()方法一般不會產生相同的結果。(兩個具有相同值的字符串可以不相同,因為它們是兩個具有不同內存地址的不同對象。)因此,assertEquals()將會驗證assertion的有效性,而assertSame()則不會。注意,對于表 1 中的每個assertion方法,你還有一種選擇,就是引入另一個參數,如果assertion失敗,該參數就會給出一條解釋性消息。例如,assertEquals(int 期望值, int 實際值)就可以與一個諸如assertEquals(字符串消息,int期望值,int實際值)的消息一起使用。
當一個assertion失敗時,該assertion方法會拋出一個AssertFailedError或ComparisonFailure。AssertionFailedError由java.lang.Error繼承而來,因此你不必在測試方法的throws語句中對其進行聲明。而ComparisonFailure由AssertionFailedError繼承而來,因此你也不必對其進行聲明。因為當一個assertion失敗時會在測試方法中拋出一個錯誤,所以后面的assertion將不會繼續運行??蚣懿蹲降竭@些錯誤并認定該測試已經失敗后,就會打印出一條說明錯誤的消息。這個消息由assertion生成,并且被傳遞到assertion方法(如果有的話)。
現在將下面一行語句添加到testIsoDate()方法的末尾:
assertEquals("This is a test",1,2);
現在編譯并運行測試:
$ javac *.java $ java junit.textui.TestRunner IsoDateTest .F. Time: 0,348 There was 1 failure: 1) testIsoDate(IsoDateTest)junit.framework .AssertionFailedError: This is a test expected:<1> but was:<2> at IsoDateTest.testIsoDate (IsoDateTest.java:29) FAILURES!!! Tests run: 2, Failures: 1, Errors: 0
JUnit為每個已處理的測試打印一個點,顯示字母"F"來表示失敗,并在assertion失敗時顯示一條消息。此消息由你發送到assertion方法的注釋和assertion的結果組成(自動生成)。從這里可以看出assertion方法的參數順序對于生成的消息非常重要。第一個參數是期望值,而第二個參數則是實際值。
如果在測試方法中出現了某種錯誤(例如,拋出了一個異常),該工具就會將其顯示為一個錯誤(而不是由assertion失敗而產生的一個"失敗")?,F在對IsoDateTest類進行修改,以將前面增加的一行語句用以下語句代替:
throw new Exception("This is a test");
然后編譯并運行測試:
$ javac *.java $ java junit.textui.TestRunner IsoDateTest .E. Time: 0,284 There was 1 error: 1) testIsoDate(IsoDateTest)java.lang. Exception: This is a test at IsoDate Test.testIsoDate(IsoDateTest.java:30) FAILURES!!! Tests run: 2, Failures: 0, Errors: 1
該工具將該異常顯示為一個錯誤。因此,一個錯誤表示一個錯誤的測試方法,而不是表示一個錯誤的測試實施。
Assert類還包括一個fail()方法(該版本帶有解釋性消息),該方法將通過拋出AssertionFailedError來中斷正在運行的測試。當你希望一個測試失敗而不會調用一個判定方法時,fail()方法是非常有用的。例如,如果一段代碼應當拋出一個異常而未拋出,那么可以調用fail()方法使該測試失敗,方法如下:
public void testIndexOutOfBounds() { try { ArrayList list=new ArrayList(); list.get(0); fail("IndexOutOfBoundsException not thrown"); } catch(IndexOutOfBoundsException e) {} }
JUnit的高級特性
在示例測試實例中,你已經同時運行了所有的測試。在現實中,你可能希望運行一個給定的測試方法來詢問你正編寫的實施方法,所以你需要定義一組要運行的測試。這就是框架的junit.framework.TestSuite類的目的,這個類其實只是一個容器,你可以向其中添加一系列測試。如果你正在進行toString()實施,并希望運行相應的測試方法,那么你可以通過重寫測試的suite()方法來通知運行器,方法如下:
public static Test suite() { TestSuite suite= new TestSuite(); suite.addTest(new IsoDateTest ("testToString")); return suite; }
在此方法中,你用具體示例說明了一個TestSuite對象,并向其中添加了測試。為了在方法級定義測試,你可以利用構造器將方法名作為參數使測試類實例化。此構造器可按如下方法實施:
public IsoDateTest(String name) { super(name); }
將上面的構造器和方法添加到IsoDateTest類(還需要引入junit.framework.Test和junit.framework.TestSuite),并在終端上輸入:
![]() |
圖3:選擇一個測試方法
$ javac *.java $ java junit.textui.TestRunner IsoDateTest . Time: 0,31 OK (1 test)
注意,在添加到測試包中的測試方法中,只運行了一個測試方法,即toString()方法。
你也可以利用圖形界面,通過在圖3所示的Test Hierarchy面板中選擇測試方法來運行一個給定的測試方法。但是,要注意當整個測試包被運行一次后,該面板將被填滿。
當你希望將一個測試實例中的所有測試方法添加到一個TestSuite對象中時,可以使用一個專用構造器,該構造器將此測試實例的類對象作為參數。例如,你可以使用IsoDateTest類實施suite()方法,方法如下:
public static Test suite() { return new TestSuite(IsoDateTest.class); }
還有一些情況,你可能希望運行一組由其他測試(如在工程發布之前的所有測試)組成的測試。在這種情況下,你必須編寫一個實施suite()方法的類,以建立希望運行的測試包。例如,假定你已經編寫了測試類Atest和Btest。為了定義那些包含了類ATest中的所有測試和在BTest中定義的測試包的集合,可以編寫下面的類:
import junit.framework.*; /** * TestSuite that runs all tests. */ public class AllTests { public static Test suite() { TestSuite suite= new TestSuite("All Tests"); suite.addTestSuite(ATest.class); suite.addTest(BTest.suite()); return suite; } }
你完全可以像運行單個測試實例那樣運行這個測試包。注意,如果一個測試在一個套件中添加了兩次,那么運行器將運行它兩次(測試包和運行器都不會檢查該測試是否是唯一的)。為了了解實際的測試包的實施,應當研究Junit本身的測試包。這些類的源代碼存在于JUnit安裝的junit/test目錄下。
![]() |
將一個main()方法添加到一個測試或一個測試包中有時是非常方便的,因此可以在不使用運行器的情況下啟動測試。例如,要將AllTests測試包作為一個標準的Java程序啟動,可以將下面的main()方法添加到類中:
public static void main(String[] args) { junit.textui.TestRunner.run(suite()); }
現在可以通過輸入java AllTests來啟動這個測試包。
JUnit框架還提供了一種有效利用代碼的方法,即將資源集合到被稱為fixture的對象集中。例如,該示例測試實例利用兩個叫作epoch和eon的參考日期。將這些日期重新編譯到每個方法測試中只是浪費時間(而且還可能出現錯誤)。你可以用fixture重新編寫測試,如清單2所示。
你定義了兩個參考日期,作為測試類的段,并將它們編譯到一個setUp()方法中。這一方法在每個測試方法之前被調用。與其對應的方法是tearDown()方法,它將在每個測試方法運行之后清除所有的資源(在這個實施中,該方法事實上什么也沒做,因為垃圾收集器為我們完成了這項工作)?,F在編譯這個測試實例(其源代碼應當放在JUnit的安裝目錄中)并運行它:
$ javac *.java $ java junit.textui.TestRunner IsoDateTest2 .setUp() testIsoDate() tearDown() .setUp() testToString() tearDown() Time: 0,373 OK (2 tests)
注意:在該測試實例中建立了參考日期,因此在任何測試方法中修改這些日期都不會對其他測試產生不利影響。你可以將代碼放到這兩個方法中,以建立和釋放每個測試所需要的資源(如數據庫連接)。
JUnit發布版還提供了擴展模式(在包junit.extensions中),即test decor-ators,以提供像重復運行一個給定的測試這樣的新功能。它還提供了一個TestSuite,以方便你在獨立的線程中同時運行所有測試,并在所有線程中的測試都完成時停止。
|
![]() | ||||||||||||||||||||||
![]() |
原文轉自:http://www.anti-gravitydesign.com