一.前言
不知道大家有沒聽過“測試先行的開發”這一說法,作為一種開發實踐,在過去進行開發時,一般是先開發用戶界面或者是類,然后再在此基礎上編寫測試。
但在TDD中,首先是進行測試用例的編寫,然后再進行類或者用戶界面的開發。由於要先開發測試用例,那么開發人員就必須清楚測試的目的,所測功能模塊的業務邏輯以及需要測試的場景。
這樣TDD確保了項目的代碼與所需的業務是匹配的,並且在日后的開發工作中也能確保之前所做的功能的可測試性。
很多同學問TDD是使用那種編程語言,或者是某種技術,這里需要明確的是,TDD並不是某種技術,而是一種項目實踐。
導語:
傳統開發模式與TDD開發模式的區別在哪里?TDD開發的困難之處和優點是什么?TDD具體開發過程中又需要用到哪些技術知識點?且看本文作者通過實例來為你闡述TDD的開發流程,讓你對TDD有一個大致的了解。
二.傳統開發模式與TDD開發模式
1. 傳統開發模式流程:
項目代碼開發 -> 編寫測試用例 –> 運行測試用例 -> 修復代碼BUG
2. TDD開發模式流程
編寫測試用例 -> 運行測試用例 –> 編寫項目代碼 -> 運行測試用例 -> 重構代碼
三.TDD入門難題
說到寫測試用例,一般的同學都覺得沒啥問題,就根據已有的代碼,順着葫蘆摸瓜的就把該測試的方法都照着“套”一遍,測試有哪些結果也得順着項目的代碼來。
至於這測試代碼能不能測出項目的問題,那是另外的問題,關鍵是測試代碼要Pass,那我的工作才能算完。
但是要在還沒項目代碼之前寫測試用例,那就是等於我要憑空想象那些個抽象又晦澀難懂的功能點,還得要在心中勾勒出它們的輪廓以及細節,這不等於讓我自己畫個餅來充飢么?問題是我連這餅應該是啥樣子都還沒個譜,這是何等的悲涼啊,要是“藍胖子”在我身邊就好了~
四.TDD的優點
1. 保證代碼質量,鼓勵開發人員僅編寫滿足需求的代碼。
“李雷”同學號稱馬虎王,經常各種物品丟失,比如女友(對象)丟失;借用物品(引用)丟失;使用“IO自來水”之后不關閥門;去倉庫(Database)取貨,因為忘記某些物品,不得不頻繁往返等等。
“韓梅梅”同學功力深厚,精心打造出了一段瑞士軍刀般的代碼,耗時5天(其實此功能客戶無擴展需求僅要求1天時間完成)。
而在TDD實踐中,我們需要注重代碼質量,並編寫剛好適量的代碼。
2. 保證代碼與業務需求的一致性
一般來講程序員都願意把功能完美的體現在代碼上,可有時候天不隨人意,心里免不得擔憂,我這代碼能滿足業務需求么?但在TDD中,首先是進行測試用例的編寫,然后再進行類或者用戶界面的開發。由於要先開發測試用例,那么開發人員就必須清楚測試的目的,這樣TDD確保了項目的代碼與所需的業務是匹配的。
3. 創建簡明有針對性的接口
一日,“李雷”接到“韓梅梅”發來的為某個功能准備的闖關寶典和核心步驟(類庫與接口)。可讀了3000遍還是沒有能理解,一方面是“韓梅梅”采用了古代文言文與現代拉丁語的混搭來書寫核心步驟,另一方面“韓梅梅”的“韓”式1到1000000的命名規則讓“李雷”在讀了30秒核心步驟后,已經不知道第幾條是第幾條,。
在TDD實踐中,我們要注重創建有意義的、簡明的接口,因為這一點在與他人合作中尤其重要。
4. 與用戶溝通,明確需求
在開發代碼的過程中,我們總會有遇到不太明確的需求點,這個時候和需求人員溝通那是必不可少的,了解了功能的輸入和輸出才能保證完美的完成任務。在溝通的過程中也加深了與客戶的信任和默契度,不知不覺中還能提高EQ,一舉兩得。
5. 回歸測試,確保新的更改不影響現有功能
在“韓梅梅”同學開發某個功能3個月后,“李雷”接到上級指示,客戶要擴展該功能,但是原有功能保持不變。在苦心操勞了之后,“李雷”同學光榮的完成了任務,正准備接受大家贊譽時,“韓梅梅”跳出來向大家訴苦,那就是“李雷”為了做擴展功能把她之前做的功能給弄壞了,當時“李雷”那個心啊,拔涼拔涼的!
TDD的開發中加入了回歸測試,這樣就確保了之前的功能的正確與完整性,減少不必要的問題。
6. 提升系統的開放性和擴展性
一直以來我們做事都要講先后順序,軟件開發也有着類似的工序。“李雷”和“韓梅梅”被一起“充軍”到某緊急功能模塊上,並且“李雷”要等“韓梅梅”完成她的功能模塊才能開始自己的模塊。為了解決這個問題,項目組決定使用某些技術來解除他們的依賴關系,比如使用到IOC以及一些設計模式,讓他們能夠同時開發,之后再將兩人的功能模塊組裝到一起。
五.TDD開發中需要使用到的技術知識點:單元測試、依賴注入框架和模擬對象
1. TDD的工作流
TDD的工作流經常被描述為“紅燈 -> 綠燈 -> 重構”:首先以一個未能通過的測試開始,隨后編寫足以通過該測試的代碼,然后再重構代碼。當然我們都不願意看到不能通過的測試CASE,當你再繼續編寫項目代碼,讓原本不能通過的測試CASE通過的時候,你會感覺心里有一絲絲的愜意,然后再將代碼優化重構,瞬間又有了些成就感。抿一口水,工作就這么快樂的完成了。
2. 偽對象、依賴注入框(DI/IOC)與模擬框架
就最簡單的實踐來說,比較常見的三層架構,UI層去調用業務邏輯層,業務邏輯層去調用數據持久層。
“韓梅梅”做業務層的代碼,“李雷”做數據層的代碼,於是乎“韓梅梅”變成了“黃世仁”,“李雷”就成了“楊白勞”,其中辛酸只有“李雷”知道!為了改變命運,“李雷”決定做個“假”的數據層對象(模擬對象)給“韓梅梅”用着,省的她每天都在那催命。
偽對象是對代替外部資源的簡單模擬,它通常會在調用一個方法時為該方法返回預定義響應,但通常不會根據輸入參數而改變響應。
於是乎“李雷”歡樂的開始了他的計划,把“韓梅梅”所需要的功能點都用接口來實現(interface),然后把這接口的方法在單獨的一個模擬類里面都只寫了個簡單的殼,里面的各種返回值都寫成“韓梅梅”想要的數據樣例,最后語重心長的對“韓梅梅”說:“東西拿走喜兒給我留下…”,“韓梅梅”當然是歡快的蹦到了自己的座位上。
“韓梅梅”拿到李雷給的偽對象后,徑直就用了起來,在所有需要偽對象的類里面都直接用了萬能的“New”關鍵字來實例化這個偽對象,這招兼顧了簡單與實惠,廣大程序員愛好者都愛這么干。
但是 “韓梅梅”后來慢慢意識到不太對,我有許多地方都用用到NEW字,那不是以后“李雷”完成了他所謂的真的對象以后,我還必須得改我的代碼,把我之前放進去的偽對象給替換為真正的對象?我這不是自己給自己找茬么。
“韓梅梅”趕緊找到帶着黑框身背雙肩包的師兄,細說了當前的苦衷。黑框師兄那舍得是師妹這么憂愁,趕緊拿出殺手鐧“控制反轉”中的一招“依賴注入”,讓使用類中僅保留被調用對象的接口,然后動態的注入實例給這接口,這樣子只要實現了這個接口的類都可以被任意替換使用,並且這個注入的動作一般是由某個框架來實現的,比如Autofac,、Unity或者Ninject等等。
這下子“韓梅梅”心里踏實了,管你“李雷,張雷,王雷”寫什么偽對象或者真的對象,只要你的對象實現了指定的接口,我都能使用,而且我還不用自己去手動創建這個對象,省心又省時。
模擬框架是一系列用於快速創建偽對象的API,它能減少重復的代碼,提高編碼效率,比較常用的為Rhino, NSubstitute, Moq等。
“李雷”和“韓梅梅”就這么和諧的合作,但是隨着任務的增多,發現問題來咯:
1) “李雷”要手工做很多的偽對象給“韓梅梅”,任務繁重。
2) 每個對象的內部需求是不一樣的,“李雷”發現要用一種通用的格式來創建這些偽對象幾乎是不可能的。
3) 很多偽對象又依賴於其他的偽對象,這樣子簡直就是要讓崩潰。
4) 很多為對象內部有狀態需要保存,手動來寫代碼很難去維護這些狀態標示。
“李雷”好不容易通過創建偽對象來擺脫“韓梅梅”的每日請安,這又掉進了創建無數偽對象的漩渦之中。於是“李雷”橫渡遠洋,爬山涉水,期望能找到一盞明燈解決這些問題,終於功夫不負有心人,“李雷”找到了神兵利器去解決這個問題,那就是模擬框架。
模擬框架幫助“李雷”快速的創建了各種“韓梅梅”需要的模擬對象,以及各種所需的API,彈指一揮間,李雷用這神兵利器已經殺敵無數,嘴角不由得上揚了一番!
3. 重構代碼
但是問題總歸還是有得,她發現自己有些功能雖然測試通過,但是代碼寫的不好,經常被黑眼圈師兄批評,說她的代碼質量不高。
所以她必須盡可能在剛測試通過之后就盡可能的優化代碼,一來是少挨罵;二來也是提高自己編碼水平的一個機會,查漏補缺;三是貴人多忘事,何況是“我這等如花似玉的姑娘!”,如果不及早優化,恐怕以后很難有時間再來弄了。
因為已經有了測試代碼,所以重構代碼那也是很有保障的事情,如果我改錯東西了,那么我寫的測試用例肯定不能通過,這樣子也能讓我信心滿滿的去把這些個有臭味的代碼大卸八塊了。
六.工序流程
下面我們來看看“韓梅梅”和“李雷”他們的工作步驟:
1. 首先,韓梅梅和李雷分析了他們各自的業務,然后韓梅梅寫出了她需要測試用例,里面嘗試使用“李雷”將要提供的方法,並通過此方法獲取數據。當然這些代碼第一次是測試不通過的,因為里面需要的實現類還沒有寫。這里我們使用到Moq這樣一個模擬框架。
測試用例的運行結果,大家也是知道的,兩個字“悲催”!
2. 然后, “李雷”那邊開始了數據持久層接口的編寫(IProductRepository),“韓梅梅”拿到李雷提供的接口后,完成了業務邏輯層(ProductService)的代碼編寫,完畢之后大吐一口氣:“小伙子終於給力了一次!”。
A. “李雷”的代碼如下,實際上“李雷”只是提供了接口(interface)給“韓梅梅”,他還並沒有開始編寫具體的實現類,但是韓梅梅已經可以通過該接口來工作了。
B. “韓梅梅”的服務類代碼如下,她獲取到“李雷”提供的數據持久層的接口后就開始歡快的編寫代碼,一切是那么的行雲流水啊:
3. 接下來“韓梅梅”添加了各種需要的引用,再次運行起了測試用例,這次順利的PASS了,心里那個激動,沒的說!
4. 工作快要接近尾聲,不過眼鏡師兄提醒過“廣大程序猿應該有高度的思想覺悟,不遺余力的提高代碼質量”,為了達成這一目標,“韓梅梅”又開始了上跳下竄的“大家來找茬”。
她發現里面有段代碼寫的不好,循環太多,也不夠整潔,她想優化下代碼,又怕把寫好邏輯弄壞了,不過現在有了測試用例,她不會再怕有這個問題,改錯代碼,測試用例自然也就無法通過。
再運行下測試用例,依然通過,此次代碼優化完畢,如果還有新的問題可以在依葫蘆畫瓢的繼續優化。
5. 與此同時,“李雷”那邊的數據持久層代碼也差不多寫好了,大家總得需要把代碼合起來作“集成測試”,這個時候就要用到IOC框架來把“李雷”編寫的數據層實例注入到業務邏輯層,注入實例使用的是Autofac這個IOC框架,我們這里使用構造函數注入,關於注入框架的更多信息,請讀者G….gle。
至此,“韓梅梅”與“李雷”各自的工作都完成了,大家也不在互相說啥,各自都優化了各自的功能代碼,快樂的工作繼續進行着,我們的TDD講解也到此結束!
參考文獻:
- Test Driven Development: By Example – Kent beck
- Refactoring: Improving the Design of Existing Code – Kent beck
- The Art of Unit Testing: With examples in .NET – Roy Osherove
- Professional test driven development with C# - James Bender, Jeff McWherter
本人最近開始學習TDD,藉此提升自己的能力。在學習過程會有一些心得體會,於是便會寫一些博客來記錄這些想法,有興趣的朋友可以和我一起交流學習。
QQ群: 32745894,歡迎大家加入討論!