軟件開發方法學的泰斗Kent Beck先生最為推崇"模式、極限編程和javascript:;" onClick="javascript:tagshow(event, '%B2%E2%CA%D4%C7%FD%B6%AF');" target="_self">測試驅動開發"。在他所創造的極限編程(XP)方法論中,就向大家推薦"測試先行"這一最佳實踐,并且還專門撰寫了《測試驅動開發》一書,詳細說明如何實現。測試驅動開發是極限編程的重要特點,它以不斷的測試推動代碼的開發,從而實現既簡化代碼,又保證質量的目標。
一看到"測試先行"、"測試驅動"這樣的名字,就深深地激起了我強烈的好奇心,開始了自己的探索之旅..
心靈震憾
一段時間的學習,讓我的內心受到了深深的震撼。我們原來的方法居然如此的笨我面對測試先行這一名字時,當時最大的疑問就是"程序都還沒有寫出來, 測試什么呀!"。后來一想,其實這是一個泥瓦匠都明白的道理,卻是自己在畫地為牢。我們來看看兩個不同泥瓦匠是
如何工作的吧:
工匠一:先拉上一根水平線,砌每一塊磚時,都與這根水平線進行比較,使得每一塊磚都保持水平。
工匠二:先將一排磚都砌完,然后拉上一根水平線,看看哪些磚有問題,再進行調整。
你會選擇哪種工作方法呢?你一定會罵工匠二笨吧!這樣多浪費時間呀! 然而你自己想想,你平時在編寫程序的時候又是怎么做的呢?我們就是按工匠二的方法在干活的呀!甚至有時候比工匠二還笨,是整面墻都砌完了,直接進行"集成測試",經常讓整面的墻倒塌?吹竭@里,你還覺得自己的方法高明嗎?
每一個程序員都知道應該為自己的代碼編寫測試程序,但卻很少這樣做。當人們問為什么的時候,最常聽到的回答就是:"我們的開發工作太緊張了"。但這樣卻導致了一個惡性循環,越是沒空編寫測試程序,代碼的效率與質量越差,花在找Bug、解決Bug的時間也越來越多, 實際效率大大降低。由于效率降低了,因此時間更緊張,壓力更大。你想想,為什么不拉上一根水平線呢?難道,我們不能夠將后面浪費的時間花在單元測試上,使得我們的程序一開始就更加健壯,更加易于修改嗎?拋棄原來的托詞吧!
我們的自動化水平太低了
有人還會解釋說,那是因為拉根水平線很簡單,而寫測試程序卻是十分復雜的。我暫且對這句話本身不置可否。不過也體現了一個新問題,我們需要更加方便、省時的編寫測試程序的方法。
要測試一個類,最簡單的方法是直接在調試器中使用表達式觀察對象的值與狀態,你也可以在程序中加上一些斷言、打印中間值等,當然還可以編寫專門的測試程序。但是這些方法都有一個很大的局限性,都需要加入人工的判斷和分析。
由此,自動化測試的引入才是解決之道。正是因為如此,提倡"測試驅動開發"的人群,開發出一系列的自動化單元測試框架xUnit,現在已經有針對Java');" target="_self">Java、Pyhton、C++、PHP等各種常用語言的測試框架。這足以搪塞住那些以"編寫測試代碼太麻煩"為理由的開發人員,讓他們沒有理由逃避單元測試。
正如Robert Martin所說:"測試套件運行起來越簡單,就會越頻繁地運行它們。測試運行越多,就會越快地發現和那些測試的任何背離。如果能夠一天多次地運行所有的測試,那么系統的失效時間就決不會超過幾分鐘"。
認清測試驅動開發
測試驅動開發理論最初源于對這些問題的思考:
1)如果我們能夠在編寫程序代碼之前先進行測試方案的設計,會怎樣?
2)如果我們保證除非沒有這個功能將導致測試失敗,否則就不在程序中實現該功能,會怎樣?
3)換一個角度,如果當測試時發現必須增加某項功能才能夠通過測試時, 我們就增加這一功能,會怎樣?
大師們通過帶著這些問題的實踐, 發現這的確是一個提高軟件代碼質量, 使得效率得到保障的一個很好出發點。
以這樣的思路進行軟件開發,可以保證程序中的每一項功能都有測試來驗證它是正確的,而且每當功能被無意修改時, 測試程序會發現。同時,也使我們獲得了一個新的觀察點,從對程序調用者有利的視角來觀察我們的程序,這使得我們在關心程序功能的本身還能夠對接口予以足夠感悟測試驅動開發的關注,使得其更容易被調用。另外,這種思路下的代碼,將變得更加易于調用,也就必須使其與其它代碼保持低耦合性。并且,當你想復用這些模塊時,測試代碼給出了很好的示例。這一切,使得軟件開發工作的質量一下子變得有保障了。
因此,測試驅動開發的精髓在于: 將測試方案設計工作提前,在編寫代碼之前先做這一項工作; 從測試的角度來驗證設計,推導設計; 同時將測試方案當作行為的準繩,有效地利用其檢驗代碼編寫的每一步,實時驗證其正確性,實現軟件開發過程的"小步快走"。
實踐測試驅動開發
下面,我就結合一個實際的小例子,來說明如何進行"測試驅動開發"。本實例在J2SE SDK 1.4.2環境下開發,以及配套工具JUnit 3.8.1。
任務簡述
隊列是一種在程序開發中十分常用的數據結構,在此我就以編寫一個實現隊列功能的類--Queue為例進行說明。該類將實現以下基本運算:
判斷隊列是否為空:empty()
插入隊列(即在隊列未尾增加一個數據元素):inqueue(x)
出隊列(也就是將隊列首數據元素刪除):outqueue()
取列頭(也就是讀者隊列首數據元素的值):gethead()
清空隊列(也就是將隊列的所有數據元素全刪除): clear()
查詢x在隊列中的位置:search(x)
測試案例分析
在測試驅動開發實踐中,第一步就是考慮測試方案,通過分析該類的功能,我們可以得到以下測試案例:
1) 隊列為空測試
TC01: 隊列新建時,應為空;
TC02: 清空隊列后,應為空;
TC03: 當出隊列操作次數與插入隊列操作次數一樣時,應為空;
2) 插入隊列測試:
TC04: 插入隊列操作后,新數據元素將插入在隊列的未尾;
TC05: 插入隊列操作后,隊列將一定不為空;
3) 出隊列測試
TC06: 出隊列操作后,第一個數據元素將被從隊列中刪除;
4) 取隊頭測試
TC07: 取隊頭操作將獲得隊列中的第一個數據元素。
5) 清空隊列測試
TC08: 清空隊列操作后,隊列將為空隊列;
注: 此處為了講解的方便,并未將所有的測試用例都列出,同時也選擇了一些十分簡單的測試用例。
第一次迭代
我們首先編寫第一個測試代碼,這一測試代碼只考慮了測試案例TC01, 也就是保證新建的隊列為空:
import junit.framework.*;
//每個使用JUnit編寫的測試代碼都應該包括本行
public class testQueue extends TestCase
//創建一個測試用例,繼承TestCase
{
protected Queue q1;
public static void main (String[] args)
{
junit.textui.TestRunner.run (suite());
//執行測試用例
}
protected void setUp() //環境變量準備
{
q1= new Queue();
}
public static Test suite() //通用格式,指定測試內容
{
return new TestSuite(testQueue.class);
}
public void testEmpty() //以下每個方法就是一個測試
{
assertTrue(q1.empty());
//當隊列新建時,應為空-TC01
}
}
安裝JUnit十分簡單,只需在www.junit.org中下載最新的軟件包(ZIP格式), 然后將其解壓縮,并且將"JUnit安裝目錄\junit.jar" 以及"JUnit安裝目錄"都加到系統環境變量CLASSPATH中去即可。
執行套件可以像上述程序一樣在main方法中使用,也可以直接在命令行調用:java junit.textui.TestRunner 測試類名(文本格式)、java junit.awtui.TestRunner 測試類名(圖形格式,AWT版)、java junit.swingui.TestRunner測試類名(圖形版,Swing版)。
編譯執行(即在命令行執行javac testQueue.java和javatestQueue), 你會發現屏幕上出現提示:
.E 一個小點說明執行了一個測試用例,E表示其失敗
Time: 0.11 說明執行測試共花費了0.11秒
There was 1 error: 說明存在一個錯誤
1) testEmpty(testQueue)java.lang.NoClassDefFoundError: Queue
at testQueue.setUp(testQueue.java:13)
at testQueue.main(testQueue.java:9)
FAILURES!!!
Tests run: 1, Failures: 0, Errors: 1
測試沒有通過是肯定的,因為Queue類都還沒有寫呢?怎么可能通過測試,因此,我們就編寫以下代碼,以使測試通過:
public class Queue extends java.util.Vector
{
public Queue()
{
super();
}
public boolean empty()
{
return super.isEmpty();
}
}
將這個類編譯后,再次執行測試程序,這時將出以下提示:
. 一個小點說明執行了一個測試用例,沒有E表示其成功
Time: 0.11
OK (1 test)
你還可以使用前面我們說到的另兩個命令,使測試反饋以圖形化的形式體現出來,例如,執行java junit.awtui.TestRunner testQueue, 將出現:
圖1
第二次迭代
接下來,我們修改測試程序,加入測試案例TC04、TC05的考慮。
import junit.framework.*;
public class testQueue extends TestCase
{
protected Queue q1,q2;
public static void main (String[] args)
{
junit.textui.TestRunner.run (suite());
}
protected void setUp() {
q1= new Queue();
q2= new Queue();
q2.inqueue("first"); /對隊列q2執行插入隊列操作
q2.inqueue("second");
}
public static Test suite()
{
return new TestSuite(testQueue.class);
}
public void testEmpty()
{
assertTrue(q1.empty());
//當隊列新建時,應為空-TC01
}
public void testInqueue()
{
assertTrue(!(q2.empty()));
//執行了插入隊列操作,隊列就應不為空-TC05
assertEquals(1,q2.search("second"));
//search方法用于確定元素在隊列中的位置
//后插入的數據元素,應在未尾-TC04
//插入兩個,第一個在位置0, 第二在位置1
}
}
根據這個測試代碼,我們需要在Queue類中添加上inqueue() 和search() 兩個方法,如下所示:
public class Queue extends java.util.Vector
{
public Queue()
{
super();
}
public boolean empty()
{
return super.isEmpty();
}
public synchronized void inqueue (Object x)
{
super.addElement(x);
}
public int search(Object x)
{
return super.indexOf(x);
}
}
編譯之后,再次執行java junit.awtui.TestRunnertestQueue, 你將再次看到成功的綠色。
圖2
我們仔細看一下這一界面。
1) 最上面列出了測試代碼的類名,右邊有一個"Run" 按鈕,當你需要再次運行這一測試代碼時,只需單擊這個按鈕。另外,將"Reload classesevery run" 選項打上勾很有用,當你測試未通過(出現紅色時), 你可以轉身去修改代碼,修改完后,只需再按"Run" 按鈕就可以再次運行。
2) 中間區域是一個狀態匯報區,紅色表示未通過,統計了共運行了多少個測試(也就是在TestCase類中方法的數量)。
3) 如果測試時出現錯誤,例如,我們不小心將"assertTrue(!(q2.empty()));" 誤寫成為"assertTrue(q2.empty());" 就將造成測試失。
注:由于第一個測試還是通過的,因此你會看到綠色條一閃。這時,你將會發現JUnit會將錯誤列出來,并且對應的"Run"按鈕也由灰變成了亮,這表示你可以轉身修改,完成后單擊這個"Run按鈕"可以只做剛才失效的這個測試,這將節省大量的時間。
同時,在最下面的窗體里,列出了失效的詳細原因。
后面的迭代
到這里,開發還沒有完成,但這種思想卻已經通過這樣兩個短小的實踐傳遞出去了,后面的活大家可以動手試一下。
另外值得一提的是,這里雖然洋洋灑灑一大篇,實際兩次迭代花費了我不到15分鐘就完成了。而且,當看到綠條時,心里十分舒暢。
一些遺憾
文章到此就告一段落,但卻有些許遺憾。
遺憾之一:這只是一篇文章,沒有辦法把所有方面都講得面面俱到,以致于大家可能無法馬上上手。
正是由于這樣的原因,本文取名為"感悟", 與大家交流一下體會,希望能夠幫助大家更好地接受"測試驅動開發"的理念,并開始著手實踐。
遺憾之二:筆者水平有限,無法解決大家的各種問題。
讓筆者感到欣慰的是,記載著這些答案的《測試驅動開發》、《敏捷軟件開發》、《擁抱變化: 解析極限編程》等大作都已悉數擺上了中國的書店。路雖難走,但明師已有。
實踐永遠是學習的最好方法,看到筆者的感悟,就開始極限之旅吧,因為那里風光無限,樂趣無限。當你掌握了測試驅動開發的精髓,那你就能夠對你自己編寫的所有代碼充滿信心,不再擔心它們什么時候在你的后面放一冷箭,從此告別這給你帶來無限壓力的苦惱。
TAG: 測試驅動