測試的概念作者:Eric Wang


測試的概念
回歸測試框架-JUnit
Design by Contract
Refactoring
IDE對JUnit的支持
JUnit簡介
安裝
Fixture
TestCase
TestSuite
TestRunner
JUnit最佳實踐
JUnit與J2EE


測試的概念

長期以來,我所接觸的軟件開發人員很少有人能在開發的過程中進行測試工作。大部分的專案都是在最終驗收的時候編寫測試文件。有些專案甚至沒有測試文件。現在情況有了改變。我們一直提倡UML、RUP、軟件工程、CMM,目的只有一個,提高軟件編寫的質量。舉一個極端的例子:如果你是一個超級程序設計師,一個傳奇般的人物。(你可以一邊喝咖啡,一邊聽著音樂,同時編寫這操作系統中關於進程調度的模塊,而且兩天時間內就完成了!)我真得承認,有這樣的人。(那個編寫UNIX中的vi編輯器的傢伙就是這種人。)然而非常遺憾的是這些神仙們並沒有留下如何修成正果的README。所以我們這些凡人──在同一時間只能將注意力集中到若干點(據科學統計,我並不太相信,一般的人只能同時考慮最多7個左右的問題,高手可以達到12個左右),而不能既縱覽全局又瞭解細節──只能期望於其他的方式來保證我們所編寫的軟件質量。

為了說明我們這些凡人是如何的笨。有一個聰明人提出了軟件熵(software entropy)的概念:一個程序從設計很好的狀態開始,隨著新的功能不斷地加入,程序逐漸地失去了原有的結構,最終變成了一團亂麻。你可能會爭辯,在這個例子中,設計很好的狀態實際上並不好,如果好的話,就不會發生你所說的情況。是的,看來你變聰明了,可惜你還應該注意到兩個問題:1)我們不能指望在恐龍紀元(大概是十年前)設計的結構到了現在也能適用吧。2)擁有簽字權的客戶代表可不理會加入一個新功能是否會對軟件的結構有什麼影響,即便有影響也是程序設計人員需要考慮的問題。如果你拒絕加入這個你認為致命的新功能,那麼你很可能就失去了你的住房貸款和麵包(對中國工程師來說也許是米飯或麵條,要看你是南方人還是北方人)。

另外,需要說明的是我看過的一些講解測試的書都沒有我寫的這麼有人情味(不好意思...)。我希望看到這片文章的兄弟姐妹能很容易地接受測試的概念,並付諸實施。所以有些地方寫的有些誇張,歡迎對測試有深入理解的兄弟姐妹能體察民情,並不吝賜教。

好了,我們現在言歸正傳。要測試,就要明白測試的目的。我認為測試的目的很簡單也極具吸引力:寫出高質量的軟件並解決軟件熵這一問題。想像一下,如果你寫的軟件和Richard Stallman(GNU、FSF的頭兒)寫的一樣有水準的話,是不是很有成就感?如果你一致保持這種高水準,我保證你的薪水也會有所變動。

測試也分類,白箱測試、黑箱測試、單元測試、集成測試、功能測試...。我們先不管有多少分類,如何分類。先看那些對我們有用的分類,關於其他的測試,有興趣的人可參閱其他資料。白箱測試是指在知道被測試的軟件如何(How)完成功能和完成什麼樣(What)的功能的條件下所作的測試。一般是由開發人員完成。因為開發人員最瞭解自己編寫的軟件。本文也是以白箱測試為主。黑箱測試則是指在知道被測試的軟件完成什麼樣(What)的功能的條件下所作的測試。一般是由測試人員完成。黑箱測試不是我們的重點。本文主要集中在單元測試上,單元測試是一種白箱測試。目的是驗證一個或若干個類別是否按所設計的那樣正常工作。集成測試則是驗證所有的類別是否能互相配合,協同完成特定的任務,目前我們暫不關心它。下面我所提到的測試,除非特別說明,一般都是指單元測試。

