測試驅動開發(TDD)是極限編程的重要特點,它以不斷的測試推動代碼的開發,既簡化了代碼,又保證了軟體品質。本文從開發人員使用的角度,介紹了 TDD 優勢、原理、過程、原則、測試技術、Tips 等方面。
背景
一個高效的軟體開發過程對軟體開發人員來說是至關重要的,決定著開發是痛苦的掙紮,還是不斷進步的喜悅。國人對軟體藍領的不屑,對繁瑣冗長的傳統開發過程的不耐,使大多數開發人員無所適從。最近興起的一些軟體開發過程相關的技術,提供一些比較高效、實用的軟體過程開發方法。其中比較基礎、關鍵的一個技術就是測試驅動開發(Test-Driven Development)。雖然TDD光大於極限編程,但測試驅動開發完全可以單獨應用。下面就從開發人員使用的角度進行介紹,使開發人員用最少的代價儘快理解、掌握、應用這種技術。下面分優勢,原理,過程,原則,測試技術,Tips等方面進行討論。
1.優勢
TDD的基本思路就是通過測試來推動整個開發的進行。而測試驅動開發技術並不只是單純的測試工作。
需求向來就是軟體開發過程中感覺最不好明確描述、易變的東西。這裡說的需求不只是指使用者的需求,還包括對代碼的使用需求。很多開發人員最害怕的就是後期還要修改某個類或者函數的介面進行修改或者擴充,為什麼會發生這樣的事情就是因為這部分代碼的使用需求沒有很好的描述。測試驅動開發就是通過編寫測試案例,先考慮代碼的使用需求(包括功能、過程、介面等),而且這個描述是無二義的,可執行驗證的。
通過編寫這部分代碼的測試案例,對其功能的分解、使用過程、介面都進行了設計。而且這種從使用角度對代碼的設計通常更符合後期開發的需求。可測試的要求,對代碼的內聚性的提高和複用都非常有益。因此測試驅動開發也是一種代碼設計的過程。
開發人員通常對編寫文檔非常厭煩,但要使用、理解別人的代碼時通常又希望能有文檔進行指導。而測試驅動開發過程中產生的測試案例代碼就是對代碼的最好的解釋。
快樂工作的基礎就是對自己有信心,對自己的工作成果有信心。當前很多開發人員卻經常在擔心:“代碼是否正確?”“辛苦編寫的代碼還有沒有嚴重bug?”“修改的新代碼對其他部分有沒有影響?”。這種擔心甚至導致某些代碼應該修改卻不敢修改的地步。測試驅動開發提供的測試集就可以作為你信心的來源。
當然測試驅動開發最重要的功能還在於保障代碼的正確性,能夠迅速發現、定位bug。而迅速發現、定位bug是很多開發人員的夢想。針對關鍵代碼的測試集,以及不斷完善的測試案例,為迅速發現、定位bug提供了條件。
我的一段功能非常複雜的代碼使用TDD開發完成,真實環境應用中只發現幾個bug,而且很快被定位解決。您在應用後,也一定會為那種自信的開發過程,功能不斷增加、完善的感覺,迅速發現、定位bug的能力所感染,喜歡這個技術的。
那麼是什麼樣的原理、方法提供上面說的這些好處哪?下面我們就看看TDD的原理。
2.原理
測試驅動開發的基本思想就是在開發功能代碼之前,先編寫測試代碼。也就是說在明確要開發某個功能後,首先思考如何對這個功能進行測試,並完成測試代碼的編寫,然後編寫相關的代碼滿足這些測試案例。然後迴圈進行添加其他功能,直到完全部功能的開發。
我們這裡把這個技術的應用領域從代碼編寫擴充到整個開發過程。應該對整個開發過程的各個階段進行測試驅動,首先思考如何對這個階段進行測試、驗證、考核,並編寫相關的測試文檔,然後開始下一步工作,最後再驗證相關的工作。是一個比較流行的測試模型:V測試模型。
【圖V 測試模型】
在開發的各個階段,包括需求分析、概要設計、詳細設計、編碼過程中都應該考慮相對應的測試工作,完成相關的測試案例的設計、測試方案、測試計劃的編寫。這裡提到的開發階段只是舉例,根據實際的開發活動進行調整。相關的測試文檔也不一定是非常詳細複雜的文檔,或者什麼形式,但應該養成測試驅動的習慣。
關於測試模型,還有X測試模型。這個測試模型,我認為,是對詳細階段和編碼階段進行建模,應該說更詳細的描述了詳細設計和編碼階段的開發行為。及針對某個功能進行對應的測試驅動開發。
【圖x測試模型】
基本原理應該說非常簡單,那麼如何進行實際操作哪,下面對開發過程進行詳細的介紹。
3.過程
軟體開發其他階段的測試驅動開發,根據測試驅動開發的思想完成對應的測試文檔即可。下面針對詳細設計和編碼階段進行介紹。
測試驅動開發的基本過程如下:
1) 明確當前要完成的功能。可以記錄成一個 TODO 列表。
2) 快速完成針對此功能的測試案例編寫。
3) 測試代碼編譯不通過。
4) 編寫對應的功能代碼。
5) 測試通過。
6) 對代碼進行重構,並保證測試通過。
7) 迴圈完成所有功能的開發。
為了保證整個測試過程比較快捷、方便,通常可以使用測試架構組織所有的測試案例。一個免費的、優秀的測試架構是 Xunit 系列,幾乎所有的語言都有對應的測試架構。我曾經寫過一篇文章介紹CppUnit的文章( http://www.ibm.com/developerworks/cn/linux/l-cppunit/index.html)。
開發過程中,通常把測試代碼和功能代碼分開存放,這裡提供一個簡單的測試架構使用例子,您可以通過它瞭解測試架構的使用。下面是檔案清單。
project/項目主目錄
project/test測試專案主目錄
project/test/testSeq.cpp測試seq_t 的測試檔案,對其他功能檔案的測試檔案複製後修改即可
project/test/testSeq.h
project/test/Makefile測試專案的 Makefile
project/test/main.cpp測試專案的主檔案,不需要修改
project/main.cpp 項目的主檔案
project/seq_t.h功能代碼,被測試檔案
project/Makefile 項目的 Makefile
主要流程基本如此,但要讓你的代碼很容易的進行測試,全面又不繁瑣的進行測試,還是有很多測試原則和技術需要考慮。
4.原則
測試隔離。不同代碼的測試應該相互隔離。對一塊代碼的測試只考慮此代碼的測試,不要考慮其實現細節(比如它使用了其他類的邊界條件)。
一頂帽子。開發人員開發過程中要做不同的工作,比如:編寫測試代碼、開發功能代碼、對代碼重構等。做不同的事,承擔不同的角色。開發人員完成對應的工作時應該保持注意力集中在當前工作上,而不要過多的考慮其他方面的細節,保證頭上只有一頂帽子。避免考慮無關細節過多,無謂地增加複雜度。
測試清單。需要測試的功能點很多。應該在任何階段想添加功能需求問題時,把相關功能點加到測試清單中,然後繼續手頭工作。然後不斷的完成對應的測試案例、功能代碼、重構。一是避免疏漏,也避免幹擾當前進行的工作。
測試驅動。這個比較核心。完成某個功能,某個類,首先編寫測試代碼,考慮其如何使用、如何測試。然後在對其進行設計、編碼。
先寫斷言。測試代碼編寫時,應該首先編寫對功能代碼的判斷用的Assert 陳述式,然後編寫相應的輔助語句。
可測試性。功能代碼設計、開發時應該具有較強的可測試性。其實遵循比較好的設計原則的代碼都具備較好的測試性。比如比較高的內聚性,盡量依賴於介面等。
及時重構。無論是功能代碼還是測試代碼,對結構不合理,重複的代碼等情況,在測試通過後,及時進行重構。關於重構,我會另撰文詳細分析。
小步前進。軟體開發是個複雜性非常高的工作,開發過程中要考慮很多東西,包括代碼的正確性、可擴充性、效能等等,很多問題都是因為複雜性太大導致的。極限編程提出了一個非常好的思路就是小步前進。把所有的規模大、複雜性高的工作,分解成小的任務來完成。對於一個類來說,一個功能一個功能的完成,如果太困難就再分解。每個功能的完成就走測試代碼-功能代碼-測試-重構的迴圈。通過分解降低整個系統開發的複雜性。這樣的效果非常明顯。幾個小的功能程式碼完成後,大的功能代碼幾乎是不用調試就可以通過。一個個類方法的實現,很快就看到整個類很快就完成啦。本來感覺很多特性需要增加,很快就會看到沒有幾個啦。你甚至會為這個速度感到震驚。(我理解,是大幅度減少調試、出錯的時間產生的這種速度感)
5.測試技術
5.1測試範圍、粒度
對哪些功能進行測試?會不會太繁瑣?什麼時候可以停止測試?這些問題比較常見。按大師 Kent Benk 的話,對那些你認為應該測試的代碼進行測試。就是說,要相信自己的感覺,自己的經驗。那些重要的功能、核心的代碼就應該重點測試。感到疲勞就應該停下來休息一下。感覺沒有必要更詳細的測試,就停止本輪測試。
測試驅動開發強調測試並不應該是負擔,而應該是協助我們減輕工作量的方法。而對於何時停止編寫測試案例,也是應該根據你的經驗,功能複雜、核心功能的代碼就應該編寫更全面、細緻的測試案例,否則測試流程即可。
測試範圍沒有靜態標準,同時也應該可以隨著時間改變。對於開始沒有編寫足夠的測試的功能代碼,隨著bug的出現,根據bug補齊相關的測試案例即可。
小步前進的原則,要求我們對大的功能塊測試時,應該先分拆成更小的功能塊進行測試,比如一個類A使用了類B、C,就應該編寫到A使用B、C功能的測試代碼前,完成對B、C的測試和開發。那麼是不是每個小類或者小函數都應該測試哪?我認為沒有必要。你應該運用你的經驗,對那些可能出問題的地方重點測試,感覺不可能出問題的地方就等它真正出問題的時候再補測試吧。
5.2怎麼寫測試案例
測試案例的編寫就用上了傳統的測試技術。
- 操作過程盡量類比正常使用的過程。
- 全面的測試案例應該盡量做到分支覆蓋,核心代碼盡量做到路徑覆蓋。
- 測試資料盡量包括:真實資料、邊界資料。
- 測試語句和測試資料應該盡量簡單,容易理解。
- 為了避免對其他代碼過多的依賴,可以實現簡單的樁函數或樁類(Mock Object)。
- 如果內部狀態非常複雜或者應該判斷流程而不是狀態,可以通過記錄日誌字串的方式進
6.TIPS
很多朋友有疑問,“測試代碼的正確性如何保障?是寫測試代碼還是寫測試文檔?”這樣是不是會陷入“雞生蛋,蛋生雞”的迴圈。其實是不會的。通常測試代碼通常是非常簡單的,通常圍繞著某個情況的正確性判斷的幾個語句,如果太複雜,就應該繼續分解啦。而傳統的開發過程通常強調測試文檔。但隨著開發節奏的加快,使用者需求的不斷變化,維護高層(需求、概要設計)的測試文檔可以,更低層的測試文檔的成本的確太大了。而且可即時驗證功能正確性的測試代碼就是對代碼最好的文檔。
軟體開發過程中,除了遵守上面提到的測試驅動開發的幾個原則外,一個需要注意的問題就是,謹防過度設計。編寫功能代碼時應該關注於完成當前功能點,通過測試,使用最簡單、直接的方式來編碼。過多的考慮後期的擴充,其他功能的添加,無疑增加了過多的複雜性,容易產生問題。應該等到要添加這些特性時在進行詳細的測試驅動開發。到時候,有整套測試案例做基礎,通過不斷重構很容易添加相關特性。
//以上摘自 http://www.ibm.com/developerworks/cn/linux/l-tdd/index.html
作為一個有理想、有追求的程式員,你成天被各種名詞包圍著,你對其中一個叫做敏捷的東西特別感興趣,因為它特彆強調人的作用,這聽著都讓做程式員的你感到舒服。為了讓自己早日敏捷起來,你從眾多的敏捷實踐中選擇了一個叫做測試驅動開發(Test Driven Development,TDD)的作為你的起始點。因為它對你周遭的環境要求是最低的:它不像結對那樣,要求其他人和你一起合作;也不像採用Story那樣改變你所在團隊的做事方式……你所需要做的,只是在你編寫業務代碼之前,把測試先寫好。這完全是一種潤物細無聲的做法,根本無需告訴你之外的任何人。就在別人忙碌的找bug時,你便開始享受敏捷帶給你的快樂了。順便帶來的好處是,下次在那裡和別人爭論敏捷的時候,你可以以一個實踐者的姿態出現,而不是在那裡信口開河。
你不會打無準備之仗,於是,你通讀了Kent Beck的那本薄冊子。通讀之下,你對TDD更是充滿了信心。因為“紅——綠——重構”的步驟實在是簡單得令人髮指。好吧!總而言之,你已經信心十足的準備開始TDD,步入敏捷的康庄大道了。
理想很美好,現實很殘酷。
當你著手在實際項目中體驗TDD的時候,一切變得並不像最初看起來的那樣美好。雖然你努力的堅持著TDD的原則,但你經常就會發現某些東西不好測,比如你遇到了資料庫,比如你遇到了GUI,比如你遇到了計時器(Timer)。敏捷並非教條,當某些事不可為的時候,你完全可以不那麼堅持。於是,你告訴自己,不好測的東西可以不測,這樣,至少從心理上來說,你覺得舒服多了。隨著工作的繼續,你發現,你不能測的東西越來越多,單元測試的覆蓋率隨著開發的進行正在逐漸降低,一絲恐懼湧上心頭。回過頭來,再去看Kent Beck的書,你突然覺得,你似乎被騙了,因為Kent Beck的例子貌似全都是邏輯,如果只是邏輯,當然好測了,但現實從來就不是這樣。
難道TDD只是看上去很美?
顯然,你不願意就這樣放棄,放棄你苦心學來的軟體開發秘籍,那些傳說中的高手極力推崇的TDD必然有一定道理,TDD確實能夠讓你感覺很好:能測試的那部分代碼確實極大的增強了你對軟體品質的信心,而且出錯了也確實好找,每次修改代碼之後運行測試出現的綠條也確實讓你身心愉悅。
那問題到底出在哪呢?你陷入了沉思。
信馬由韁,你翻開了自己寫過的代碼。看著自己寫的這些代碼,你忽然意識到一個問題,自己遇到的問題並不屬於TDD,而是屬於單元測試。正如你之前所想到的那樣,TDD做法本身的結果是讓你感到快樂的。對,一定是單元測試本身出了問題。那單元測試出了什麼問題,很顯然,一大堆不能測試的部分讓單元測試變得很難寫,降低了單元測試的覆蓋度。那是不是這會是一個無解的問題呢?你顯然不願意就此放棄,所以,順著這個思路繼續向前。
TDD之所以讓你安心,主要是每次編寫代碼之後,運行測試會出現一個綠條,告訴你測試通過。這樣,你可以放心大膽的向前繼續,因為你的代碼並沒有破壞任何東西。究竟是什麼讓你感到不安,顯然是那些測試沒有覆蓋到的代碼。你又仔細翻看了一下那些沒有測試覆蓋的代碼,你的思路一下子清晰起來。之所以這部分讓你不安,因為裡面除了那些確實不好測試的部分之外,裡面還有一些邏輯。如果只是那些真正不好測試的部分沒有被測試覆蓋到,你會覺得心裡還有一些安慰。你確定了,真正使你不安的就是與不好測試的代碼共存亡的這些邏輯部分。
如果測試可以覆蓋到這些邏輯的部分,至少從感情上來說,就可以接受了。那怎麼才能讓這些部分被測試覆蓋到呢?你仔細觀察著那些沒有測試的代碼,如果這樣做,這個部分就可以測試了,如果那樣做,那個部分也可以測試了,一來二去,這些貌似不可測試的代碼可以分解出許多可以測試的部分。
你的心情一下子好了許多,因為這麼做終於可以讓測試的覆蓋度達到讓你心理上可以接受的範圍。不過,新的問題也隨之而來。我在做什嗎?拆來分去,這不就是設計嗎?怎麼走到這裡來了。我不是在分析單元測試的問題嗎?對了,我最初的問題是TDD,怎麼一路跑到設計上來了?
TDD?設計?
你突然發覺自己對TDD的理解有一些偏差。TDD,並不代表不需要設計。讀過很多書的你突然想起了Robert Martin那本著名的《敏捷式軟體開發 (Agile Software Development)》,上面有一個關於資料庫訪問的例子。那個例子裡面,前後兩個版本的差異正好就是考慮設計的結果。通常,在設計中考慮測試,會很容易找到設計中僵硬的部分,讓程式更加靈活。再進一步,如果在開始動手之前,稍微進行一些設計,這些問題還是可能注意得到的。你突然覺得,正是因為TDD本身過於強調測試的價值所在,讓你忽略軟體開發中很重要的部分:設計。
思路一下子清楚起來,TDD其實不只是“紅——綠——重構”,它還是與設計相關的:在動手之前,還是要有一定的設計,而且,在設計中要考慮測試的問題。終於解開了心中的困惑,現在的你,對於TDD有了一個新的認識,雖然這個認識不見得是什麼終極真理,但至少是通過自己的思考得來的,這讓你更加相信實踐出真知的道理。
理清思路後,你更加堅信TDD本身的價值所在,也堅定了在日後開發中繼續使用TDD的念頭,當然,目光遠大的你已經盯上了其它的敏捷實踐。
//以上出處http://dreamhead.blogbus.com/logs/14189175.html