圖 3. NameFunction 類中的代碼覆蓋率
Cobertura 是 jcoverage 的分支(請參閱 參考資料)。GPL 版本的 jcoverage 已經有一年沒有更新過了,并且有一些長期存在的 bug,Cobertura 修復了這些 bug。原來的那些 jcoverage 開發人員不再繼續開發開放源碼,他們轉向開發 jcoverage 的商業版和 jcoverage+,jcoverage+ 是一個從同一代碼基礎中發展出來的封閉源代碼產品。開放源碼的奇妙之處在于:一個產品不會因為原開發人員決定讓他們的工作獲得相應的報酬而消亡。
確認遺漏的測試
利用 Cobertura 報告,可以找出代碼中未測試的部分并針對它們編寫測試。例如,圖 3 顯示 Jaxen 需要進行一些測試,運用 name() 函數對文字節點、注釋節點、處理指令節點、屬性節點和名稱空間節點進行測試。
如果有許多未覆蓋的代碼,像 Cobertura 在這里報告的那樣,那么添加所有缺少的測試將會非常耗時,但也是值得的。不一定要一次完成它。您可以從被測試的少的代碼開始,比如那些所有沒有覆蓋的包。在測試所有的包之后,可以對每一個顯示為沒有覆蓋的類編寫一些測試代碼。對所有類進行專門測試后,還要為所有未覆蓋的方法編寫測試代碼。在測試所有方法之后,可以開始分析對未測試的語句進行測試的必要性。
(幾乎)不留下任何未測試的代碼
是否有一些可以測試但不應測試的內容?這取決于您問的是誰。在 JUnit FAQ 中,J. B. Rainsberger 寫到“一般的看法是:如果 自身 不會出問題,那么它會因為太簡單而不會出問題。第一個例子是 getX() 方法。假定 getX() 方法只提供某一實例變量的值。在這種情況下,除非編譯器或者解釋器出了問題,否則 getX() 是不會出問題的。因此,不用測試 getX(),測試它不會帶來任何好處。對于 setX() 方法來說也是如此,不過,如果 setX() 方法確實要進行任何參數驗證,或者說確實有副作用,那么還是有必要對其進行測試。”
理論上,對未覆蓋的代碼編寫測試代碼不一定會發現 bug。但在實踐中,我從來沒有碰到沒有發現 bug 的情況。未測試的代碼充滿了 bug。所做的測試越少,在代碼中隱藏的、未發現的 bug 會越多。
我不同意。我已經記不清在“簡單得不會出問題”的代碼中發現的 bug 的數量了。確實,一些 getter 和 setter 很簡單,不可能出問題。但是我從來沒有辦法區分哪些方法是真的簡單得不會出錯,哪些方法只是看上去如此。編寫覆蓋像 setter 和 getter 這樣簡單方法的測試代碼并不難。為此所花的少量時間會因為在這些方法中發現未曾預料到的 bug 而得到補償。
一般來說,開始測量后,達到 90% 的測試覆蓋率是很容易的。將覆蓋率提高到 95% 或者更高需要動一下腦筋。例如,可能需要裝載不同版本的支持庫,以測試沒有在所有版本的庫中出現的 bug。或者需要重新構建代碼,以便測試通常執行不到的部分代碼。可以對類進行擴展,讓它們的受保護方法變為公共方法,這樣可以對這些方法進行測試。這些技巧看起來像是多此一舉,但是它們曾幫助我在一半的時間內發現更多的未發現的 bug。
并不總是可以得到完美的、 的代碼覆蓋率。有時您會發現,不管對代碼如何改造,仍然有一些行、方法、甚至是整個類是測試不到的。下面是您可能會遇到的挑戰的一些例子:
只在特定平臺上執行的代碼。例如,在一個設計良好的 GUI 應用程序中,添加一個 Exit 菜單項的代碼可以在 Windows PC 上運行,但它不能在 Mac 機上運行。
捕獲不會發生的異常的 catch 語句,比如在從 ByteArrayInputStream 進行讀取操作時拋出的 IOException。
非公共類中的一些方法,它們永遠也不會被實際調用,只是為了滿足某個接口契約而必須實現。
處理虛擬機 bug 的代碼塊,比如說,不能識別 UTF-8 編碼。
考慮到上面這些以及類似的情況,我認為一些極限程序員自動刪除所有未測試代碼的做法是不切實際的,并且可能具有一定的諷刺性。不能總是獲得完美的測試覆蓋率并不意味著不會有更好的覆蓋率。
然而,比執行不到的語句和方法更常見的是殘留代碼,它不再有任何作用,并且從代碼基中去掉這些代碼也不會產生任何影響。有時可以通過使用反射來訪問私有成員這樣的怪招來測試未測試的代碼。還可以為未測試的、包保護(package-protected)的代碼來編寫測試代碼,將測試類放到將要測試的類所在那個包中。但好不要這樣做。所有不能通過發布的(公共的和受保護的)接口訪問的代碼都應刪除。執行不到的代碼不應當成為代碼基的一部分。代碼基越小,它越容易被理解和維護。