{關(guān)鍵字}
測試驅動(dòng)開(kāi)發(fā)/Test Driven Development/TDD
測試用例/TestCase/TC
設計/Design
重構/Refactoring
{TDD的目標}
Clean Code That Works
這句話(huà)的含義是,事實(shí)上我們只做兩件事情:讓代碼奏效(Work)和讓代碼潔凈(Clean),前者是把事情做對,后者是把事情做好。想想看,其實(shí)我們平時(shí)所做的所有工作,除去無(wú)用的工作和錯誤的工作以外,真正正確的工作,并且是真正有意義的工作,其實(shí)也就只有兩大類(lèi):增加功能和提升設計,而TDD 正是在這個(gè)原則上產(chǎn)生的。如果您的工作并非我們想象的這樣,(這意味著(zhù)您還存在第三類(lèi)正確有意義的工作,或者您所要做的根本和我們在說(shuō)的是兩回事),那么這告訴我們您并不需要TDD,或者不適用TDD。而如果我們偶然猜對(這對于我來(lái)說(shuō)是偶然,而對于Kent Beck和Martin Fowler這樣的大師來(lái)說(shuō)則是辛勤工作的成果),那么恭喜您,TDD有可能成為您顯著(zhù)提升工作效率的一件法寶。請不要將信將疑,若即若離,因為任何一項新的技術(shù)——只要是從根本上改變人的行為方式的技術(shù)——就必然使得相信它的人越來(lái)越相信,不信的人越來(lái)越不信。這就好比學(xué)游泳,唯一能學(xué)會(huì )游泳的途徑就是親自下去游,除此之外別無(wú)他法。這也好比成功學(xué),即使把卡耐基或希爾博士的書(shū)倒背如流也不能擁有積極的心態(tài),可當你以積極的心態(tài)去成就了一番事業(yè)之后,你就再也離不開(kāi)它了。相信我,TDD也是這樣!想試用TDD的人們,請遵循下面的步驟:
編寫(xiě)TestCase –> 實(shí)現TestCase –> 重構
(確定范圍和目標) (增加功能) (提升設計)
[友情提示:敏捷建模中的一個(gè)相當重要的實(shí)踐被稱(chēng)為:Prove it With Code,這種想法和TDD不謀而合。]
{TDD的優(yōu)點(diǎn)}
『充滿(mǎn)吸引力的優(yōu)點(diǎn)』
完工時(shí)完工。表明我可以很清楚的看到自己的這段工作已經(jīng)結束了,而傳統的方式很難知道什么時(shí)候編碼工作結束了。
全面正確的認識代碼和利用代碼,而傳統的方式?jīng)]有這個(gè)機會(huì )。
為利用你成果的人提供Sample,無(wú)論它是要利用你的源代碼,還是直接重用你提供的組件。
開(kāi)發(fā)小組間降低了交流成本,提高了相互信賴(lài)程度。
避免了過(guò)渡設計。
系統可以與詳盡的測試集一起發(fā)布,從而對程序的將來(lái)版本的修改和擴展提供方便。
TDD給了我們自信,讓我們今天的問(wèn)題今天解決,明天的問(wèn)題明天解決,今天不能解決明天的問(wèn)題,因為明天的問(wèn)題還沒(méi)有出現(沒(méi)有TestCase),除非有TestCase否則我決不寫(xiě)任何代碼;明天也不必擔心今天的問(wèn)題,只要我亮了綠燈。
『不顯而易見(jiàn)的優(yōu)點(diǎn)』
逃避了設計角色。對于一個(gè)敏捷的開(kāi)發(fā)小組,每個(gè)人都在做設計。
大部分時(shí)間代碼處在高質(zhì)量狀態(tài),100%的時(shí)間里成果是可見(jiàn)的。
由于可以保證編寫(xiě)測試和編寫(xiě)代碼的是相同的程序員,降低了理解代碼所花費的成本。
為減少文檔和代碼之間存在的細微的差別和由這種差別所引入的Bug作出杰出貢獻。
在預先設計和緊急設計之間建立一種平衡點(diǎn),為你區分哪些設計該事先做、哪些設計該迭代時(shí)做提供了一個(gè)可靠的判斷依據。
『有爭議的優(yōu)點(diǎn)』
事實(shí)上提高了開(kāi)發(fā)效率。每一個(gè)正在使用TDD并相信TDD的人都會(huì )相信這一點(diǎn),但觀(guān)望者則不同,不相信TDD的人甚至堅決反對這一點(diǎn),這很正常,世界總是這樣。
發(fā)現比傳統測試方式更多的Bug。
使IDE的調試功能失去意義,或者應該說(shuō),避免了令人頭痛的調試和節約了調試的時(shí)間。
總是處在要么編程要么重構的狀態(tài)下,不會(huì )使人抓狂。(兩頂帽子)
單元測試非常有趣。
{TDD的步驟}
編寫(xiě)TestCase –> 實(shí)現TestCase –> 重構
(不可運行) (可運行) (重構)
步驟 制品
(1)快速新增一個(gè)測試用例 新的TestCase
(2)編譯所有代碼,剛剛寫(xiě)的那個(gè)測試很可能編譯不通過(guò) 原始的TODO List
(3)做盡可能少的改動(dòng),讓編譯通過(guò) Interface
(4)運行所有的測試,發(fā)現最新的測試不能編譯通過(guò) -(Red Bar)
(5)做盡可能少的改動(dòng),讓測試通過(guò) Implementation
(6)運行所有的測試,保證每個(gè)都能通過(guò) -(Green Bar)
(7)重構代碼,以消除重復設計 Clean Code That Works
{FAQ}
[什么時(shí)候重構?]
如果您在軟件公司工作,就意味著(zhù)您成天都會(huì )和想通過(guò)重構改善代碼質(zhì)量的想法打交道,不僅您如此,您的大部分同事也都如此??墒?,究竟什么時(shí)候該重構,什么情況下應該重構呢?我相信您和您的同事可能有很多不同的看法,最常見(jiàn)的答案是“該重構時(shí)重構”,“寫(xiě)不下去的時(shí)候重構”,和“下一次迭代開(kāi)始之前重構”,或者干脆就是“最近沒(méi)時(shí)間,就不重構了,下次有時(shí)間的時(shí)候重構吧”。正如您已經(jīng)預見(jiàn)到我想說(shuō)的——這些想法都是對重構的誤解。重構不是一種構建軟件的工具,不是一種設計軟件的模式,也不是一個(gè)軟件開(kāi)發(fā)過(guò)程中的環(huán)節,正確理解重構的人應該把重構看成一種書(shū)寫(xiě)代碼的方式,或習慣,重構時(shí)時(shí)刻刻有可能發(fā)生。在TDD中,除去編寫(xiě)測試用例和實(shí)現測試用例之外的所有工作都是重構,所以,沒(méi)有重構任何設計都不能實(shí)現。至于什么時(shí)候重構嘛,還要分開(kāi)看,有三句話(huà)是我的經(jīng)驗:實(shí)現測試用例時(shí)重構代碼,完成某個(gè)特性時(shí)重構設計,產(chǎn)品的重構完成后還要記得重構一下測試用例哦。
[什么時(shí)候設計?]
這個(gè)問(wèn)題比前面一個(gè)要難回答的多,實(shí)話(huà)實(shí)說(shuō),本人在依照TDD開(kāi)發(fā)軟件的時(shí)候也常常被這個(gè)問(wèn)題困擾,總是覺(jué)得有些問(wèn)題應該在寫(xiě)測試用例之前定下來(lái),而有些問(wèn)題應該在新增一個(gè)一個(gè)測試用例的過(guò)程中自然出現,水到渠成。所以,我的建議是,設計的時(shí)機應該由開(kāi)發(fā)者自己把握,不要受到TDD方式的限制,但是,不需要事先確定的事一定不能事先確定,免得捆住了自己的手腳。
[什么時(shí)候增加新的TestCase?]
沒(méi)事做的時(shí)候。通常我們認為,如果你要增加一個(gè)新的功能,那么先寫(xiě)一個(gè)不能通過(guò)的 TestCase;如果你發(fā)現了一個(gè)bug,那么先寫(xiě)一個(gè)不能通過(guò)的TestCase;如果你現在什么都沒(méi)有,從0開(kāi)始,請先寫(xiě)一個(gè)不能通過(guò)的 TestCase。所有的工作都是從一個(gè)TestCase開(kāi)始。此外,還要注意的是,一些大師要求我們每次只允許有一個(gè)TestCase亮紅燈,在這個(gè) TestCase沒(méi)有Green之前不可以寫(xiě)別的TestCase,這種要求可以適當考慮,但即使有多個(gè)TestCase亮紅燈也不要緊,并未違反TDD 的主要精神。
[TestCase該怎么寫(xiě)?]
測試用例的編寫(xiě)實(shí)際上就是兩個(gè)過(guò)程:使用尚不存在的代碼和定義這些代碼的執行結果。所以一個(gè) TestCase也就應該包括兩個(gè)部分——場(chǎng)景和斷言。第一次寫(xiě)TestCase的人會(huì )有很大的不適應的感覺(jué),因為你之前所寫(xiě)的所有東西都是在解決問(wèn)題,現在要你提出問(wèn)題確實(shí)不大習慣,不過(guò)不用擔心,你正在做正確的事情,而這個(gè)世界上最難的事情也不在于如何解決問(wèn)題,而在于ask the right question!
[TDD能幫助我消除Bug嗎?]
答:不能!千萬(wàn)不要把“測試”和“除蟲(chóng)”混為一談!“除蟲(chóng)”是指程序員通過(guò)自己的努力來(lái)減少bug的數量(消除bug這樣的字眼我們還是不要講為好^_^),而“測試”是指程序員書(shū)寫(xiě)產(chǎn)品以外的一段代碼來(lái)確保產(chǎn)品能有效工作。雖然TDD所編寫(xiě)的測試用例在一定程度上為尋找bug提供了依據,但事實(shí)上,按照TDD的方式進(jìn)行的軟件開(kāi)發(fā)是不可能通過(guò)TDD再找到bug的(想想我們前面說(shuō)的“完工時(shí)完工”),你想啊,當我們的代碼完成的時(shí)候,所有的測試用例都亮了綠燈,這時(shí)隱藏在代碼中的bug一個(gè)都不會(huì )露出馬腳來(lái)。
但是,如果要問(wèn)“測試”和“除蟲(chóng)”之間有什么聯(lián)系,我相信還是有很多話(huà)可以講的,比如TDD事實(shí)上減少了bug的數量,把查找bug戰役的關(guān)注點(diǎn)從全線(xiàn)戰場(chǎng)提升到代碼戰場(chǎng)以上。還有,bug的最可怕之處不在于隱藏之深,而在于滿(mǎn)天遍野。如果你發(fā)現了一個(gè)用戶(hù)很不容易才能發(fā)現的bug,那么不一定對工作做出了什么杰出貢獻,但是如果你發(fā)現一段代碼中,bug的密度或離散程度過(guò)高,那么恭喜你,你應該拋棄并重寫(xiě)這段代碼了。TDD避免了這種情況,所以將尋找bug的工作降低到了一個(gè)新的低度。
[我該為一個(gè)Feature編寫(xiě)TestCase還是為一個(gè)類(lèi)編寫(xiě)TestCase?]
初學(xué)者常問(wèn)的問(wèn)題。雖然我們從TDD 的說(shuō)明書(shū)上看到應該為一個(gè)特性編寫(xiě)相應的TestCase,但為什么著(zhù)名的TDD大師所寫(xiě)的TestCase都是和類(lèi)/方法一一對應的呢?為了解釋這個(gè)問(wèn)題,我和我的同事們都做了很多試驗,最后我們得到了一個(gè)結論,雖然我不知道是否正確,但是如果您沒(méi)有答案,可以姑且相信我們。
我們的研究結果表明,通常在一個(gè)特性的開(kāi)發(fā)開(kāi)始時(shí),我們針對特性編寫(xiě)測試用例,如果您發(fā)現這個(gè)特性無(wú)法用TestCase表達,那么請將這個(gè)特性細分,直至您可以為手上的特性寫(xiě)出TestCase為止。從這里開(kāi)始是最安全的,它不會(huì )導致任何設計上重大的失誤。但是,隨著(zhù)您不斷的重構代碼,不斷的重構 TestCase,不斷的依據TDD的思想做下去,最后當產(chǎn)品伴隨測試用例集一起發(fā)布的時(shí)候,您就會(huì )不經(jīng)意的發(fā)現經(jīng)過(guò)重構以后的測試用例很可能是和產(chǎn)品中的類(lèi)/方法一一對應的。
[什么時(shí)候應該將全部測試都運行一遍?]
Good Question!大師們要求我們每次重構之后都要完整的運行一遍測試用例。這個(gè)要求可以理解,因為重構很可能會(huì )改變整個(gè)代碼的結構或設計,從而導致不可預見(jiàn)的后果,但是如果我正在開(kāi)發(fā)的是一個(gè)ERP怎么辦?運行一遍完整的測試用例可能將花費數個(gè)小時(shí),況且現在很多重構都是由工具做到的,這個(gè)要求的可行性和前提條件都有所動(dòng)搖。所以我認為原則上你可以挑幾個(gè)你覺(jué)得可能受到本次重構影響的TestCase去run,但是如果運行整個(gè)測試包只要花費數秒的時(shí)間,那么不介意你按大師的要求去做。
[什么時(shí)候改進(jìn)一個(gè)TestCase?]
增加的測試用例或重構以后的代碼導致了原來(lái)的TestCase的失去了效果,變得無(wú)意義,甚至可能導致錯誤的結果,這時(shí)是改進(jìn)TestCase的最好時(shí)機。但是有時(shí)你會(huì )發(fā)現,這樣做僅僅導致了原來(lái)的TestCase在設計上是臃腫的,或者是冗余的,這都不要緊,只要它沒(méi)有失效,你仍然不用去改進(jìn)它。記住,TestCase不是你的產(chǎn)品,它不要好看,也不要怎么太科學(xué),甚至沒(méi)有性能要求,它只要能完成它的使命就可以了——這也證明了我們后面所說(shuō)的“用Ctrl-C/Ctrl-V編寫(xiě)測試用例”的可行性。
但是,美國人的想法其實(shí)跟我們還是不太一樣,拿托尼巴贊的MindMap來(lái)說(shuō)吧,其實(shí)畫(huà)MindMap只是為了表現自己的思路,或記憶某些重要的事情,但托尼卻建議大家把MindMap畫(huà)成一件藝術(shù)品,甚至還有很多藝術(shù)家把自己畫(huà)的抽象派MindMap拿出來(lái)幫助托尼做宣傳。同樣,大師們也要求我們把TestCase寫(xiě)的跟代碼一樣質(zhì)量精良,可我想說(shuō)的是,現在國內有幾個(gè)公司能把產(chǎn)品的代碼寫(xiě)的精良??還是一步一步慢慢來(lái)吧。
[為什么原來(lái)通過(guò)的測試用例現在不能通過(guò)了?]
這是一個(gè)警報,Red Alert!它可能表達了兩層意思——都不是什么好意思——1)你剛剛進(jìn)行的重構可能失敗了,或存在一些錯誤未被發(fā)現,至少重構的結果和原來(lái)的代碼不等價(jià)了。2)你剛剛增加的TestCase所表達的意思跟前面已經(jīng)有的TestCase相沖突,也就是說(shuō),新增的功能違背了已有的設計,這種情況大部分可能是之前的設計錯了。但無(wú)論哪錯了,無(wú)論是那層意思,想找到這個(gè)問(wèn)題的根源都比TDD的正常工作要難。
[我怎么知道那里該有一個(gè)方法還是該有一個(gè)類(lèi)?]
這個(gè)問(wèn)題也是常常出現在我的腦海中,無(wú)論你是第一次接觸TDD或者已經(jīng)成為 TDD專(zhuān)家,這個(gè)問(wèn)題都會(huì )纏繞著(zhù)你不放。不過(guò)問(wèn)題的答案可以參考前面的“什么時(shí)候設計”一節,答案不是唯一的。其實(shí)多數時(shí)候你不必考慮未來(lái),今天只做今天的事,只要有重構工具,從方法到類(lèi)和從類(lèi)到方法都很容易。
[我要寫(xiě)一個(gè)TestCase,可是不知道從哪里開(kāi)始?]
從最重要的事開(kāi)始,what matters most?從腳下開(kāi)始,從手頭上的工作開(kāi)始,從眼前的事開(kāi)始。從一個(gè)沒(méi)有UI的核心特性開(kāi)始,從算法開(kāi)始,或者從最有可能耽誤時(shí)間的模塊開(kāi)始,從一個(gè)最嚴重的bug開(kāi)始。這是TDD主義者和鼠目寸光者的一個(gè)共同點(diǎn),不同點(diǎn)是前者早已成竹在胸。
[為什么我的測試總是看起來(lái)有點(diǎn)愚蠢?]
哦?是嗎?來(lái),握個(gè)手,我的也是!不必擔心這一點(diǎn),事實(shí)上,大師們給的例子也相當愚蠢,比如一個(gè)極端的例子是要寫(xiě)一個(gè)兩個(gè)int變量相加的方法,大師先斷言2+3=5,再斷言5+5=10,難道這些代碼不是很愚蠢嗎?其實(shí)這只是一個(gè)極端的例子,當你初次接觸TDD時(shí),寫(xiě)這樣的代碼沒(méi)什么不好,以后當你熟練時(shí)就會(huì )發(fā)現這樣寫(xiě)沒(méi)必要了,要記住,謙虛是通往TDD的必經(jīng)之路!從經(jīng)典開(kāi)發(fā)方法轉向TDD就像從面向過(guò)程轉向面向對象一樣困難,你可能什么都懂,但你寫(xiě)出來(lái)的類(lèi)沒(méi)有一個(gè)純OO的!我的同事還告訴我真正的太極拳,其速度是很快的,不比任何一個(gè)快拳要慢,但是初學(xué)者(通常是指學(xué)習太極拳的前10年)太不容易把每個(gè)姿勢都做對,所以只能慢慢來(lái)。
[什么場(chǎng)合不適用TDD?]
問(wèn)的好,確實(shí)有很多場(chǎng)合不適合使用TDD。比如對軟件質(zhì)量要求極高的軍事或科研產(chǎn)品——神州六號,人命關(guān)天的軟件——醫療設備,等等,再比如設計很重要必須提前做好的軟件,這些都不適合TDD,但是不適合TDD不代表不能寫(xiě)TestCase,只是作用不同,地位不同罷了。
{Best Practise}
[微笑面對編譯錯誤]
學(xué)生時(shí)代最害怕的就是編譯錯誤,編譯錯誤可能會(huì )被老師視為上課不認真聽(tīng)課的證據,或者同學(xué)間相互嘲笑的砝碼。甚至離開(kāi)學(xué)校很多年的老程序員依然害怕它就像害怕遲到一樣,潛意識里似乎編譯錯誤極有可能和工資掛鉤(或者和智商掛鉤,反正都不是什么好事)。其實(shí),只要提交到版本管理的代碼沒(méi)有編譯錯誤就可以了,不要擔心自己手上的代碼的編譯錯誤,通常,編譯錯誤都集中在下面三個(gè)方面:
(1)你的代碼存在低級錯誤
(2)由于某些Interface的實(shí)現尚不存在,所以被測試代碼無(wú)法編譯
(3)由于某些代碼尚不存在,所以測試代碼無(wú)法編譯
請注意第二點(diǎn)與第三點(diǎn)完全不同,前者表明設計已存在,而實(shí)現不存在導致的編譯錯誤;后者則指僅有TestCase而其它什么都沒(méi)有的情況,設計和實(shí)現都不存在,沒(méi)有Interface也沒(méi)有Implementation。
另外,編譯器還有一個(gè)優(yōu)點(diǎn),那就是以最敏捷的身手告訴你,你的代碼中有那些錯誤。當然如果你擁有Eclipse這樣可以及時(shí)提示編譯錯誤的IDE,就不需要這樣的功能了。
[重視你的計劃清單]
在非TDD的情況下,尤其是傳統的瀑布模型的情況下,程序員不會(huì )不知道該做什么,事實(shí)上,總是有設計或者別的什么制品在引導程序員開(kāi)發(fā)。但是在TDD的情況下,這種優(yōu)勢沒(méi)有了,所以一個(gè)計劃清單對你來(lái)說(shuō)十分重要,因為你必須自己發(fā)現該做什么。不同性格的人對于這一點(diǎn)會(huì )有不同的反應,我相信平時(shí)做事沒(méi)什么計劃要依靠別人安排的人(所謂將才)可能略有不適應,不過(guò)不要緊,Tasks和Calendar(又稱(chēng)效率手冊)早已成為現代上班族的必備工具了;而平時(shí)工作生活就很有計劃性的人,比如我:),就會(huì )更喜歡這種自己可以掌控Plan的方式了。
[廢黜每日代碼質(zhì)量檢查]
如果我沒(méi)有記錯的話(huà),PSP對于個(gè)人代碼檢查的要求是蠻嚴格的,而同樣是在針對個(gè)人的問(wèn)題上, TDD卻建議你廢黜每日代碼質(zhì)量檢查,別起疑心,因為你總是在做TestCase要求你做的事情,并且總是有辦法(自動(dòng)的)檢查代碼有沒(méi)有做到這些事情 ——紅燈停綠燈行,所以每日代碼檢查的時(shí)間可能被節省,對于一個(gè)嚴格的PSP實(shí)踐者來(lái)說(shuō),這個(gè)成本還是很可觀(guān)的!
此外,對于每日代碼質(zhì)量檢查的另一個(gè)好處,就是幫助你認識自己的代碼,全面的從宏觀(guān)、微觀(guān)、各個(gè)角度審視自己的成果,現在,當你依照TDD做事時(shí),這個(gè)優(yōu)點(diǎn)也不需要了,還記得前面說(shuō)的TDD的第二個(gè)優(yōu)點(diǎn)嗎,因為你已經(jīng)全面的使用了一遍你的代碼,這完全可以達到目的。
但是,問(wèn)題往往也并不那么簡(jiǎn)單,現在有沒(méi)有人能告訴我,我如何全面審視我所寫(xiě)的測試用例呢?別忘了,它們也是以代碼的形式存在的哦。呵呵,但愿這個(gè)問(wèn)題沒(méi)有把你嚇到,因為我相信到目前為止,它還不是瓶頸問(wèn)題,況且在編寫(xiě)產(chǎn)品代碼的時(shí)候你還是會(huì )自主的發(fā)現很多測試代碼上的沒(méi)考慮到的地方,可以就此修改一下。道理就是如此,世界上沒(méi)有任何方法能代替你思考的過(guò)程,所以也沒(méi)有任何方法能阻止你犯錯誤,TDD僅能讓你更容易發(fā)現這些錯誤而已。
[如果無(wú)法完成一個(gè)大的測試,就從最小的開(kāi)始]
如果我無(wú)法開(kāi)始怎么辦,教科書(shū)上有個(gè)很好的例子:我要寫(xiě)一個(gè)電影列表的類(lèi),我不知道如何下手,如何寫(xiě)測試用例,不要緊,首先想象靜態(tài)的結果,如果我的電影列表剛剛建立呢,那么它應該是空的,OK,就寫(xiě)這個(gè)斷言吧,斷言一個(gè)剛剛初始化的電影列表是空的。這不是愚蠢,這是細節,奧運會(huì )五項全能的金牌得主瑪麗蓮·金是這樣說(shuō)的:“成功人士的共同點(diǎn)在于……如果目標不夠清晰,他們會(huì )首先做通往成功道路上的每一個(gè)細小步驟……”。
[嘗試編寫(xiě)自己的xUnit]
Kent Beck建議大家每當接觸一個(gè)新的語(yǔ)言或開(kāi)發(fā)平臺的時(shí)候,就自己寫(xiě)這個(gè)語(yǔ)言或平臺的xUnit,其實(shí)幾乎所有常用的語(yǔ)言和平臺都已經(jīng)有了自己的 xUnit,而且都是大同小異,但是為什么大師給出了這樣的建議呢。其實(shí)Kent Beck的意思是說(shuō)通過(guò)這樣的方式你可以很快的了解這個(gè)語(yǔ)言或平臺的特性,而且xUnit確實(shí)很簡(jiǎn)單,只要知道原理很快就能寫(xiě)出來(lái)。這對于那些喜歡自己寫(xiě)底層代碼的人,或者喜歡控制力的人而言是個(gè)好消息。
[善于使用Ctrl-C/Ctrl-V來(lái)編寫(xiě)TestCase]
不必擔心TestCase會(huì )有代碼冗余的問(wèn)題,讓它冗余好了。
[永遠都是功能First,改進(jìn)可以稍后進(jìn)行]
上面這個(gè)標題還可以改成另外一句話(huà):避免過(guò)渡設計!
[淘汰陳舊的用例]
舍不得孩子套不著(zhù)狼。不要可惜陳舊的用例,因為它們可能從概念上已經(jīng)是錯誤的了,或僅僅會(huì )得出錯誤的結果,或者在某次重構之后失去了意義。當然也不一定非要刪除它們,從TestSuite中除去(JUnit)或加上Ignored(NUnit)標簽也是一個(gè)好辦法。
[用TestCase做試驗]
如果你在開(kāi)始某個(gè)特性或產(chǎn)品的開(kāi)發(fā)之前對某個(gè)領(lǐng)域不太熟悉或一無(wú)所知,或者對自己在該領(lǐng)域里的能力一無(wú)所知,那么你一定會(huì )選擇做試驗,在有單元測試作工具的情況下,建議你用TestCase做試驗,這看起來(lái)就像你在寫(xiě)一個(gè)驗證功能是否實(shí)現的 TestCase一樣,而事實(shí)上也一樣,只不過(guò)你所驗證的不是代碼本身,而是這些代碼所依賴(lài)的環(huán)境。
[TestCase之間應該盡量獨立]
保證單獨運行一個(gè)TestCase是有意義的。
[不僅測試必須要通過(guò)的代碼,還要測試必須不能通過(guò)的代碼]
這是一個(gè)小技巧,也是不同于設計思路的東西。像越界的值或者亂碼,或者類(lèi)型不符的變量,這些輸入都可能會(huì )導致某個(gè)異常的拋出,或者導致一個(gè)標示“illegal parameters”的返回值,這兩種情況你都應該測試。當然我們無(wú)法枚舉所有錯誤的輸入或外部環(huán)境,這就像我們無(wú)法枚舉所有正確的輸入和外部環(huán)境一樣,只要TestCase能說(shuō)明問(wèn)題就可以了。
[編寫(xiě)代碼的第一步,是在TestCase中用Ctrl-C]
這是一個(gè)高級技巧,呃,是的,我是這個(gè)意思,我不是說(shuō)這個(gè)技巧難以掌握,而是說(shuō)這個(gè)技巧當且僅當你已經(jīng)是一個(gè)TDD高手時(shí),你才能體會(huì )到它的魅力。多次使用TDD的人都有這樣的體會(huì ),既然我的TestCase已經(jīng)寫(xiě)的很好了,很能說(shuō)明問(wèn)題,為什么我的代碼不能從TestCase拷貝一些東西來(lái)呢。當然,這要求你的TestCase已經(jīng)具有很好的表達能力,比如斷言f (5)=125的方式顯然沒(méi)有斷言f(5)=5^(5-2)表達更多的內容。
[測試用例包應該盡量設計成可以自動(dòng)運行的]
如果產(chǎn)品是需要交付源代碼的,那我們應該允許用戶(hù)對代碼進(jìn)行修改或擴充后在自己的環(huán)境下run整個(gè)測試用例包。既然通常情況下的產(chǎn)品是可以自動(dòng)運行的,那為什么同樣作為交付用戶(hù)的制品,測試用例包就不是自動(dòng)運行的呢?即使產(chǎn)品不需要交付源代碼,測試用例包也應該設計成可以自動(dòng)運行的,這為測試部門(mén)或下一版本的開(kāi)發(fā)人員提供了極大的便利。
[只亮一盞紅燈]
大師的建議,前面已經(jīng)提到了,僅僅是建議。
[用TestCase描述你發(fā)現的bug]
如果你在另一個(gè)部門(mén)的同事使用了你的代碼,并且,他發(fā)現了一個(gè)bug,你猜他會(huì )怎么做?他會(huì )立即走到你的工位邊上,大聲斥責說(shuō):“你有bug!”嗎?如果他膽敢這樣對你,對不起,你一定要冷靜下來(lái),不要當面回罵他,相反你可以微微一笑,然后心平氣和的對他說(shuō):“哦,是嗎?那么好吧,給我一個(gè)TestCase證明一下?!爆F在局勢已經(jīng)倒向你這一邊了,如果他還沒(méi)有準備好回答你這致命的一擊,我猜他會(huì )感到非常羞愧,并在內心責怪自己太莽撞。事實(shí)上,如果他的TestCase沒(méi)有過(guò)多的要求你的代碼(而是按你們事前的契約),并且亮了紅燈,那么就可以確定是你的bug,反之,對方則無(wú)理了。用TestCase描述bug的另一個(gè)好處是,不會(huì )因為以后的修改而再次暴露這個(gè)bug,它已經(jīng)成為你發(fā)布每一個(gè)版本之前所必須檢查的內容了。
{關(guān)于單元測試}
單元測試的目標是
Keep the bar green to keep the code clean
這句話(huà)的含義是,事實(shí)上我們只做兩件事情:讓代碼奏效(Keep the bar green)和讓代碼潔凈(Keep the code clean),前者是把事情做對,后者是把事情做好,兩者既是TDD中的兩頂帽子,又是xUnit架構中的因果關(guān)系。
單元測試作為軟件測試的一個(gè)類(lèi)別,并非是xUnit架構創(chuàng )造的,而是很早就有了。但是xUnit架構使得單元測試變得直接、簡(jiǎn)單、高效和規范,這也是單元測試最近幾年飛速發(fā)展成為衡量一個(gè)開(kāi)發(fā)工具和環(huán)境的主要指標之一的原因。正如Martin Fowler所說(shuō):“軟件工程有史以來(lái)從沒(méi)有如此眾多的人大大收益于如此簡(jiǎn)單的代碼!”而且多數語(yǔ)言和平臺的xUnit架構都是大同小異,有的僅是語(yǔ)言不同,其中最有代表性的是JUnit和NUnit,后者是前者的創(chuàng )新和擴展。一個(gè)單元測試框架xUnit應該:1)使每個(gè)TestCase獨立運行;2)使每個(gè)TestCase可以獨立檢測和報告錯誤;3)易于在每次運行之前選擇TestCase。下面是我枚舉出的xUnit框架的概念,這些概念構成了當前業(yè)界單元測試理論和工具的核心:
[測試方法/TestMethod]
測試的最小單位,直接表示為代碼。
[測試用例/TestCase]
由多個(gè)測試方法組成,是一個(gè)完整的對象,是很多TestRunner執行的最小單位。
[測試容器/TestSuite]
由多個(gè)測試用例構成,意在把相同含義的測試用例手動(dòng)安排在一起,TestSuite可以呈樹(shù)狀結構因而便于管理。在實(shí)現時(shí),TestSuite形式上往往也是一個(gè)TestCase或TestFixture。
[斷言/Assertion]
斷言一般有三類(lèi),分別是比較斷言(如assertEquals),條件斷言(如isTrue),和斷言工具(如fail)。
[測試設備/TestFixture]
為每個(gè)測試用例安排一個(gè)SetUp方法和一個(gè)TearDown方法,前者用于在執行該測試用例或該用例中的每個(gè)測試方法前調用以初始化某些內容,后者在執行該測試用例或該用例中的每個(gè)方法之后調用,通常用來(lái)消除測試對系統所做的修改。
[期望異常/Expected Exception]
期望該測試方法拋出某種指定的異常,作為一個(gè)“斷言”內容,同時(shí)也防止因為合情合理的異常而意外的終止了測試過(guò)程。
[種類(lèi)/Category]
為測試用例分類(lèi),實(shí)際使用時(shí)一般有TestSuite就不再使用Category,有Category就不再使用TestSuite。
[忽略/Ignored]
設定該測試用例或測試方法被忽略,也就是不執行的意思。有些被拋棄的TestCase不愿刪除,可以定為Ignored。
[測試執行器/TestRunner]
執行測試的工具,表示以何種方式執行測試,別誤會(huì ),這可不是在代碼中規定的,完全是與測試內容無(wú)關(guān)的行為。比如文本方式,AWT方式,swing方式,或者Eclipse的一個(gè)視圖等等。
{實(shí)例:Fibonaclearcase/" target="_blank" >cci數列}
下面的Sample展示TDDer是如何編寫(xiě)一個(gè)旨在產(chǎn)生Fibonacci數列的方法。
(1)首先寫(xiě)一個(gè)TC,斷言fib(1) = 1;fib(2) = 1;這表示該數列的第一個(gè)元素和第二個(gè)元素都是1。
public void testFab() {
assertEquals(1, fib(1));
assertEquals(1, fib(2));
}
(2)上面這段代碼不能編譯通過(guò),Great!——是的,我是說(shuō)Great!當然,如果你正在用的是Eclipse那你不需要編譯,Eclipse 會(huì )告訴你不存在fib方法,單擊mark會(huì )問(wèn)你要不要新建一個(gè)fib方法,Oh,當然!為了讓上面那個(gè)TC能通過(guò),我們這樣寫(xiě):
public int fib( int n ) {
return 1;
}
(3)現在那個(gè)TC亮了綠燈,wow!應該慶祝一下了。接下來(lái)要增加TC的難度了,測第三個(gè)元素。
public void testFab() {
assertEquals(1, fib(1));
assertEquals(1, fib(2));
assertEquals(2, fib(3));
}
不過(guò)這樣寫(xiě)還不太好看,不如這樣寫(xiě):
public void testFab() {
assertEquals(1, fib(1));
assertEquals(1, fib(2));
assertEquals(fib(1)+fib(2), fib(3));
}
(4)新增加的斷言導致了紅燈,為了扭轉這一局勢我們這樣修改fib方法,其中部分代碼是從上面的代碼中Ctrl-C/Ctrl-V來(lái)的:
public int fib( int n ) {
if ( n == 3 ) return fib(1)+fib(2);
return 1;
}
(5)天哪,這真是個(gè)賤人寫(xiě)的代碼!是啊,不是嗎?因為T(mén)C就是產(chǎn)品的藍本,產(chǎn)品只要恰好滿(mǎn)足TC就ok。所以事情發(fā)展到這個(gè)地步不是fib方法的錯,而是TC的錯,于是TC還要進(jìn)一步要求:
public void testFab() {
assertEquals(1, fib(1));
assertEquals(1, fib(2));
assertEquals(fib(1)+fib(2), fib(3));
assertEquals(fib(2)+fib(3), fib(4));
}
(6)上有政策下有對策。
public int fib( int n ) {
if ( n == 3 ) return fib(1)+fib(2);
if ( n == 4 ) return fib(2)+fib(3);
return 1;
}
(7)好了,不玩了?,F在已經(jīng)不是賤不賤的問(wèn)題了,現在的問(wèn)題是代碼出現了冗余,所以我們要做的是——重構:
public int fib( int n ) {
if ( n == 1 || n == 2 ) return 1;
else return fib( n - 1 ) + fib( n - 2 );
}
(8)好,現在你已經(jīng)fib方法已經(jīng)寫(xiě)完了嗎?錯了,一個(gè)危險的錯誤,你忘了錯誤的輸入了。我們令0表示Fibonacci中沒(méi)有這一項。
public void testFab() {
assertEquals(1, fib(1));
assertEquals(1, fib(2));
assertEquals(fib(1)+fib(2), fib(3));
assertEquals(fib(2)+fib(3), fib(4));
assertEquals(0, fib(0));
assertEquals(0, fib(-1));
}
then change the method fib to make the bar grean:
public int fib( int n ) {
if ( n <= 0 ) return 0;
if ( n == 1 || n == 2 ) return 1;
else return fib( n - 1 ) + fib( n - 2 );
}
(9)下班前最后一件事情,把TC也重構一下:
public void testFab() {
int cases[][] = {
{0, 0}, {-1, 0}, //the wrong parameters
{1, 1}, {2, 1}}; //the first 2 elements
for (int i = 0; i < cases.length; i++)
assertEquals( cases[i][1], fib(cases[i][0]) );
//the rest elements
for (int i = 3; i < 20; i++)
assertEquals(fib(i-1)+fib(i-2), fib(i));
}
(10)打完收工。
{關(guān)于本文的寫(xiě)作}
在本文的寫(xiě)作過(guò)程中,作者也用到了TDD的思維,事實(shí)上作者先構思要寫(xiě)一篇什么樣的文章,然后寫(xiě)出這篇文章應該滿(mǎn)足的幾個(gè)要求,包括功能的要求(要寫(xiě)些什么)和性能的要求(可讀性如何)和質(zhì)量的要求(文字的要求),這些要求起初是一個(gè)也達不到的(因為正文還一個(gè)字沒(méi)有),在這種情況下作者的文章無(wú)法編譯通過(guò),為了達到這些要求,作者不停的寫(xiě)啊寫(xiě)啊,終于在花盡了兩個(gè)月的心血之后完成了當初既定的所有要求(make the bar green),隨后作者整理了一下文章的結構(重構),在滿(mǎn)意的提交給了Blog系統之后,作者穿上了一件綠色的汗衫,趴在地上,學(xué)了兩聲青蛙叫。。。。。。。^_^
{后記:Martin Fowler在中國}
從本文正式完成到發(fā)表的幾個(gè)小時(shí)里,我偶然讀到了Martin Fowler先生北京訪(fǎng)談錄,其間提到了很多對測試驅動(dòng)開(kāi)發(fā)的看法,摘抄在此:
Martin Fowler:當然(值得花一半的時(shí)間來(lái)寫(xiě)單元測試)!因為單元測試能夠使你更快的完成工作。無(wú)數次的實(shí)踐已經(jīng)證明這一點(diǎn)。你的時(shí)間越是緊張,就越要寫(xiě)單元測試,它看上去慢,但實(shí)際上能夠幫助你更快、更舒服地達到目的。
Martin Fowler:什么叫重要?什么叫不重要?這是需要逐漸認識的,不是想當然的。我為絕大多數的模塊寫(xiě)單元測試,是有點(diǎn)煩人,但是當你意識到這工作的價(jià)值時(shí),你會(huì )欣然的。
Martin Fowler:對全世界的程序員我都是那么幾條建議:……第二,學(xué)習測試驅動(dòng)開(kāi)發(fā),這種新的方法會(huì )改變你對于軟件開(kāi)發(fā)的看法?!?/P>
——《程序員》,2005年7月刊
{鳴謝}
fhawk
Dennis Chen
般若菩提
Kent Beck
Martin Fowler
c2.com
原文轉自:http://kjueaiud.com