本文很多內容來自選自TDD實例一書。
預備知識
最好有一些預備知識,例如xUnit,Moq,如何編寫易於測試的代碼,這些內容我都寫了文章:https://www.cnblogs.com/cgzl/p/9178672.html#test。
Test Driven Development
什么是TDD(Test Driven Development)?
TDD是一個軟件開發過程,這個過程依賴於重復性的小開發周期:需求被轉化為具體的測試用例,然后改進程序以便通過測試。
在TDD里有兩條規則:
- 只在有未通過的自動化測試的情況下,你才會去寫新的代碼
- 消滅重復
這兩條規則在技術上的含義是:
- 你必須進行良好的設計,運行的代碼可在決策之間提供反饋
- 開發人員得寫自己的測試
- 開發環境可以針對微小的變化需要提供快速的響應
- 您的設計必須由眾多高內聚、低耦合的組件組成,這樣測試會更簡單。
這兩條規則也意味着編程的三個任務:
- Red - 先寫一個不能工作/通過的小測試,甚至根本無法編譯
- Green - 快速讓這個測試通過,無論代碼有多爛
- Refactor - 消除上個步驟中的代碼重復。
Red,Green,Refactor,這就是TDD的咒語。
如果TDD可以很好的執行,那么它就會大幅度減少代碼缺陷的密度,也使工作的主題對於相關人員來說更加清晰。所以,TDD也具有社會含義:
- 如果缺陷密度可以降低到足夠的程度,那么QA就會從被動變為主動的工作。
- 如果那些“讓人討厭的驚喜”可以減少到足夠的程度,那么項目經理就可以精確的評估以便讓客戶參與到每日的開發工作中。
- 如果技術會議的主題足夠清晰,那么程序員就會按分鍾去工作而不是按天或周來安排和進行工作。
- 如果缺陷密度可以降低到足夠的程度,那么我們每天都可以交付出具有新功能的軟件,這就會與客戶建立新的業務關系。
這些概念都很簡單,但是動機是什么?為什么開發人員要去寫自動測試代碼?為什么開發人員在他們的思維能夠大幅飆升的設計時,卻只進行小步工作? 勇氣。
勇氣
TDD是編程過程中管理恐懼的一種辦法。
這個恐懼不是壞事,它是一種合理的恐懼,例如:”這個問題確實很難,我從開始的感覺看不到盡頭“。
如果疼痛是喊停的自然表達,那么恐懼就是告訴你要“小心”。
小心是很好的,但是恐懼還有一些其它的影響:
- 讓你不得不進行更多試探性操作
- 讓你交流的更少
- 讓你羞於反饋
- 讓你脾氣暴躁
這些影響在開發的時候對你都沒有任何幫助,尤其是遇到困難問題的時候。那么你如何面對困難處境並且:
- 取代嘗試/試探,而是盡快進行具體學習
- 取代爭吵,而是進行更清楚的溝通
- 取代避免反饋,而是尋求幫助,和具體的反饋
- 控制你自己的脾氣
TDD會管理這些事情。
為什么要TDD
從業務角度:
- 提供了需求的確認。通過編寫測試以及RGR周期,需求確認很自然的在軟件開發的過程中就完成了。
- 捕獲回歸問題。回歸問題就是指隨着軟件新功能的發布,以前的某些功能卻不好用了。TDD可以很早的發現回歸問題。
- 綜上兩點,TDD也降低了維護成本。
從開發人員角度講,TDD還有以下好處:
- 設計為先的心態。寫測試的時候,我們就得考慮與軟件的交互應該如何實現,以便把這些功能需求編程可能。
- 防止過度工程。關注於如何讓測試通過和滿足客戶的期待,就會讓我們保持正軌,而不是迷失於架構設計和幻想那么無法提供很多價值的最佳抽像設計中。
- 增加開發人員的動力。取代了花費幾天時間想盡辦法來實現某個功能這樣的操作,TDD把需求分解成一些測試,並結合RGR流程,這就允許你可以持續快速的進展並建立成功循環。
- 收獲自信。通過大量的測試結果,你感動支配的力量,無論修改、重構、增加功能都變得很簡單。
第一個實例
在本例中,您將會看到TDD的如下步驟:
- 快速添加一個測試
- 運行所有的測試(包括以前寫的),可以看到新添加的測試Fail了
- 修改一點代碼
- 運行所有測試,都成功了
- 重構,移除重復
建立.NET Core 項目
這個很簡單,首先建立一個Console App:
然后再添加一個xUnit項目:
這個測試項目需要引用Console項目。
需求
有這樣一份報表:
現在想要做成支持多幣種的:
這里還提供了匯率:
目標就是產生第二張圖那樣的報表。
開始操作
我們需要做哪些工作?
- 讓兩種幣種的錢數可以進行加法操作,並通過給定的匯率算出結果。
- 讓股票單價可以乘以股票數並得出總額。
上面是一個待辦問題列表(To-Do List)。我們就關注於這個待辦列表即可。
列表里的問題應該是逐個解決的,解決完一個划掉一個;如果有新問題,就在后邊加上一條。
編寫測試
下面我們開始,先不建立對象,先寫測試:
讓編譯通過
這里有很多問題,編譯也無法通過,這些問題我們也是一個一個來解決。
1. 首先,沒有Dollar這個類,那就建立Dollar這個類:
第一個問題解決了。
2. 沒有相應的構造函數,那就建立構造函數:
又解決了一個問題!
3. 沒有Times()這個方法,那就建立該方法:
又解決了一個問題!
4. 沒有Amount屬性,建立該屬性:
編譯問題都解決了!!
看一下測試方法:
編譯錯誤肯定是沒有了。
測試Fail
然后跑測試:
不出意料肯定會Fail。
讓測試通過
現在有了具體的這個Fail的測試,我們現在的任務就是讓該測試變成Pass,而不是實現多幣種報表,先讓這個測試通過,再慢慢讓其它測試通過。
您可能不喜歡這樣,但是現在的目標不是做出完美的解決方案,目標就是讓這個測試通過,所以這時候代碼可能很爛:
我寫死了數字10。
然后再跑測試:
測試Pass了!!
重構,移除重復
別着急,周期還沒結束。
現在,我們需要移除重復。但是重復在哪?
通常你看到的重復是指代碼的重復,這里是指測試中的數據和代碼中數據的重復。
這個10是哪來的? 它實際上是:
是通過5乘以2得來的。
所以代碼中的5*2和測試中的5*2是重復的。 我們需要移除這個重復,但是可能需要不止一步來實現。
先把乘法移動到Times方法里試試:
這樣的話,測試仍然會pass:
這是一小步。
那么5是哪里來的?
應該是從構造函數傳遞進來的,我們可以把它存到Amount屬性里:
所以我們可以在Times方法里使用它:
現在處理這個2,它應該可以使用參數multiplier代替:
OK!
此外,我們可以對代碼的語法進行一些優化:
其實某些優化也應該通過TDD的RGR周期來實現。
第一篇文章就簡單介紹這些。