01、前言
很早之前,曾在網絡上見到過 TDD 這 3 個大寫的英文字母,它是 Test Driven Development 這三個單詞的縮寫,也就是“測試驅動開發”的意思——聽起來很不錯的一種理念。
其理念主要是確保兩件事:
- 確保所有的需求都能被照顧到。
- 在代碼不斷增加和重構的過程中,可以檢查所有的功能是否正確。
但后來很長一段時間里,都沒再聽過 TDD 的消息。有人說,TDD 已經死了,給出的意見如下:
1)通常來說,開發人員不應該在沒有失敗的測試用例下編寫代碼——這似乎是合理的,但是它可能導致過度測試。例如,為了保證一行生產代碼的正確性,你不由得寫了 4 行測試代碼,這意味著一旦這一行生產代碼需要修改,你也得修改那 4 行測試代碼。
2)為了遵循 TDD 而寫的代碼,容易進入一個誤區:代碼是為了滿足測試用的,而忽略了實際需求。
02、TDD 到底是什么?
不管 TDD 到底死了沒有,先讓我們來回顧一下 TDD 到底是什么。
TDD 的基本思想就是在開發功能代碼之前,先編寫測試代碼。也就是說在明確要開發某個功能后,首先思考如何對這個功能進行測試,并完成測試代碼的編寫,然后編寫相關的代碼滿足這些測試用例。然后循環進行添加其他功能,直到完成全部功能的開發。
TDD 的基本過程可以拆解為以下 6 個步驟:
1) 分析需求,把需求拆分為具體的任務。
2) 從任務列表中取出一個任務,并對其編寫測試用例。
3) 由于沒有實際的功能代碼,測試代碼不大可能會通過(紅)。
4) 編寫對應的功能代碼,盡快讓測試代碼通過(綠)。
5) 對代碼進行重構,并保證測試通過(重構)。
6) 重復以上步驟。
可以用下圖來表示上述過程。
?
03、TDD 的實踐過程
通常情況下,我們都習慣在需求分析完成之后,盡快地投入功能代碼的編寫工作中,之后再去調用和測試。
而 TDD 則不同,它假設我們已經有了一個“測試用戶”了,它是功能代碼的第一個使用者,盡管功能代碼還不太完善。
當我們站在“測試用戶”的角度去寫測試代碼的時候,我們要考慮的是,這個“測試用戶”該如何使用功能代碼呢?是通過一個類直接調用方法呢(靜態方法),還是構建類的實例去調用方法呢(實例方法)?這個方法如何傳參呢?方法如何命名呢?方法有返回值嗎?
有了測試代碼后,我們開始編寫功能代碼,并且要以最快地速度讓測試由“紅”變為“綠”,可能此時的功能代碼很不優雅,不過沒關系。
當測試通過以后,我們就可以放心大膽的對功能代碼進行“重構”了——優化原來比較丑陋、臃腫、性能偏差的代碼。
接下來,假設我們接到了一個開發需求:
汪汪隊要到小鎮冒險島進行表演,門票為 99 元,冒險島上唯一的一個程序員王二需要開發一款可以計算門票收入的小程序。
按照 TDD 的流程,王二需要先使用 Junit 編寫一個簡單的測試用例,測試預期是:銷售一張門票的收入是 99 元。
public?class?TicketTest?{private?Ticket?ticket;@Beforepublic?void?setUp()?throws?Exception?{ticket?=?new?Ticket();}@Testpublic?void?test()?{BigDecimal?total?=?new?BigDecimal("99");assertEquals(total,?ticket.sale(1));}}
為了便于編譯能夠順利通過,王二需要一個簡單的 Ticket 類:
public?class?Ticket?{public?BigDecimal?sale(int?count)?{return?BigDecimal.ZERO;}}
測試用例運行結果如下圖所示,紅色表示測試沒有通過:預期結果是 99,實際結果是 0。
那接下來,王二需要快速讓測試通過,Ticket.sale()
?方法修改后的結果如下:
public?class?Ticket?{public?BigDecimal?sale(int?count)?{if?(count?==?1)?{return?new?BigDecimal("99");}return?BigDecimal.ZERO;}}
再運行一下測試用例,結果如下圖所示,綠色表示測試通過了:預期結果是 99,實際結果是 99。
綠了,綠了,測試通過了,到了該重構功能代碼的時候了。99 元是個魔法數字,至少應該聲明成常量,對吧?
public?class?Ticket?{private?final?static?int?PRICE?=?99;public?BigDecimal?sale(int?count)?{if?(count?==?1)?{return?new?BigDecimal(PRICE);}return?BigDecimal.ZERO;}}
重構完后再運行一下測試用例,確保測試通過的情況下,再增加幾個測試用例,比如說門票銷量為負數、零甚至一千的情況。
public?class?TicketTest?{private?Ticket?ticket;@Beforepublic?void?setUp()?throws?Exception?{ticket?=?new?Ticket();}@Testpublic?void?testOne()?{BigDecimal?total?=?new?BigDecimal("99");assertEquals(total,?ticket.sale(1));}@Test(expected=IllegalArgumentException.class)public?void?testNegative()?{ticket.sale(-1);}@Testpublic?void?testZero()?{assertEquals(BigDecimal.ZERO,?ticket.sale(0));}@Testpublic?void?test1000()?{assertEquals(new?BigDecimal(99000),?ticket.sale(1000));}}
銷量為負數的時候,王二希望功能代碼能夠拋出異常;銷量為零的時候,功能代碼的計算結果應該為零;銷量為一千的時候,計算結果應該為 99000。
有兩個測試用例沒有通過,那么王二需要繼續修改功能代碼,調整如下:
public?class?Ticket?{private?final?static?int?PRICE?=?99;public?BigDecimal?sale(int?count)?{if?(count?<?0)?{throw?new?IllegalArgumentException("銷量不能為負數");}if?(count?==?0)?{return?BigDecimal.ZERO;}if?(count?==?1)?{return?new?BigDecimal(PRICE);}return?new?BigDecimal(PRICE?*?count);}}
再運行一下測試用例,發現都通過了。又到了重構的時候了,銷量為零、或者大于等于一的時候,代碼可以合并,于是重構結果如下:
public?class?Ticket?{private?final?static?int?PRICE?=?99;public?BigDecimal?sale(int?count)?{if?(count?<?0)?{throw?new?IllegalArgumentException("銷量不能為負數");}return?new?BigDecimal(PRICE?*?count);}}
重構結束后,再運行測試用例,確保重構后的代碼依然可用。
04、最后
從上面的實踐過程可以得出如下結論:
TDD 想要做的就是讓我們對自己的代碼充滿信心,因為我們可以通過測試代碼來判斷這段代碼是否正確無誤。
也就是說,TDD 流程比較關鍵的一環在于如何寫出有效的測試代碼,這里有 4 個原則可以參考:
1)測試過程應該盡量模擬正常使用的過程。
2)應該盡量做到分支覆蓋。
3)測試數據應該盡量包括真實數據,以及邊界數據。
4)測試語句和測試數據應該盡量簡單,容易理解。
注意,這 4 個原則不僅適用于 TDD,同樣適用于任何流程下的單元測試。
最后,我想說的是,不管 TDD 有沒有死,TDD 都不是銀彈,不可能適合所有的場景,但這不應該成為我們拒絕它的理由。
【B站最全最易學】十年大佬終于將測試開發路線整理出來了,小白一學就會,拿走不謝,允許白嫖!!