測試驅動開發:我們要的不僅僅是“質量”
測試驅動開發是極限編程里很重要的一個實踐,很多其他實踐都是以這個實踐為基礎的。
測試驅動開發核心就是所有的實現都是測試“逼”出來的,所有的實現代碼都是為了讓測試通過而編寫的,如果測試都通過了我們就可以拒絕再添加新的功能了或新的代碼了,要讓我寫更多的代碼,好,添加一個測試吧。
測試驅動開發的道理很簡單:要知道好壞,用用才知道。比如,你想要買個東西,你肯定最想知道的是使用過這個東西人的評價,因為沒有使用這個東西很難知道這個東西是不是好的。軟件和代碼也是一樣,你設計一塊代碼為的是什么,為的是別的代碼可以調用這塊代碼干一些事情,那怎么才能知道你寫的代碼是不是好的呢。那么測試驅動開發就是,我先以調用方的角度來調用這塊代碼,并且從調用方的角度說出我期望的結果。在這里我們不僅僅對期望的結果進行驗證,很重要的一點是,我們先用用你這個接口,好不好用。一般來講如果你的代碼依賴很多其他元素,那么將是很難測試的,因為在單元測試中你必須解除對其他元素的依賴,所以使用測試驅動開發出來的代碼,往往具有很好的低耦合性。
當我們寫單元測試時,我們往往在一個測試里只測試“目的”,如果你發現你的測試方法名稱里總是帶有and/or之類的單詞時,這就表明被測試的方法職責可能不單一,這也有助于幫助我們找出職責不單一的代碼。
單元測試一個最重要的作用估計就是設置安全網,將你的代碼放置在這個網中,當以后需要改進代碼的時候可以快速的自動化回歸以前所有的測試,以保證你改進代碼的時候沒有破壞以前的功能。但人都是懶惰的,如果是先寫功能實現代碼,后寫測試,有的時候我們就得過且過算了,因為覺得那測試實在太簡單了,而且有時候因為功能代碼寫得不好,造成后面很難添加測試,然后懶惰心理作怪,得過且過,就把測試給跳過去了。如果我們確定一個規則:沒有測試失敗我拒絕寫代碼,那么在你寫功能代碼的時候這張安全網就已經設置好了。后面所有的任何代碼都是為了修復失敗的測試。而且看見綠條一次又一次的亮起,總是給我們無比的信心,一個接一個的測試通過了,就是告訴我們正在穩步前進,我們離成功又接近了一步。
測試驅動開發環
下面是經典的測試驅動環:
我們先寫一個失敗的測試(實際上第一個測試可能編譯都無法通過),然后編寫很少的代碼讓測試迅速通過,然后我們看看代碼是不是需要重構,在這里我們不僅僅需要重構功能代碼,測試代碼也需要重構。然后我們接著編寫下一個測試,這個循環將不斷的推動下去。
編寫測試時我就僅僅關注測試,我不想去如何實現,我只想我要什么(what),至于怎么做(how)是下一個環節要考慮的。這樣有什么好處呢?首先,這樣可以避免測試被實現牽著鼻子走,以前為什么反對程序員自己給自己的軟件寫測試,就因為實現是他寫的,他可能因為定性思維自動的而且無意識的繞過了一些陷阱,所以在測試驅動開發的時候,有的人常常出現這種情況:鍵盤上敲的是測試代碼,但大腦中全部是實現。所以在上一篇談論結對編程的文章中,有一種結對編程就是一個人寫測試,另外一個人寫實現就是為了避免這種情況。
拿個最簡單的例子來說,如果你用MVC來開發一個web,你要寫一個action方法,那么你想要什么?第一個:也許是視圖的名字,第二個:也許是要put到視圖中的模型數據。好,那我們就驗證這個:
1: ModelAndView mv = bookController.show(bookId);
2: assertThat(mv.getViewName(),is("book"));
3: assertThat(mv.getModel().get("book"),is(book));
嗯,我開始就寫出上面的代碼,這是我想要的,至于怎么實現那是后面的事情,我在這兒并不關心。在這里也說明了一種測試驅動開發時候的技巧:因為測試時我們需要先準備數據,準備設施,因為這個時候功能實現代碼還不存在,有可能我們很難寫出測試代碼,所以往往碰到無從下手的情況,對于這種情況,我們會先寫出assert語句,我們把我們想要的東西先放在這兒,然后往反向推導,畢竟不管怎樣我們想要什么,我們還是知道滴。
在編寫實現的時候要主意的是千萬不要過度,我們編寫剛剛好的代碼,讓我們失敗的那個測試通過就好了,不需要你一氣呵成,寫一大堆代碼出來。因為你寫實現代碼的時候總是在假想你所寫的代碼是真實的實現,但往往這會走向過度設計。關注現在是避免過度設計最好的良藥。
在測試通過后,千萬別忘記了重構代碼,因為之前的環節我們總是在關注與當前的一小塊范圍,可能產生了很多重復的代碼,或者變量命名都是草草了事,這個時候更應該從更高的視角來審視剛才的代碼,做有必要的重構,然后編寫下一個測試。
大部分測試驅動開發說的都是使用單元測試然后驅動出功能代碼,實際上測試驅動開發可以上升到更高的層次,從功能測試開始。往往一個用戶故事來了,QA(知道有哪些測試用例)和開發人員結對(當然,有些QA是可以獨立編寫功能測試的)編寫出該用戶故事如果要驗收的話需要通過的功能測試。這是測試驅動開發的外層反饋環,然后使用單元測試驅動功能代碼,這是內層反饋環(反饋是極限編程四個準則之一,這四個準則是溝通、反饋、簡單、勇氣)。我們就是通過不斷的向前探索,不斷的收到反饋來穩妥的完成我們的任務的,我們的信心也在不斷地增強,進度也在不斷地推進。
在《測試驅動開發藝術》這本書里提到,測試驅動開發應該遵守三項基本原則:
絕不跳過重構
不跳過重構,保證我們的代碼質量不走向腐化,在這個時候我們不僅僅只重構我們的功能代碼,對測試代碼也要公平對待,隨著我們測試代碼的不斷加入,肯定有重復的地方可以提取,這些都要重構,保持測試代碼整潔對以后的重構工作非常重要。
盡快變綠
反饋是極限編程四個基本原則之一(其他三個是:溝通、簡單、勇氣),運行測試的一個作用就是能快速地提供反饋。運行一下測試,測試就會告訴你,你剛才的那一步走的怎樣。保證每踏出一腳都是穩穩當當的很重要,不僅能建立起信心,而且如果某一步失敗了,我們可以立馬確定是哪里出錯了。
有些開發人員習慣一下子寫出四五個測試,然后再去實現,他們還爭辯說,我很清楚需要這么幾個測試用例,我何必要在測試代碼和功能實現代碼之間跳來跳去呢。首先不說你是不是能一下子想清楚這些測試用例。再次,寫出太多的測試,可能讓這些測試編譯通過就要費一番周折(因為功能代碼還不存在,可能有的類和方法都不存在),然后還要費更多的功夫讓這所有的測試通過,那么你停留在紅的階段就太久了,不利于給自己打氣事關事小,步驟邁得太大,最后跌倒了會更痛。
出錯后放慢腳步
后記
測試驅動開發是敏捷實踐中一個非常重要的話題,本篇從理論上稍稍觸及了一下測試驅動開發的一些原則,下篇會從代碼上說明一些具體的測試驅動開發中的慣用法。
推薦書籍
《測試驅動開發的藝術》 我為這本書寫了個書評
《測試驅動的面向對象軟件開發》
《Test-Driven Development by Example》
《xUnit Patterns》