使用單元測試來逐步改進代碼
?
下載 IBM 開源 J2EE 應用服務器 WAS CE 新版本 V1.1
級別: 初級
Malcolm Davis, 顧問
2000 年 11 月 01 日
軟件開發習慣中一個細微更改都可能會對軟件質量產生巨大改進。將單元測試合并到開發過程中,然后從長遠角度來看它可以節省多少時間和精力。本文通過使用代碼樣本說明了單元測試的種種好處,特別是使用 Ant 和 JUnit 帶來的各種方便。
測試是大型開發過程中的基本原則之一。在任何職業中,驗證都是一個重要部分。醫生要通過驗血來確診。波音公司在研制 777 的過程中對飛機的每個組件都進行了精心測試。為什么軟件開發就應該例外呢?
以前,由于在應用程序中將 GUI 和商業邏輯緊密聯系在一起,這就限制了創建自動測試的能力。當我們學會通過抽象層將商業邏輯從界面中分離出來時,各個單獨代碼模塊的自動測試就替代了通過 GUI 進行的手工測試。
現在,集成開發環境 (IDE) 能在您輸入代碼的同時顯示錯誤,對于在類中快速查找方法具有智能探測功能,可以利用語法結構生成彩色代碼,而且具有許多其它功能。因此,在編譯更改過的代碼之前,您已經全盤考慮了將構建的類,但您是否考慮過這樣的修改會破壞某些功能呢?
每個開發者都碰到過更改“臭蟲”。代碼修改過程可能會引入“臭蟲”,而如果通過用戶界面手工測試代碼的話,在編譯完成之前是不會發現它的。然后,您就要花費幾天的時間追蹤由更改所引起的錯誤。最近在我做的一個項目中,當我把后端數據庫由 Informix 更改到 Oracle 時就遇到了這種情況。大部分更改都十分順利,但由于數據庫層或使用數據庫層的系統缺少單元測試,從而導致將大量時間花費在嘗試解決更改“臭蟲”上。我花了兩天的時間查到別人代碼中的一個數據庫語法更改。(當然,那個人仍是我的朋友。)
盡管測試有許多好處,但一般的程序員對測試都不太感興趣,開始時我也沒有。您聽到過多少次“它編譯了,所以它一定能用”這種言論?但“我思,故我在”這種原則并 不適用于高質量軟件。要鼓勵程序員測試他們的代碼,過程必須簡單無痛。
本文從某人學習用 Java 語言編程時所寫的一個簡單的類開始。然后,我會告訴您我是如何為這個類編寫單元測試,以及在編寫完它以后又是如何將單元測試添加到構建過程中的。最后,我們將看到將“臭蟲”引入代碼時發生的情況。
從一個典型類開始
第一個典型的 Java 程序一般都包含一個打印 "Hello World" 的 main() 。在清單 1 中,我創建了一個 HelloWorld 對象的實例并調用 sayHello() 方法,該方法會打印這句習慣說法。
main()
sayHello()
main() 方法是我的測試。哦噢!我將代碼、文檔、測試和樣本代碼包含在了一個模塊中。保佑 Java!但隨著程序越變越大,這種開發方法很快就開始顯現出了缺陷:
類開發 對我來說,類開發是從編寫 main() 方法開始的。我在編寫 main() 的時候就定義類和類的用法,然后實現接口。它的一些明顯的缺陷也開始顯現出來。一個缺陷是我傳遞給 main() 來執行測試的參數個數。其次, main() 本身在進行調用子方法、設置代碼等操作時變得很混亂。有時 main() 會比類實現的其余部分還要大。
更簡單的過程 我原來的做法有一些很明顯的缺陷。因此,讓我們看看有什么別的方法可以使問題簡化。我仍然通過接口設計代碼并給出應用示例,正如原來的 main() 一樣。不同的是我將代碼放到了另一個單獨的類中,而這個類恰好是我的“單元測試”。這種技術有以下幾點好處:
接下來我們將這個單獨的單元測試對象放入構建過程中。這樣,我們就可以提供自動確認過程的方法。
使用 JUnit 自動化單元測試
要使測試自動化,您需要一個測試框架。您可以自己開發或購買,也可以使用某些開放源代碼工具,例如 JUnit。我選擇 JUnit 出于以下幾個原因:
測試布局 圖 1 顯示了使用樣本 TestSuite 的 JUnit TestSuite 布局。每個測試都由若干單獨的測試案例構成。每個測試案例都是一個單獨的類,它擴展了 TestClass 類并包含了我的測試代碼,即那些曾在 main() 中出現的代碼。在該例中,我向 TestSuite 添加了兩個測試:一個是 SkeletonTest,我將它用作所有新類和 HelloWorld 類的起點。
測試類 HelloWorldTest.java 按照約定,測試類的名稱中包含我所測試的類的名稱,但將 Test 附加到結尾。在本例中,我們的測試類是 HelloWorldTest.java 。我復制了 SkeletonTest 中的代碼,并添加了 testSayHello() 來測試 sayHello() 。請注意 HelloWorldTest 擴展了 TestCase。JUnit 框架提供了 assert 和 assertEquals 方法,我們可以使用這些方法來進行驗證。 HelloWorldTest.java 顯示在清單 2 中。
HelloWorldTest.java
testSayHello()
assert
assertEquals
testSayHello() 看上去和 HelloWorld.java 中原來的 main 方法類似,但有一個主要的不同之處。它不是執行 System.out.println 并顯示結果,而是添加了一個 assertEquals() 方法。如果兩個值不同, assertEquals 將打印出兩個輸入的值。您可能已經注意到這個方法不起作用!HelloWorld 中的 sayHello() 方法不返回字符串。如果我先寫過測試,就會捕捉到這一點。我將 "Hello World" 字符串與輸出流聯結起來。這樣,按照清單 3 中顯示的那樣重寫了 HelloWorld,去掉 main() ,并更改了 sayHello() 的返回類型。
HelloWorld.java
System.out.println
assertEquals()
如果我保留了 main() 并修改了聯系,代碼看上去如下:
新的 main() 與我測試程序中的 testSayHello() 非常相似。是的,它看上去不象是一個現實世界中的問題(這是人為示例的問題),但它說明了問題。在單獨的應用程序中編寫 main() 可以改進您的設計,同時幫助您設計測試?,F在我們已經創建了一個測試類,讓我們使用 Ant 來將它集成到構建中。
使用 Ant 將測試集成到構建中
Jakarta Project 將 Ant 工具說成“不帶 make 缺點的 make”。Ant 正在成為開放源代碼世界中實際上的標準。原因很簡單:Ant 是使用 Java 語言編寫的,這種語言可以讓構建過程在多種平臺上使用。這種特性簡化了在不同 OS 平臺之間的程序員的合作,而合作是開放源代碼社區的一種需要。您可以在自己選擇的平臺上進行開發 和構建。Ant 的特性包括:
圖 2 簡要介紹了一個配置文件。配置文件由目標樹構成。每個目標都包含了要執行的任務,其中任務就是可以執行的代碼。在本例中, mkdir是目標 compile的任務。 mkdir是建立在 Ant 中的一個任務,用于創建目錄。 Ant 帶有一套健全的內置任務。您也可以通過擴展 Ant 任務類來添加自己的功能。
每個目標都有唯一的名稱和可選的相關性。目標相關性需要在執行目標任務列表之前執行。例如圖 2 所示,在執行 compile 目標中的任務之前需要先運行 JUNIT 目標。這種類型的配置可以讓您在一個配置中有多個樹。
與經典 make 實用程序的相似性是非常顯著的。這是理所當然的,因為 make 就是 make。但也要記住有一些差異:通過 Java 實現的跨平臺和可擴展性,通過 XML 實現的可配置,還有開放源代碼。
下載和安裝 Ant 首先下載 Ant(請參閱 參考資料 )。將 Ant 解壓縮到 tools 目錄,再將 Ant bin 目錄添加到路徑中。(在我的機器上是 e:\tools\ant\bin 。)設置 ANT_HOME 環境變量。在 NT 中,這意味著進入系統屬性,然后以帶有值的變量形式添加 ANT_HOME。ANT_HOME 應該設置為 Ant 根目錄,即包含 bin 和 lib 目錄的目錄。(對我來說,是 e:\tools\ant 。)確保 JAVA_HOME 環境變量設置為安裝了 JDK 的目錄。Ant 文檔有關于安裝的詳細信息。
bin
e:\tools\ant\bin
lib
e:\tools\ant
下載和安裝 JUnit 下載 JUnit 3.2(請參閱 參考資料 )。解開 junit.zip ,并將 junit.jar 添加到 CLASSPATH。如果將 junit.zip 解包到類路徑中,可以通過運行以下命令來測試安裝: java junit.textui.TestRunner junit.samples.AllTests
junit.zip
junit.jar
java junit.textui.TestRunner junit.samples.AllTests
定義目錄結構 在開始我們的構建和測試過程之前,需要一個項目布局。圖 3 顯示了我的樣本項目的布局。下面描述了布局的目錄結構:
build
src
Src
test
main
在實際中,我們有多個目錄,例如 distribution 和 documentation 。我們還會在 main 下有多個用于包的目錄,例如 com.company.util 。
distribution
documentation
com.company.util
因為目錄結構經常變動,所以在 build.xml 中有這些變動的全局字符串常數是很重要的。
build.xml
Ant 構建配置文件示例 下一步,我們要創建配置文件。清單 4 顯示了一個 Ant 構建文件示例。構建文件中的關鍵就是名為 runtests 的目標。這個目標進行分支判斷并運行外部程序,其中外部程序是前面已安裝的 junit.textui.TestRunner 。我們指定要使用語句 test.com.company.AllJUnitTests 來運行哪個測試套件。
junit.textui.TestRunner
test.com.company.AllJUnitTests
運行 Ant 構建示例 開發過程中的下一步是運行將創建和測試 HelloWorld 類的構建。清單 5 顯示了構建的結果,其中包括了各個目標部分。最酷的那部分是 runtests 輸出語句:它告訴我們整個測試套件都正確運行了。
我在圖 4 和圖 5 中顯示了 JUnit GUI,其中所要做的就是將 runtest 目標從 junit.textui.TestRunner 改為 junit.ui.TestRunner 。當您使用 JUnit 的 GUI 部分時,您必須選擇退出按鈕來繼續構建過程。如果使用 Junit GUI 構建包,那么它將更難與大型的構建過程相集成。另外,文本輸出也與構建過程更一致,并可以定向輸出到一個用于主構建記錄的文本文件。這對于每天晚上都要進行的構建非常合適。
junit.ui.TestRunner
了解測試的工作原理
讓我們搞點破壞,然后看看會發生什么事。夜深了,我們決定把 "Hello World" 變成一個靜態字符串。在更改期間,我們 不小心打錯了字母,將 "o" 變成了 "0",如清單 6 所示。
在構建包時,我們看到了錯誤。清單 7 顯示了 runtest 中的錯誤。它顯示了失敗的測試類和測試方法,并說明了為什么會失敗。我們返回到代碼中,改正錯誤后離開。
并非完全無痛
新的過程并不是完全無痛的。為使單元測試成為開發的一部分,您必須采取以下幾個步驟:
但好處遠遠超過了痛苦。通過使單元測試成為開發過程的一部分,您可以:
實現 24x7
保證產品的質量要花費很多錢,但如果質量有缺陷,花費的錢就更多。如何才能使所花的錢獲得最大價值,來保證產品質量呢?
在我 10 年的開發生涯里,為 emageon.com 工作是最重要的部分之一。在 emageon.com 時,設計評審、代碼評審和單元測試是每天都要做的事。這種日常開發習慣造就了最高質量的產品。軟件在客戶地點第一年的當機次數為零,是一個真正的 24x7 產品。單元測試就象刷牙:您不一定要做,但如果做了,生活質量就更好。
參考資料
關于作者
Malcolm G. Davis 擁有自己的咨詢公司,并任公司的總裁,該公司位于美國阿拉巴馬州的伯明翰 (Birmingham)。他把自己看做是個 Java 傳道者。在工作之余,他喜歡跑步,以及和他的孩子們一起玩耍。您可以通過 malcolm@nuearth.com 與 Malcolm 聯系。
posted on 2007-01-02 13:04 會飛的魚 閱讀(119) 評論(0) 編輯 收藏 所屬分類: Ant
Powered by: BlogJava Copyright © 會飛的魚