天天看點

如何說服你的同僚使用TDDBob大叔的保齡球訓練TDD的三項法則TDD的優勢TDD的局限行動起來參考内容

TDD(Test-driven development),也就是我們常說的“測試驅動開發”,是由 Kent Beck 在1996年提出的概念。TDD這個術語,經常被人挂在嘴邊,然而真正在項目實施,卻寥寥無幾。

是TDD對開發者要求太高?還是TDD根本就不值得去做?

非也。為了讓大家對TDD有一個具體而親切的認識,我先給大家舉一個在程式設計中使用TDD進行開發的實際例子。

Bob大叔的保齡球訓練

這是一道計算保齡球比賽一局總得分的程式設計題,保齡球的計分規則非常簡單:

  • 每一局總共有十輪,每輪一開始會有十支球瓶,球手可以扔兩次球,目标就是用盡量少的球把全部球瓶擊倒。
  • 如果第一球就把全部的球瓶都擊倒了,也就是STRIKE,畫面出現“X”,就算完成一輪了,所得分數是10分再加後面兩球的倒瓶數,
  • 如果第一球沒有全倒,就要再打一球,如果第二球将剩下的球瓶全都擊倒,也就是SPARE,畫面出現“/”,也算完成一格,所得分數為10分再加下一格第一球的倒瓶數,
  • 如果第二球也沒有把球瓶全部擊倒的話,那分數就是第一球加第二球倒的瓶數,沒有獎勵(bonus),再接着打下一格。依此類推。
  • 第十輪有機會扔三次球。如果在第十輪出現STRIKE或者SPARE,則球手可再加打第三球。
  • 全部十輪的得分相加就等于這一局的總得分。

題目要求我們提供一個名字為Game的類,這個類有兩個方法:

  • roll(pins : int):每次球員扔球後執行這個方法,入參是此次扔球擊倒的球瓶數量。
  • score():每局比賽結束時執行的方法,傳回這局比賽的總得分。

下面開始使用TDD來完成這個程式設計訓練。

如果此時你已經在開始構思要如何實作,請打住!因為這不是TDD的風格。

記住,先别想着怎麼去實作,先寫測試用例,也就是先把你調用這個Game類的代碼寫下來。

首先,我們建立一個BowlingGameTest類:

import junit.framework.TestCase;

public class BowlingGameTest extends TestCase {
}
           

接着添加第一個測試用例:

如何說服你的同僚使用TDDBob大叔的保齡球訓練TDD的三項法則TDD的優勢TDD的局限行動起來參考内容

當我們剛剛new了一個Game對象時,編譯器就提示錯誤了,此時暫停測試用例的編寫,開始編寫産品代碼!(這麼做似乎有點過于耿直,不過對于加深對TDD的印象還是很有幫助的)

我們建立了Game類,此時編譯通過,執行所有單元測試,綠條!

如何說服你的同僚使用TDDBob大叔的保齡球訓練TDD的三項法則TDD的優勢TDD的局限行動起來參考内容

接着我們在第一個單元測試中調用roll方法和score方法,同樣的,我們遇到編譯不通過的問題,再依次給Game加上對應方法後,我們得到了下面這段代碼:

如何說服你的同僚使用TDDBob大叔的保齡球訓練TDD的三項法則TDD的優勢TDD的局限行動起來參考内容

我們心裡很清楚,這個代碼是經不住考驗的,我們随便添加一個單元測試,都可以讓測試用例不通過。比如我們讓一個保齡球世界排名倒數第一的球手去比賽,每輪他都隻擊倒一個球瓶,測試用例毫不猶豫地失敗了:

如何說服你的同僚使用TDDBob大叔的保齡球訓練TDD的三項法則TDD的優勢TDD的局限行動起來參考内容

于是我們要修改一下邏輯,在每次roll的時候,加上分數。細心的讀者可能還發現了,下面這段代碼還對測試代碼進行了重構,把每個單元測試都要做的new Game()操作抽取到了setUp方法中:

如何說服你的同僚使用TDDBob大叔的保齡球訓練TDD的三項法則TDD的優勢TDD的局限行動起來參考内容

接着我們又發現我們經常要模拟很多次擊倒相同數量球瓶的操作,是以我們把這個操作抽取成一個rollMany方法:

如何說服你的同僚使用TDDBob大叔的保齡球訓練TDD的三項法則TDD的優勢TDD的局限行動起來參考内容

我們的代碼到這裡還完成不到1/3,但是我們已經做了兩次重構,沒錯,TDD的過程,也是不斷小步重構的過程。