需要強調的是:測試是一個持續的過程。也就是說測試貫穿與開發的整個過程中,單元測試尤其適合於迭代增量式(iterative and incremental)的開發過程。Martin Fowler(有點兒像引用孔夫子的話)甚至認為:「在你不知道如何測試代碼之前,就不應該編寫程序。而一旦你完成了程序,測試代碼也應該完成。除非測試成功,你不能認為你編寫出了可以工作的程序。」我並不指望所有的開發人員都能有如此高的覺悟,這種層次也不是一蹴而就的。但我們一旦瞭解測試的目的和好處,自然會堅持在開發過程中引入測試。 因為我們是測試新手,我們也不理會那些複雜的測試原理,先說一說最簡單的:測試就是比較預期的結果是否與實際執行的結果一致。如果一致則通過,否則失敗。看下面的例子:

//將要被測試的類別
public class Car{
public int getWheels() {
return 4;
}
}

//執行測試的類別
public class testCar {
public static void main(String[] args) {
testCar myTest = new testCar();
myTest.testGetWheels();
}

public testGetWheels () {
int expectedWheels = 4;
Car myCar = Car();
if (expectedWheels==myCar.getWheels())
System.out.println("test [Car]: getWheels works perfected!");
else
System.out.println("test [Car]: getWheels DOESN'T work!");
}
}
 

如果你立即動手寫了上面的代碼,你會發現兩個問題,第一,如果你要執行測試的類別testCar,你必須必須手工敲入如下命令:

[Windows] d:>java testCar
[Unix] % java testCar
 

即便測試如例示的那樣簡單,你也有可能不願在每次測試的時候都敲入上面的命令,而希望在某個集成環境中(IDE)點擊一下滑鼠就能執行測試。後面的章節會介紹到這些問題。第二,如果沒有一定的規範,測試類別的編寫將會成為另一個需要定義的標準。沒有人希望查看別人是如何設計測試類別的。如果每個人都有不同的設計測試類別的方法,光維護被測試的類別就夠煩了,誰還顧得上維護測試類別?另外有一點我不想提,但是這個問題太明顯了,測試類別的代碼多於被測試的類別!這是否意味這雙倍的工作?不!1)不論被測試類別-Car 的 getWheels 方法如何複雜,測試類別-testCar 的testGetWheels 方法只會保持一樣的代碼量。2)提高軟件的質量並解決軟件熵這一問題並不是沒有代價的。testCar就是代價。

我們目前所能做的就是盡量降低所付出的代價:我們編寫的測試代碼要能被維護人員容易的讀取,我們編寫測試代碼要有一定的規範。最好IDE工具可以支持這些規範。好了,你所需要的就是JUnit。一個Open Source的項目。用其主頁上的話來說就是:「 JUnit是由 Erich Gamma 和 Kent Beck 編寫的一個回歸測試框架(regression testing framework)。用於Java開發人員編寫單元測試之用。」所謂框架就是Erich Gamma 和 Kent Beck 定下了一些條條框框,你編寫的測試代碼必須遵循這個條條框框:繼承某個類別,實現某個介面。其實也就是我們前面所說的規範。好在JUnit目前得到了大多數軟件工程師的認可。遵循JUnit我們會得到很多的支持。回歸測試就是你不斷地對所編寫的代碼進行測試:編寫一些,測試一些,調試一些,然後循環這一過程,你會不斷地重複先前的測試,哪怕你正編寫其他的類別,由於軟件熵的存在,你可能在編寫第五個類別的時候發現,第五個類別的某個操作會導致第二個類別的測試失敗。通過回歸測試我們抓住了這條大Bug。

回歸測試框架-JUnit

通過前面的介紹,我們對JUnit有了一個大概的輪廓。知道了它是幹什麼的。現在讓我們動手改寫上面的測試類別testCar使其符合Junit的規範──能在JUnit中運行。

//執行測試的類別(JUnit版)
import junit.framework.*;

public class testCar extends TestCase {

protected int expectedWheels;
protected Car myCar;

public testCar(String name) {
super(name);
}

protected void setUp() {
expectedWheels = 4;
myCar = new Car();
}

public static Test suite() {
/*
* the type safe way
*
TestSuite suite= new TestSuite();
suite.addTest(
new testCar("Car.getWheels") {
protected void runTest() { testGetWheels(); }
}
);
return suite;
*/

/*
* the dynamic way
*/
return new TestSuite(testCar.class);

}

public void testGetWheels() {
assertEquals(expectedWheels, myCar.getWheels());
}
}

