TDD 生存手冊
1. 做 TDD是為什麼?
關於TDD的概念、工具、技巧等,經典的書籍材料可能介紹的更為全面細緻。我所能分享的是從一個普通開發的角度怎麼看待 TDD 的。以及我是怎麼從感興趣,到充滿困惑,再到有限的嘗試,直到有一天驀然回首發現已經自然而然的用起了TDD的過程。希望能對有著類似困惑仍在探索的同學有所幫助。
遺憾的是,在開始所謂「乾貨」以前,首先還是要談談理念。因為我發現這是一個繞不過去的問題。
你為什麼要使用 TDD / 寫 unit test?
不同的人也許有不同的答案:
因為這是現在流行的,「正確」的開發方式;
因為這樣寫出來的代碼質量更高;
因為 TDD 和 unit test 能產生更好的設計;
因為老闆要求必須達到 xx% 的覆蓋率;
……
以及一個派生的問題,
如果說:測試只能用來證明 bug 的存在,而不能證明程序沒有bug。
那麼:寫 Unit Test 的意義是什麼?程序員寫出的 Unit Test 與軟體質量有什麼關係?
2. 一切不以重構為目標的單元測試都是耍流氓
當然,這裡是指在 TDD 語境下的單元測試。
在與同道交流 TDD 經驗,特別是與測試人員交流時。我們明顯的發現,TDD 所說的 Test,與測試人員口中的 Test 完全不是一回事。我們甚至討論過能不能用其它的詞語替換「測試」或 Test,來避免歧義。
經過朋友的啟發和反思自己對 TDD 的執念的來源後,我發現對於我來說,TDD 中寫測試的真正目的,是重構。
我常常會在讀代碼或寫代碼時產生種種的衝動:「這是什麼鬼」,「我為什麼要把生命浪費在這種東西上」, 「一定有更好的辦法」。
我需要通過重構來寫出更合理的代碼。
為了安全的重構,我需要測試。
而與TDD相關的其它好處,比如文檔化,將來作為回歸測試集,促使開發人員從用戶角度思考等等,都只是在更高效的改進代碼的過程中附帶產生的。
換句話說,如果你不準備在將來修改代碼,無論主動(重構)還是被動(改bug,加功能),那麼寫單元測試對你完全是浪費時間。
但是話說回來,如果你真的確信這段代碼永遠無需修改,那麼不要說單元測試,源代碼也是沒有必要的。不是么?
回到前面的另一個問題,TDD中的單元測試與代碼質量之間的關係。
我的回答是:測試用例本身不能保證質量。
並不是有了更多的測試數量,更高的覆蓋比例,代碼就自然變好了。如果說TDD能提高質量,那一定是因為TDD給了開發者安全和快速反饋的環境進行重構,從而幫助開發者不斷改進寫出更好的代碼。
打個比方,同一個作者,一篇文章是在交稿前半個小時趕著寫完,錯別字都沒改就發出來的;另一篇發布以前斟酌再三,幾易其稿。哪一篇的質量會更高一些呢?
答案是顯而易見的吧。不過請再想一想,寫代碼與寫文章的相似程度有多少?代碼真的是越改質量越高么?
3.反脆弱的代碼
「反脆弱」是《反脆弱》這本書的作者生造的一個詞。描述的是脆弱的反面,一種我們都知道卻沒有名稱的性質。一般我們認為脆弱的反面是堅固,然而堅固僅僅是對外部變化不敏感。反脆弱指的是具有這種性質的東西可以從外部變化中獲利,正如同脆弱的東西會被外部變化損害一樣。
對大部分的程序員而言,變化是個不受歡迎的詞。在我們談論健壯的代碼,合理的設計時,針對的假想敵就是未來的變化。關於將來的變化,我們能想到的最好結果不過是不要搞砸現在設計好的一切。
換句話說,我們追求的是堅固的代碼,歷經變化的侵蝕屹立不倒。
那麼,有沒有反脆弱的代碼,在變化的滋養中生長壯大呢?
對待可能的變化,不外乎三種態度:
這段代碼不打算在將來再被利用了,所以完全不用考慮改變。這不失為一種實用的態度。但是現實中這樣的情況太少。
現在寫出一個完美的設計,為所有可能的改變做好準備,這樣將來就不會改變了。但是這是可望而不可即的目標。暫且不論需求變更等不受我們控制的,外部變化。僅僅就開發者自己而言,不論我們今天作出多少努力,隨著我們在解決問題過程中的成長,往往在明天我們就是會遺憾當初沒有作出更好的選擇。
為改變做好準備,並且主動地,時時刻刻地進行改變。這就是TDD的選擇,可靠地對代碼進行改變。並在這種改變中不斷改善。
對於不熟悉的人而言,初看起來,TDD最大的特點是在實現代碼之前寫測試這個反直覺的實踐。卻往往忽略了藏在後面重構的那一步。事實上,前兩步的紅燈、綠燈,都是在為第三步的重構做準備。
第一步,寫出失敗的測試。是在為即將發生的重構建起保護網。
第二步,儘快的綠燈通過。是刻意寫出需要重構的代碼。
既然認為改變是有潛在破壞性的,那就儘早地、儘可能頻繁地去改變代碼。
測試與重構,像是硬幣的兩面一樣,密不可分。
所以,如果你仍然認為重構像是吃完飯要洗碗一樣的必要但是附屬性的工作。如果你還沒有感受到 TDD 帶給你的保護與自由,讓你放開對改變的恐懼,心安理得的寫下將來必然會被改掉的代碼。
那麼就算你按照三步循環去寫代碼,恐怕也難以從中獲得益處。很快就退回盡量預先思索,想小步卻慢不下來的老路上。
4.何謂持續
事實上,任何一個老練的程序員必然都有自己的一套方法來反覆驗證和調整正在開發的代碼。這些方法可能包括,可控環境下的調試,添加一個臨時的main方法作為實驗入口,把代碼片段複製到外部環境進行驗證等等。
TDD 中的增量開發、小步快跑,用這些方法也可以做到。
我想這大概就是為什麼有人會提出其實人人都在做 TDD 吧。儘管我不是特別認同這種說法。
如果沒有那個點的話,也許做不做 TDD 確實無所謂。
這個點有時候叫做交付,也可能叫集成、發布;甚至有時候並沒有一個清晰的事件點,不過是寫完放下,過了幾個星期而已。
但是這個點是實實在在存在的,它就是「鮮活」代碼和遺留代碼的分界點。越過了這一點,你手中的代碼就會搖身一變,從那個開朗敏捷的少年,變做陰鬱固執喜怒無常的怪獸。
TDD 的獨特之處,是讓測試伴隨代碼從生到死的整個生命周期,始終為代碼變化提供保護網,讓代碼的「保鮮期」儘可能的長,抹平這個轉變的節點。
現在持續集成、持續交付的概念已經是主流了。但是什麼是持續呢?個人淺見,不是說設置了一個伺服器,定時跑幾個任務就是持續了。而是不再有那個代碼保鮮期的拐點,可以一直平滑的發展下去。
TDD無疑是它的重要保證環節。
5.開發者體驗
TDD 的好處有哪些?關於這個問題,我原來總是嘗試從客觀的角度來回答。比如質量,比如可維護性,比如鼓勵好的設計等等。總之,就是剔除了人的因素。
然而,當我認真探索自己這一路走來的歷程。是怎麼在 TDD 上略有心得後情不自禁的在社區分享。
重新開始寫博客,幾乎每篇都是關於 TDD 的。自發的公司里組織編程道場(Dojo)推廣 TDD。背後的動力其實很簡單,這樣開發讓我很爽。
這個答案聽起來實在太不正式,好像也沒啥說服力。但是確實是我的真實想法。
有人可能會說:工作嘛哪能那麼理想化,老闆給你工資就行了,誰管你開心不開心。
且不說更開心的程序員應該效率更高,而且開心本身就是公司狀況良好的體現之類的客觀化的理由。
從開發者個人而言,就算僅僅為了心情愉快、延年益壽,也是值得去做些努力去改進代碼的。因為改善代碼質量和開發流程,本身就是改善工作環境。
最近恰好讀了一篇研究程序員各種不爽的論文。其中統計了上千個程序員的答卷。對工作中的不爽進行了分類。
可以看到儘管工作中有不少不受我們控制的部分,比如人的原因(416個)和公司流程(544個), 但是最大的一部分還是來源於代碼相關問題(788個)
再來看看常見的讓程序員不爽的原因。前三位中的兩個是:
解決問題被卡住。
糟糕的代碼質量以及代碼習慣。
而這些都是可以通過開發者自己努力來改善的。我的切身感受,TDD 帶給了我如下變化:
交付代碼的時候充滿了信心。
從測試或者客戶那裡得到意外的錯誤後,不是感覺恐慌,而是回顧一遍測試,往往已經能定位到原因了。
幾乎從不調試程序。
要修改遺留代碼,對質量又不滿意的時候,不再一邊忍耐一邊抱怨。因為我心裡很清楚,我能可靠的改掉它,只要有必要這麼做。
我想這大概就是 TDD 為什麼給我帶來這麼大幸福感的原因吧。
6. 成長路徑
下面我結合個人體驗寫一下從初識 TDD,到能夠得心應手的使用的過程,希望能有所幫助。
6.1著土
掌握最基本的,讓 TDD 成為可能的技術。比如:什麼是單元測試,如何在不同環境下運行單元測試,有哪些可選的框架等等。
在網路時代,這個階段應該是最容易的,各種資源和教程觸手可及。另外隨著業界對測試越來越重視,較新的語言、框架、平台都把測試作為標配提供支持。所以這個階段應該很容易就能度過。
6.2出芽
嘗試使用TDD做一些簡單程序。體會紅燈、綠燈、重構的循環過程。
本階段往往有兩個結果,一種是試了試完全摸不著頭腦;另一種是試了試非常好用,然後拿去實用發現完全不是那麼回事。
正像前面提到的 TDD 最重要的不是表面上的三步循環,而是轉變編寫程序的思路。如果你仍懷著對修改代碼的恐懼,依賴於提前「想清楚」,那麼先寫測試並不會幫到你多少。這更像是學習騎自行車或游泳一樣,僅僅理解並沒太大用處,需要一個過程去體會和掌握。
本階段可以說是一大難點,很多人可能就是在這裡覺得TDD可望不可即,或者僅僅是看起來很美。下面是我的一些建議。
一開始可以亦步亦趨根據教程示例做一遍。但是之後一定要找一個沒有做過的題目嘗試自己解決。
不宜選擇簡單到你一下就可以在腦子裡寫出偽代碼的問題,但是也不要選過於複雜的問題。練習常用的Kata是個不錯的選擇。詳情見後面的Kata介紹。
很有可能嘗試了卻沒有成功,別擔心這是正常的。如果你練習的是熟知的Kata的話,可以在網上找找別人解的過程,很多都是有視頻的。看完有心得了以後再做一次。
「裝傻」是本階段的一個技巧。因為你已經有了一套如何解決問題的方法,在轉換到新的做法的中間過程里,往往不自覺的用原有的信念來評判新的做法。
這時需要靠裝傻來暫時放下已有的東西。 學習的時候不妨把它作為一項挑戰,看看自己能寫出多傻的代碼,能用多慢的節奏達到目標。
「一次一個問題」是另一個需要練習才能掌握的技巧。嘗試在循環的每一步只關注於一個問題:編寫測試、實現功能、或是改善設計。 這個建議也適用於更高層面的問題。
比如,在練習的時候不要去擔心諸如:「這樣性能太差了」,或者「如果我每段代碼都花這麼長時間寫測試,明天老闆就會炒了我」之類的問題。
如果你不把自己限定為一個「Java 程序員」或「PHP 程序員」,可以考慮用一種不熟悉的語言結合 TDD 來解決某個熟悉的問題。
在重拾初學者身份後,往往會意識到一個看似簡單的問題在解決過程中有多少需要高清的地方,更容易體會到 TDD 的方式在這個過程中所起的作用。
事實上這個階段實在有點挑戰,我建議最好找人一起練習。代碼道場(Dojo)和代碼靜修(Code Retreat)是很好的練習活動。如果有機會可以考慮參加。
當然很可能你在周邊找不到這樣的活動,但是又很想參加。可以考慮自己組織,沒錯我是認真的。從中你會獲得更多意外的收穫。
6.3生根
當你在上個階段獲得了收穫,對 TDD 方法有了足夠的信心。這時就可以開始考慮在工作中玩真格的了。
如果在上個階段學到的夠多,那麼用在工作中並不是很困難的一件事。但是,還是有很多的坑要注意,畢竟這不再是自己搗鼓了。
最好選擇新增的,相對較為獨立的模塊開始嘗試。
一方面這是因為可以避開很多技術上的難點,更重要的是因為這種代碼涉及的人比較少。相對而言更不容易受到阻力。
可能你會覺得日常工作中更多的是修改老代碼,並沒有多少機會新增一塊。是的,所以一定要珍惜這樣的機會啊!
每當我看到已經有了足夠能力的程序員在寫嶄新的代碼時,卻沒有為它配上足夠的測試保護,任由它慢慢的變得混亂脆弱。總是無比的惋惜。
如果的確沒有新模塊的機會,可以把比較基礎的代碼,比如工具類的部分進行抽取,用單元測試圍起來,然後進行重構也是不錯的。
一個常見的困難是感覺採用了TDD後進度慢了很多,擔心領導或者老闆不答應。
這還真不是個簡單問題:
首先,要區分真的進度慢了,還是感覺進度慢了。有些時候在壓力之下,我們往往是自欺欺人的估算一個「理想情況」下的進度,然後假定真的能趕上。
如果是這種情況,實打實的寫出測試來更有利於做出現實的估算。雖然拿到任務的第一天就告訴老闆延期很難說出口,我認為還是要比最後一天再說要好一些。
有可能是因為僅僅關注在「開發」的進度上,卻沒有考慮在調試和測試階段省下的時間。如果有這樣的壓力,可以先在不引起太大抵觸的範圍內採用TDD,並且關注是否在後續的階段大幅提高了效率。
如果的確有效果,相信大家會越來越理解和接受;如果毫無效果,那可能要反省一下是不是哪裡做的有問題了。
學習新的方法是需要一個過程的。這也是為什麼在上個階段特別提出要做專門練習的原因。
如果公司和領導並不是特別給你支持,而你又真的希望通過掌握新方法來提高。那可能還是需要自己在工作之外做些努力來度過這個階段。
在壓力之下人總是會傾向於採用熟悉的方法。哪怕明知道最後會搞得一團糟也還是這樣,畢竟那一團糟是自己熟悉的一團糟。 因此實做中發現沒有練習中那麼行雲流水是很正常的。給自己定下實際的期望值,逐步提高。比如:
寫了這麼多代碼,至少要有一個測試。
我寫的每句代碼在交付前至少都用測試驗證過。
每次我都先試試先寫個測試小步前進,實在不行了再退回原來的方式。
在實際工作中發現退回老路,建議抽出專門的時間按照上個階段的方式繼續練習。我在學習TDD的過程中的最大附帶收穫就是養成了練習的習慣。
可能很多程序員聽到練習兩個字就煩。畢竟懶惰是程序員的一大美德嘛。我們是腦力工作者又不是搬磚。練那麼熟、記那麼多東西又有什麼用呢?總還是比不過自動化的程序和搜索引擎。
的確是這樣的。不過練習的目的不是超過程序和搜索引擎,而是遷就我們大腦有限的運算量。只有熟練到一定程度,大腦才可以不再疲於應對各種細節,有空去關心真正重要的問題。在改變的過程中這一點非常重要。
6.4破土
隨著越來越多的使用新方法,自然而然地會想把它推廣到更大的範圍。這時就要面對遺留代碼這塊硬骨頭了。
假如你是團隊中最早採用 TDD 的人,很可能碰到很多沒有測試,而且難以測試的代碼。
這裡一定要隆重介紹《修改代碼的藝術》(Working Effectively with Legacy Code)。在這個階段我曾經疑惑了很久,陷入了一個無解的死循環里,多虧了這本書的點撥才得以突破。
這個無解的問題是這樣的:
代碼好爛,想要重構。
為了重構,需要寫測試。
代碼好爛,沒法測試,先要重構。
為了重構,需要寫測試。
……
破解的方法嘛,其實說來很簡單。以最少的代價邁出第一步,在沒有測試保護的情況下進行重構,為後續有序的循環打開大門。
具體的手法和技巧,這本書里講的非常好了。建議帶著問題去讀,一定收穫滿滿。
需要注意的是,有些時候為了在板結的陳舊代碼上敲開一條縫,必須要採用一些不是那麼「最佳實踐」的方式。比如放寬可見性,取消 final 限制等等。
這些做法很有可能會遭到反對。最極端的情況下,為了方便測試修改哪怕一行代碼,有些人都會覺得是荒謬的。
這時候反覆爭論是沒有太大意義的。反對者有他們正當的理由。正如前面談到的堅固與反脆弱的代碼的兩種心態。他們把這種改變看作千里大堤上的一個蟻穴,還看不到在將來的改善中能帶來的收益。
所以,重要的不是誰說服誰,而是做出實效。首先表明自己的做法,然後在互相可接受的限度內去做。
有一點特別特別要注意:不要用 PowerMock 之類的「黑魔法」去遷就代碼,費盡心力只是為了避免因為加測試而修改代碼。別忘了,寫測試的目的是圈起一塊領地來馴服遺留代碼,而不是把測試當作一層粉飾去貼在代碼之上。
6.5 成材
上個階段可以說是一個分水嶺,就像學游泳學會踩水,一旦掌握就「淹不死」了。到了這個階段你應該已經很有信心的在各種場合使用 TDD 了。後面主要考慮的是如何更加高效的使用這種方法,怎麼帶動更多的人。
這個階段我也還在路上,只能說說我觀察到的大規模的推動 TDD 中可能會碰到的一些坑。
小心 Mock 濫用。Mock,包括相當一部分的Stub,應該用來表達對象間的職責。而不是模擬不必要的實現細節。
避免深的測試類繼承結構。極端情況就是「雙樹結構」,測試類將生產代碼的類結構依樣畫葫蘆又做了一遍。其實我的個人看法是測試類和測試幫助類都根本不應該出現繼承。
不要過於執著完全的、絕對一致的方法論。
這可以說是程序員的職業病,無論什麼方法聽到的第一反應是找反例,即使一萬個場合有用,只要一個場合不行,立即就覺得這是個無效的方法。
對於寫程序這可能是很好的習慣,畢竟一個萬分之一機率崩潰的軟體基本上是沒用的。但是人不同於機器,並不會碰到一個方法論不能解釋的情況就進入死循環。80% 情況下好用的方法就已經很有幫助了。
這種心態的另一面,是一旦相信了一種方法,就認定它必須 100% 貫徹到每個角落。
特別是在剛剛開始進入這一階段的時候,很容易雄心勃勃的規劃一個嶄新的版圖,一套絕對化的規則來改天換地。
為什麼不要這麼做?
往往缺乏投入產出比,為了寫測試而寫測試,花費大量精力在已死的代碼或等死的代碼上。
在團隊和組織中對 TDD 有疑慮的情況下徒增反對的可能。
規劃大,見效慢,有違小步快跑的精神。
將乾巴巴的規則凌駕於活生生的個例之上,實際上是期待自己的道理能一勞永逸的解決所有問題的懶惰思維。更重要的是堵塞了將來進一步改進的機會。
幾個啟發性的問題:
一個測試從寫好以後就再也沒有失敗過,說明它非常有效還是完全無用?
看看你最新寫的測試,什麼時候可以安全的刪掉它?到了那個時候,如果是另一個程序員維護,他有沒有信心刪除?
回顧最新一次TDD的過程,能不能用更少的測試達到同樣的信心級別?
7. 附錄7.1一些 Kata 題目:
FizzBuzz(http://codingdojo.org/kata/FizzBuzz/):由於問題非常簡單。適合用來講解TDD的概念。這樣學習者的注意力可以全部集中在流程和方法上。但也是因為問題太過簡單,不適合自己拿來練習如何用TDD解決問題。
因數分解(http://butunclebob.com/ArticleS.UncleBob.ThePrimeFactorsKata):來自Uncle Bob的題目和解題過程,很好的展示了TDD如何超出預期簡單地解決這個問題。
羅馬數字(http://codingdojo.org/kata/RomanNumerals/):有一定複雜度的題目。適合用來練習如何分解問題,以及怎麼通過重構簡化代碼。
網球記分(http://codingdojo.org/kata/Tennis/):對於不熟悉業務規則的人需要花一點時間搞清楚邏輯。問題本身較為簡單但是繁瑣。適合用來練習如何對付If套If的代碼。
String Calculator(http://osherove.com/tdd-kata-1/):練習需求不斷變更的情況下如何寫代碼。一定要老老實實按照題目要求做一步再看下一步。
LCD(http://codingdojo.org/kata/NumberToLCD/)和Bank OCR(http://codingdojo.org/kata/BankOCR/):兩個題目有類似的地方,比較適合練習如何分解單一職責。
生命遊戲(http://codingdojo.org/kata/GameOfLife/):經典的題目,對於設計測試和測試的順序比較有挑戰。
哈利波特(http://codingdojo.org/kata/Potter/):偏演算法,有一定的難度。
7.2網路資源
姚若舟老師的各種Kata視頻:http://video.tudou.com/v/XMjIwOTM3ODIzNg==?f=35151150
TDD社區Kata接力(https://www.evernote.com/shard/s188/sh/55ba699d-05f7-4baa-9032-1692fff28cad/c08df590e81753fe?from=timeline&isappinstalled=0)
Cyber Dojo是一個非常好的刻意練習 TDD 的網站。想要進一步了解的可以看看這篇介紹,Cyber Dojo 設計者談 Cyber Dojo——為了好玩執行代碼(http://www.jianshu.com/p/148b898342a3)
Codewars (https://www.codewars.com/)提供了很多題目,並且有由易到難的升級系統。相對於 Cyber Dojo,它最大的優勢是可以看到其他人的優秀解法。不足的是沒有對於 TDD 流程的支持。


※一張塗鴉搞定探索式測試
※持續交付和 DevOps 的前世今生
※為什麼「領域模型」需要充血?
TAG:謝工的GitChat |
※方舟生存D版安裝MOD
※《INTOUCHABLES》 希望使人生存
※物競天擇,「ADAPT」者生存
※PD-1抑製劑Opdivo可延長NSCLC患者生存期
※MOBA+生存競技 網易首款上帝視角生存競技手游《孤島先鋒》
※《生存者同盟》HD重置 今秋登陸PS4,Switch及PC
※MOBA+生存競技 《孤島先鋒》網易首款上帝視角生存競技手游
※MIKIMOTO:小眾品牌中國大市場的生存法則 | 財富品質
※宅客模玩秀——SIC混搭 wizard生存
※CODOL新版本生存法則即將上線 反向縮圈絕命逃生
※《Dead In Vinland》糅合了RPG、冒險和生存管理
※PD-1聯合化療史上首次被證實可改善NSCLC總生存期—抗癌管家
※VR生存類遊戲《Echoes VR》登陸KS開啟眾籌 目標金額7,819英鎊
※在絕地求生的3D生存競技之後,網易推出了MOBA生存競技並有8.8分
※The Walking Dead 聯手 Puma,打造末日生存裝備
※強敵環伺 MMORPG如何在險境中生存
※Steam周銷量排行榜:《絕地求生》「生存通行證:維寒迪」DLC登頂 《ATLAS》位居榜單第二
※VR生存冒險遊戲《PROZE》免費序章將上線
※上海時裝周AW19 preview RICO LEE,星際生存
※「MD PHOTO」河智苑等藝人出席 火星生存體驗真人秀《伽利略》發布會