對於許多團隊來說,單元測試現在是開發過程的一個主要部分;JUnit 之類 的框架可以進行無損測試,盡管我們並不喜歡它,寧願為某些 代碼編寫某些 測 試。單元測試運行效率很低,只能測試單個代碼片段,並且,一般情況下,測試 代碼的重用性通常很也低 —— 昨天為組件 A 編寫的測試不能很好地用於測試 組件 B(示例代碼除外)。
典型的單元測試場景
在發現 bug 時,要做的第一件事是什麼?您可能只是想去修復它,但是,在 長時間的運行中,這不是一個最有效的方法。在許多開發部門中,處理 bug 的 過程如下:
針對 bug 編寫測試用例
確保測試用例在遇到 bug 時運行失敗
修復 bug
確保測試用例通過
確保其他測試套件仍能通過
檢查修正和測試用例,形成版本控制
將修正記錄在 bug 跟蹤系統中
盡管此方法在短期內比僅修復 bug 要多做許多工作,但它提供了許多更有價 值的東西:獲得修復 bug 的更多信心,因為您已經對它進行了測試;獲得 bug 將不會再出現的更多信心,因為測試用例是回歸測試套件的一部分。在版本控制 系統和 bug 跟蹤系統之間,還可以獲得一個記錄,該記錄描述了 bug 是什麼以 及如何修復它 —— 這是非常有用的信息,其他人會從中受益。
如果進取心較強,那麼可以思考一下 bug 是怎樣出現的,並在其他位置查找 同一錯誤。如果在別處發現同一錯誤,那麼可以對這些 bug 進行測試和修復。 單元測試作為質量管理工具的主要弱點是每個測試用例只能測試一個代碼片段。 因為測試用例是專為每個組件和每個潛在錯誤模式設計的,所以只有編寫足夠多 的單元測試才能測試大量的產品,這非常耗時並且代價高昂。
QA 經濟
測試是一種基本的質量管理工具,我們知道僅有多組測試用例還不足以找出 復雜軟件片段中的所有 bug。事實上,對於任何優秀程序而言,“查找所有 bug ” 是不可能實現的目標。據估計,NASA 向每個開發人員提供了 20 個測試程序 (大大超過任何商業實體)來負責質量評價 (QA) —— 但軟件仍有缺陷。因此 ,質量評價的目標不應是查找所有的 bug,因為這是不可能的。相反,質量評價 的目標應該是提高代碼運行良好的信心,從而最大程度地提供可用資源。
要高效運行質量評估量 (QA),則需要對可用 QA 方法中的可用資源做預算, 這樣才能最大限度地提高信心。覆蓋范圍大的測試套件可以提高我們對代碼使用 的信心,因為它進行了一次徹底的代碼審查。執行兩次比執行一次較果好,因為 每次都會發現另一次可能錯過的錯誤。兩次同樣遵循收益遞減規則,所以測試價 值為 X 美元和代碼審查價值為 Y 的 QA 計劃要比價值為 X+Y 的任何一次測試 或代碼審查的效果好。
添加靜態分析
靜態分析是在不運行代碼的情況下對其進行分析的過程,它與進行前面的代 碼審查時我們執行的操作非常相似,或者與標記可疑結構時 IDE 執行的操作非 常相似。靜態分析是添加到 QA 混合(QA mix)中的一項優良技術,因為它擅長 查找其他方法(如測試和代碼審查)可能錯過的錯誤。靜態分析相對比較容易一 些,不像單元測試那樣必須為要測試的每個類重新編寫測試,您可以在任何代碼 上運行靜態分析工具。
FindBugs 是一種開放源碼的靜態分析工具,它包含用於許多常見 bug 模式 的 bug 模式檢測器,令人驚訝的是,即使在測試良好的軟件中,FindBugs 也常 常會發現一些 “沉默” 的 bug,但是單元測試和專業代碼審查都可能錯過這些 bug。FindBugs 還允許編寫新的 bug 模式檢測器,並將它們包裝為插件,所以 如果一組標准的檢測器不能按您的需要執行,那麼您可以很容易地編寫自已的檢 測器。此擴展性使 FindBugs 成為非常強大的質量管理工具,因為當發現新類型 的錯誤時,可以針對該錯誤編寫檢測器,並在整個代碼基址中搜索該錯誤。
靜態分析的主要作用是分析輸出,並確定報告的條目是真的 bug 還是假警報 。編寫的部分優秀分析工具或 bug 模式檢測器會管理誤報率;核心 FindBugs 包中的檢測器已經進行了調優,目的是使誤報率不超過 50 %,這樣分析輸出時 不會有太多的煩麻。(將此阈值與針對 C 的 lint-like 工具進行比較,後者常 常發出許多假警報,使用時相當耗時。)
將它提升一個級別
前面描述修復 bug 的方法(首先編寫測試用例,然後檢查修復和測試用例) 反映了這樣一個願望:不僅要修復 bug,還要提高修復它的信心,並記錄如何修 復它,以及何時修復它。此方法比僅修復 bug 要多做許多工作,但是它給我們 提供了更多的信心,我們的代碼在經過多個開發人員的不斷修改後可以繼續使用 。不過,僅為所發現的 bug 編寫測試用例是一種消極方法。在代碼失敗之前, 我們希望盡可能以最佳實踐分析代碼。
清單 1 通過 BigDecimal 類說明了常見的 bug。BigDecimal 是固定不變的 ,所以算術方法(如 add())會返回一個新的 BigDecimal 作為其結果,而不修 改調用它們的對象。清單 1 中的代碼顯然被假定為有條件地將運輸費用添加到 總體訂購價格中,但是,實際上不能隨意添加任何內容,因為 add() 的返回值 被丟棄了:
清單 1. 典型的 bug 模式 —— 使用 mutator 方法配置 factory 方法
public class ShoppingCart {
private BigDecimal totalCost;
private boolean qualifiesForFreeShipping() { ... }
private BigDecimal getShippingCost() { ... }
public void checkout() {
...
if (!qualifiesForFreeShipping())
totalCost.add(getShippingCost()); //WRONG!
}
}
清單 1 中的錯誤是一種常見的錯誤,它忘記了對象是不可變的,從而將 factory 方法誤認為 mutator 方法。如果在代碼中查找此類錯誤,就會發現存 在同一錯誤多次發生的情況,因為它來源於對特定庫類工作方式的誤解。對於查 找此 bug,負責任的開發人員可能會搜索整個代碼基址來查找對 BigDecimal.add()、subtract() 等方法的調用,並尋找忽略返回值的其他實例 。
此策略是一個好的開頭,但我們可以做得更好。在這裡識別 bug 模式是非常 容易的 —— 忽略不可變對象上的求值方法(value-bearing method)的結果。 識別出該模式後,構建識別此模式的檢測器是相對簡單的一件事件。(FindBugs 在核心檢測器集中有這樣一個檢測器。)此技術不僅可以應用於 BigDecimal, 還可以應用於其他不可變類(如 BigInteger、String 或 Color)中。
花費一點時間為 bug 模式創建一個 bug 檢測器,它會為您帶來可觀的收益 。不僅可以用比手工操作更少的工作和更高的信心來審核整個項目,從中尋找 bug,而且還可以在現在和將來將同一檢測器應用到其他項目中。您已針對不斷 恢復、隨時可能出現的 bug 類型建立了防御機制,而不是在逐個實例的基礎上 解決 bug。
示例 bug 檢測器
為說明編寫 FindBugs 檢測器的過程,我們編寫了一個簡單的檢測器,它可 以查找對 System.gc() 的調用。雖然調用的 System.gc() 不一定是 bug,但在 實踐中,它會帶來更多的問題(多於它解決的問題 )。尤其是,如果錯誤地調 用了庫中隱藏的 System.gc(),則會降低使用該庫的應用程序的性能,開發人員 可能會感到很茫然,對性能會如此低下感動很奇怪。
編寫 bug 檢測器的第一步是識別被檢測的 bug 模式。在本例中,該模式非 常簡單,只需調用 System.gc() 即可。要編寫識別字節碼中此模式的檢測器, 則需要知道對應於 bug 模式的字節碼是什麼。了解此問題的最好方法是編寫一 個包含 bug 的小程序,對它進行編譯,並使用 javap -c 解開 .class 文件。 清單 2 顯示了一個展示該 bug 的類:
清單 2. 展示 bug 模式(我們想為它構建一個檢測器)的代碼
public class BadClass {
public void doBadStuff() {
System.gc();
}
}
清單 3 顯示了運行示例類時 javap -c 的輸出:
清單 3. 清單 2 中代碼的字節碼清單
public void doBadStuff ();
Code:
0: invokestatic #2; //Method java/lang/System.gc:()V
3: return
我們很快知道靜態方法是通過 invokestatic JVM 指令調用的, invokestatic 的操作數是 java/lang/system 類的 gc:()V 方法。字節碼中的 方法簽名和類型名稱與源代碼中的略有不同,但它很容易用於字節碼使用的編碼 。
使用 bug 模式示例編寫 FindBugs 檢測器非常簡單。清單 4 顯示了擴展 BytecodeScanningDetector 基礎類並重寫 sawOpcode() 方法的檢測器。當它遇 到 invokestatic 指令時,它會檢查被調用方法的類和名稱,如果是 System.gc() 指令,它會報告 bug 實例。
清單 4. 查找調用 System.gc() 的 Bug 檢測器
public class CallSystemGC extends BytecodeScanningDetector {
private BugReporter bugReporter;
public CallSystemGC(BugReporter bugReporter) {
this.bugReporter = bugReporter;
}
public void sawOpcode(int seen) {
if (seen == INVOKESTATIC) {
if (getClassConstantOperand().equals ("java/lang/System")
&& getNameConstantOperand().equals("gc")) {
bugReporter.reportBug(new BugInstance("SYSTEM_GC", NORMAL_PRIORITY)
.addClassAndMethod(this)
.addSourceLine(this));
}
}
}
}
將檢測器包裝為插件
創建新的 bug 檢測所需的最後一步是將其打包為一個插件。FindBugs 插件 包含一個或多個 bug 檢測器、一個部署描述符和一個資源文件,它們被打包成 一個 JAR 文件,放在 FindBugs 安裝的插件目錄中。稱為 findbugs.xml 的部 署描述符將定義已知的 bug 檢測器和它報告的錯誤。稱為 messages.xml (對 於本地化版本稱為 messages_xx.xml)的資源文件定義特定於語言的、將由 FindBugs GUI 使用的字符串,用它描述所報告的 bug。清單 5 和清單 6 顯示 了示例 bug 檢測器的部署描述符和資源文件。插件 JAR 中可以包括多個資源文 件的本地版本;部署描述符和資源文件放置在插件 JAR 的頂級目錄中。
清單 5. 示例 bug 檢測器的部署描述符
<FindbugsPlugin xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="findbugsplugin.xsd"
pluginid="com.briangoetz.findbugs.plugin"
defaultenabled="true"
provider="Brian Goetz"
website="http://www.briangoetz.com">
<Detector class="com.briangoetz.findbugs.plugin.CallSystemGC"
speed="fast"
reports="SYSTEM_GC" />
<BugPattern abbrev="GC" type="SYSTEM_GC" category="PERFORMANCE" />
</FindbugsPlugin>
<
清單 6. 示例 bug 檢測器的資源文件
<MessageCollection xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="messagecollection.xsd">
<Plugin>
<ShortDescription>Brian's plugin</ShortDescription>
<Details></Details>
</Plugin>
<Detector class="com.briangoetz.findbugs.plugin.CallSystemGC">
<Details>
<![CDATA[
Finds calls to System.gc().
]]>
</Details>
</Detector>
<BugPattern type="SYSTEM_GC">
<ShortDescription>Method calls System.gc() </ShortDescription>
<LongDescription>Call to System.gc() method in {1} </LongDescription>
<Details>
<![CDATA[
Library code should not call System.gc()
]]>
</Details>
</BugPattern>
<BugCode abbrev="GC" >Garbage collection</BugCode>
</MessageCollection>
根據 JDK 1.4.2 類庫構建和包裝插件,並運行它,這會為我們帶來意想不到 的效果:com.sun.imageio 中的幾個類(包括 JPEGImageReader 和 JPEGImageWriter)將調用 System.gc()!此結果還有另一個好處,即靜態分析 的靈活性:創建 bug 檢測器後,它可以在任何地方查找 bug。
結束語
靜態分析和自定義 bug 檢測器是提高軟件質量的非常有效的方法。通過為已 知 bug 模式創建檢測器,我們不僅可以在特定項目的當前代碼基址中搜索 bug 模式,還可以在當前或以後的任何項目中搜索 bug 模式。創建 bug 檢測器所付 出的額外努力將來會為您帶來質量方面的豐厚回報。
本文配套源碼