TDD:Test-driven development測試驅動開發。是一種開發流程,觀念是「先寫測試,在進入開發工作」。在進行開發工作以前,編寫測試,預先模擬欲測試的情境
好處有:
- 可以確保每次改動的狀況,不會改A壞B
- 新進人員可以透過測試更加了解每個function在做什麼事,包含呼叫情境,傳入的參數及預期的結果值
- 做refactor不再擔心受怕
- 在開發前,可以利用測試case, 清楚確認使用情境,減少溝通成本
實際上的開發步驟:
- 寫測試。編寫測試,加入test case (此時test會fail)
- 寫程式。開始寫code, 目的是要讓 test pass
- 重構程式碼。並循環以上步驟refactor 你的code, 但test 還是要pass
我自己實作起來的痛點有:
- 通常寫完function在寫測試會不知道如何下手,然後就不寫
- 要測試的功能與其他功能耦合程度太高,不知如何下手
- mock一大堆東西,寫測試變得很痛苦
雖然已經有很多文章討論過寫測試的好處,但依我自己的想法,寫test的好處有:
- 更清楚將來的function必須符合哪些功用(預期哪些行為)
- 該抓出哪些錯誤,code中的error該怎麼噴
- 比較敢改code, 改完跑測試就知道有沒有改爆了XD
- 可以依功能來測試,不用等別人的部分(如果是多人合作專案的話更為重要)
- 可以減少修bug的時間(也不用怕別人改到你的code在拉你去debug的可怕情境)
若看完以上,對於如何實踐還有疑問,接下來會有個實作範例。
以一個演算法的經典題目:704.Binary Search 實作TDD吧!
第一步、確定需求,寫出Test Case.
在Binary Search 這個函數中,讓使用者輸入一個array、一個目標數字,函數會判斷目標數字是否在array中,回傳目標數字的index,或是目標數字沒有在array內則回傳-1。
因此可以先定義一個binary_search函式,會有兩個input,一個是nums(格式是list),另一個是target(格式為int)。此外,這個函式的回傳值是index/-1(格式是int)。
接下來要開始寫測試,根據這的函式的功能,對它的預期行為有:
1.目標數字在數列中,可以正確返回index
2.邊界條件:不論數列長為奇數或偶數,皆可正確返回最首端與最末端的index
3.目標數字不在數列中,可以正確返回-1
4.邊界條件:空數列及長數列皆可正常運行
因此可以寫出test case:
unit test之可以這麼直觀的寫的原因,是因為單元測試有冪等性,每次有相同的input就必須有相同的output,因此可以直接使用斷言(assert)來檢查test case是否被滿足。
這樣就完成測試了!此時跑測試應該會失敗:
值得注意的是,這邊的舉例是將所有test case寫在同一個測試函式內,因此只要有個test case fail了,下面的test case都會跑不到。所以這邊的寫法可以依據需求拆成很多個測試函式,也可以用一個測試類來囊括這個函式的子測試,可以在測試失敗的時候,一目了然fail的測試有哪些。
另外還有一個點,測試通常會獨立放在專案目錄的tests 資料夾下,不會散落在專案之間。
第二步、修改function, 直到測試通過
測試寫完之後的下一步,就是把函式完成。
因為本篇著重於TDD如何實踐,binary search的功能實踐就以較容易上手的二分法來處理。
此時再跑一次測試,就會通過了
第三步、refactor function, 要讓測試一直保持通過狀態
函式的基本功能完成了,接下來就是要讓函式優化。
基本的優化函式可以從三個層面下手:
1. 程式碼的可讀性
2. 記憶體的優化
3. 運算效能的優化
更高層級的架構面的優化將不在這篇討論,有機會再與讀者分享。
如此就完成了一次TDD的開發流程!
完成了一次測試的基本實作,相信大家還是有很多的問題:
1. 要搭配api的呼叫該怎麼寫測試?
2. 在測試中如何操作資料庫?
...
有一個基本的大原則,就是把一個功能再拆分,拆分成一個可以測試的功能跟不能測試的功能,所謂的humble object pattern。例如:把api內的邏輯拆出來成另一個function,對function做unit test。
此外,TDD搭配持續整合(CI)一同使用,可以提升團隊開發的效率。
希望看完這篇,可以讓讀者對於TDD的實作可以有更深入的了解 :)
有想法跟問題也歡迎提出來一同討論!