測試覆蓋工具對單元測試具有重要的意義,但是經常被誤用。這個月,Andrew Glover 會在他的新系列 —— 追求代碼質量 中向您介紹值得參考的專家意見。第一部分深入地介紹覆蓋報告中數字的真實含義。然后他會提出您可以盡早并經常地利用覆蓋來確保代碼質量的三個方法。
您還記得以前大多數開發人員是如何追求代碼質量的嗎。在那時,有技巧地放置 main()
方法被視為靈活且適當的測試方法。經歷了漫長的道路以后,現在自動測試已經成為高質量代碼開發的基本保證,對此我很感謝。但是這還不是我所要感謝的全部。Java? 開發人員現在擁有很多通過代碼度量、靜態分析等方法來度量代碼質量的工具。我們甚至已經設法將重構分類成一系列便利的模式!
![]() |
|
所有的這些新的工具使得確保代碼質量比以前簡單得多,不過您還需要知道如何使用它們。在這個系列中,我將重點闡述有關保證代碼質量的一些有時看上去有點神秘的東西。除了帶您一起熟悉有關代碼質量保證的眾多工具和技術之外,我還將為您說明:
- 定義并有效度量最影響質量的代碼方面。
- 設定質量保證目標并照此規劃您的開發過程。
- 確定哪個代碼質量工具和技術可以滿足您的需要。
- 實現最佳實踐(清除不好的),使確保代碼質量及早并經常地 成為開發實踐中輕松且有效的方面。
在這個月,我將首先看看 Java 開發人員中最流行也是最容易的質量保證工具包:測試覆蓋度量。
這是一個晚上鏖戰后的早晨,大家都站在飲水機邊上。開發人員和管理人員們了解到一些經過良好測試的類可以達到超過 90% 的覆蓋率,正在高興地互換著 NFL 風格的點心。團隊的集體信心空前高漲。從遠處可以聽到 “放任地重構吧” 的聲音,似乎缺陷已成為遙遠的記憶,響應性也已微不足道。但是一個很小的反對聲在說:
女士們,先生們,不要被覆蓋報告所愚弄。
現在,不要誤解我的意思:并不是說使用測試覆蓋工具是愚蠢的。對單元測試范例,它是很重要的。不過更重要的是您如何理解所得到的信息。許多開發團隊會在這兒犯第一個錯。
高覆蓋率只是表示執行了很多的代碼,并不意味著這些代碼被很好地 執行。如果您關注的是代碼的質量,就必須精確地理解測試覆蓋工具能做什么,不能做什么。然后您才能知道如何使用這些工具去獲取有用的信息。而不是像許多開發人員那樣,只是滿足于高覆蓋率。
![]() ![]() |
![]()
|
測試覆蓋工具通常可以很容易地添加到確定的單元測試過程中,而且結果可靠。下載一個可用的工具,對您的 Ant 和 Maven 構建腳本作一些小的改動,您和您的同事就有了在飲水機邊上談論的一種新報告:測試覆蓋報告。當 foo
和 bar
這樣的程序包令人驚奇地顯示高 覆蓋率時,您可以得到不小的安慰。如果您相信至少您的部分代碼可以保證是 “沒有 BUG” 的,您會覺得很安心。但是這樣做是一個錯誤。
存在不同類型的覆蓋度量,但是絕大多數的工具會關注行覆蓋,也叫做語句覆蓋。此外,有些工具會報告分支覆蓋。通過用一個測試工具執行代碼庫并捕獲整個測試過程中與被 “觸及” 的代碼對應的數據,就可以獲得測試覆蓋度量。然后這些數據被合成為覆蓋報告。在 Java 世界中,這個測試工具通常是 JUnit 以及名為 Cobertura、Emma 或 Clover 等的覆蓋工具。
行覆蓋只是指出代碼的哪些行被執行。如果一個方法有 10 行代碼,其中的 8 行在測試中被執行,那么這個方法的行覆蓋率是 80%。這個過程在總體層次上也工作得很好:如果一個類有 100 行代碼,其中的 45 行被觸及,那么這個類的行覆蓋率就是 45%。同樣,如果一個代碼庫包含 10000 個非注釋性的代碼行,在特定的測試運行中有 3500 行被執行,那么這段代碼的行覆蓋率就是 35%。
報告分支覆蓋 的工具試圖度量決策點(比如包含邏輯 AND
或 OR
的條件塊)的覆蓋率。與行覆蓋一樣,如果在特定方法中有兩個分支,并且兩個分支在測試中都被覆蓋,那么您可以說這個方法有 100% 的分支覆蓋率。
問題是,這些度量有什么用?很明顯,很容易獲得所有這些信息,不過您需要知道如何使用它們。一些例子可以闡明我的觀點。
![]() ![]() |
![]()
|
我在清單 1 中創建了一個簡單的類以具體表述類層次的概念。一個給定的類可以有一連串的父類,例如 Vector
,它的父類是 AbstractList
,AbstractList
的父類又是 AbstractCollection
,AbstractCollection
的父類又是 Object
:
清單 1. 表現類層次的類
|
正如您看到的,清單 1 中的 Hierarchy
類具有一個 baseClass
實例以及它的父類的集合。清單 2 中的 HierarchyBuilder
通過兩個復制 buildHierarchy
的重載的 static
方法創建了 Hierarchy
類。
清單 2. 類層次生成器
|
![]() ![]() |
![]()
|
有關測試覆蓋的文章怎么能缺少測試案例呢?在清單 3 中,我定義了一個簡單的有三個測試案例的 JUnit 測試類,它將試圖執行 Hierarchy
類和 HierarchyBuilder
類:
清單 3. 測試 HierarchyBuilder!
|
因為我是一個狂熱的測試人員,我自然希望運行一些覆蓋測試。對于 Java 開發人員可用的代碼覆蓋工具中,我比較喜歡用 Cobertura,因為它的報告很友好。而且,Corbertura 是開放源碼項目,它派生出了 JCoverage 項目的前身。
![]() ![]() |
![]()
|
運行 Cobertura 這樣的工具和運行您的 JUnit 測試一樣簡單,只是有一個用專門邏輯在測試時檢查代碼以報告覆蓋率的中間步驟(這都是通過工具的 Ant 任務或 Maven 的目標完成的)。
正如您在圖 1 中看到的,HierarchyBuilder
的覆蓋報告說明部分代碼沒有 被執行。事實上,Cobertura 認為 HierarchyBuilder
的行覆蓋率為 59%,分支覆蓋率為 75%。
圖 1. Cobertura 的報告

