測試驅動開發
? 測試驅動開發 (Test-Driven Development, TDD )是通過測試定義所要開發的功能的接口,然后實現功能的開發過程。????? Test-Driven Development(TDD),是 Extreme Programming ( XP )-- 極限編程 的一個重要組成部分。
????? 在上面的圖中,列出的的是XP的12個團隊實踐。Test-Driven Development是其中之一。
????? Kent Beck 的著作TDD(Test Driven Development) 中詳細講述了測試驅動開發。
????? 當你使用TDD的時候一定要說明是測試驅動開發還是測試驅動設計。這兩者是有區別的。測試驅動開發,是通過測試定義所要開發的功能的接口,然后實現功能的開發過程。對于測試驅動設計,在XP中似乎已經消失了,而是被測試驅動開發所取代。另外在XP中有用于描述設計的,SimpleDesign ,Design Improvement.
一、測試驅動開發的基本過程
????? 1) 明確當前要完成的功能??梢杂涗洺梢粋€ TODO 列表。
????? 2) 快速完成針對此功能的測試用例編寫。
????? 3) 測試代碼編譯不通過。
????? 4) 編寫對應的功能代碼。
????? 5) 測試通過。
????? 6) 對代碼進行重構,并保證測試通過。
????? 7) 循環完成所有功能的開發。
二、測試驅動開發的原則
????? 1)測試隔離。不同代碼的測試應該相互隔離。對一塊代碼的測試只考慮此代碼的測試,不要考慮其實現細節(比如它使用了其他類的邊界條件)。
????? 2)一頂帽子。開發人員開發過程中要做不同的工作,比如:編寫測試代碼、開發功能代碼、對代碼重構等。做不同的事,承擔不同的角色。開發人員完成對應的工作時應該保持注意力集中在當前工作上,而不要過多的考慮其他方面的細節,保證頭上只有一頂帽子。避免考慮無關細節過多,無謂地增加復雜度。
????? 3)測試列表。需要測試的功能點很多。應該在任何階段想添加功能需求問題時,把相關功能點加到測試列表中,然后繼續手頭工作。然后不斷的完成對應的測試用例、功能代碼、重構。一是避免疏漏,也避免干擾當前進行的工作。
????? 4)測試驅動。這個比較核心。完成某個功能,某個類,首先編寫測試代碼,考慮其如何使用、如何測試。然后在對其進行設計、編碼。
????? 5)先寫斷言。測試代碼編寫時,應該首先編寫對功能代碼的判斷用的斷言語句,然后編寫相應的輔助語句。
????? 6)可測試性。功能代碼設計、開發時應該具有較強的可測試性。其實遵循比較好的設計原則的代碼都具備較好的測試性。比如比較高的內聚性,盡量依賴于接口等。
????? 7)及時重構。無論是功能代碼還是測試代碼,對結構不合理,重復的代碼等情況,在測試通過后,及時進行重構。
三、測試驅動開發的測試范圍
????? 按大師 Kent Benk 的話,對那些你認為應該測試的代碼進行測試,測試驅動開發強調測試并不應該是負擔,而應該是幫助我們減輕工作量的方法。
四、TDD的優點
????? 『充滿吸引力的優點』
????? 完工時完工。表明我可以很清楚的看到自己的這段工作已經結束了,而傳統的方式很難知道什么時候編碼工作結束了。
????? 全面正確的認識代碼和利用代碼,而傳統的方式沒有這個機會。
????? 為利用你成果的人提供Sample,無論它是要利用你的源代碼,還是直接重用你提供的組件。
????? 開發小組間降低了交流成本,提高了相互信賴程度。
????? 避免了過渡設計。
????? 系統可以與詳盡的測試集一起發布,從而對程序的將來版本的修改和擴展提供方便。
????? TDD給了我們自信,讓我們今天的問題今天解決,明天的問題明天解決,今天不能解決明天的問題,因為明天的問題還沒有出現(沒有TestCase),除非有TestCase否則我決不寫任何代碼;明天也不必擔心今天的問題,只要我亮了綠燈。
????? 『不顯而易見的優點』
????? 逃避了設計角色。對于一個敏捷的開發小組,每個人都在做設計。
????? 大部分時間代碼處在高質量狀態,100%的時間里成果是可見的。
????? 由于可以保證編寫測試和編寫代碼的是相同的程序員,降低了理解代碼所花費的成本。
????? 為減少文檔和代碼之間存在的細微的差別和由這種差別所引入的Bug作出杰出貢獻。
????? 在預先設計和緊急設計之間建立一種平衡點,為你區分哪些設計該事先做、哪些設計該迭代時做提供了一個可靠的判斷依據。
????? 『有爭議的優點』
?????? 事實上提高了開發效率。每一個正在使用TDD并相信TDD的人都會相信這一點,但觀望者則不同,不相信TDD的人甚至堅決反對這一點,這很正常,世界總是這樣。
????? 發現比傳統測試方式更多的Bug。
????? 使IDE的調試功能失去意義,或者應該說,避免了令人頭痛的調試和節約了調試的時間。
????? 總是處在要么編程要么重構的狀態下,不會使人抓狂。(兩頂帽子)
????? 單元測試非常有趣。
五、TDD的 優勢
????? TDD的基本思路就是通過測試來推動整個開發的進行。而測試驅動開發技術并不只是單純的測試工作。
????? 需求向來就是軟件開發過程中感覺最不好明確描述、易變的東西。這里說的需求不只是指用戶的需求,還包括對代碼的使用需求。很多開發人員最害怕的就是后期還要修改某個類或者函數的接口進行修改或者擴展,為什么會發生這樣的事情就是因為這部分代碼的使用需求沒有很好的描述。測試驅動開發就是通過編寫測試用例,先考慮代碼的使用需求(包括功能、過程、接口等),而且這個描述是無二義的,可執行驗證的。
????? 通過編寫這部分代碼的測試用例,對其功能的分解、使用過程、接口都進行了設計。而且這種從使用角度對代碼的設計通常更符合后期開發的需求。可測試的要求,對代碼的內聚性的提高和復用都非常有益。因此測試驅動開發也是一種代碼設計的過程。
????? 開發人員通常對編寫文檔非常厭煩,但要使用、理解別人的代碼時通常又希望能有文檔進行指導。而測試驅動開發過程中產生的測試用例代碼就是對代碼的最好的解釋。
????? 快樂工作的基礎就是對自己有信心,對自己的工作成果有信心。當前很多開發人員卻經常在擔心:“代碼是否正確?”“辛苦編寫的代碼還有沒有嚴重bug?”“修改的新代碼對其他部分有沒有影響?”。這種擔心甚至導致某些代碼應該修改卻不敢修改的地步。測試驅動開發提供的測試集就可以作為你信心的來源。
????? 當然測試驅動開發最重要的功能還在于保障代碼的正確性,能夠迅速發現、定位bug。而迅速發現、定位bug是很多開發人員的夢想。針對關鍵代碼的測試集,以及不斷完善的測試用例,為迅速發現、定位bug提供了條件。
????? 我的一段功能非常復雜的代碼使用TDD開發完成,真實環境應用中只發現幾個bug,而且很快被定位解決。您在應用后,也一定會為那種自信的開發過程,功能不斷增加、完善的感覺,迅速發現、定位bug的能力所感染,喜歡這個技術的。
????? 那么是什么樣的原理、方法提供上面說的這些好處哪?下面我們就看看TDD的原理。
六、TDD的原理
????? 測試驅動開發的基本思想就是在開發功能代碼之前,先編寫測試代碼。也就是說在明確要開發某個功能后,首先思考如何對這個功能進行測試,并完成測試代碼的編寫,然后編寫相關的代碼滿足這些測試用例。然后循環進行添加其他功能,直到完全部功能的開發。
????? 我們這里把這個技術的應用領域從代碼編寫擴展到整個開發過程。應該對整個開發過程的各個階段進行測試驅動,首先思考如何對這個階段進行測試、驗證、考核,并編寫相關的測試文檔,然后開始下一步工作,最后再驗證相關的工作。下圖是一個比較流行的測試模型:V測試模型。
【圖 V測試模型】
????? 在開發的各個階段,包括需求分析、概要設計、詳細設計、編碼過程中都應該考慮相對應的測試工作,完成相關的測試用例的設計、測試方案、測試計劃的編寫。這里提到的開發階段只是舉例,根據實際的開發活動進行調整。相關的測試文檔也不一定是非常詳細復雜的文檔,或者什么形式,但應該養成測試驅動的習慣。
????? 關于測試模型,還有X測試模型。這個測試模型,我認為,是對詳細階段和編碼階段進行建模,應該說更詳細的描述了詳細設計和編碼階段的開發行為。及針對某個功能進行對應的測試驅動開發。
【圖 X測試模型】
???? 基本原理應該說非常簡單,那么如何進行實際操作哪,下面對開發過程進行詳細的介紹。
七、測試技術
????? 1. 測試范圍、粒度
????? 對哪些功能進行測試?會不會太繁瑣?什么時候可以停止測試?這些問題比較常見。按大師 Kent Benk 的話,對那些你認為應該測試的代碼進行測試。就是說,要相信自己的感覺,自己的經驗。那些重要的功能、核心的代碼就應該重點測試。感到疲勞就應該停下來休息一下。感覺沒有必要更詳細的測試,就停止本輪測試。
????? 測試驅動開發強調測試并不應該是負擔,而應該是幫助我們減輕工作量的方法。而對于何時停止編寫測試用例,也是應該根據你的經驗,功能復雜、核心功能的代碼就應該編寫更全面、細致的測試用例,否則測試流程即可。
????? 測試范圍沒有靜態的標準,同時也應該可以隨著時間改變。對于開始沒有編寫足夠的測試的功能代碼,隨著bug的出現,根據bug補齊相關的測試用例即可。
????? 小步前進的原則,要求我們對大的功能塊測試時,應該先分拆成更小的功能塊進行測試,比如一個類A使用了類B、C,就應該編寫到A使用B、C功能的測試代碼前,完成對B、C的測試和開發。那么是不是每個小類或者小函數都應該測試哪?我認為沒有必要。你應該運用你的經驗,對那些可能出問題的地方重點測試,感覺不可能出問題的地方就等它真正出問題的時候再補測試吧。
????? 2. 怎么編寫測試用例
????? 測試用例的編寫就用上了傳統的測試技術。
????? 操作過程盡量模擬正常使用的過程。
????? 全面的測試用例應該盡量做到分支覆蓋,核心代碼盡量做到路徑覆蓋。
????? 測試數據盡量包括:真實數據、邊界數據。
????? 測試語句和測試數據應該盡量簡單,容易理解。
????? 為了避免對其他代碼過多的依賴,可以實現簡單的樁函數或樁類(Mock Object)。
????? 如果內部狀態非常復雜或者應該判斷流程而不是狀態,可以通過記錄日志字符串的方式進行驗證。
八、Tips
????? 很多朋友有疑問,“測試代碼的正確性如何保障?是寫測試代碼還是寫測試文檔?”這樣是不是會陷入“雞生蛋,蛋生雞”的循環。其實是不會的。通常測試代碼通常是非常簡單的,通常圍繞著某個情況的正確性判斷的幾個語句,如果太復雜,就應該繼續分解啦。而傳統的開發過程通常強調測試文檔。但隨著開發節奏的加快,用戶需求的不斷變化,維護高層(需求、概要設計)的測試文檔可以,更低層的測試文檔的成本的確太大了。而且可實時驗證功能正確性的測試代碼就是對代碼最好的文檔。
????? 軟件開發過程中,除了遵守上面提到的測試驅動開發的幾個原則外,一個需要注意的問題就是,謹防過度設計。編寫功能代碼時應該關注于完成當前功能點,通過測試,使用最簡單、直接的方式來編碼。過多的考慮后期的擴展,其他功能的添加,無疑增加了過多的復雜性,容易產生問題。應該等到要添加這些特性時在進行詳細的測試驅動開發。到時候,有整套測試用例做基礎,通過不斷重構很容易添加相關特性。
九、FAQ
[什么時候重構?]
如果您在軟件公司工作,就意味著您成天都會和想通過重構改善代碼質量的想法打交道,不僅您如此,您的大部分同事也都如此。可是,究竟什么時候該重構,什么情況下應該重構呢?我相信您和您的同事可能有很多不同的看法,最常見的答案是“該重構時重構”,“寫不下去的時候重構”,和“下一次迭代開始之前重構”,或者干脆就是“最近沒時間,就不重構了,下次有時間的時候重構吧”。正如您已經預見到我想說的——這些想法都是對重構的誤解。重構不是一種構建軟件的工具,不是一種設計軟件的模式,也不是一個軟件開發過程中的環節,正確理解重構的人應該把重構看成一種書寫代碼的方式,或習慣,重構時時刻刻有可能發生。在TDD中,除去編寫測試用例和實現測試用例之外的所有工作都是重構,所以,沒有重構任何設計都不能實現。至于什么時候重構嘛,還要分開看,有三句話是我的經驗:實現測試用例時重構代碼,完成某個特性時重構設計,產品的重構完成后還要記得重構一下測試用例哦。
[什么時候設計?]
這個問題比前面一個要難回答的多,實話實說,本人在依照TDD開發軟件的時候也常常被這個問題困擾,總是覺得有些問題應該在寫測試用例之前定下來,而有些問題應該在新增一個一個測試用例的過程中自然出現,水到渠成。所以,我的建議是,設計的時機應該由開發者自己把握,不要受到TDD方式的限制,但是,不需要事先確定的事一定不能事先確定,免得捆住了自己的手腳。
[什么時候增加新的TestCase?]
沒事做的時候。通常我們認為,如果你要增加一個新的功能,那么先寫一個不能通過的TestCase;如果你發現了一個bug,那么先寫一個不能通過的TestCase;如果你現在什么都沒有,從0開始,請先寫一個不能通過的TestCase。所有的工作都是從一個TestCase開始。此外,還要注意的是,一些大師要求我們每次只允許有一個TestCase亮紅燈,在這個TestCase沒有Green之前不可以寫別的TestCase,這種要求可以適當考慮,但即使有多個TestCase亮紅燈也不要緊,并未違反TDD的主要精神。
[TestCase該怎么寫?]
測試用例的編寫實際上就是兩個過程:使用尚不存在的代碼和定義這些代碼的執行結果。所以一個TestCase也就應該包括兩個部分——場景和斷言。第一次寫TestCase的人會有很大的不適應的感覺,因為你之前所寫的所有東西都是在解決問題,現在要你提出問題確實不大習慣,不過不用擔心,你正在做正確的事情,而這個世界上最難的事情也不在于如何解決問題,而在于ask the right question!
[TDD能幫助我消除Bug嗎?]
答:不能!千萬不要把“測試”和“除蟲”混為一談!“除蟲”是指程序員通過自己的努力來減少bug的數量(消除bug這樣的字眼我們還是不要講為好^_^),而“測試”是指程序員書寫產品以外的一段代碼來確保產品能有效工作。雖然TDD所編寫的測試用例在一定程度上為尋找bug提供了依據,但事實上,按照TDD的方式進行的軟件開發是不可能通過TDD再找到bug的(想想我們前面說的“完工時完工”),你想啊,當我們的代碼完成的時候,所有的測試用例都亮了綠燈,這時隱藏在代碼中的bug一個都不會露出馬腳來。
但是,如果要問“測試”和“除蟲”之間有什么聯系,我相信還是有很多話可以講的,比如TDD事實上減少了bug的數量,把查找bug戰役的關注點從全線戰場提升到代碼戰場以上。還有,bug的最可怕之處不在于隱藏之深,而在于滿天遍野。如果你發現了一個用戶很不容易才能發現的bug,那么不一定對工作做出了什么杰出貢獻,但是如果你發現一段代碼中,bug的密度或離散程度過高,那么恭喜你,你應該拋棄并重寫這段代碼了。TDD避免了這種情況,所以將尋找bug的工作降低到了一個新的低度。
[我該為一個Feature編寫TestCase還是為一個類編寫TestCase?]
初學者常問的問題。雖然我們從TDD的說明書上看到應該為一個特性編寫相應的TestCase,但為什么著名的TDD大師所寫的TestCase都是和類/方法一一對應的呢?為了解釋這個問題,我和我的同事們都做了很多試驗,最后我們得到了一個結論,雖然我不知道是否正確,但是如果您沒有答案,可以姑且相信我們。
我們的研究結果表明,通常在一個特性的開發開始時,我們針對特性編寫測試用例,如果您發現這個特性無法用TestCase表達,那么請將這個特性細分,直至您可以為手上的特性寫出TestCase為止。從這里開始是最安全的,它不會導致任何設計上重大的失誤。但是,隨著您不斷的重構代碼,不斷的重構TestCase,不斷的依據TDD的思想做下去,最后當產品伴隨測試用例集一起發布的時候,您就會不經意的發現經過重構以后的測試用例很可能是和產品中的類/方法一一對應的。
[什么時候應該將全部測試都運行一遍?]
Good Question!大師們要求我們每次重構之后都要完整的運行一遍測試用例。這個要求可以理解,因為重構很可能會改變整個代碼的結構或設計,從而導致不可預見的后果,但是如果我正在開發的是一個ERP怎么辦?運行一遍完整的測試用例可能將花費數個小時,況且現在很多重構都是由工具做到的,這個要求的可行性和前提條件都有所動搖。所以我認為原則上你可以挑幾個你覺得可能受到本次重構影響的TestCase去run,但是如果運行整個測試包只要花費數秒的時間,那么不介意你按大師的要求去做。
[什么時候改進一個TestCase?]
增加的測試用例或重構以后的代碼導致了原來的TestCase的失去了效果,變得無意義,甚至可能導致錯誤的結果,這時是改進TestCase的最好時機。但是有時你會發現,這樣做僅僅導致了原來的TestCase在設計上是臃腫的,或者是冗余的,這都不要緊,只要它沒有失效,你仍然不用去改進它。記住,TestCase不是你的產品,它不要好看,也不要怎么太科學,甚至沒有性能要求,它只要能完成它的使命就可以了——這也證明了我們后面所說的“用Ctrl-C/Ctrl-V編寫測試用例”的可行性。
但是,美國人的想法其實跟我們還是不太一樣,拿托尼巴贊的MindMap來說吧,其實畫MindMap只是為了表現自己的思路,或記憶某些重要的事情,但托尼卻建議大家把MindMap畫成一件藝術品,甚至還有很多藝術家把自己畫的抽象派MindMap拿出來幫助托尼做宣傳。同樣,大師們也要求我們把TestCase寫的跟代碼一樣質量精良,可我想說的是,現在國內有幾個公司能把產品的代碼寫的精良??還是一步一步慢慢來吧。
[為什么原來通過的測試用例現在不能通過了?]
這是一個警報,Red Alert!它可能表達了兩層意思——都不是什么好意思——1)你剛剛進行的重構可能失敗了,或存在一些錯誤未被發現,至少重構的結果和原來的代碼不等價了。2)你剛剛增加的TestCase所表達的意思跟前面已經有的TestCase相沖突,也就是說,新增的功能違背了已有的設計,這種情況大部分可能是之前的設計錯了。但無論哪錯了,無論是那層意思,想找到這個問題的根源都比TDD的正常工作要難。
[我怎么知道那里該有一個方法還是該有一個類?]
這個問題也是常常出現在我的腦海中,無論你是第一次接觸TDD或者已經成為TDD專家,這個問題都會纏繞著你不放。不過問題的答案可以參考前面的“什么時候設計”一節,答案不是唯一的。其實多數時候你不必考慮未來,今天只做今天的事,只要有重構工具,從方法到類和從類到方法都很容易。
[我要寫一個TestCase,可是不知道從哪里開始?]
從最重要的事開始,what matters most?從腳下開始,從手頭上的工作開始,從眼前的事開始。從一個沒有UI的核心特性開始,從算法開始,或者從最有可能耽誤時間的模塊開始,從一個最嚴重的bug開始。這是TDD主義者和鼠目寸光者的一個共同點,不同點是前者早已成竹在胸。
[為什么我的測試總是看起來有點愚蠢?]
哦?是嗎?來,握個手,我的也是!不必擔心這一點,事實上,大師們給的例子也相當愚蠢,比如一個極端的例子是要寫一個兩個int變量相加的方法,大師先斷言2+3=5,再斷言5+5=10,難道這些代碼不是很愚蠢嗎?其實這只是一個極端的例子,當你初次接觸TDD時,寫這樣的代碼沒什么不好,以后當你熟練時就會發現這樣寫沒必要了,要記住,謙虛是通往TDD的必經之路!從經典開發方法轉向TDD就像從面向過程轉向面向對象一樣困難,你可能什么都懂,但你寫出來的類沒有一個純OO的!我的同事還告訴我真正的太極拳,其速度是很快的,不比任何一個快拳要慢,但是初學者(通常是指學習太極拳的前10年)太不容易把每個姿勢都做對,所以只能慢慢來。
[什么場合不適用TDD?]
問的好,確實有很多場合不適合使用TDD。比如對軟件質量要求極高的軍事或科研產品——神州六號,人命關天的軟件——醫療設備,等等,再比如設計很重要必須提前做好的軟件,這些都不適合TDD,但是不適合TDD不代表不能寫TestCase,只是作用不同,地位不同罷了。
posted on 2007-01-17 13:42 liaojiyong 閱讀(408) 評論(0) 編輯 收藏 所屬分類: Mixture