什么TDD,讓它見鬼去吧!
張大胖是個積極進取的程序員, 在日常工作之余,他還學(xué)習(xí)單元測試,重構(gòu)等編程實踐。
這一天晚上他看到微信群里在激烈地爭論一個叫TDD的東西,不由地來了興致,上網(wǎng)搜索了一下。
原來TDD就是Test Driven Development(測試驅(qū)動開發(fā)),強調(diào)測試先行,小步快跑,用測試用例驅(qū)動出程序的接口和代碼。
1
TDD步驟看起來異常簡單:
1. 寫一個失敗的測試用例
2. 寫一點代碼,讓這個測試通過
3. 重構(gòu)代碼(如果需要的話),轉(zhuǎn)到第一步
張大胖心想,這三個步驟不就是“把大象關(guān)到冰箱里”嘛,太抽象了! 一點兒都不實用!
他又搜了一些文章,發(fā)現(xiàn)這些文章中講的都是一些極其簡單的例子,如加減法計算器,貨幣轉(zhuǎn)換等等。
比如這個計算器的例子,第一步先寫一個簡單的測試用例,用來測試兩個數(shù)字相加的行為。
- public class CalculatorTest {
- @Test
- public void testAdd(){
- Calculator calculator = new Calculator();
- int result = calculator.add(10,20);
- Assert.assertEquals(30, result);
- }
- }
第二步在Calculator中實現(xiàn)add方法,完成兩個數(shù)相加的邏輯,讓測試通過。
- public class Calculator{
- public int add(int a, int b){
- return a + b;
- }
- }
這個邏輯極其簡單,就不用重構(gòu)了。直接寫下一個測試用例, 測試兩個數(shù)字相減的行為。這樣周而復(fù)始下去,直到所有功能都完成。
張大胖撇撇嘴:這就是TDD? 太沒技術(shù)含量了,我明天就在項目中嘗試一把!
2
第二天,張大胖看了一下自己的任務(wù)列表,里邊有這么一個需求:
在下訂單的時候,根據(jù)訂單的金額,扣除優(yōu)惠券,按照規(guī)則給用戶增加相應(yīng)積分
張大胖看了看這個計算規(guī)則,非常簡單,估計一個函數(shù)就能搞定。
好,就拿你來試一試TDD這把刀吧,看看TDD到底有沒有那么好,或者那么差。
第一步,先寫一個失敗的測試!
張大胖心中非常清楚,這個系統(tǒng)用的是Spring,典型的Controller -> Service -> DAO。
在Controller中根本沒有邏輯,就是調(diào)用Service而已。所以直接對Service層寫單元測試吧, 張大胖很快就定位到這個新需求相關(guān)的類, 即OrderService的submit方法。
TDD本來是要驅(qū)動出接口的,現(xiàn)在看來不用了,已經(jīng)存在了,張大胖看了一下接口的輸入輸出:
- public class OrderService{
- public String submit(String requestBody){
- ......
- }
- }
這個方法的輸入?yún)?shù)居然是一個XML字符串! 其中包含了像couponID, addressID這樣的東西。
- <createOrder>
- .....
- <addressID>xxxx</addressID>
- <couponID>xxxx</couponID>
- <paymentType>xxxx</paymentType>
- ......
- </createOrder>
返回值也是一個XML字符串, 表示成功或者失敗(以及對應(yīng)的失敗消息)。
- <result>
- <status>xxxx</status>
- <msg>xxxx</msg>
- </result>
這年頭還用XML做參數(shù),只能說這是一個老應(yīng)用了!
3
按照TDD的節(jié)奏, 張大胖寫下第一個測試用例,并且讓它失敗。
- public void OrderServiceTest{
- public void testBonusPoints(){
- String requestBody= ......;
- //執(zhí)行submit方法
- String result = orderService.submit( requestBody);
- ?? 驗證積分, 可是怎么驗證??
- }
- }
等一下,這個測試的輸入?yún)?shù)容易構(gòu)建,但是submit方法的返回值中根本就不會包含積分信息!那怎么才能我計算出的積分是正確的?
難道讓submit方法返回積分數(shù)據(jù)?那就修改了本來是通用的接口協(xié)議,太不像話了!
第二個問題也很快浮現(xiàn),積分計算的邏輯很簡單,但是需要訂單總金額和優(yōu)惠券這兩個信息,可是在測試用例中,這兩個信息從哪里來?
訂單總金額需要購物車,這是在數(shù)據(jù)庫存放的,優(yōu)惠券ID在submit方法的參數(shù)中,詳情也在數(shù)據(jù)庫中。
積分的計算這么簡單,難道我還得先在數(shù)據(jù)庫中創(chuàng)建一個購物車和優(yōu)惠券,然后通過ShopCartService和CouponService從數(shù)據(jù)庫讀出來?這也太變態(tài)了吧?
不,單元測試一定要避開數(shù)據(jù)庫,必須得用Mock的方式吧,張大胖知道一個Mock框架叫Mockito,挺好用的,就用它了。
張大胖又瀏覽了一下OrderService.submit這個長達2000多行的函數(shù),這一看不打緊,張大胖發(fā)現(xiàn)這個函數(shù)依賴了另外七八個Service: UserService, ShopCartService, CouponService ......
這幾個Service有的嚴重依賴數(shù)據(jù)庫, 有的嚴重依賴Http ,有的依賴消息隊列。
也就是說要想讓submit方法順利執(zhí)行,必須得把這七八個Service都Mock出來,讓它們能協(xié)調(diào)工作,例如:
給一個userID,就能返回一個正確的user對象。
給一個couponID,就能返回一個正確的coupon對象。
Mockito能實現(xiàn)這個功能,但是協(xié)調(diào)七八個個Service的相關(guān)對象,需要寫出大量代碼才行!測試用例中的代碼會變得非常復(fù)雜、非常脆弱。
張大胖傻眼了 !自己連一個測試用例都寫不出來,還搞什么TDD?
4
張大胖嘆了一口氣, 放棄了寫測試用例的想法, 在OrderService.submit方法中,找到了合適的地方,然后根據(jù)訂單金額和優(yōu)惠券信息,寫了幾十行代碼,把積分計算了出來,保存到數(shù)據(jù)庫中。
然后啟動程序,通過界面的方式提交了幾個訂單,涵蓋了各種情況, 做了手工測試,然后檢查數(shù)據(jù)庫,他高興地發(fā)現(xiàn),積分計算完全正確,這才花了不到一個小時。
什么TDD, 讓它見鬼去吧!
后記:
實際上真正的TDD并不是文章中那么簡單的三個步驟, TDD正確的做法是根據(jù)需求先寫粗粒度的功能測試,這些測試能驅(qū)動出程序的接口, 然后寫細粒度的單元測試,驅(qū)動出細節(jié)代碼。今天這篇文章實在太長了,就不展開了,再寫一篇文章來講吧。
理解了TDD的思路以后,改變思維,實施TDD并不是一件特別難的事情。
我這些年遇到的主要困難是遺留項目,代碼很亂,可測試性很差,想寫出清晰良好的測試,經(jīng)常需要重構(gòu)大量代碼,這就得不償失了,說是做TDD,其實大量的時間是在重構(gòu)代碼,開發(fā)進度緩慢,看不到立竿見影的好處,于是就放棄了。
【本文為51CTO專欄作者“劉欣”的原創(chuàng)稿件,轉(zhuǎn)載請通過作者微信公眾號coderising獲取授權(quán)】