這樣看來,我的第一次覆蓋測試是失敗的。首先,帶有 String
參數的 buildHierarchy()
方法根本沒有被測試。其次,另一個 buildHierarchy()
方法中的兩個條件都沒有被執行。有趣的是,所要關注的正是第二個沒有被執行的 if
塊。
因為我所需要做的只是增加一些測試案例,所以我并不擔心這一點。一旦我到達了所關注的區域,我就可以很好地完成工作。注意我這兒的邏輯:我使用測試報告來了解什么沒有 被測試。現在我已經可以選擇使用這些數據來增強測試或者繼續工作。在本例中,我準備增強我的測試,因為我還有一些重要的區域未覆蓋。
清單 4 是一個更新過的 JUnit 測試案例,增加了一些附加測試案例,以試圖完全執行 HierarchyBuilder
:
清單 4. 更新過的 JUnit 測試案例
|
當我使用新的測試案例再次執行測試覆蓋過程時,我得到了如圖 2 所示的更加完整的報告。現在,我覆蓋了未測試的 buildHierarchy()
方法,也處理了另一個 buildHierarchy()
方法中的兩個 if
塊。然而,因為 HierarchyBuilder
的構造器是 private
類型的,所以我不能通過我的測試類測試它(我也不關心)。因此,我的行覆蓋率仍然只有 88%。
圖 2. 誰說沒有第二次機會

正如您看到的,使用一個代碼覆蓋工具可以 揭露重要的沒有相應測試案例的代碼。重要的事情是,在閱讀報告(特別 是覆蓋率高的)時需要小心,它們也許隱含危險的信息。讓我們看看兩個例子,看看在高覆蓋率后面隱藏著什么。
![]() ![]() |
![]()
|
正如您已經知道的,代碼中的許多變量可能有多種狀態;此外,條件的存在使得執行有多條路徑。在留意這些問題之后,我將在清單 5 中定義一個極其簡單只有一個方法的類:
清單 5.您能看出下面的缺陷嗎?
|
您是否發現了清單 5 中有一個隱藏的缺陷呢?如果沒有,不要擔心,我會在清單 6 中寫一個測試案例來執行 pathExample()
方法并確保它正確地工作:
清單 6. JUnit 來救援!
|
我的測試案例正確運行,我的神奇的代碼覆蓋報告(如下面圖 3 所示)使我看上去像個超級明星,測試覆蓋率達到了 100%!
圖 3. 覆蓋率明星