改版後的testCar已經面目全非。先讓我們瞭解這些改動都是什麼含義,再看如何執行這個測試。

1>import語句,引入JUnit的類別。(沒問題吧)

2>繼承 TestCase 。可以暫時將一個TestCase看作是對某個類別進行測試的方法的集合。詳細介紹請參看JUnit資料

3>setUp()設定了進行初始化的任務。我們以後會看到setUp會有特別的用處。

4>testGetWheeels()對預期的值和myCar.getWheels()返回的值進行比較,並打印比較的結果。assertEquals是junit.framework.Assert中所定義的方法,junit.framework.TestCase繼承了junit.framework.Assert。

5>suite()是一個很特殊的靜態方法。JUnit的TestRunner會呼叫suite方法來確定有多少個測試可以執行。上面的例子顯示了兩種方法:靜態的方法是構造一個內部類別,並利用構造函數給該測試命名(test name, 如 Car.getWheels ),其覆蓋的runTest()方法,指明了該測試需要執行那些方法──testGetWheels()。動態的方法是利用內省(reflection )來實現runTest(),找出需要執行那些測試。此時測試的名字即是測試方法(test method,如testGetWheels)的名字。JUnit會自動找出並呼叫該類別的測試方法。

6>將TestSuite看作是包裹測試的一個容器。如果將測試比作葉子節點的話,TestSuite就是分支節點。實際上TestCase,TestSuite以及TestSuite組成了一個composite Pattern。 JUnit的文件中有一篇專門講解如何使用Pattern構造Junit框架。有興趣的朋友可以查看JUnit資料。
如何運行該測試呢?手工的方法是鍵入如下命令:

[Windows] d:>java junit.textui.TestRunner testCar
[Unix] % java junit.textui.TestRunner testCar
 

別擔心你要敲的字符量,以後在IDE中,只要點幾下滑鼠就成了。運行結果應該如下所示,表明執行了一個測試,並通過了測試:

.
Time: 0

OK (1 tests)
 

如果我們將Car.getWheels()中返回的的值修改為3,模擬出錯的情形,則會得到如下結果:

.F
Time: 0
There was 1 failure:
1) testGetWheels(testCar)junit.framework.AssertionFailedError: expected:<4> but was:<3>
at testCar.testGetWheels(testCar.java:37)

FAILURES!!!
Tests run: 1, Failures: 1, Errors: 0
 

注意:Time上的小點表示測試個數,如果測試通過則顯示OK。否則在小點的後邊標上F,表示該測試失敗。注意,在模擬出錯的測試中,我們會得到詳細的測試報告「expected:<4> but was:<3>」,這足以告訴我們問題發生在何處。下面就是你調試,測試,調試,測試...的過程,直至得到期望的結果。


Design by Contract(這句話我沒法翻譯)

Design by Contract本是Bertrand Meyer(Eiffel語言的創始人)開發的一種設計技術。我發現在JUnit中使用Design by Contract會帶來意想不到的效果。Design by Contract的核心是斷言(assersion)。斷言是一個布爾語句,該語句不能為假,如果為假,則表明出現了一個bug。Design by Contract使用三種斷言:前置條件(pre-conditions)、後置條件(post-conditions)和不變式(invariants)這裡不打算詳細討論Design by Contract的細節,而是希望其在測試中能發揮其作用。 前置條件在執行測試之前可以用於判斷是否允許進入測試,即進入測試的條件。如 expectedWheels > 0, myCar != null。後置條件用於在測試執行後判斷測試的結果是否正確。如 expectedWheels==myCar.getWheels()。而不變式在判斷交易(Transaction)的一致性(consistency)方面尤為有用。我希望JUnit可以將Design by Contract作為未來版本的一個增強。


Refactoring(這句話我依然沒法翻譯)