接下來,我們測試一下Spare的場景,這一次測試用例又理所當然的失敗了(不要擔心一次次失敗會打擊自信心,因為這些都是我們刻意制造的失敗,人們面對意料之中的失敗往往更有激情)。而當我們準備動手修改産品代碼時,卻發現了一個代碼設計層面的問題,那就是我們在roll方法裡面做了roll不應該做的事情,roll意味着扔球,而我們卻在裡面修改了得分:

如何說服你的同僚使用TDDBob大叔的保齡球訓練TDD的三項法則TDD的優勢TDD的局限行動起來參考内容

此時我們需要把新增的用例暫時屏蔽掉,然後對産品代碼進行重構,roll專心做它的事,把計算得分的活交給score來做:

如何說服你的同僚使用TDDBob大叔的保齡球訓練TDD的三項法則TDD的優勢TDD的局限行動起來參考内容

接下來,就是繼續放開我們之前屏蔽掉的測試用例,繼續修改産品代碼,讓測試用例通過,然後再添加STRIKE的測試場景、添加更多的測試場景…… 這些過程就不再贅述了,因為作為一個TDD的例子,前面這幾個步驟,已經足夠讓大家對TDD有一個比較深刻地了解了。

關于這道題目的完整解答過程,大家可以到Bob大叔的TheBowlingGameKata去下載下傳對應的PPT。

TDD的三項法則

上面的保齡球訓練中,我們一直在遵循着TDD的三項法則:

  • 在編寫好失敗的單元測試之前,不要寫任何産品代碼。
  • 隻要有一個單元測試失敗了,就不要再寫測試代碼。無法通過編譯也是一種失敗。
  • 産品代碼恰好能夠讓目前失敗的單元測試成功通過即可,不要多寫。

遵循着三項法則,我們開發的過程就是下面這五個步驟不斷循環、小步疊進的過程:

  1. 添加測試用例。
  2. 運作所有測試用例,如果新用例失敗了,執行下一步,否則傳回上一步。
  3. 編寫産品代碼。
  4. 運作所有測試用例,如果通過,執行下一步,否則傳回上一步,直到寫出滿足測試用例的代碼。
  5. 重構。
  6. 傳回第一步,繼續循環。

TDD的優勢

TDD帶來的最大好處是提高了單元測試的覆寫率。 傳統的先寫産品代碼,再寫單元測試,有兩個弊端:

  • 一方面是由于産品代碼已經成型,生米已經煮成熟飯,你再來寫,很容易就會陷入思維定式中,起不到發現Bug的作用;
  • 另一方面,這往往會讓單元測試成為一項政治任務,産品釋出前幾天,發現單元測試覆寫率不足,來一場全員寫測試用例的運動,這樣寫出來的代碼品質肯定不好高。

那麼提高了單元測試的覆寫率對産品有什麼好處呢?健康的單元測試覆寫率是在90%以上,為什麼要那麼高?高覆寫率帶來的好處主要有以下三點:

  • 确定性。每輪疊代我們的産品都會新增不少代碼,這些代碼對之前的功能有沒有影響?如果我們有一套覆寫率達到90%的單元測試,那麼我們隻需執跑一遍測試用例,如果全部通過,那麼我們至少就有90%的把握可以傳遞。反之,如果覆寫率越低,我們也就越心虛,越需要更多的人力去進行手動驗證。
  • 讓你有勇氣重構代碼。當你看到糟糕的代碼時,你的第一反應是:WTF!!! 接着,你會說,我才不去碰它,萬一碰出問題了,還不都是我的鍋! 但是如果你能夠确信自己對代碼進行的大刀闊斧的修改,不會破壞任何東西,那麼你是不是就更有勇氣去重構它了?這就是TDD最強大的地方,它讓你擁有一套值得信賴的測試,打消你對修改代碼的恐懼。Martin Flower在他的《重構》中也指出,完善的單元測試是他進行重構的基石。
  • 單元測試即是文檔。同僚離職了,他之前負責的子產品交到了你手上,你要盡快熟悉這個子產品的業務邏輯。看文檔?程式員寫的文章一般都不太容易看,而且文檔經常會和代碼不同步,代碼修改了文檔沒跟着改的事情經常發生。看源碼?看完也不一定知道為什麼要這麼實作呀。如果這時候有一套非常完整的單元測試,那絕對是所有接手别人代碼的程式員的福音!首先,代碼不會撒謊,其次,測試用例明确告訴了你這個函數是做什麼的,什麼輸入對應的都有什麼預期輸出。單元測試就是最好的底層文檔,哪個專業人士不想提供這樣一份文檔呢?

除了提高單元測試的覆寫率,TDD還能夠促成良好的代碼設計。由于你先寫測試代碼,你會盡可能的讓代碼調用起來更加簡單友善,這也就促使你去考慮如何更好的設計代碼。如果不先寫測試,最後很有可能就會出現一個函數裡實作的功能過多,或者和其他代碼過于耦合而無法測試的情況。

