在一種傳統的結構化編程語言中,比如C,要進行測試的單元一般是函數或子過程。在象C++這樣的面向對象的語言中, 要進行測試的基本單元是類。對Ada語言來說,開發人員可以選擇是在獨立的過程和函數,還是在Ada包的級別上進行單元測試。單元測試的原則同樣被擴展到第四代語言(4GL)的開發中,在這里基本單元被典型地劃分為一個菜單或顯示界面。 經常與單元測試聯系起來的另外一些開發活動包括代碼走讀(Code review),靜態分析(Static analysis)和動態分析(Dynamic analysis)。靜態分析就是對軟件的源代碼進行研讀,查找錯誤或收集一些度量數據,并不需要對代碼進行編譯和執行。動態分析就是通過觀察軟件運行時的動作,來提供執行跟蹤,時間分析,以及測試覆蓋度方面的信息。
最近公司要求重新回顧單元測試的實際效果,作為一個開發經理,我個人對單元測試也有很多疑惑。就個人而言,我自己也寫過很多單元測試,也鼓勵程序員寫單元測試,但實際效果似乎不盡如人意。因此,寫了這篇短文,想和大家一起探討。
1. 背景介紹
我所在的公司是一家外資軟件公司,主要工作是開發一個復雜的在線系統(java based web applicaiton). 該系統的主要特點是:定制化程度比較高,業務邏輯相當復雜。系統的技術棧是Struts, EJB (JBoss)and Hibernate。我管理的小組一共有10個左右開發人員,6個左右測試人員,平均工作經驗在3年以上。
公司在兩年前開始推行單元測試。在開始推行單元測試之前,系統已經正式上線,也就是意味著有海量的沒有單元測試的代碼。推行之后,應該說投入了相當多的時間,總共覆蓋的行數有20k, 其中行覆蓋率(line coverage)有55%左右,分支覆蓋率(branch coverage)有40%左右。我相信經過這么多嘗試,應該說,我帶的這個組不是一個單元測試的新手,有資格討論單元測試的得失。
2. 實踐中的問題和疑惑(開發人員怎么說?)
我一向主張:一項技術值不值得或者好不好用歸根結底是要問實際的使用者和開發者的。作為一個經理,我不傾向于推行一項程序員極力反對的技術,不管這項技術是不是業界的標準或者是評論者的寵兒。一項技術必須要解決實際問題,也就是mark your life easier。所以下面是開發人員的回答。
2.1為什么需要單元測試和TDD (Test Driven Development)?
2.2.1 單元測試可以發現代碼缺陷(Defect)么?投入/產出比(Defect count/Effort)是多少?
只能發現待測單元的缺陷,不能發現單元交互(集成)之間的缺陷。在實踐過程中,很少有defect通過單元測試發現。
基本不能用于發現表現層(JSP, Java scripts, css, UI etc)的代碼缺陷投入/產出比太高。換句話說,相比于單元測試,人工測試(munual testing)可以很大程度得更快更好的發現系統缺陷。
2.2.2單元測試可以用來防止Regression Defect么?
如果我們特地為某個regression defect加了相應的單元測試,那么單元測試在某種程度上可以防止regression defect的再一次出現。但是同樣的,單元測試只能防止待測單元中的Regression Defect,而且需要通過猜測來加入相應的測試案例。
2.2.3 單元測試對設計有幫助么?
單元測試本身不一定能幫助設計。據說TTD可以幫助設計,實踐過程中沒有很深的體會。
2.2.4你投入了多少時間寫單元測試?需要多少時間維護單元測試?
單元測試:代碼= 2:1,也就是說一行代碼需要兩行單元測試。也有些人說1:1。
維護成本基本上決定于單元接口的變化頻率:對于一些比較穩定的代碼單元,維護成本還可以接受。但對于一些需求變化劇烈的單元,基本上需要重寫。在實際實踐中,可能的比例為穩定的單元測試:重寫的單元測試 = 80%:20%。但是這里有一個悖論:其實我們更希望單元測試可以用于驗證(verify)核心單元的正確性,然而這些單元的測試單元確基本上需要重寫。這是為什么呢?其中一個可能的原因是:對于一個在線系統(web based application)來說,系統的主要邏輯和用戶接口(user interface)綁定過于緊密,所以,用戶接口的變化導致從表現層到數據庫層的垂直變化。即使業務需求只是加了一個新的屬性,但是這個數據將被加入核心的對象當中,所有涉及這個對象的單元測試需要改變。
2.2.5單元測試的主要挑戰是什么?
挑戰之一:如何在多個測試用例之間共享測試數據。
公司產品支持一個很復雜的在線向導,由七步組成,每一步可以單獨保存然后退出,下次繼續編輯。如果你想測試最后一步的API,你需要準備很多其他頁面的數據。因此,需要花很多時間準備測試數據。另外,公司產品還支持相似功能的其他向導。作為一個程序員,我們一直想在多個類似功能的向導API之間共享測試數據。然而,如果待測對象本身有些微變化,所有共享該數據的測試代碼全部需要重寫。這是一個巨大的維護費用。
挑戰之二:劇烈的需求變化導致維護成本劇增,收益減少。
正如2.2.4中描述的,一個典型的在線系統(web based application),通?梢苑譃槿龑樱罕憩F層,主要是用戶界面,包括HTML/JSP/CSS/Java Scripts/Ajex等等;業務層,主要是業務邏輯;數據層,存取數據。根據面向對象設計(OOD)的原則,業務層主要由一組領域對象(Business Object/Domain Object)構成。這些領域對象只提供一組數目相對有限的,接口比較清晰的,時間比較穩定的API。對這組API進行單元測試是有必要的,也是有意義的。
然而,系統還有相當一部分的代碼用于調用不同領域對象之間的API,轉變成表現層需要的對象。表現層其他的邏輯還包含大量的代碼用于連接不同的頁面,以及構建不同的向導。
正如和絕大多數的系統一樣,產品需求的變化是極其劇烈的,可以預測的,不可避免的。在這種情況下,需求變化將導致領域對象API以上的代碼(包括絕大多數表現層代碼和一部分業務層代碼)將發生劇烈變化,與之相應的單元測試代碼都需要相應的改變。也就是說,這些代碼的單元測試代碼的維護成本很好。
挑戰之三:海量的遺留代碼(Legacy Codes)
正如前面描述的,我們是在產品已經上線之后才開始推行單元測試的。因此,大量的遺留代碼并不適用于單元測試。換句話說,單元測試必須要在API實現之前予以仔細得考慮。如果API本身沒有得到很好的設計,單元測試基本上是不可能的。
2.2.6 拿什么來衡量單元測試?
一般來說,業界使用行覆蓋率(line coverage)和分支覆蓋率(branch coverage)來衡量單元測試的測量。但在實際過程中,我們發現這些衡量標準和我們對單元測試的期望有很大差距:比如說,高覆蓋率不見得較少的代碼缺陷。高覆蓋率也不能防止regression缺陷。高覆蓋率也似乎和設計沒有直接關聯。從另外一個角度說,達到高覆蓋率所花費的時間也是相當驚人的。
從另外一個角度來說,我們希望找到一個方法可以簡單直接地衡量單元測試的測量:比如說代碼缺陷或regression defect數量。
3. 聆聽和討論(業界怎么說)
帶著這些問題和困惑,我在網上查詢了大量相關資料,牛人的文章和業界的討論。很容易看出,業界對于單元測試的目標,作用,方法和手段都有很多爭議。
3.1 什么是(不是)單元測試?
3.1.1 單元測試和發現缺陷無關(http://blog.stevensanderson.com/2009/08/24/writing-great-unit-tests-best-and-worst-practises/)
【摘要】單元測試不是一個發現缺陷或者檢測regression defect的有效方法。其一,單元測試,根據定義,是用來測試特定代碼單元。然而,一個系統往往是一個大量單元的復雜集成,單元測試很難發現集成的缺陷。其二,相比單元測試,手工測試或者自動化集成測試更容易用于檢測缺陷。
文章來源于領測軟件測試網 http://www.k11sc111.com/