JUnit入門

 

 摘要
本文展示如何使用JUnit測試框架撰寫及執行簡單的測試案例及測試系列。
 

原作者:Mike Clark Clarkware Consulting, Inc. October 7, 2000

翻譯:Areca Chen 2002/1/23

 
  簡介

本文的目的是展示一個撰寫及執行JUnit測試案例及測試系列(suites)(或測試包)簡單快速的方法。我們一開始先探討使用JUnit的主要優點然後撰寫一些測試範例以展示其簡單性及效力。

本文包含下列各節:

 

在你開始之前,請確認你已下載並安裝下列的軟體:

 
 為什麼使用JUnit?

在我們開始之前,我們要問為什麼我們要使用JUnit?單元測試的主題呈現在我們腦中的往往是長夜的思考試著去符合專案測試案例的配額。不管如何,不像傳統單元測試的殘酷 特性,使用JUnit實際上在你提升程式碼的品質時JUnit測試仍允許你更快速的撰寫程式。只要你開始使用JUnit你將開始注意到介於程式碼及測試之間有一個強大的結合力,最終你的開發風格是只有當一個 失敗的測試時才寫新的程式碼(only writing new code when a test is failing)。

以下列出使用JUnit的理由:

 

 
 JUnit的設計

JUnit是以兩個關鍵設計樣式來設計的:指令樣式(Command pattern)及合成樣式(Composite pattern)。

TestCase是一個指令物件。任何包含測試方法的類別都是TestCase的子類別。TestCase可以定義任意數量的testXXX()方法。當你要檢查期望與實際的測試結果,你啟動assert()方法的各種類型(variaton)。

TestCase子類別包含多個testXXX()方法;可以使用setUp()及tesrDown()方法初始化及釋放測試下的任何一般的物件,這個子類別形同測 試的基礎設備(fixture)。每一個測試在其本身基礎設備的背景下執行,在每一個測試方法之前呼叫setUp()及之後呼叫tearDown()以確保沒有副作用影響測試的執行。

TestCase實例物件可以合成為TestSuite層級架構;在這個TestSuite層級架構中可以自動啟動定義在TestCase實例物件中的所有testXXX()方法。一個TestSuite是其他多個測試的一個合成物件(composite), 其中包括TestCase實例物件及其他的TestSuite實例物件。這個由TestSuite代表的合成物件行為允許你組合測試的測試系列的測試系列到任意深度,並且自動一致性(uniformly)的執行所有測試以產出個別的通過或失敗的狀態。

 
第一步:撰寫一個測試案例

首先,我們將撰寫一個測試案例以便檢測單一軟體元件。我們將聚焦於撰寫測試;這個測試檢驗這個元件的行為;而這個物件的行為是有著最高的破損的可能性,因此可以從測試的投資獲得最大的回收。

撰寫測試案例請依據下列的步驟:

  1. 定義一個TestCase的子類別。
  2. 覆寫setUp()方法以初始化測試中的一個或多個物件。
  3. 覆寫tearDown()方法以釋放測試中的一個或多個物件。
  4. 定義一個或多個公開的testXXX()方法;這些方法檢驗這些測試中的物件並且評估期望的結果。
  5. 定義一個靜態的suite()工廠方法;這個工廠方法構建一個TestSuite其中包含TestCase的所有testXXX()方法。
  6. 隨意的定義一個main()方法以批次的方式執行TestCase。

 

下列是一個測試案例的範例:

(這個範例完整的程式碼可以在資源一節中找的到。)

測試案例的範例(Example Test Case)
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;

public class ShoppingCartTest extends TestCase {

    private ShoppingCart _bookCart;
    private Product _defaultBook;

    /**
     *以特定名稱建構一個ShoppingCartTest。
     *
     *建構函數是以測試案例的名稱當作參數
     */
    public ShoppingCartTest(String name) {
        super(name);
    }

    /**
     * 設定測試設備
     *在測試案例方法之前呼叫
     * /
    protected void setUp() {

        _bookCart = new ShoppingCart();

        _defaultBook = new Product("Extreme Programming", 23.95);
        _bookCart.addItem(_defaultBook);
    }

    /**
     *釋放測試設備
     * 
     *在測試案例方法之後呼叫
     * 
     */
    protected void tearDown() {
        _bookCart = null;
    }