TDD的局限

在學習一項技術時,總是要提醒自己——“沒有銀彈”,任何技術都有其局限性。然而,由于用TDD的人實在不多,在網上搜了很久,也看不到什麼特别有建設性的觀點。下面是我找到的一些關于TDD局限性的看法:

  • 對開發者有較高要求。要想做好TDD,必須掌握好單元測試、重構等技能,還要能夠寫出整潔代碼,不然如果寫出來的單元測試都很糟糕,那隻會加重維護的負擔。 —— 評論:那我更要用了,這才能顯得我技術過硬啊…..
  • 習慣的轉變。對于一直習慣上來就寫代碼的程式員,現在要他們先寫測試用例,難免會讓他們有些不習慣。 —— 評論:這不跟戒煙一個道理麼,改掉壞習慣、養成好習慣的過程總是痛苦的,等我練成TDD大法之後,哼哼…
  • 過多的測試用例使得建構變得緩慢。 —— 評論:這就要求我們減少沒有意義的測試用例了,比如一些簡單的get和set方法,就不需要寫測試用例啦。

行動起來

回到這篇文章的主題,“如何說服你的同僚使用TDD”,首先,你被我說服了麼?

如果你的回答是Yes,你被我說服了,你打算開始使用TDD,好,下面我給出一些練習和使用TDD的建議。

1)TDD Katas 訓練

先不要急着在工作中去使用TDD,Bob大叔的網站上還有很多跟保齡球訓練類似的題目,你可以去練習一下。而且,相同的練習可以反複訓練,Bob大叔在他的《程式員的職業素養》裡是這麼說的:

和習武者一樣,程式員應該懂得很多種不同的卡塔,并定期練習,確定不會淡化或遺忘…

真正的挑戰是把一個卡塔練習到爐火純青,你可以窺見其中的規律。要做到這一點可不容易。

下面是Bob大叔推薦的卡塔:

  • 保齡球
  • 素因子
  • 自動換行

Bob大叔的首頁上還有很多其他的Kata,大家可以上去探索探索。

2)程式設計題訓練

網上TDD的例子确實有限,但程式設計訓練題卻是一大把,很多網站也都收錄了許多經典的算法和資料結構的題目,我們完全可以使用TDD來對付這些題,把代碼送出上去,如果沒有通過,就說明自己的測試用例不全。

使用TDD來對付程式設計題,非但不會影響你的答題時間,反而讓你一小步一小步的完成題目,而不是像以前一樣,思考了很久,卻一行代碼都沒寫出來。

網上提供線上程式設計練習的網站很多,我自己現在在用的是LintCode,大家也可以去自己喜歡的網站上練習。

3)面試時使用TDD

面試時,如果面試官讓你在紙上寫代碼,那就給面試官show一下TDD吧,将紙張一分為二,一半寫測試用例,一半寫實際代碼,當然,你可以先寫僞碼,因為總免不了要重構,全部寫完再用實際代碼寫一遍,交給面試官。

同樣的,TDD可以緩解你一行代碼都寫不出來的緊張心情。

4)運用到實際項目中

實際項目中運用TDD,通常不像做程式設計題那麼輕松,你可能需要使用Mock/Stub之類的東西,不過這些都有現成的代碼庫,比如Spring就提供了一套很友善的測試庫,你隻需要花費一點時間了解一下如何使用即可。

如果你有個開明的上司,不妨在做TDD之前跟他說一下,說不定他和你有一樣的想法,并且會給你一些支援;如果你的上司看起來并不那麼開明,你覺得有很大可能性他會禁止你TDD,那就别告訴他,悄悄執行,如果最後确實有成效,跟他說,并且要求在組内做一次技術分享,如果最後沒什麼成效,也不要緊,上司看到的隻是你測試用例非常完善的代碼。

好了,我似乎又走題了,到底“如何說服你的同僚使用TDD”,很簡單,用實際行動告訴他們!如果你在使用了TDD之後,确實提升了代碼品質,降低了代碼缺陷率,那麼請理直氣壯地和他們分享你使用TDD的心得。

TDD是專業人士的選擇,它是一項能夠提升代碼确定性,給程式員激勵、降低代碼缺陷率、優化文檔和設計的原則。對TDD的各項嘗試表明,不使用TDD就說明你可能還不夠專業。 —— Bob,《程式員的職業素養》

參考内容

  • 維基百科 - TDD
  • 《程式員的職業素養》
  • UncleBob.TheBowlingGameKata
  • 《重構》
  • 《測試驅動開發》
  • 保齡球規則
  • The Pros and Cons of Test-Driven Development

繼續閱讀