Refactoring本來與測試沒有直接的聯繫,而是與軟件熵有關,但既然我們說測試能解決軟件熵問題,我們也就必須說出解決之道。(僅僅進行測試只能發現軟件熵,Refactoring則可解決軟件熵帶來的問題。)軟件熵引出了一個問題:是否需要重新設計整個軟件的結構?理論上應該如此,但現實不允許我們這麼做。這或者是由於時間的原因,或者是由於費用的原因。重新設計整個軟件的結構會給我們帶來短期的痛苦。而不停地給軟件打補丁甚至是補丁的補丁則會給我們帶來長期的痛苦。(不管怎樣,我們總處於水深火熱之中)

Refactoring是一個術語,用於描述一種技術,利用這種技術我們可以免於重構整個軟件所帶來的短期痛苦。當你refactor時,你並不改變程序的功能,而是改變程序內部的結構,使其更易理解和使用。如:該變一個方法的名字,將一個成員變量從一個類別移到另一個類別,將兩個類別似方法抽像到父類別中。所作的每一個步都很小,然而1-2個小時的Refactoring工作可以使你的程序結構更適合目前的情況。Refactoring有一些規則:

1> 不要在加入新功能的同時refactor已有的代碼。在這兩者間要有一個清晰的界限。如每天早上1-2個小時的Refactoring,其餘時間添加新的功能。

2> 在你開始Refactoring前,和Refactoring後都要保證測試能順利通過。否則Refactoring沒有任何意義。

3> 進行小的Refactoring,大的就不是Refactoring了。如果你打算重構整個軟件,就沒有必要Refactoring了。

只有在添加新功能和調試bug時才又必要Refactoring。不要等到交付軟件的最後關頭才Refactoring。那樣和打補丁的區別不大。Refactoring 用在回歸測試中也能顯示其威力。要明白,我不反對打補丁,但要記住打補丁是應該最後使用的必殺絕招。(打補丁也需要很高的技術,詳情參看微軟網站)
 

IDE對JUnit的支持

目前支持JUnit的Java IDE 包括

IDE | 方式 | (1-5,滿分5)

Forte for Java 3.0 Enterprise Edition | plug-in | 3

JBuilder 6 Enterprise Edition | integrated with IDE | 4

Visual Age for Java | support | N/A

在IDE中如何使用JUnit,是非常具體的事情。不同的IDE有不同的使用方法。一旦理解了JUnit的本質,使用起來就十分容易了。所以我們不依賴於具體的IDE,而是集中精力講述如何利用JUnit編寫單元測試代碼。心急的人可參看資料。


JUnit簡介

既然我們已經對JUnit有了一個大致的瞭解,我希望能給大家提供一個稍微正式一些的編寫JUnit測試文件的手冊,明白其中的一些關鍵術語和概念。但我要聲明的是這並不是一本完全的手冊,只能認為是一本入門手冊。同其他OpenSource的軟件有同樣的問題,JUnit的文件並沒有商業軟件文件的那種有規則,簡潔和完全。由開發人員編寫的文件總是說不太清楚問題,全整的文件需要參考"官方"指南,API手冊,郵件討論組的郵件,甚至包括源代碼中及相關的註釋。 事實上問題並沒有那麼複雜,除非你有非常特別的要求,否則,只需參考本文你就可以得到所需的大部分信息。

安裝

首先你要獲取JUnit的軟件包,從JUnit下載最新的軟件包(截至寫作本文時,JUnit的最新版本是3.7)。將其在適當的目錄下解包。這樣在安裝目錄(也就是你所選擇的解包的目錄)下你找到一個名為junit.jar的文件。將這個jar文件加入你的CLASSPATH系統變量。(IDE的設置會有所不同,參看你所喜愛的IDE的配置指南)JUnit就安裝完了。太easy了!