    /**
     *測試在cart中增加一個產品
     * 
     */
    public void testProductAdd() {

        Product newBook = new Product("Refactoring", 53.95);
        _bookCart.addItem(newBook);

        double expectedBalance = _defaultBook.getPrice() + newBook.getPrice();
 
        assertEquals(expectedBalance, _bookCart.getBalance(), 0.0);

        assertEquals(2, _bookCart.getItemCount());
    }

    /**
     *測試清空cart
     * 
     */
    public void testEmpty() {

        _bookCart.empty();
    
        assertTrue(_bookCart.isEmpty());
    }

    /**
     *測試從cart中移除產品
     * 
     *如果此產品不在cart中丟出一個ProductNotFoundException的例外
     * 
     */
    public void testProductRemove() throws ProductNotFoundException {

        _bookCart.removeItem(_defaultBook);

        assertEquals(0, _bookCart.getItemCount());

        assertEquals(0.0, _bookCart.getBalance(), 0.0);
    }

    /**
     *測試從cart中移除一個未知的產品
     * 
     *如果ProductNotFoundException例外產生表示測試成功
     * 
     */
    public void testProductNotFound() {

        try {

            Product book = new Product("Ender's Game", 4.95);
            _bookCart.removeItem(book);

            fail("Should raise a ProductNotFoundException");

        } catch(ProductNotFoundException success) {
            // 測試成功
        }
    }

    /**
     *組合並傳回一個這個測試案例所有測試方法的測試系列
     *傳回一個非空值(non-null)的測試系列
     */
    public static Test suite() {

        //這裡使用的想法是加入所有的testXXX()方法到測試系列中。
        //
        TestSuite suite = new TestSuite(ShoppingCartTest.class);

        //下面是另一種作法,但增加愈多的測試案例方法愈有可能發生錯誤
        //
        // TestSuite suite = new TestSuite();
        // suite.addTest(new ShoppingCartTest("testEmpty"));
        // suite.addTest(new ShoppingCartTest("testProductAdd"));
        // suite.addTest(new ShoppingCartTest("testProductRemove"));
        // suite.addTest(new ShoppingCartTest("testProductNotFound"));
        //

        return suite;
    }

    /**
     *執行此測試案例(Runs the test case)
     */
    public static void main(String args[]) {
        junit.textui.TestRunner.run(suite());
    }
}
 
  第二步:撰寫一個測試系列

其次,我們將撰寫一個測試系列其中包含許多測試案例。此測試系列將允許我們從頭到尾執行其所有的測試案例。

撰寫測試系列請依循下列的步驟:

  1. 定義一個TestCase的子類別。
  2. 定義一個靜態的suite()工廠方法;這個方法構建一個TestSuite以包含所以的測試。
  3. 隨意定義一個main()方法以批次方式執行這個TestSuite。

下列是測試系列的範例:

測試系列範例(Example Test Suite)
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;

public class EcommerceTestSuite extends TestCase {
	
    /**
     * 以特定名稱建構一個EcommerceTestSuite
     *
     *建構函數是以測試案例的名稱當作參數
     */
    public EcommerceTestSuite(String name) {
        super(name);
    }

    /**
     *組合並傳回一個測試系列包含所有已知的測試。
     *新的測試應該在此加入
     *傳回一個非空值(non-null)的測試系列
     */
    public static Test suite() {

        TestSuite suite = new TestSuite();
	
        //我們在前面構建的ShoppingCartTest
        //
        suite.addTest(ShoppingCartTest.suite());

        //另一個測試系列的範例,在測試系列中加入其他的測試系列
        // 
        suite.addTest(CreditCardTestSuite.suite());

        return suite;
    }

    /**
     *執行此測試系列
     */
    public static void main(String args[]) {
        junit.textui.TestRunner.run(suite());
    }
}
 
第三步:執行測試

現在我們撰寫了一個測試系列其中包含一堆測試案例及其他的測試系列,我們可以執行這個測試系列或者其中任何個別的測試案例。執行TestSuite將自動執行所有的TestCase及TestSuite實例物件。執行一個TestCase將自動啟動其所有公開的testXXX()方法。

JUnit提供文字及圖形使用者界面。兩種使用者介面都可以指出多少個測試被執行、任何錯誤或失敗、及一個簡單的完成狀態。簡化使用者介面是快速執行測試的關鍵。你應該簡單瞭解就能夠執行你的測試並知道測試的狀態,就像你在編譯上所做的一樣。

文字使用者介面(junit.textui.TestRunner)如果通過所有測試則顯示『OK』而如果失敗則顯示失敗訊息。

