循序漸進學習JUnit

使用最流行的開放資源測試框架之一學習單元測試基礎。

使用JUnit可以大量減少Java代碼中程序錯誤的個數,JUnit是一種流行的單元測試框架,用於在發佈代碼之前對其進行單元測試。現在讓我們來詳細研究如何使用諸如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免費下載。該軟件包很小(約400KB),但其中包括了源代碼和文檔。要安裝此程序,應首先對該軟件包進行解壓縮(junitxxx.zip)。它將創建一個目錄(junitxxx),在此目錄下有文檔(在doc目錄中)、框架的應用編程接口(API)文檔(在javadoc目錄中)、運行程序的庫文件(junit.jar)以及示例測試實例(在junit目錄中)。截至我撰寫本文時,JUnit的最新版本爲3.8.1,我是在此版本上對示例進行測試的。

IsoDate Test

圖1 運行IsoDate測試

要運行此測試實例,將源文件(IsoDate.javaIsoDateTest.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也不能找到這些測試類。

Swing interface

圖2 用於運行測試的Swing界面

這並不是一種啓動測試的方便方法,但幸運的是,JUnit已經被集成到了其他工具(如Ant和Oracle9i JDeveloper)中,以幫助你開發測試並使測試能夠自動運行。

編寫Junit測試實例

你已經看到了測試類的源代碼對IsoDate實施進行了詢問。現在讓我們來研究這樣的測試文件的實施。

測試實例由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失敗而產生的一個"失敗")。現在對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),並在終端上輸入:

selecting a test method

圖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目錄下。

test results

圖4:顯示測試結果的報告

將一個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()方法,它將在每個測試方法運行之後清除所有的資源(在這個實施中,該方法事實上什麼也沒做,因爲垃圾收集器爲我們完成了這項工作)。現在編譯這個測試實例(其源代碼應當放在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,以方便你在獨立的線程中同時運行所有測試,並在所有線程中的測試都完成時停止。

發佈了41 篇原創文章 · 獲贊 0 · 訪問量 33萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章