你一旦安裝完JUnit,就有可能想試試我們的Car和testCar類別,沒問題,我已經運行過了,你得到的結果應該和我列出的結果類似。(以防新版JUnit使我的文章過時) 接下來,你可能會先寫測試代碼,再寫工作代碼,或者相反,先寫工作代碼,再寫測試代碼。我更贊成使用前一種方法:先寫測試代碼,再寫工作代碼。因為這樣可以使我們編寫工作代碼時清晰地瞭解工作類別的行為。 要注意編寫一定能通過的測試代碼(如文中的例子)並沒有任何意義,只有測試代碼能幫助我們發現bug,測試代碼才有其價值。此外測試代碼還應該對工作代碼進行全面的測試。如給方法呼叫的參數傳入空值、錯誤值和正確的值,看看方法的行為是否如你所期望的那樣。

你現在已經知道了編寫測試類別的基本步驟:

1> 擴展TestCase類別;

2> 覆蓋runTest()方法(可選);

3> 寫一些testXXXXX()方法;

Fixture

接下來的問題是,如果你要對一個或若干個的類別執行多個測試,該怎麼辦?JUnit對此有特殊的解決辦法。 如果需要在一個或若干個的類別執行多個測試,這些類別就成為了測試的context。在JUnit中被稱為Fixture(如testCar類別中的 myCar 和 expectedWheels )。當你編寫測試代碼時,你會發現你花費了很多時間配置/初始化相關測試的Fixture。將配置Fixture的代碼放入測試類別的構造方法中並不可取,因為我們要求執行多個測試,我並不希望某個測試的結果意外地(如果這是你要求的,那就另當別論了)影響其他測試的結果。通常若干個測試會使用相同的Fixture,而每個測試又各有自己需要改變的地方。 為此,JUnit提供了兩個方法,定義在TestCase類別中。

protected void setUp() throws java.lang.Exception
protected void tearDown() throws java.lang.Exception

覆蓋setUp()方法,初始化所有測試的Fixture(你甚至可以在setUp中建立網絡連接),將每個測試略有不同的地方在testXXX()方法中進行配置。 覆蓋tearDown()(我總想起一首叫雨滴的吉他曲),釋放你在setUp()中分配的永久性資源,如資料庫連接。 當JUnit執行測試時,它在執行每個testXXXXX()方法前都呼叫setUp(),而在執行每個testXXXXX()方法後都呼叫tearDown()方法,由此保證了測試不會相互影響。

TestCase

需要提醒一下,在junit.framework.Assert類別中定義了相當多的assert方法,主要有assert(), assert(), assertEquals(), assertNull(), assertSame(), assertTrue(), fail()等方法。如果你需要比較自己定義的類別,如Car。assert方法需要你覆蓋Object類別的equals()方法,以比較兩個對象的不同。實踐表明:如果你覆蓋了Object類別的equals()方法,最好也覆蓋Object類別的hashCode()方法。再進一步,連帶Object類別的toString()方法也一併覆蓋。這樣可以使測試結果更具可讀性。

當你設置好了Fixture後,下一步是編寫所需的testXXX()方法。一定要保證testXXX()方法的public屬性,否則無法通過內省(reflection)對該測試進行呼叫。 每個擴展的TestCase類別(也就是你編寫的測試類別)會有多個testXXX()方法。一個testXXX()方法就是一個測試。要想運行這個測試,你必須定義如何運行該測試。如果你有多個testXXX()方法,你就要定義多次。JUnit支持兩種運行單個測試的方法:靜態的和動態的方法。

靜態的方法就是覆蓋TestCase類別的runTest()方法,一般是採用內部類別的方式創建一個測試實例:

TestCase test01 = new testCar("test getWheels") {
public void runTest() {
testGetWheels();
}
}
 

採用靜態的方法要注意要給每個測試一個名字(這個名字可以任意起,但你肯定希望這個名字有某種意義),這樣你就可以區分那個測試失敗了。 動態的方法是用內省來實現runTest()以創建一個測試實例。這要求測試的名字就是需要呼叫的測試方法的名字:

TestCase test01 = new testCar("testGetWheels");
 

JUnit會動態查找並呼叫指定的測試方法。動態的方法很簡潔,但如果你鍵入了錯誤的名字就會得到一個令人奇怪的NoSuchMethodException異常。動態的方法和靜態的方法都很好,你可以按照自己的喜好來選擇。(先別著急選擇,後面還有一種更酷的方法等著你呢。)

