最近公司要求重新回顧單元測試的實際效果,作為一個開發(fā)經(jīng)理,我個人對單元測試也有很多疑惑。就個人而言,我自己也寫過很多單元測試,也鼓勵程序員寫單元測試,但實際效果似乎不盡如人意。因此,寫了這篇短文,想和大家一起探討。
1. 背景介紹
我所在的公司是一家外資軟件公司,主要工作是開發(fā)一個復(fù)雜的在線系統(tǒng)(java based web applicaiton). 該系統(tǒng)的主要特點是:定制化程度比較高,業(yè)務(wù)邏輯相當復(fù)雜。 系統(tǒng)的技術(shù)棧是Struts, EJB (JBoss) and Hibernate。我管理的小組一共有10個左右開發(fā)人員,6個左右測試人員,平均工作經(jīng)驗在3年以上。
公司在兩年前開始推行單元測試。在開始推行單元測試之前,系統(tǒng)已經(jīng)正式上線,也就是意味著有海量的沒有單元測試的代碼。推行之后,應(yīng)該說投入了相當多的時間,總共覆蓋的行數(shù)有20k, 其中行覆蓋率(line coverage)有55%左右,分支覆蓋率(branch coverage)有40%左右。我相信經(jīng)過這么多嘗試,應(yīng)該說,我?guī)У倪@個組不是一個單元測試的新手,有資格討論單元測試的得失。
2. 實踐中的問題和疑惑(開發(fā)人員怎么說?)
我一向主張:一項技術(shù)值不值得或者好不好用歸根結(jié)底是要問實際的使用者和開發(fā)者的。作為一個經(jīng)理,我不傾向于推行一項程序員極力反對的技術(shù),不管這項技術(shù)是不是業(yè)界的標準或者是評論者的寵兒。一項技術(shù)必須要解決實際問題,也就是mark your life easier。所以下面是開發(fā)人員的回答。
2.1 為什么需要單元測試和TDD (Test Driven Development)?
2.2.1 單元測試可以發(fā)現(xiàn)代碼缺陷(Defect)么?投入/產(chǎn)出比(Defect count/Effort)是多少?
只能發(fā)現(xiàn)待測單元的缺陷,不能發(fā)現(xiàn)單元交互(集成)之間的缺陷。在實踐過程中,很少有defect通過單元測試發(fā)現(xiàn)。
基本不能用于發(fā)現(xiàn)表現(xiàn)層(JSP, Java scripts, css, UI etc)的代碼缺陷
投入/產(chǎn)出比太高。換句話說,相比于單元測試,人工測試(munual testing)可以很大程度得更快更好的發(fā)現(xiàn)系統(tǒng)缺陷。
2.2.2 單元測試可以用來防止Regression Defect么?
如果我們特地為某個regression defect加了相應(yīng)的單元測試,那么單元測試在某種程度上可以防止regression defect的再一次出現(xiàn)。但是同樣的,單元測試只能防止待測單元中的Regression Defect,而且需要通過猜測來加入相應(yīng)的測試案例。
2.2.3 單元測試對設(shè)計有幫助么?
單元測試本身不一定能幫助設(shè)計。據(jù)說TTD可以幫助設(shè)計,實踐過程中沒有很深的體會。
2.2.4 你投入了多少時間寫單元測試?需要多少時間維護單元測試?
單元測試:代碼 = 2:1,也就是說一行代碼需要兩行單元測試。也有些人說1:1。
維護成本基本上決定于單元接口的變化頻率:對于一些比較穩(wěn)定的代碼單元,維護成本還可以接受。但對于一些需求變化劇烈的單元,基本上需要重寫。在實際實踐中,可能的比例為穩(wěn)定的單元測試:重寫的單元測試 = 80%:20%。但是這里有一個悖論:其實我們更希望單元測試可以用于驗證(verify)核心單元的正確性,然而這些單元的測試單元確基本上需要重寫。這是為什么呢?其中一個可能的原因是:對于一個在線系統(tǒng)(web based application)來說,系統(tǒng)的主要邏輯和用戶接口(user interface)綁定過于緊密,所以,用戶接口的變化導(dǎo)致從表現(xiàn)層到數(shù)據(jù)庫層的垂直變化。即使業(yè)務(wù)需求只是加了一個新的屬性,但是這個數(shù)據(jù)將被加入核心的對象當中,所有涉及這個對象的單元測試需要改變。
2.2.5 單元測試的主要挑戰(zhàn)是什么?
挑戰(zhàn)之一:如何在多個測試用例之間共享測試數(shù)據(jù)。
公司產(chǎn)品支持一個很復(fù)雜的在線向?qū)В善卟浇M成,每一步可以單獨保存然后退出,下次繼續(xù)編輯。如果你想測試最后一步的API, 你需要準備很多其他頁面的數(shù)據(jù)。因此,需要花很多時間準備測試數(shù)據(jù)。另外,公司產(chǎn)品還支持相似功能的其他向?qū)АW鳛橐粋€程序員,我們一直想在多個類似功能的向?qū)PI之間共享測試數(shù)據(jù)。然而,如果待測對象本身有些微變化,所有共享該數(shù)據(jù)的測試代碼全部需要重寫。這是一個巨大的維護費用。
挑戰(zhàn)之二:劇烈的需求變化導(dǎo)致維護成本劇增,收益減少。
正如2.2.4中描述的,一個典型的在線系統(tǒng)(web based application),通常可以分為三層:表現(xiàn)層,主要是用戶界面,包括HTML/JSP/CSS/Java Scripts/Ajex等等;業(yè)務(wù)層,主要是業(yè)務(wù)邏輯;數(shù)據(jù)層,存取數(shù)據(jù)。根據(jù)面向?qū)ο笤O(shè)計(OOD)的原則,業(yè)務(wù)層主要由一組領(lǐng)域?qū)ο?Business Object/Domain Object)構(gòu)成。這些領(lǐng)域?qū)ο笾惶峁┮唤M數(shù)目相對有限的,接口比較清晰的,時間比較穩(wěn)定的API。對這組API進行單元測試是有必要的,也是有意義的。
然而,系統(tǒng)還有相當一部分的代碼用于調(diào)用不同領(lǐng)域?qū)ο笾g的API, 轉(zhuǎn)變成表現(xiàn)層需要的對象。表現(xiàn)層其他的邏輯還包含大量的代碼用于連接不同的頁面,以及構(gòu)建不同的向?qū)А?nbsp;
正如和絕大多數(shù)的系統(tǒng)一樣,產(chǎn)品需求的變化是極其劇烈的,可以預(yù)測的,不可避免的。在這種情況下,需求變化將導(dǎo)致領(lǐng)域?qū)ο驛PI以上的代碼(包括絕大多數(shù)表現(xiàn)層代碼和一部分業(yè)務(wù)層代碼)將發(fā)生劇烈變化,與之相應(yīng)的單元測試代碼都需要相應(yīng)的改變。也就是說,這些代碼的單元測試代碼的維護成本很好。
挑戰(zhàn)之三:海量的遺留代碼(Legacy Codes)
正如前面描述的,我們是在產(chǎn)品已經(jīng)上線之后才開始推行單元測試的。因此,大量的遺留代碼并不適用于單元測試。換句話說,單元測試必須要在API實現(xiàn)之前予以仔細得考慮。如果API本身沒有得到很好的設(shè)計,單元測試基本上是不可能的。
2.2.6 拿什么來衡量單元測試?
一般來說,業(yè)界使用行覆蓋率(line coverage)和分支覆蓋率(branch coverage)來衡量單元測試的測量。但在實際過程中,我們發(fā)現(xiàn)這些衡量標準和我們對單元測試的期望有很大差距:比如說,高覆蓋率不見得較少的代碼缺陷。高覆蓋率也不能防止regression缺陷。高覆蓋率也似乎和設(shè)計沒有直接關(guān)聯(lián)。從另外一個角度說,達到高覆蓋率所花費的時間也是相當驚人的。
從另外一個角度來說,我們希望找到一個方法可以簡單直接地衡量單元測試的測量:比如說代碼缺陷或regression defect數(shù)量。
3. 聆聽和討論(業(yè)界怎么說)
帶著這些問題和困惑,我在網(wǎng)上查詢了大量相關(guān)資料,牛人的文章和業(yè)界的討論。很容易看出,業(yè)界對于單元測試的目標,作用,方法和手段都有很多爭議。
3.1 什么是(不是)單元測試?
3.1.1 單元測試和發(fā)現(xiàn)缺陷無關(guān)(http://blog.stevensanderson.com/2009/08/24/writing-great-unit-tests-best-and-worst-practises/)
【摘要】單元測試不是一個發(fā)現(xiàn)缺陷或者檢測regression defect的有效方法。其一,單元測試,根據(jù)定義,是用來測試特定代碼單元。然而,一個系統(tǒng)往往是一個大量單元的復(fù)雜集成,單元測試很難發(fā)現(xiàn)集成的缺陷。其二,相比單元測試,手工測試或者自動化集成測試更容易用于檢測缺陷。
3.1.2 一個測試不是單元測試 如果 -
和數(shù)據(jù)庫交互
需要跨越網(wǎng)絡(luò)
需要訪問文件系統(tǒng)
不能和其他的單元測試同時運行
需要配置文件才能運行
3.1.3 測試驅(qū)動開發(fā)和驗證(verification)無關(guān),和規(guī)范(specification)相關(guān)
測試驅(qū)動開發(fā)并不意味著單元測試驅(qū)動的開發(fā)。實際上,測試驅(qū)動開發(fā)這個詞非常容易引起誤解。在TDD的大佬們眼中,單元測試只是測試驅(qū)動開發(fā)的工具,而不是目的。測試驅(qū)動開發(fā)更關(guān)注這個代碼單元應(yīng)該如何運行(behave)而不是這個代碼單元實現(xiàn)是否正確(verification)。之所以叫測試驅(qū)動開發(fā),意義及在于此。最近你可以看到一個新名字的興起-行為驅(qū)動開發(fā)(Behaviour Driven Developemnt)。BDD更強調(diào)一個組件應(yīng)該如何工作,以及尋找一個簡易的方法來規(guī)范行為。
3.1.4 為什么需要單元測試?
可以單獨地測試每個單元
可以用于驗證重構(gòu)后的代碼
可以用來確保不會破壞其他人的代碼
可以用來提高系統(tǒng)設(shè)計,比如說,不能單元測試的代碼將不會出現(xiàn)
3.2 對怎么樣的代碼做(不做)單元測試?
參考http://blog.stevensanderson.com/2009/11/04/selective-unit-testing-costs-and-benefits/
【摘要】 該文作者在總結(jié)3年的TDD實踐經(jīng)驗的時候,反復(fù)認識一個情況:對于某些類型的代碼,單元測試工作得很好,也極大的提高了待測代碼的質(zhì)量;但對另外一些類型的代碼,單元測試耗費了大量的時間,并沒有起到輔助設(shè)計和減少缺陷的作用,同時還導(dǎo)致代碼更加難以維護。
基于這個想法,作者畫了一張圖來表示:

作者認為,
(1)代碼本身很復(fù)雜但是對外部的依賴很少(左上),最適合單元測試,因為耗費較少和收益較多。一般來說,某種算法(排序),核心業(yè)務(wù)規(guī)則,數(shù)據(jù)解析類似的模塊屬于這樣的代碼
(2)帶有很多依賴的瑣碎代碼(Trivial Code with many dependencies,右下):稱為協(xié)調(diào)者(Coordinators),因為這些代碼主要用于集成多個代碼單元和安排代碼單元之間的交互。這些代碼不適用于單元測試,因為耗費很高,收益卻不大。
(3)代碼很復(fù)雜而且外部依賴很多(右上):過于復(fù)雜的代碼,需要重構(gòu)。
(4)外部依賴較少的瑣碎代碼(左下):這些代碼可測可不測,因為重要性不高,復(fù)雜度也不高,加單元測試的意義不是很大。
對于作者的這個想法,我比較同意。正如我在2.2.4和2.2.5中提到的,對于一個在線系統(tǒng)(web-based application)來說,可以分為內(nèi)外兩個圈:內(nèi)圈由核心領(lǐng)域?qū)ο螅–ore Domain Object)構(gòu)成,外圈由大量鏈接匯編代碼構(gòu)成。所謂核心領(lǐng)域?qū)ο螅@些對象包含核心業(yè)務(wù)邏輯,數(shù)目相對較少,邏輯相對比較復(fù)雜,接口(API)相對比較穩(wěn)定,輸入輸出相對比較清晰,屬于算法類代碼,最合適單元測試,也需要單元測試來提高相應(yīng)的質(zhì)量。同時我想強調(diào)一下:這些對象一定要精心設(shè)計,精挑細選,一定要滿足數(shù)量少,接口穩(wěn)定,輸入輸出清晰這三個要求。數(shù)量少意味著需要測的代碼少,相應(yīng)的單元測試數(shù)量也少。接口穩(wěn)定意味著用戶界面的變化(或需求變化)對單元測試的影響較小。輸入輸出清晰意味著容易驗證邏輯的正確性。相對的,外圈的代碼主要用來鏈接多個領(lǐng)域?qū)ο螅才潘麄冎g的交互,轉(zhuǎn)換成用戶界面需要的數(shù)據(jù)結(jié)構(gòu)。也就意味著外圈代碼和需求緊密相連,微小的用戶界面變化將會導(dǎo)致相應(yīng)的代碼代碼,寫單元測試得不償失。
3.3 單元測試最佳實踐和測試驅(qū)動開發(fā)反模式
參考http://blog.stevensanderson.com/2009/08/24/writing-great-unit-tests-best-and-worst-practises/
(1)單元測試之間應(yīng)該完全獨立的
不要寫不必要的斷言,每次只測一個代碼單元,模擬(Mock)外部依賴,避免不必要的前提條件。
(2)單元測試應(yīng)該運行得很快(比如說少于5 min)
單元測試需要運行得很快,這樣程序員才愿意經(jīng)常運行單元測試。這就意味著(1)單元測試不要訪問數(shù)據(jù)庫(2)單元測試不要訪問網(wǎng)絡(luò)(3)外部依賴需要被模擬(mock)
(3)給單元測試一個清晰和一致的命名
一個單元測試的名字應(yīng)該包含3項內(nèi)容:待測對象_待測案例_期望結(jié)果,比如說ProductPurchaseAction_IfStockIsZero_RendersOutOfStockView()。
(4)常見的TDD反模式
撒謊者:所有測試案例都通過了,看上去是有效的。但如果靠近看,你會發(fā)現(xiàn)這些案例完全沒有測試預(yù)期的內(nèi)容
過度配置:一個單元測試需要一堆配置然后才能開始測試。有的時候需要幾百行代碼配置環(huán)境,設(shè)計數(shù)十個對象。
巨人:一個單元測試需要測試數(shù)千行代碼,并且包含大量的測試用例。
無用者 :有的時候模擬是有效的方便的。但是其他一些時候,過多的模擬對象,Stub對象,假對象,導(dǎo)致單元測試主要在測模擬對象而不是實際的系統(tǒng)。
檢察官:單元測試對待測代碼非常了解,任何對待測代碼的變化將影響單元測試代碼。
慷慨的富有者:多個單元測試共享測試數(shù)據(jù),任何一個單元測試改動了一些數(shù)據(jù),其他的測試全部失敗了。
本地英雄:單元測試依賴于開發(fā)環(huán)境某些特定的配置。這意味著:在其他環(huán)境上運行單元測試將會失敗。
采集者:單元測試驗證所有的輸出盡管它只對某些數(shù)據(jù)感興趣。
狗仔隊:單元測試依賴于特定的實現(xiàn)細節(jié),比如說,單元測試捕獲待測代碼中拋出的每一個異常。
欺瞞者:單元測試驗證了一大堆無關(guān)的細節(jié),但從不測試核心行為。比如說,待測代碼訪問數(shù)據(jù)庫并返回數(shù)據(jù),單元測試驗證每個返回的數(shù)據(jù)。
大聲說話的人:單元測試輸出一大堆診斷信息,日志信息,然而沒有清晰的成功/失敗標志。
please refer to http://tdd-antipatterns.net/index.php?title=Main_Page
4. 結(jié)論(前途在哪里)
現(xiàn)在,我想答案應(yīng)該比較清楚了。
第一,重新強調(diào)單元測試和測試驅(qū)動開發(fā)的目地:通過寫單元測試來澄清接口(幫助系統(tǒng)設(shè)計),確保核心代碼(i.e.Domain Object)的正確性。它不是一個檢測缺陷的有效工具,也不應(yīng)該只用覆蓋率來衡量單元測試的質(zhì)量
第二,只對核心代碼(比如說主要業(yè)務(wù)對象)進行單元測試。待測對象需要有相對有限的API數(shù)量,比較穩(wěn)定的接口定義和清晰的輸入輸出。不要把寶貴的時間花在測試大量的非核心代碼,比如說,鏈接(多個核心對象),協(xié)調(diào)(多個核心對象),和需求/用戶接口緊密相關(guān)的代碼。
第三,不要濫用單元測試,請時刻關(guān)注最佳實踐和TDD反模式。
第四,好的單元測試來源于好的設(shè)計。設(shè)計不好,代碼不會好,單元測試也不會好。
最后,我還想繼續(xù)研究行為驅(qū)動開發(fā)(BDD),看看BDD是否更實用,請繼續(xù)關(guān)注。
posted on 2010-07-13 17:34
Justin Chen 閱讀(2897)
評論(2) 編輯 收藏 所屬分類:
Unit Test & Mock Test