本文的目的是展示一個撰寫及執行JUnit測試案例及測試系列(suites)(或測試包)簡單快速的方法。我們一開始先探討使用JUnit的主要優點然後撰寫一些測試範例以展示其簡單性及效力。
本文包含下列各節:
在你開始之前,請確認你已下載並安裝下列的軟體:
在我們開始之前,我們要問為什麼我們要使用JUnit?單元測試的主題呈現在我們腦中的往往是長夜的思考試著去符合專案測試案例的配額。不管如何,不像傳統單元測試的殘酷
特性,使用JUnit實際上在你提升程式碼的品質時JUnit測試仍允許你更快速的撰寫程式。只要你開始使用JUnit你將開始注意到介於程式碼及測試之間有一個強大的結合力,最終你的開發風格是只有當一個
失敗的測試時才寫新的程式碼(only
writing new code when a test is failing)。
以下列出使用JUnit的理由:
- 在你提升程式碼的品質時JUnit測試仍允許你更快速的撰寫程式
是的,我知道,那聽起來似乎不是很直覺,但那是事實。當你使用JUnit撰寫測試,你將花更少的時間除蟲,同時對你程式碼的改變更
俱有信心。這個信心讓你更積極重整程式碼並增加新的功能。沒有測試,對於重整及增加新功能你會變得沒有信心;因為你不知道有甚麼東西會破壞產出的結果。採用一個綜合的測試系列,你可以在改變程式碼之後快速的執行多個測試並對於你的變動並未破壞任何東西感到有信心。在執行測試時如果發現臭蟲,原始碼仍然清楚的在你腦中,因此很容易找到臭蟲。在JUnit中撰寫的測試幫助你以一種極
大(extreme)的步伐撰寫程式及快速的找出缺點。
- JUnit非常簡單
撰寫測試應該很簡單--這是重點!如果撰寫測試太複雜或太耗時間,便無法要求程式設計師撰寫測試。使用JUnit你可以快速的撰寫測試並檢測你的程式碼並逐
步隨著程式碼的成長增加測試。只要你寫了一些測試,你想要快速並頻繁的執行測試而不至於中斷建立設計及開發程序。使用JUnit執行測試就像編譯你的程式碼那麼容易。事實上,你應該執行編譯時也執行測試。編譯是
檢測程式碼的語法而測試是檢查程式碼的完整性(integrity)。
- JUnit測試檢驗其結果並提供立即的回饋。
如果你是以人工比對測試的期望與實際結果那麼測試是很不好玩的,而且讓你的速度慢下來。JUnit測試可以自動執行並且檢查他們自己的結果。當你執行測試,你獲得簡單且立即的回饋;
比如測試是通過或失敗。而不再需要人工檢查測試結果的報告。
- JUnit測試可以合成一個測試系列的層級架構。
JUnit可以把測試組織成測試系列;這個測試系列可以包含其他的測試或測試系列。JUnit測試的合成行為允許你組合多個測試並自動的回歸(regression)從頭到尾測試整個測試系列。你也可以執行測試系列層級架構中任何一層的測試。
- 撰寫JUnit測試所費不多。
使用Junit測試框架,你可以很便宜的撰寫測試並享受由測試框架所提供的信心。撰寫一個測試就像寫一個方法一樣簡單;測試是檢驗要測試的程式碼並定義期望的結果。這個測試框架提供自動執行測
試的背景;這個背景並成為其他測試集合的一部份。在測試少量的投資將持續讓你從時間及品質中獲得回收。
- JUnit測試提升軟體的穩定性。
JUnit測試是高度區域性(localized)測試;用以改善開發者的生產力及程式碼品質。不像功能測試(function
test)視系統為一個黑箱以確認軟體整體的工作性為主,單元測試是由內而外測試系統基礎的建構區塊。開發者撰寫並擁有JUnit測試。每當一個開發反覆(iteration)完成,這個測試便包裹成為交付軟體的一部份
提供一種溝通的方式,「這是我交付的軟體並且是通過測試的。」
- JUnit測試是以Java寫成的。
使用Java測試Java軟體形成一個介於測試及程式碼間的無縫(seamless)邊界。在測試的控制下測試變成整個軟體的擴充同時程式碼可以被重整。Java編譯器的單元測試靜態語法檢查可已幫助測試程序並且確認遵守軟體介面的約定。
- JUnit是免費的(JUnit is free.)
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)的執行所有測試以產出個別的通過或失敗的狀態。
首先,我們將撰寫一個測試案例以便檢測單一軟體元件。我們將聚焦於撰寫測試;這個測試檢驗這個元件的行為;而這個物件的行為是有著最高的破損的可能性,因此可以從測試的投資獲得最大的回收。
撰寫測試案例請依據下列的步驟:
- 定義一個TestCase的子類別。
- 覆寫setUp()方法以初始化測試中的一個或多個物件。
- 覆寫tearDown()方法以釋放測試中的一個或多個物件。
- 定義一個或多個公開的testXXX()方法;這些方法檢驗這些測試中的物件並且評估期望的結果。
- 定義一個靜態的suite()工廠方法;這個工廠方法構建一個TestSuite其中包含TestCase的所有testXXX()方法。
- 隨意的定義一個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());
}
}
|
|
其次,我們將撰寫一個測試系列其中包含許多測試案例。此測試系列將允許我們從頭到尾執行其所有的測試案例。
撰寫測試系列請依循下列的步驟:
- 定義一個TestCase的子類別。
- 定義一個靜態的suite()工廠方法;這個方法構建一個TestSuite以包含所以的測試。
- 隨意定義一個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
可以以類似的方法執行。
最後一步是決定測試在我們的開發環境中應存在於何處。
這裡有一些建議如何組織我們的測試:
- 把測試案例建立在與我們要測試的程式碼相同的包裹(package)中。例如:
com.mydotcom.ecommerce
包裹包含所有的應用程式階
級的類別及這些元件的測試案例。
-
在你的原始碼資料夾中避免結合應用程式與測試程式碼,建立一個鏡射(mirrored)的資料夾結構對應於此包裹結構;並在鏡射資料夾中存放你的測試碼。
- 為你的應用程式中的Java包裹定義一個TestSuite類別;在這個TestSuite類別中包含所有測試這個包裹內之程式的測試。
- 定義類似的TestSuite類別;此TestSuite類別在此應用程式中的其他包裹(及子包裹)中構建高階及低階測試系列。
-
確認你的建構程序包含所有測試的編輯物(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
|
|
當你測試時請謹記下面的事項:
- 軟體運作良好的事物都是經過測試檢驗的。
- 測試一點點,程式碼寫一點點,測試一點點,程式碼寫一點點......
- 請確認所有測試都能100%通過。
- 每天(夜)至少執行系統中所有的測試一次。
- 要測試的程式碼是最可能錯誤的區域。
- 撰寫最可能回收測試投資的測試。
- 如果你使用
System.out.println()
除蟲,寫一個測試自動檢查其結果。
- 如果發現臭蟲,寫一個測試揭露這個臭蟲。
- 如果下次有人要求你幫他除蟲,幫他寫一個測試。
- 撰寫程式碼之前先寫單元測試;而且只有當一個測試失敗才寫新的程式碼。
想要學習更多有關測試嗎?身為Clarkware Consulting的首席顧問,我提供線上JUnit訓練及顧問指導服務幫助你的團隊使用JUnit提升生產力及有效的應用JUnit測試你的專案。開發服務有可以幫你的專案成功。這些服務可以量身訂製以符合你特殊專案的需求。
請以email
或電話720.851.2014與我聯絡。
-
Source Code -ShoppingCartTest範例完整的原始碼。
- JUnit
- 官方網站
-
JUnit Cookbook -手冊
-
JUnit
FAQ
-
"JUnit Test Infected: Programmers Love Writing Tests" by Gamma, E. and
Beck, K.
-
"JUnit A Cook's Tour" by Gamma, E. and Beck, K.
-
JUnitPerf -收集JUnit測試修飾者(decorators)用以衡量包含於現有JUnit測試的功能其執行效率及可達成性(scalability)
(A collection of JUnit test decorators used to measure the
performance and scalability of functionality contained within existing JUnit
tests.)
-
JDepend - 一個Java包裹相關的分析器包含JUnit測試案例的範例。(A Java package dependency analyzer with example JUnit test cases)
-
Java Tools for Extreme Programming: Mastering Open Source Tools Including Ant,
JUnit, and Cactus, by Richard Hightower, Nicholas Lesiecki (John Wiley &
Sons, 2001)
- Refactoring: Improving The Design Of Existing Code,
by Fowler, M. (Addison-Wesley, 1999)
- Extreme Programming Explained, by Beck, K.
(Addison-Wesley, 2000)
Copyright © 1999-2001
Clarkware Consulting, Inc.
All Rights Reserved
本文獲授權同意翻譯函:
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
|