TestSuite

一旦你創建了一些測試實例,下一步就是要讓他們能一起運行。我們必須定義一個TestSuite。在JUnit中,這就要求你在TestCase類別中定義一個靜態的suite()方法。suite()方法就像main()方法一樣,JUnit用它來執行測試。在suite()方法中,你將測試實例加到一個TestSuite對像中,並返回這個TestSuite對象。一個TestSuite對象可以運行一組測試。TestSuite和TestCase都實現了Test介面(interface),而Test介面定義了運行測試所需的方法。這就允許你用TestCase和TestSuite的組合創建一個TestSuite。這就是為什麼我們前面說TestCase,TestSuite以及TestSuite組成了一個composite
Pattern的原因。例子如下:

public static Test suite() {
TestSuite suite= new TestSuite();
suite.addTest(new testCar("testGetWheels"));
suite.addTest(new testCar("testGetSeats"));
return suite;
}
 

從JUnit 2.0開始,有一種更簡單的動態定義測試實例的方法。你只需將類別傳遞給TestSuite,JUnit會根據測試方法名自動創建相應的測試實例。所以你的測試方法最好取名為testXXX()。例子如下:

public static Test suite() {
return new TestSuite(testCar.class);
}
 

從JUnit的設計我們可看出,JUnit不僅可用於單元測試,也可用於集成測試。關於如何用JUnit進行集成測試請參考相關資料。

為了兼容性的考慮,下面列出使用靜態方法的例子:

public static Test suite() {
TestSuite suite= new TestSuite();
suite.addTest(
new testCar("getWheels") {
protected void runTest() { testGetWheels(); }
}
);

suite.addTest(
new testCar("getSeats") {
protected void runTest() { testGetSeats(); }
}
);
return suite;
}


TestRunner

有了TestSuite我們就可以運行這些測試了,JUnit提供了三種界面來運行測試

[Text UI] junit.textui.TestRunner
[AWT UI] junit.awtui.TestRunner
[Swing UI] junit.swingui.TestRunner
 

我們前面已經看過文本界面了,下面讓我們來看一看圖形界面:
界面很簡單,鍵入類別名-testCar。或在啟動UI的時候鍵入類別名:

[Windows] d:>java junit.swingui.TestRunner testCar
[Unix] % java junit.swingui.TestRunner testCar
 

從圖形UI可以更好的運行測試可查單測試結果。還有一個問題需要注意:如果JUnit報告了測試沒有成功,JUnit會區分失敗(failures)和錯誤(errors)。失敗是一個期望的被assert方法檢查到的結果。而錯誤則是意外的問題引起的,如ArrayIndexOutOfBoundsException。 由於TestRunner十分簡單,界面也比較直觀,故不多介紹。朋友們可自行參考相關資料。
 

JUnit最佳實踐

Martin Fowler(又是這位高人)說過:「當你試圖打印輸出一些信息或調試一個表達式時,寫一些測試代碼來替代那些傳統的方法。」一開始,你會發現你總是要創建一些新的Fixture,而且測試似乎使你的編程速度慢了下來。然而不久之後,你會發現你重複使用相同的Fixture,而且新的測試通常只涉及添加一個新的測試方法。

你可能會寫許多測試代碼,但你很快就會發現你設想出的測試只有一小部分是真正有用的。你所需要的測試是那些會失敗的測試,即那些你認為不會失敗的測試,或你認為應該失敗卻成功的測試。
我們前面提到過測試是一個不會中斷的過程。一旦你有了一個測試,你就要一直確保其正常工作,以檢驗你所加入的新的工作代碼。不要每隔幾天或最後才運行測試,每天你都應該運行一下測試代碼。這種投資很小,但可以確保你得到可以信賴的工作代碼。你的返工率降低了,你會有更多的時間編寫工作代碼。

不要認為壓力大,就不寫測試代碼。相反編寫測試代碼會使你的壓力逐漸減輕,應為通過編寫測試代碼,你對類別的行為有了確切的認識。你會更快地編寫出有效率地工作代碼。下面是一些具體的編寫測試代碼的技巧或較好的實踐方法:

1. 不要用TestCase的構造函數初始化Fixture,而要用setUp()和tearDown()方法。

2. 不要依賴或假定測試運行的順序,因為JUnit利用Vector保存測試方法。所以不同的平台會按不同的順序從Vector中取出測試方法。

3. 避免編寫有副作用的TestCase。例如:如果隨後的測試依賴於某些特定的交易數據,就不要提交交易數據。簡單的會滾就可以了。

4. 當繼承一個測試類別時,記得呼叫父類別的setUp()和tearDown()方法。

5. 將測試代碼和工作代碼放在一起,一邊同步編譯和更新。(使用Ant中有支持junit的task.)

6. 測試類別和測試方法應該有一致的命名方案。如在工作類別名前加上test從而形成測試類別名。

7. 確保測試與時間無關,不要依賴使用過期的數據進行測試。導致在隨後的維護過程中很難重現測試。

8. 如果你編寫的軟件面向國際市場,編寫測試時要考慮國際化的因素。不要僅用母語的Locale進行測試。

9. 盡可能地利用JUnit提供地assert/fail方法以及異常處理的方法,可以使代碼更為簡潔。

10.測試要盡可能地小,執行速度快。

事實上,JUnit還可用於集成測試,但我並沒涉及到,原因有兩個:一是因為沒有單元測試,集成測試無從談起。我們接受測試地概念已經很不容易了,如果再引入集成測試就會更困難。二是我比較懶,希望將集成測試的任務交給測試人員去做。在JUnit的網站上有一些相關的文章,有空大家可以翻一翻。


JUnit與J2EE

如果大家仔細考慮一下的話,就會發現,JUnit有自己的局限性,比如對圖形界面的測試,對servlet/JSP以及EJB的測試我們都沒有舉相關的例子。實際上,JUnit對於GUI界面,servlet/JSP,JavaBean以及EJB都有辦法測試。關於GUI的測試比較複雜,適合用一整篇文章來介紹。這裡就不多說了。

前面我們所做的測試實際上有一個隱含的環境,JVM我們的類別需要這個JVM來執行。而在J2EE框架中,servlet/JSP,EJB都要求有自己的運行環境:Web Container和EJB Container。所以,要想對servlet/JSP,EJB進行測試就需要將其部署在相應的Container中才能進行測試。由於EJB不涉及UI的問題(除非EJB操作XML數據,此時的測試代碼比較難寫,有可能需要你比較兩棵DOM樹是否含有相同的內容)只要部署上去之後就可以運行測試代碼了。此時setUp()方法顯得特別有用,你可以在setUp()方法中利用JNDI查找特定的EJB。而在testXXX()方法中呼叫並測試這些EJB的方法。

這裡所指的JavaBean同樣沒有UI的問題,比如,我們用JavaBean來訪問資料庫,或用JavaBean來包裹EJB。如果這類JavaBean沒有用到Container的提供的服務,則可直接進行測試,同我們前面所說的一般的類別的測試方法一樣。如果這類JavaBean用到了Container的提供的服務,則需要將其部署在Container中才能進行測試。方法與EJB類似。 對於servlet/JSP的測試則比較棘手,有人建議在測試代碼中構造HttpRequest和HttpResponse,然後進行比較,這就要求開發人員對HTTP協議以及servlet/JSP的內部實現有比較深的認識。我認為這招不太現實。也有人提出使用HttpUnit。由於我對Cactus和HttpUnit 瞭解不多,所以無法做出合適的建議。希望各位先知們能不吝賜教。

正是由於JUnit的開放性和簡單易行,才會引出這篇介紹文章。但技術總在不斷地更新,而且我對測試並沒有非常深入的理解;我可以將一個複雜的概念簡化成一句非常容易理解的話。但我的本意只是希望能降低開發人員步入測試領域的門檻,而不是要修改或重新定義一些概念。這一點是特別要強調的。最後,如果有些兄弟姐妹能給我指出一些注意事項或我對某些問題的理解有誤,我會非常感激的。
2001年冬雨(唉,已經下了10幾天的雨了)上海
作者:Eric