圖形使用者界面(junit.swingui.TestRunner)顯示浮動視窗;如果所有測試皆通過則其中有一個進度桿顯示為綠色,否則進度桿顯示為紅色。

一般而言,TestSuite及TestCase類別應定義一個main()方法;main()利用適當的使用者介面。我們寫的測試到目前為止皆定義一個main()方法來使用文字使用者介面。

由main()定義的使用文字使用者介面執行我們的測試案例時,使用:

java ShoppingCartTest

另一種方式,使用文字使用者介面執行測試,使用:

java junit.textui.TestRunner ShoppingCartTest

或這使用圖形使用者介面時,使用:

java junit.swingui.TestRunner ShoppingCartTest

EcommerceTestSuite可以以類似的方法執行。

 
第四步:組織測試

最後一步是決定測試在我們的開發環境中應存在於何處。

這裡有一些建議如何組織我們的測試:

  1. 把測試案例建立在與我們要測試的程式碼相同的包裹(package)中。例如:com.mydotcom.ecommerce包裹包含所有的應用程式階 級的類別及這些元件的測試案例。
  2. 在你的原始碼資料夾中避免結合應用程式與測試程式碼,建立一個鏡射(mirrored)的資料夾結構對應於此包裹結構;並在鏡射資料夾中存放你的測試碼。
  3. 為你的應用程式中的Java包裹定義一個TestSuite類別;在這個TestSuite類別中包含所有測試這個包裹內之程式的測試。
  4. 定義類似的TestSuite類別;此TestSuite類別在此應用程式中的其他包裹(及子包裹)中構建高階及低階測試系列。
  5. 確認你的建構程序包含所有測試的編輯物(compilation)。這樣做有助於確認你的測試可以保持與最後的程式碼同步以維持測試是最新的。

經由在每一個Java包裹中建立一個TestSuite,在各種包裹層次中,你可以在每一個抽象層中執行一個TestSuite。例如,你可以定義一個com.mydotcom.AllTests執行系統中所有的測試,及定義一個com.mydotcom.ecommerce.EcommerceTestSuite只有執行電子交易元件的測試。

測試層級架構可以擴充到任意的深度。其深度與你開發系統的抽象層次有關,你可以執行一個相稱的測試。只要選擇你的系統層次並測試它即可。

下面的範例是測是的層級架構:

測試層級架構範例(Example Testing Hierarchy)
AllTests (Top-level Test Suite)
    SmokeTestSuite (Structural Integrity Tests)
        EcommerceTestSuite
            ShoppingCartTestCase
            CreditCardTestSuite
                AuthorizationTestCase
                CaptureTestCase
                VoidTestCase
            UtilityTestSuite 
                MoneyTestCase
        DatabaseTestSuite
            ConnectionTestCase
            TransactionTestCase
    LoadTestSuite (Performance and Scalability Tests)
        DatabaseTestSuite
            ConnectionPoolTestCase
        ThreadPoolTestCase
 
測試慣例

當你測試時請謹記下面的事項:

 
訓練及顧問指導

想要學習更多有關測試嗎?身為Clarkware Consulting的首席顧問,我提供線上JUnit訓練及顧問指導服務幫助你的團隊使用JUnit提升生產力及有效的應用JUnit測試你的專案。開發服務有可以幫你的專案成功。這些服務可以量身訂製以符合你特殊專案的需求。

請以email 或電話720.851.2014與我聯絡。

 
資源

作者簡介(About the author)
Mike ClarkClarkware Consulting, Inc.的首席顧問,提供客戶的軟體架構、設計、開發、及執行效率諮詢。Mike有十年左右使用各種技術開發及交付軟體的經驗及五年實際經驗的Java專家;在Java中的經驗強調使用J2EE技術於伺服器端的開發。
 

本文獲授權同意翻譯函:

Hi Areca,

You may translate the JUnit Primer as long as the original copyright
remains intact and the text is translated without further modification.

Enjoy!

Mike

=====
Mike Clark
Clarkware Consulting, Inc.
http://www.clarkware.com
720.851.2014

Dear  Mike,
I've read the articles " Junit Primer"on the web site and like it very much.
It will be great if I could translate it into traditional Chinese and publish on our own site
http://www.dotspace.idv.tw) to help those who want to explore JUnit more. I will, of course, declare the source and "hyperlink" those to your original documents. I'll be very grateful if you could give your authorization for me to do this.
Our web site contents about software engineering topics like XP, UML, Rup, Design Patterns, Framework, those are all in traditional Chinese.
        Regards,
Areca Chen
2002/2/8