軟件開發習慣中一個細微更改都可能會對軟件質量產生巨大改進將單元測試合並到開發過程中然後從長遠角度來看它可以節省多少時間和精力本文通過使用代碼樣本說明了單元測試的種種好處特別是使用 Ant 和 JUnit 帶來的各種方便
測試是大型開發過程中的基本原則之一在任何職業中驗證都是一個重要部分醫生要通過驗血來確診波音公司在研制 的過程中對飛機的每個組件都進行了精心測試為什麼軟件開發就應該例外呢?
以前由於在應用程序中將 GUI 和商業邏輯緊密聯系在一起這就限制了創建自動測試的能力當我們學會通過抽象層將商業邏輯從界面中分離出來時各個單獨代碼模塊的自動測試就替代了通過 GUI 進行的手工測試
現在集成開發環境 (IDE) 能在您輸入代碼的同時顯示錯誤對於在類中快速查找方法具有智能探測功能可以利用語法結構生成彩色代碼而且具有許多其它功能因此在編譯更改過的代碼之前您已經全盤考慮了將構建的類但您是否考慮過這樣的修改會破壞某些功能呢?
每個開發者都碰到過更改臭蟲代碼修改過程可能會引入臭蟲而如果通過用戶界面手工測試代碼的話在編譯完成之前是不會發現它的然後您就要花費幾天的時間追蹤由更改所引起的錯誤最近在我做的一個項目中當我把後端數據庫由 Informix 更改到 Oracle 時就遇到了這種情況大部分更改都十分順利但由於數據庫層或使用數據庫層的系統缺少單元測試從而導致將大量時間花費在嘗試解決更改臭蟲上我花了兩天的時間查到別人代碼中的一個數據庫語法更改(當然那個人仍是我的朋友)
盡管測試有許多好處但一般的程序員對測試都不太感興趣開始時我也沒有您聽到過多少次它編譯了所以它一定能用這種言論?但我思故我在這種原則並 不 適用於高質量軟件要鼓勵程序員測試他們的代碼過程必須簡單無痛
本文從某人學習用 Java 語言編程時所寫的一個簡單的類開始然後我會告訴您我是如何為這個類編寫單元測試以及在編寫完它以後又是如何將單元測試添加到構建過程中的最後我們將看到將臭蟲引入代碼時發生的情況
從一個典型類開始
第一個典型的 Java 程序一般都包含一個打印 Hello World 的 main() 在清單 中我創建了一個 HelloWorld 對象的實例並調用 sayHello() 方法該方法會打印這句習慣說法
清單
我的第一個 Java 應用程序
Hello world
/* * HelloWorld
java * My first java program */ class HelloWorld { /** * Print
Hello World
*/ void sayHello() { System
out
println(
Hello World
); } /** * Test */ public static void main( String[] args ) { HelloWorld world = new HelloWorld(); world
sayHello(); } }
main() 方法是我的測試哦噢!我將代碼文檔測試和樣本代碼包含在了一個模塊中保佑 Java!但隨著程序越變越大這種開發方法很快就開始顯現出了缺陷
- 混亂
類接口越大 main() 就越大類可能僅僅因為正常的測試而變得非常龐大
- 代碼膨脹
由於加入了測試所以產品代碼比所需要的要大但我不想交付測試而只想交付產品
- 測試不可靠
既然 main() 是代碼的一部分 main() 就對其他開發者通過類接口無法訪問的私有成員和方法享有訪問權出於這個原因這種測試方法很容易出錯
- 很難自動測試
要進行自動測試我仍然必須創建另一程序來將參數傳遞給 main()
類開發
對我來說類開發是從編寫 main() 方法開始的我在編寫 main() 的時候就定義類和類的用法然後實現接口它的一些明顯的缺陷也開始顯現出來一個缺陷是我傳遞給 main() 來執行測試的參數個數其次 main() 本身在進行調用子方法設置代碼等操作時變得很混亂有時 main() 會比類實現的其余部分還要大
更簡單的過程
我原來的做法有一些很明顯的缺陷因此讓我們看看有什麼別的方法可以使問題簡化我仍然通過接口設計代碼並給出應用示例正如原來的 main() 一樣不同的是我將代碼放到了另一個單獨的類中而這個類恰好是我的單元測試這種技術有以下幾點好處
- 設計類的一種機制
因為是通過接口進行開發所以不太可能利用類的內部功能但因為我是目標類的開發者我有到其內部工作的窗口所以測試並不是個真正的黑箱僅憑這一點就足夠推斷出需要開發者本人在編寫目標類的同時負責測試的開發而不是由其他任何人代勞
- 類用法的示例
通過將示例從實現中分離出來開發者可以更快地提高速度而且再不用在源代碼上糾纏不清這種分離還有助於防止開發者利用類的內部功能因為這些功能將來可能已經不存在了
- 沒有類混亂的 main()
我不再受到 main() 的限制了以前我得將多個參數傳遞給 main() 來測試不同的配置現在我可以創建許多單獨的測試類每一個都維護各自的設置代碼
接下來我們將這個單獨的單元測試對象放入構建過程中這樣我們就可以提供自動確認過程的方法
- 確保所做的任何更改都不會對其他人產生不利影響
- 我們在進行源碼控制之前就可以測試代碼而無需等待匯編測試或在夜晚進行的構建測試這有助於盡早捕捉到臭蟲從而降低產生高質量代碼的成本
- 通過提供增量測試過程我們提供了更好的實現過程如同 IDE 幫助我們在輸入時捕捉到語法或編譯臭蟲一樣增量單元測試也幫助我們在構建時捕捉到代碼更改臭蟲
使用 JUnit 自動化單元測試
要使測試自動化您需要一個測試框架您可以自己開發或購買也可以使用某些開放源代碼工具例如 JUnit我選擇 JUnit 出於以下幾個原因
- 不需要編寫自己的框架
- 它是開放源代碼因此不需要購買框架
- 開放源代碼社區中的其他開發者會使用它因此可以找到許多示例
- 它可以讓我將測試代碼與產品代碼分開
- 它易於集成到我的構建過程中
測試布局
圖 顯示了使用樣本 TestSuite 的 JUnit TestSuite 布局每個測試都由若干單獨的測試案例構成每個測試案例都是一個單獨的類它擴展了 TestClass 類並包含了我的測試代碼即那些曾在 main() 中出現的代碼在該例中我向 TestSuite 添加了兩個測試一個是 SkeletonTest我將它用作所有新類和 HelloWorld 類的起點
圖 TestSuite 布局
測試類 HelloWorldTestjava
按照約定測試類的名稱中包含我所測試的類的名稱但將 Test 附加到結尾在本例中我們的測試類是 HelloWorldTestjava 我復制了 SkeletonTest 中的代碼並添加了 testSayHello() 來測試 sayHello() 請注意 HelloWorldTest 擴展了 TestCaseJUnit 框架提供了 assert 和 assertEquals 方法我們可以使用這些方法來進行驗證 HelloWorldTestjava 顯示在清單 中
清單
HelloWorldTest
java
package test
com
company; import com
company
HelloWorld; import junit
framework
TestCase; import junit
framework
AssertionFailedError; /** * JUnit
testcases for HelloWorld */ public class HelloWorldTest extends TestCase { public HelloWorldTest(String name) { super(name); } public static void main(String args[]) { junit
textui
TestRunner
run(HelloWorldTest
class); } public void testSayHello() { HelloWorld world = new HelloWorld(); assert( world!=null ); assertEquals(
Hello World
world
sayHello() ); } }
testSayHello() 看上去和 HelloWorldjava 中原來的 main 方法類似但有一個主要的不同之處它不是執行 Systemoutprintln 並顯示結果而是添加了一個 assertEquals() 方法如果兩個值不同 assertEquals 將打印出兩個輸入的值您可能已經注意到這個方法不起作用!HelloWorld 中的 sayHello() 方法不返回字符串如果我先寫過測試就會捕捉到這一點我將 Hello World 字符串與輸出流聯結起來這樣按照清單 中顯示的那樣重寫了 HelloWorld去掉 main() 並更改了 sayHello() 的返回類型
清單
Hello world 測試案例
package com
company; public class HelloWorld { public String sayHello() { return
Hello World
; } }
如果我保留了 main() 並修改了聯系代碼看上去如下
public static void main( String[] args ) { HelloWorld world = new HelloWorld(); Systemoutprintln(worldsayHello()); }
新的 main() 與我測試程序中的 testSayHello() 非常相似是的它看上去不象是一個現實世界中的問題(這是人為示例的問題)但它說明了問題在單獨的應用程序中編寫 main() 可以改進您的設計同時幫助您設計測試現在我們已經創建了一個測試類讓我們使用 Ant 來將它集成到構建中
使用 Ant 將測試集成到構建中
Jakarta Project 將 Ant 工具說成不帶 make 缺點的 makeAnt 正在成為開放源代碼世界中實際上的標准原因很簡單Ant 是使用 Java 語言編寫的這種語言可以讓構建過程在多種平台上使用這種特性簡化了在不同 OS 平台之間的程序員的合作而合作是開放源代碼社區的一種需要您可以在自己選擇的平台上進行開發 和 構建Ant 的特性包括
- 類可擴展性 Java 類可用於擴展構建特性而不必使用基於 shell 的命令
- 開放源代碼 因為 Ant 是開放源代碼因此類擴展示例很充足我發現通過示例來學習非常棒
- XML 可配置 Ant 不僅是基於 Java 的它還使用 XML 文件配置構建過程假設構建實際上是分層的那麼使用 XML 描述 make 過程就是其邏輯層另外如果您了解 XML要學習如何配置構建就更簡單一些
圖 簡要介紹了一個配置文件配置文件由目標樹構成每個目標都包含了要執行的任務其中任務就是可以執行的代碼在本例中 mkdir 是目標 compile 的任務 mkdir 是建立在 Ant 中的一個任務用於創建目錄 Ant 帶有一套健全的內置任務您也可以通過擴展 Ant 任務類來添加自己的功能
每個目標都有唯一的名稱和可選的相關性目標相關性需要在執行目標任務列表之前執行例如圖 所示在執行 compile 目標中的任務之前需要先運行 JUNIT 目標這種類型的配置可以讓您在一個配置中有多個樹
圖 Ant XML 構建圖
與經典 make 實用程序的相似性是非常顯著的這是理所當然的因為 make 就是 make但也要記住有一些差異通過 Java 實現的跨平台和可擴展性通過 XML 實現的可配置還有開放源代碼
下載和安裝 Ant
首先下載 Ant(請參閱參考資料)將 Ant 解壓縮到 tools 目錄再將 Ant bin 目錄添加到路徑中(在我的機器上是 e:\tools\ant\bin )設置 ANT_HOME 環境變量在 NT 中這意味著進入系統屬性然後以帶有值的變量形式添加 ANT_HOMEANT_HOME 應該設置為 Ant 根目錄即包含 bin 和 lib 目錄的目錄(對我來說是 e:\tools\ant )確保 JAVA_HOME 環境變量設置為安裝了 JDK 的目錄Ant 文檔有關於安裝的詳細信息
下載和安裝 JUnit
下載 JUnit (請參閱參考資料)解開 junitzip 並將 junitjar 添加到 CLASSPATH如果將 junitzip 解包到類路徑中可以通過運行以下命令來測試安裝 java junittextuiTestRunner junitsamplesAllTests
定義目錄結構
在開始我們的構建和測試過程之前需要一個項目布局圖 顯示了我的樣本項目的布局下面描述了布局的目錄結構
- build 類文件的臨時構建位置構建過程將創建這個目錄
- src 源代碼的位置 Src 被分為 test 文件夾和 main 文件夾前者用於所有的測試代碼而後者包含可交付的代碼將測試代碼與主要代碼分離提供了幾點特性首先使主要代碼中的混亂減少其次它允許包對齊我就熱衷與將類和與其相關的包放置在一起測試就應該和測試在一起它還有助於分發過程因為你不可能打算將單元測試分發給客戶
在實際中我們有多個目錄例如 distribution 和 documentation 我們還會在 main 下有多個用於包的目錄例如 comcompanyutil
因為目錄結構經常變動所以在 buildxml 中有這些變動的全局字符串常數是很重要的
圖 項目布局圖
Ant 構建配置文件示例
下一步我們要創建配置文件清單 顯示了一個 Ant 構建文件示例構建文件中的關鍵就是名為 runtests 的目標這個目標進行分支判斷並運行外部程序其中外部程序是前面已安裝的 junittextuiTestRunner 我們指定要使用語句 testcomcompanyAllJUnitTests 來運行哪個測試套件
清單
構建文件示例
<property name=
app
name
value=
sample
/> <property name=
build
dir
value=
build/classes
/> <target name=
JUNIT
> <available property=
junit
present
classname=
junit
framework
TestCase
/> </target> <target name=
compile
depends=
JUNIT
> <mkdir dir=
${build
dir}
/> <javac srcdir=
src/main/
destdir=
${build
dir}
> <include name=
**/*
java
/> </javac> </target> <target name=
jar
depends=
compile
> <mkdir dir=
build/lib
/> <jar jarfile=
build/lib/${app
name}
jar
basedir=
${build
dir}
includes=
com/**
/> </target> <target name=
compiletests
depends=
jar
> <mkdir dir=
build/testcases
/> <javac srcdir=
src/test
destdir=
build/testcases
> <classpath> <pathelement location=
build/lib/${app
name}
jar
/> <pathelement path=
/> </classpath> <include name=
**/*
java
/> </javac> </target> <target name=
runtests
depends=
compiletests
if=
junit
present
> <java fork=
yes
classname=
junit
textui
TestRunner
taskname=
junit
failonerror=
true
> <arg value=
test
com
company
AllJUnitTests
/> <classpath> <pathelement location=
build/lib/${app
name}
jar
/> <pathelement location=
build/testcases
/> <pathelement path=
/> <pathelement path=
${java
class
path}
/> </classpath> </java> </target> </project>
運行 Ant 構建示例
開發過程中的下一步是運行將創建和測試 HelloWorld 類的構建清單 顯示了構建的結果其中包括了各個目標部分最酷的那部分是 runtests 輸出語句它告訴我們整個測試套件都正確運行了
我在圖 和圖 中顯示了 JUnit GUI其中所要做的就是將 runtest 目標從 junittextuiTestRunner 改為 junituiTestRunner 當您使用 JUnit 的 GUI 部分時您必須選擇退出按鈕來繼續構建過程如果使用 Junit GUI 構建包那麼它將更難與大型的構建過程相集成另外文本輸出也與構建過程更一致並可以定向輸出到一個用於主構建記錄的文本文件這對於每天晚上都要進行的構建非常合適
清單
構建輸出示例
E:\projects\sample>ant runtests Searching for build
xml
Buildfile: E:\projects\sample\build
xml JUNIT: compile: [mkdir] Created dir: E:\projects\sample\build\classes [javac] Compiling
source file to E:\projects\sample\build\classes jar: [mkdir] Created dir: E:\projects\sample\build\lib [jar] Building jar: E:\projects\sample\build\lib\sample
jar compiletests: [mkdir] Created dir: E:\projects\sample\build\testcases [javac] Compiling
source files to E:\projects\sample\build\testcases runtests: [junit]
[junit] Time:
[junit] [junit] OK (
tests) [junit] BUILD SUCCESSFUL Total time:
second
圖 JUnit GUI 測試成功
圖 JUnit GUI 測試失敗
了解測試的工作原理
讓我們搞點破壞然後看看會發生什麼事夜深了我們決定把 Hello World 變成一個靜態字符串在更改期間我們 不小心 打錯了字母將 o 變成了 如清單 所示
清單
Hello world 類更改
package com
company; public class HelloWorld { private final static String HELLO_WORLD =
Hell
World
; public String sayHello() { return HELLO_WORLD; } }
在構建包時我們看到了錯誤清單 顯示了 runtest 中的錯誤它顯示了失敗的測試類和測試方法並說明了為什麼會失敗我們返回到代碼中改正錯誤後離開
清單
構建錯誤示例
E:\projects\sample>ant runtests Searching for build
xml
Buildfile: E:\projects\sample\build
xml JUNIT: compile: jar: compiletests: runtests: [junit]
F [junit] Time:
[junit] [junit] FAILURES!!! [junit] Test Results: [junit] Run:
Failures:
Errors:
[junit] There was
failure: [junit]
) testSayHello(test
com
company
HelloWorldTest)
expected:<Hello World> but was:<Hell
World>
[junit] BUILD FAILED E:\projects\sample\build
xml:
: Java returned:
Total time:
seconds
並非完全無痛
新的過程並不是完全無痛的為使單元測試成為開發的一部分您必須采取以下幾個步驟
- 下載和安裝 JUnit
- 下載和安裝 Ant
- 為構建創建單獨的結構
- 實現與主類分開的測試類
- 學習 Ant 構建過程
但好處遠遠超過了痛苦通過使單元測試成為開發過程的一部分您可以
- 自動驗證以捕捉更改臭蟲
- 從接口角度設計類
- 提供干淨的示例
- 在發行包中避免代碼混亂和類膨脹
實現 x
保證產品的質量要花費很多錢但如果質量有缺陷花費的錢就更多如何才能使所花的錢獲得最大價值來保證產品質量呢?
- 評審設計和代碼 評審可以達到的效果是單純測試的一半
- 通過單元測試來確認模塊可以使用 盡管測試早就存在但隨著開發實踐的不斷發展單元測試逐漸成為日常開發過程的一個部分
在我 年的開發生涯裡為 emageoncom 工作是最重要的部分之一在 emageoncom 時設計評審代碼評審和單元測試是每天都要做的事這種日常開發習慣造就了最高質量的產品軟件在客戶地點第一年的當機次數為零是一個真正的 x 產品單元測試就象刷牙您不一定要做但如果做了生活質量就更好
參考資料
- 您可以參閱本文在 developerWorks 全球站點上的 英文原文
- 下載在本文中引用的 示例代碼
- 從 Apache 網站下載 Ant如需 Ant 文檔FAQ 和其他下載請訪問 Jakarta 項目的Ant 主頁
- JUnit 主頁 提供了額外的測試示例文檔文章和 FAQ您可以從 wwwxprogrammingcom 下載 JUnit
- Kent Beck 所寫的 簡單的 Smalltalk 測試(Simple Smalltalk Testing) 討論了一個簡單的測試策略和支持它的框架
- 請參閱其它開發者的有關單元測試的評論 (comments on unit testing)
- 要了解其它有用的開發習慣請訪問 終極編程主頁 (Extreme Programming Home page)
關於作者 Malcolm G
Davis 擁有自己的咨詢公司
並任公司的總裁
該公司位於美國阿拉巴馬州的伯明翰 (Birmingham)
他把自己看做是個 Java 傳道者
在工作之余
他喜歡跑步
以及和他的孩子們一起玩耍
您可以通過 malcolm@nuearth
com 與 Malcolm 聯系
From:http://tw.wingwit.com/Article/program/Java/ky/201311/29211.html