我想現在應該到飲水機邊上去說了,但是等等,我不是懷疑代碼中有什么缺陷呢?認真檢查清單 5 會發現,如果 condition
為 false
,那么第 13 行確實會拋出 NullPointerException
。Yeesh,這兒發生了什么?
這表明行覆蓋的確不能很好地指示測試的有效性。
![]() ![]() |
![]()
|
在清單 7 中,我定義了另一個包含 indirect 的簡單例子,它仍然有不能容忍的缺陷。請注意 branchIt()
方法中 if
條件的后半部分。(HiddenObject
類將在清單 8 中定義。)
清單 7. 這個代碼足夠簡單
|
呀!清單 8 中的 HiddenObject
是有害的。與清單 7 中一樣,調用 doWork()
方法會導致 RuntimeException
:
清單 8. 上半部分!
|
但是我的確可以通過一個良好的測試捕獲這個異常!在清單 9 中,我編寫了另一個好的測試,以圖挽回我的超級明星光環:
清單 9. 使用 JUnit 規避風險
|
您對這個測試案例有什么想法?您也許會寫出更多的測試案例,但是請設想一下清單 7 中不確定的條件有不止一個的縮短操作會如何。設想如果前半部分中的邏輯比簡單的 int
比較更復雜,那么您 需要寫多少測試案例才能滿意?
現在,對清單 7、8、9 的測試覆蓋率的分析結果不再會使您感到驚訝。在圖 4 的報告中顯示我達到了 75% 的行覆蓋率和 100% 的分支覆蓋率。最重要的是,我執行了第 10 行!
圖 4.愚弄的報酬

