軟體的「僵硬化」往往是悄悄發生的。某天當我們想加一個小功能,卻發現必須動到一大片程式碼,才驚覺自己不小心把軟體寫成了硬體。為了改善這個問題,本章開始介紹一個近年來大幅改變許多開發者習慣的方法:測試驅動開發(TDD, Test-Driven Development)。

起源#

一般認為,TDD 是由 Kent Beck 在 2000 年前後提出的。不過 Kent Beck 自己也曾說過,他並非原創者,他只是把一個原本就有人在使用、且他覺得很棒的方法系統化整理出來,讓大家更容易上手。

二十多年來,這個方法確實改變了許多人的 coding 習慣,包括他的好友 Robert C. Martin(鮑伯大叔)。鮑伯大叔曾不只一次在公開場合分享,他第一次親眼看到 Kent Beck 用 TDD 以「數秒 commit 一次」的節奏推進工作,而程式、測試與重構(Refactor)三件事在完工同時就一起完成的震撼。從此之後,他成了 TDD 的愛好者與推廣者。

TDD 的基本操作#

TDD 的核心是由三個動作組成的循環:

  • 先寫一個會失敗的測試
  • 動手讓測試通過
  • 重構

把一個功能拆成多個小小的驗收條件,反覆執行以上三步,就是 TDD 的基本套路。然而要在實際工作中真的用得起來,並不容易,每一個循環裡該包含什麼內容,本身就是一門學問。

在第一步裡,根據需求寫出測試之後,直接執行,這時測試一定會失敗(fail),畢竟產品程式還沒寫,會過才奇怪。IDE 會以紅色提示,因此稱為「紅燈(Red)」狀態。

接下來要做的,就是寫剛剛好讓這個測試通過的程式。這裡要堅持「Baby Step」原則,能少做就少做,只求這個測項通過,其他先不要管。再跑一次測試後,IDE 顯示綠色,進入「綠燈(Green)」狀態。

很多人到此會迫不及待地切到下一個測項,但 Kent Beck 建議我們先停一下,看看眼前的程式有沒有可以重構的地方。重構之後再跑一次測試,依 Martin Fowler 對重構的定義,物件對外的行為不該改變,因此測試應該維持綠燈。

重構這一步是大多數人在 TDD 上摔跤的地方。

實際操作時,因為每一步都很小,我們很容易誤以為「沒什麼可重構」。但程式的擴張與變醜其實是一下子的事,如果不在這時停下來看,等多寫了幾個測項才回頭,往往已經醜到回不去。

為什麼這樣設計#

TDD 之所以被設計成這樣,是希望我們隨時保持程式的可運行性與整潔度,並避免過度設計。

第一步先寫一個會失敗的測試,原因有兩個:

  • 用測試先把需求描述出來,等程式寫完,測試通過,就能說明需求被滿足。
  • 確認此時測試會失敗。如果還沒加任何程式,測試就通過了,代表它什麼都沒測到,是無效測試,應立即刪除,否則會傷害測試套件的整潔度。許多人 TDD 越做越慢,測試品質劣化是主因之一。

第二步只做剛好能多通過這個測試的邏輯。再多做的事都是「超前部署」,在下一個測試還沒出現前,這些設計通常都是浪費。

很多人喜歡一開始就花大把時間思考完整設計,但實作到一半往往會冒出更好的解法,因為手上的證據變多了。如果先期投入越多,要丟的也越多。先抓住大方向動手,反而比較經濟。

第三步重構的目的是「及早發現,及早治療」。每完成一點點事就花二三十秒回頭看一眼,多半就能找到改善之處。剛寫完邏輯時,整體結構最熟悉,又有測試保護,正是重構的最佳時機。如果拖到所有功能完工才一起整理,壞味道往往已經累積到無法重構,只能重寫了,許多人這時就乾脆放棄,留下技術債。

Kuma 表示#

如果讀者對 TDD 的印象只是「先寫測試再寫程式」,那是很大的誤會。雖然名字裡有 Test,但 TDD 的本質不是測試,而是需求。它要我們做的事,是「先試著把需求描述清楚」,這樣等程式寫完才有依據判斷它是對是錯。

那為什麼非得用單元測試來描述需求?作者整理了兩個理由:

  • 單元測試的結構就是「準備、執行、對答案」,剛好對應到需求的「前提、過程、結果」。這也就是後續會看到的三 A 原則(Arrange-Act-Assert, AAA)以及 Given-When-Then 的精神。
  • 單元測試會被執行,而且「錯了會叫」。寫在紙上的需求錯了可不會發出警告。

有了會自動報錯的需求描述,開發者就能放心對程式做修改與設計。這並不是硬性規定,而是開發者在追求效率與正確率的過程中自然演化出來的結果。

如果跟客戶或 PM 溝通需求時,你習慣問「現在情況如何?想要怎麼進行?變成怎樣會更好?」那 TDD 的思路應該很適合你。

原文出處#

  • 原書/iThome:https://ithelp.ithome.com.tw/articles/10266679