前言
由于現在公司進行Unit Test Case的整理階段,所以抽空對Junit進行了一下了解,以下是集合了眾家所長之精華(考慮的是按我的思路總結的,也許不能完全表達作者的思路,所以在附錄中有所有我參考的文章地址,大家不妨去看看原文)。
一、測試的概念
長期以來,我所接觸的軟件開發人員很少有人能在開發的過程中進行測試工作。大部分的項目都是在最終驗收的時候編寫測試文檔,有些項目甚至沒有測試文檔?,F在情況有了改變。我們一直提倡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 void testGetWheels ()
{
int eXPectedWheels = 5;
Car myCar =new Car();
if (expectedWheels==myCar.getWheels())
System.out. 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我們會得到很多的支持?;貧w測試就是你不斷地對所編寫的代碼進行測試:編寫一些,測試一些,調試一些,然后循環這一過程,你會不斷地重復先前的測試,哪怕你正編寫其他的類,由于軟件熵的存在,你可能在編寫第五個類的時候發現,第五個類的某個操作會導致第二個類的測試失敗。通過回歸測試我們抓住了這條大Bug。 二、JUnit簡介及為什么要使用JUint JUnit就是對程序代碼進行單元測試的一種Java框架。通過每次修改程序之后測試代碼,程序員就可以保證代碼的的少量變動不會破壞整個系統。要不是有Junit這樣的自動化測試工具,代碼的的反復測試簡直會把人累死而且還可能不準確?,F在好了,測試過程可以頻繁進行而且還是自動的,所以你可以令程序錯誤降低到最少。它寫的是單元測試(Unit Test):軟件工程里的白盒測試,就是測試某個類的某個方法的功能。XP 中推崇的 test first design 就是基于以上的技術。 假如你要寫一段代碼: 1. 先用 junit 寫測試,然后再寫代碼 2. 寫完代碼,運行測試,測試失敗 3. 修改代碼,運行測試,直到測試成功 假如以后對程序進行修改,優化 ( refactoring ),只要再運行測試代碼,假如所有的測試都成功,則代碼修改完成。 Java 下的 team 開發,一般采用 cvs(版本控制) + ant(項目治理) + junit(集成測試) 的模式: 1. 天天早上上班,每個開發人員從 cvs server 獲取一個整個項目的工作拷貝; 2. 拿到自己的任務,先用 junit 寫今天的任務的測試代碼; 3. 然后寫今天任務的代碼,運行測試,直到測試通過,任務完成; 4. 在下班前一兩個小時,各個開發人員把任務提交到 cvs server; 5. 然后由主管對整個項目運行自動測試,哪個測試出錯,就找相關人員修改,直到所有測試通過。下班... 先寫測試,再寫代碼的好處: 從技術上強制你先考慮一個類的功能,也就是這個類提供給外部的接口,而不至于太早陷入它的細節。這是面向對象提倡的一種設計原則。好的測試其實就是一個好的文檔,這個類使用者往往可以通過查看這個類的測試代碼了解它的功能。非凡的,假如你拿到別人的一個程序,對他寫測試是最好的了解這個程序的功能的方法。 xp的原則是 make it simple,不是很推薦另外寫文檔,因為項目在開發過程中往往處于變動中,假如在早期寫文檔,以后代碼變動后還得同步文檔,多了一個工作,而且由于項目時間緊往往文檔寫的不全或與代碼不一致,與其這樣,不如不寫。而假如在項目結束后再寫文檔,開發人員往往已經忘記當時寫代碼時的種種考慮,況且有下一個項目的壓力,治理人員也不愿意再為舊的項目寫文檔,導致以后維護的問題。沒有人能保證需求不變動,以往項目往往對需求的變動大為頭疼,害怕這個改動會帶來其他地方的錯誤。為此,除了設計好的結構以分割項目外(松耦合),但假如有了測試,并已經建立了一個好的測試框架,對于需求的變動,修改完代碼后,只要重新運行測試代碼,假如測試通過,也就保證了修改的成功,假如測試中出現錯誤,也會馬上發現錯在哪里,修改相應的部分,再運行測試,直至測試完全通過。 軟件公司里往往存在開發部門和測試部門之間的矛盾:由于開發和測試分為兩個部門,多了一層溝通的成本和時間,溝通往往會產生錯誤的發生。而且極易形成一個怪圈:開發人員為了趕任務,寫了爛爛的代碼,就把它扔給測試人員,然后寫其他的任務,測試當然是失敗的,又把代碼拿回去重寫,而且在國內往往一個軟件公司技術最差的部門就是測試部門(好的人都跑去寫代碼了),測試就成了一個很頭疼的問題。這種怪圈的根源是責任不清,根據 xp 中的規定:寫這個代碼的人必須為自己的代碼寫測試,而且只有測試通過,才算完成這個任務(這里的測試包括所有的測試,假如測試時發現由于你的程序導致別的模塊的測試失敗,你有責任通知相關人員修改直至集成測試通過),這樣就可以避免這類問題的發生。 三、安裝 1. 獲取JUnit的軟件包,從Junit(http://www.junit.org/index.htm或http://download.sourceforge.net/junit/)下載最新的軟件包。這里我使用的是http://download.sourceforge.net/junit/junit2.zip。 2. 將其在適當的目錄下解包(我安裝在D:/junit2)。這樣在安裝目錄(也就是你所選擇的解包的目錄)下你找到一個名為junit.jar的文件。將這個jar文件加入你的CLASSPATH系統變量。(IDE的設置會有所不同,參看你所喜愛的IDE的配置指南)JUnit就安裝完了。 四、運行 通過前面的介紹,我們對JUnit有了一個大概的輪廓。知道了它是干什么的?,F在讓我們動手改寫上面的測試類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 ??梢詴簳r將一個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()。動態的方法是利用內?。╮eflection)來實現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.16 FAILURES!!! Test Results: Run: 1 Failures: 1 Errors: 0 There was 1 failure: 1) testCar.testGetWheels "expected:<3> but was:<4>" 注重:Time上的小點表示測試個數,假如測試通過則顯示OK。否則在小點的后邊標上F,表示該測試失敗。注重,在模擬出錯的測試中,我們會得到具體的測試報告“expected:<3> but was:<4>”,這足以告訴我們問題發生在何處。下面就是你調試,測試,調試,測試...的過程,直至得到期望的結果。 五、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
新聞熱點
疑難解答