從第一印象看,這讓我驕傲。但是這個報告有什么誤導嗎?只是粗略地看一看報告中的數字,會導致您相信代碼是經過良好測試的。基于這一點,您也許會認為出現缺陷的風險很低。這個報告并不能幫助您確定 or
縮短操作的后半部分是一個定時炸彈!
![]() ![]() |
![]()
|
我不止一次地說:您可以(而且應該)使用測試覆蓋工具作為您的測試過程的一部分。但是不要被覆蓋報告所愚弄。關于覆蓋報告您需要了解的主要事情是,覆蓋報告最好用來檢查哪些代碼沒有經過 充分的測試。當您檢查覆蓋報告時,找出較低的值,并了解為什么特定的代碼沒有經過充分的測試。知道這些以后,開發人員、管理人員以及 QA 專業人員就可以在真正需要的地方使用測試覆蓋工具。通常有下列三種情況:
- 估計修改已有代碼所需的時間
- 評估代碼質量
- 評定功能測試
現在我可以斷定對測試覆蓋報告的一些使用方法會將您引入歧途,下面這些最佳實踐可以使得測試覆蓋報告可以真正為您所用。
對一個開發團隊而言,針對代碼編寫測試案例自然可以增加集體的信心。與沒有相應測試案例的代碼相比,經過測試的代碼更容易重構、維護和增強。測試案例因為暗示了代碼在測試工作中是如何 工作的,所以還可以充當內行的文檔。此外,如果被測試的代碼發生改變,測試案例通常也會作相應的改變,這與諸如注釋和 Javadoc 這樣的靜態代碼文檔不同。
在另一方面,沒有經過相應測試的代碼更難于理解和安全地 修改。因此,知道代碼有沒有被測試,并看看實際的測試覆蓋數值,可以讓開發人員和管理人員更準確地預知修改已有代碼所需的時間。
再次回到飲水機邊上,可以更好地闡明我的觀點。
市場部的 Linda:“我們想讓系統在用戶完成一筆交易時做 x 工作。這需要多長時間。我們的用戶需要盡快實現這一功能。”
管理人員 Jeff:“讓我看看,這個代碼是 Joe 在幾個月前編寫的,需要對業務層和 UI 做一些變動。Mary 也許可以在兩天內完成這項工作。”
Linda:“Joe?他是誰?”
Jeff:“哦,Joe,因為他不知道自己在干什么,所以被我解雇了。”
情況似乎有點不妙,不是嗎?盡管如此,Jeff 還是將任務分配給了 Mary,Mary 也認為能夠在兩天內完成工作 —— 確切地說,在看到代碼之前她是這么認為的。
Mary:“Joe 寫這些代碼時是不是睡著了?這是我所見過的最差的代碼。我甚至不能確認這是 Java 代碼。除非推倒重來,要不我根本沒法修改。”
情況對 “飲水機” 團隊不妙,不是嗎?但是我們假設,如果在這個不幸的事件的當初,Jeff 和 Mary 就擁有一份測試報告,那么情況會如何呢?當 Linda 要求實現新功能時,Jeff 做的第一件事就是檢查以前生成的覆蓋報告。注意到需要改動的軟件包幾乎沒有被覆蓋,然后他就會與 Mary 商量。
Jeff:“Joe 編寫的這個代碼很差,絕大多數沒經過測試。您認為要支持 Linda 所說的功能需要多長時間?”
Mary:“這個代碼很混亂。我甚至都不想看到它。為什么不讓 Mark 來做呢?”
Jeff:“因為 Mark 不編寫測試,剛被我解雇了。我需要您測試這個代碼并作一些改動。告訴我您需要多長時間。”
Mary:“我至少需要兩天編寫測試,然后我會重構這個代碼,增加新的功能。我想總共需要四天吧。”
正如他們所說的,知識的力量是強大的。開發人員可以在試圖修改代碼之前 使用覆蓋報告來檢查代碼質量。同樣,管理人員可以使用覆蓋數據更好地估計開發人員實際所需的時間。
開發人員的測試可以降低代碼中存在缺陷的風險,因此現在很多開發團隊在新開發和更改代碼的同時需要編寫單元測試。然而正如前面所提到的 Mark 一樣,并不總是在編碼的同時進行單元測試,因而會導致低質量代碼的出現。
監控覆蓋報告可以幫助開發團隊迅速找出不斷增長的沒有 相應測試的代碼。例如,在一周開始時運行覆蓋報告,顯示項目中一個關鍵的軟件包的覆蓋率是 70%。如果幾天后,覆蓋率下降到了 60%,那么您可以推斷:
- 軟件包的代碼行增加了,但是沒有為新代碼編寫相應的測試(或者是新增加的測試不能有效地覆蓋新代碼)。
- 刪除了測試案例。
- 上述兩種情況都發生了。
能夠監控事情的發展,無疑是件好事。定期地查閱報告使得設定目標(例如獲得覆蓋率、維護代碼行的測試案例的比例等)并監控事情的發展變得更為容易。如果您發現測試沒有如期編寫,您可以提前采取一些行動,例如對開發人員進行培訓、指導或幫助。與其讓用戶 “在使用中” 發現程序缺陷(這些缺陷本應該在幾個月前通過簡單的測試暴露出來),或者等到管理人員發現沒有編寫單元測試時再感到驚訝(和憤怒),還不如采取一些預防性的措施。
使用覆蓋報告來確保正確的測試是一項偉大的實踐。關鍵是要訓練有素地完成這項工作。例如,使每晚生成并查閱覆蓋報告成為連續累計 過程的一部分。
假設覆蓋報告在指出沒有經過 足夠測試的代碼部分方面非常有效,那么質量保證人員可以使用這些數據來評定與功能測試有關的關注區域。讓我們回到 “飲水機” 團隊來看看 QA 的負責人 Drew 是如何評價 Joe 的代碼的:
Drew 對 Jeff 說:“我們為下一個版本編寫了測試案例,我們注意到很多代碼沒有被覆蓋。那好像是與股票交易有關的代碼。”
Jeff:“哦,我們在這個領域有好些問題。如果我是一個賭徒的話,我會對這個功能區域給予特別的關注。Mary 正在對這個應用程序做一些其他的修改 —— 她在編寫單元測試方面做得很好,但是這個代碼也太差了點。”
Drew:“是的,我正在確定工作的資源和級別,看上去我沒必要那么擔心了,我估計我們的團隊會對股票交易模塊引起足夠的關注。”
知識再次顯示了其強大的力量。與其他軟件生命周期中的風險承擔者(例如 QA)配合,您可以利用覆蓋報告所提供的信息來降低風險。在上面的場景中,也許 Jeff 可以為 Drew 的團隊提供一個早期的不包含 Mary 的所有修改的版本。不過無論如何,Drew 的團隊都應該關注應用程序的股票交易方面,與其他具有相應單元測試的代碼相比,這個地方似乎存在更大的缺陷風險。
![]() ![]() |
![]()
|
對單元測試范例而言,測試覆蓋度量工具是一個有點奇怪的組成部分。對于一個已存在的有益的過程,覆蓋度量可以增加其深度和精度。然而,您應該仔細地閱讀代碼覆蓋報告。單獨的高覆蓋率并不能確保代碼的質量。對于減少缺陷,代碼的高覆蓋并不是必要條件,盡管高覆蓋的代碼的確更少 有缺陷。
測試覆蓋度量的竅門是使用覆蓋報告找出未經 測試的代碼,分別在微觀和宏觀兩個級別。通過從頂層開始分析您的代碼庫,以及分析單個類的覆蓋,可以促進深入的覆蓋測試。一旦您能夠綜合這些原則,您和您的組織就可以在真正需要的地方使用覆蓋度量工具,例如估計一個項目所需的時間,持續監控代碼質量以及促進與 QA 的協作。