單元測試是軟件開發過程當中重要的質量保證環節。單元測試能夠減小代碼中潛在的錯誤,使缺陷更早地被發現,從而下降了軟件的維護成本。軟件代碼的質量由單元測試來保證,而單元測試自身的質量與效率問題也不容忽視。提升單元測試的質量與效率,不只可以使軟件代碼更加有保證,並且可以節省開發人員編寫或者修改單元測試代碼的時間。衡量單元測試質量與效率的指標多種多樣,代碼覆蓋率是其中一個極爲重要的指標。通常而言,代碼覆蓋率越高,單元測試覆蓋的範圍就越大,代碼中潛在錯誤的數量就越少,軟件質量就越高。本文首先介紹代碼覆蓋率的統計指標類型及經常使用統計工具,而後重點選取具備表明性的行覆蓋率進行分析,介紹兩種方法用於提升代碼的行覆蓋率。html
回頁首java
代碼覆蓋率指的是一種衡量代碼覆蓋程度的方式,一般會對如下幾種方式進行統計分析:正則表達式
行覆蓋。它又被稱做語句覆蓋或基本塊覆蓋。這是一種較爲經常使用且具備表明性的指標,度量的是被測代碼中每一個可執行語句是否被執行到。併發
條件覆蓋。它度量的是當代碼中存在分支時,是否能覆蓋進入分支和不進入分支這兩種狀況。這要求開發人員編寫多個測試用例以分別知足進入分支與不進入分支這兩種狀況。maven
路徑覆蓋。它度量的是當代碼中存在多個分支時,是否覆蓋到分支之間不一樣組合方式所產生的所有路徑。這是一種力度最強的覆蓋檢測,相對而言,條件覆蓋只是路徑覆蓋中的一部分。函數
在這三種覆蓋指標中,行覆蓋簡單,適用性廣,但可能會被認爲是「最弱的覆蓋」,其實否則。行覆蓋相對於條件或路徑覆蓋,可使開發人員經過儘量少的測試數據和用例,覆蓋儘量多的代碼。一般狀況下,是先經過工具檢測一遍整個工程單元測試的行覆蓋狀況,而後針對沒有被覆蓋到的代碼,分析其沒有被覆蓋到的緣由。若是是因爲該代碼所在分支因爲不知足進入該分支的條件而沒有被覆蓋,那麼開發人員纔會進一步修改或增長測試代碼,完成該部分的條件或路徑覆蓋。工具
可見,高效高質量的行覆蓋是有效進行條件覆蓋與路徑覆蓋的前提。行覆蓋率越高,說明沒有被覆蓋到的代碼越少,這樣開發人員便會集中精力修改測試用例,覆蓋這些數量很少的代碼。相反,若是行覆蓋率低,開發人員須要逐個檢查沒有被覆蓋到的代碼,精力被分散,所以很難提升剩餘代碼單元測試的質量。單元測試
代碼覆蓋率 = 被測代碼行數 / 參測代碼總行數 * 100%。 從代碼覆蓋率的計算方式中能夠看出,要提升代碼覆蓋率,可經過提升被測代碼行數,或減小參測代碼總行數的方式進行。如下將會從這兩個角度分別入手,分析如何提升被測代碼行數及減小參測代碼總行數。測試
回頁首優化
Cobertura 是一款優秀的開源測試覆蓋率統計工具,它與單元測試代碼結合,標記並分析在測試包運行時執行了哪些代碼和沒有執行哪些代碼以及所通過的條件分支,來測量測試覆蓋率。除了找出未測試到的代碼並發現 bug 外,Cobertura 還能夠經過標記無用的、執行不到的代碼來優化代碼,最終生成一份美觀詳盡的 HTML 覆蓋率檢測報告。
Cobertura 基本工具包裏有四個基本過程及對應的工具:cobertura-check, cobertura-instrument, cobertura-merge, cobertura-report; 這個腳本獨立使用較爲繁瑣,不方便也不利於自動化。不過, Cobertura 在 Maven 編譯平臺上有相應的 cobertura-maven-plugin 插件,使代碼編譯、檢測、集成等各個週期能夠流水線式自動化完成。
Cobertura-maven-plugin 官方版有五個主要目標指令 (goal),如表 1:
目標指令 | 做用解釋 |
---|---|
Cobertura:check | 檢查最後一次標註(instrumentation) 正確與否 |
Cobertura:clean | 清理插件生產的中間及最終報告文件 |
Cobertura:dump-datafile | Cobertura 數據文件 dump 指令 , 不經常使用 |
Cobertura:instrument | 標註編譯好的 javaclass 文件 |
Cobertura:cobertura | 標註、運行測試併產生 Cobertura 覆蓋率報告 |
Cobertura 一般會與 Maven 一塊兒使用。所以工程目錄結構若是遵循 Maven 推薦的標準的話,一個集成 Cobertura 的基本 POM 文件如清單 1 所示:
<project> <reporting> <plugins> <plugin> <!-- 此處用於將 Cobertura 插件集成到 Maven 中 --> <groupId>org.codehaus.mojo</groupId> <artifactId>cobertura-maven-plugin</artifactId> <version>2.5.2</version> </plugin> </plugins> </reporting> </project>
若是工程目錄結構沒有采用 Maven 推薦標準,則須要進行以下額外設置:
<build> <!-- Java 源代碼的路徑配置 --> <sourceDirectory>src/main/java</sourceDirectory> <scriptSourceDirectory>src/main/scripts</scriptSourceDirectory> <!-- 測試代碼的路徑配置 --> <testSourceDirectory>src/test/java</testSourceDirectory> <!-- 源碼編譯後的 class 文件的路徑配置 --> <outputDirectory>target/classes</outputDirectory> <!-- 測試源碼編譯後的 class 文件的路徑配置 --> <testOutputDirectory>target/test-classes</testOutputDirectory> <plugin> .... </plugin> </build>
單元測試代碼編寫完成,全部設置配製好後,在工程根目錄運行「mvn cobertura:cobertura」Maven 就會對代碼進行編譯。編譯完成以後,就會在項目中運行測試代碼並輸出測試報告結果到目錄 project_base$\target\site\cobertura\index.html,效果如圖 1 所示。
從以上報告中可見,
代碼總體的行覆蓋率並不高,有些包或類覆蓋率很低,甚至爲 0。考慮到這些包或類的特殊性(例如它們已被其餘類取代),無需對它們進行單元測試,所以須要從整個測試範圍中剔除。
部分類的行覆蓋率雖然已接近 100%,但仍存在一些方法(如 set 和 get 方法)因爲沒有測試的必要卻被列入了統計範圍,這些方法須要被過濾掉。
針對上述兩種改進措施,均可以使用 Cobertura 進行實現。第一種改進措施 Cobertura 能夠支持,而第二改進措施則須要對 Cobertura 源碼進行修改,重編譯後方可支持。下面將詳細介紹如何使用 Cobertura 對上述問題進行優化。
針對項目中不需進行單元測試的包和類,咱們能夠利用 POM 文件中 Cobertura 的標註 (instrument) 設置,對相應的包和類進行剔除 (exclude) 或篩選 (include),使之不體如今覆蓋率報告中,去除它們對整個覆蓋率的影響,從而使報告更具針對性。其基本 POM 標籤設置及解析如清單 3 中所示。
<configuration> <instrumentation> <excludes> <!--此處用於指定哪些類會從單元測試的統計範圍中被剔除 --> <exclude>exs/res/process/egencia/Mock*.class</exclude> <exclude>exs/res/process/test/**/*Test.class</exclude> </excludes> </instrumentation> </configuration> <executions> <execution> <goals> <goal>clean</goal> </goals> </execution> </executions>
經過在配置文件中使用 Include 與 Exclude,能夠顯式地指定哪些包和類被列入單元測試的統計範圍,哪些包和類被剔除在此範圍以外。正則表達式支持豐富的匹配條件,能夠知足大多數項目對單元測試範圍的要求。以上代碼將 exs.res.process.egencia 下面全部的名稱 Mock 開頭的類,以及 exs.res.process.egencia.test 包下面以 Test 結尾的類都剔除在測試範圍之外。在使用這種配置以後,代碼總體的範圍被縮小,所以在被覆蓋到的代碼數量不變的基礎上,整個代碼覆蓋率會較之前提升。輸出結果如圖 2 所示。
最新版本中的 Cobertura 只能支持到類級別的過濾,而對於類中方法的過濾是不支持的。所以咱們須要經過修改 Cobertura 源碼,使 Cobertura 支持對類中方法的過濾。
對 Cobertura 及其插件改動所依據的主要原理是 : 修改 Cobertura-maven-plugin 項目中的 InstrumentationTask 類,增長 Ignoretrival,IgnoreMethod 等新增 POM 參數。配製正則表達式,修改 Cobertura 核心,在標註(instrumentation) 階段遍歷函數名時,檢測函數名是否匹配傳入的正則表達式,過濾函數體代碼,從而把這些函數代碼排除在代碼覆蓋統計以外,節省開發人員對這類代碼的測試精力。
清單 4 至清單 6 是對 Cobertura 的幾處核心改動,僅供讀者參考。
private void checkForTrivialSignature() { Type[] args = Type.getArgumentTypes(myDescriptor); Type ret = Type.getReturnType(myDescriptor); if (myName.equals("<init>")) { isInit = true; mightBeTrivial = true; return; } if (myName.startsWith("set") && args.length == 1 && ret.equals(Type.VOID_TYPE)) { isSetter = true; mightBeTrivial = true; return; } if ((myName.startsWith("get") || myName.startsWith("is") || myName.startsWith("has")) && args.length == 0 && !ret.equals(Type.VOID_TYPE)) { isGetter = true; mightBeTrivial = true; return; } }
private String ignoreMethodAnnotation; private String ignoreTrivial; /** * 建立一個新的對象,用於進行配置。 */ public ConfigInstrumentation() /** * * 該方法用於設置annotation的名字以用於過濾類內部的方法 * @param ignoreMethodAnnotation */ public void setIgnoreMethodAnnotation(String ignoreMethodAnnotation) { this.ignoreMethodAnnotation = ignoreMethodAnnotation; } public String getIgnoreTrivial() { return ignoreTrivial; } /** * 該方法用於標識測試類中的方法是否可有可無不須要測試。 * @param ignoreTrivial */ public void setIgnoreTrivial(String ignoreTrivial) { this.ignoreTrivial = ignoreTrivial; }
<reporting> <plugins> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>cobertura-maven-plugin</artifactId> <version>2.5.2</version> <configuration> <ignores> <!--通過修改的 cobertura, 支持方法級別的過濾 --> <ignore>*main*</ignore> <!--以上修改指的是過濾項目中全部類中的方法名中含有 main 的方法 --> </ignores> <IgnoreTrival>true</IgnoreTrival> </configuration> </plugin> </plugins> </reporting>
以上修改都完成以後, 就能夠運行「mvn:site」命令獲得報告。圖 4 是使用沒有被修改的 Cobertura 產生的結果報告,無函數過濾效果。圖 5 是使用被修改後的 Cobertura 產生的結果報告,能夠從中看出,幾個 set 與 get 方法已被排除在統計範圍以外。
不一樣的人對反射有不一樣的理解,大部分人認同的一種觀點是:反射使得程序能夠檢查自身結構以及軟件環境,而且根據程序檢測到的實際狀況改變行爲。
爲了實現自檢,一段程序須要有些信息來表示自身,這些信息就稱爲元數據(metadata)。Java 運行過程當中對這些元數據的自檢稱爲內省(introspection)。內省過程以後每每進行行爲改變。總的來講,反射 API 利用如下三種技術來實現行爲改變:
直接修改元數據。
利用元數據進行操做。
調解(Intercession), 代碼被容許在程序各類運行期進行調整。
Java 語言反射機制提供一組豐富的 API 函數來操做元數據,且提供了少部分重要的 API 來實現 Intercession 能力。
實際項目中,爲了保證軟件代碼的總體質量,單元測試不只要覆蓋類的公有成員,還要覆蓋重要的私有成員。而有些私有成員的調用,會被放入到極爲複雜的條件分支中。而構造進入這個私有方法的相關條件,可能須要開發人員編寫大量測試代碼及測試數據。這無疑增長了單元測試的成本。有時爲了節省成本,該類私有方法便跳過不測,從而在無形中下降了代碼的行覆蓋率,影響了軟件的總體質量。
而利用反射的一系列特性,咱們能夠在不改變源代碼的狀況下,直接對複雜的私有方法進行單元測試,無需增長行覆蓋檢查中被覆蓋的代碼行數,從而能夠在不增長單元測試成本的前提下,提升代碼的行覆蓋率與單元測試的總體質量。
清單 7 給出了一段簡單的目標測試代碼示例。
package exs.res.util; public class Customer{ private String message; public String greet; private String sayHello() { return "Hello"; } public String pHello() { return "pHello"; } }
爲了測試私有函數 sayHello(),利用反射元數據操做 API 的測試代碼爲:
@Test public void privateMethodTest() { final Method methods[] = Customer.class.getDeclaredMethods(); for (int i = 0; i < methods.length; ++i) { if ("sayHello".equals(methods[i].getName())) { //這裏會將 sayHello 方法由 private 變爲 public,從而能夠直接被外部對象訪問 methods[i].setAccessible(true); try{ String anotherString =(String)methods[i].invoke(new Customer(), new Object[0]); assertTrue("Hello".equalsIgnoreCase(anotherString)); }catch(Exception e){ e.printStackTrace(); } break; } } } @Test public void privateFieldTest() throws NoSuchFieldException, SecurityException{ try{ Field message = Customer.class.getDeclaredField("message"); Customer testCustomer = new Customer(); //這裏會將 message 屬性由 private 變爲 public,從而能夠直接被外部對象訪問 message.setAccessible(true); message.set(testCustomer, "newMessage"); assertTrue("newMessage".equalsIgnoreCase((String)message.get(testCustomer))); }catch(Exception e){ e.printStackTrace(); } }
運行以上單元測試用例來分別對 Customer 的私有方法 sayHello 以及私有屬性 message 進行直接訪問,結果如圖 6 所示。
從圖中咱們能夠看到 Customer 成員的私有方法 sayHello 被測試代碼覆蓋到。因此,當一些代碼函數複雜度太高,到經過構造測試數據或測試用例的方法很難使非公有成員獲得運行時,咱們就能夠利用 Java 反射機制,直接在測試類中調用和測試目標類的非公有成員,從而提升覆蓋率。
本文使用兩種方法,從兩個不一樣的角度對單元測試中的代碼覆蓋率進行了加強。改進 Cobertura 來提升單元測試代碼覆蓋率,主要從縮小參與測試的代碼總範圍的角度入手,適用於代碼總數龐大而被測代碼數量很少的狀況。而使用 Java 反射機制提升單元測試代碼覆蓋率,主要從提升被測代碼數量的角度入手,適用於被測代碼私有成員多且觸發條件苛刻的狀況。針對項目中對單元測試的不一樣需求,選取合適的技術來加強單元測試,才能真正提升代碼以致項目的整體質量。