天天看點

使用 Cobertura 和反射機制提高 Java 單元測試中的代碼覆寫率

本文将介紹兩種開發實踐,用于提高 Java 單元測試中的代碼覆寫率。代碼覆寫率 = (被測代碼 / 代碼總數)* 100%。提高被測代碼數量或降低代碼總數,均可達到提高代碼覆寫率的效果。在本文中,您将看到如何通過使用反射機制,在外部直接對目标類中的不可通路成員進行測試,以提高被測代碼數量;以及通過修改 Cobertura 源碼,使其支援通過正規表達式來過濾不需要進行單元測試的代碼,以降低代碼總數。代碼覆寫率的提高,減少了單元測試過程中未被覆寫的代碼數量,降低了開發人員編寫或修改單元測試用例的時間成本,進而提高了整個單元測試的效率。

引言

單元測試是軟體開發過程中重要的品質保證環節。單元測試可以減少代碼中潛在的錯誤,使缺陷更早地被發現,進而降低了軟體的維護成本。軟體代碼的品質由單元測試來保證,而單元測試自身的品質與效率問題也不容忽視。提高單元測試的品質與效率,不僅能夠使軟體代碼更加有保證,而且能夠節省開發人員編寫或者修改單元測試代碼的時間。衡量單元測試品質與效率的名額多種多樣,代碼覆寫率是其中一個極為重要的名額。一般而言,代碼覆寫率越高,單元測試覆寫的範圍就越大,代碼中潛在錯誤的數量就越少,軟體品質就越高。本文首先介紹代碼覆寫率的統計名額類型及常用統計工具,然後重點選取具有代表性的行覆寫率進行分析,介紹兩種方法用于提高代碼的行覆寫率。

代碼覆寫率的統計名額

代碼覆寫率指的是一種衡量代碼覆寫程度的方式,通常會對以下幾種方式進行統計分析:

  • 行覆寫。它又被稱作語句覆寫或基本塊覆寫。這是一種較為常用且具有代表性的名額,度量的是被測代碼中每個可執行語句是否被執行到。
  • 條件覆寫。它度量的是當代碼中存在分支時,是否能覆寫進入分支和不進入分支這兩種情況。這要求開發人員編寫多個測試用例以分别滿足進入分支與不進入分支這兩種情況。
  • 路徑覆寫。它度量的是當代碼中存在多個分支時,是否覆寫到分支之間不同組合方式所産生的全部路徑。這是一種力度最強的覆寫檢測,相對而言,條件覆寫隻是路徑覆寫中的一部分。

在這三種覆寫名額中,行覆寫簡單,适用性廣,但可能會被認為是“最弱的覆寫”,其實不然。行覆寫相對于條件或路徑覆寫,可以使開發人員通過盡可能少的測試資料和用例,覆寫盡可能多的代碼。通常情況下,是先通過工具檢測一遍整個工程單元測試的行覆寫情況,然後針對沒有被覆寫到的代碼,分析其沒有被覆寫到的原因。如果是由于該代碼所在分支由于不滿足進入該分支的條件而沒有被覆寫,那麼開發人員才會進一步修改或增加測試代碼,完成該部分的條件或路徑覆寫。

可見,高效高品質的行覆寫是有效進行條件覆寫與路徑覆寫的前提。行覆寫率越高,說明沒有被覆寫到的代碼越少,這樣開發人員便會集中精力修改測試用例,覆寫這些數量不多的代碼。相反,如果行覆寫率低,開發人員需要逐個檢查沒有被覆寫到的代碼,精力被分散,是以很難提高剩餘代碼單元測試的品質。

代碼覆寫率 = 被測代碼行數 / 參測代碼總行數 * 100%。 從代碼覆寫率的計算方式中可以看出,要提高代碼覆寫率,可通過提高被測代碼行數,或減少參測代碼總行數的方式進行。以下将會從這兩個角度分别入手,分析如何提高被測代碼行數及減少參測代碼總行數。

使用 Cobertura 統計并提高代碼的行覆寫率

Cobertura 是一款優秀的開源測試覆寫率統計工具,它與單元測試代碼結合,标記并分析在測試包運作時執行了哪些代碼和沒有執行哪些代碼以及所經過的條件分支,來測量測試覆寫率。除了找出未測試到的代碼并發現 bug 外,Cobertura 還可以通過标記無用的、執行不到的代碼來優化代碼,最終生成一份美觀詳盡的 HTML 覆寫率檢測報告。

Cobertura 基本工具包裡有四個基本過程及對應的工具:cobertura-check, cobertura-instrument, cobertura-merge, cobertura-report; 這個腳本獨立使用較為繁瑣,不友善也不利于自動化。不過, Cobertura 在 Maven 編譯平台上有相應的 cobertura-maven-plugin 插件,使代碼編譯、檢測、內建等各個周期可以流水線式自動化完成。

Cobertura-maven-plugin 官方版有五個主要目标指令 (goal),如表 1:

表 1. Cobertura 目标指令及作用解釋

目标指令 作用解釋
Cobertura:check 檢查最後一次标注(instrumentation) 正确與否
Cobertura:clean 清理插件生産的中間及最終報告檔案
Cobertura:dump-datafile Cobertura 資料檔案 dump 指令 , 不常用
Cobertura:instrument 标注編譯好的 javaclass 檔案
Cobertura:cobertura 标注、運作測試并産生 Cobertura 覆寫率報告

Cobertura 通常會與 Maven 一起使用。是以工程目錄結構如果遵循 Maven 推薦的标準的話,一個內建 Cobertura 的基本 POM 檔案如清單 1 所示:

清單 1. POM 檔案的基本結構

          Java  

1 2 3 4 5 6 7 8 9 10 11 12 <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 推薦标準,則需要進行如下額外設定:

清單 2. 适合 Maven 的工程目錄結構配置

          Java  

1 2 3 4 5 6 7 8 9 10 11 12 <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 所示。

圖 1. Cobertura 覆寫分析報告

使用 Cobertura 和反射機制提高 Java 單元測試中的代碼覆寫率

從以上報告中可見,

  • 代碼整體的行覆寫率并不高,有些包或類覆寫率很低,甚至為 0。考慮到這些包或類的特殊性(例如它們已被其他類取代),無需對它們進行單元測試,是以需要從整個測試範圍中剔除。
  • 部分類的行覆寫率雖然已接近 100%,但仍存在一些方法(如 set 和 get 方法)由于沒有測試的必要卻被列入了統計範圍,這些方法需要被過濾掉。

針對上述兩種改進措施,都可以使用 Cobertura 進行實作。第一種改進措施 Cobertura 可以支援,而第二改進措施則需要對 Cobertura 源碼進行修改,重編譯後方可支援。下面将詳細介紹如何使用 Cobertura 對上述問題進行優化。

過濾不需進行單元測試的包和類

針對項目中不需進行單元測試的包和類,我們可以利用 POM 檔案中 Cobertura 的标注 (instrument) 設定,對相應的包和類進行剔除 (exclude) 或篩選 (include),使之不展現在覆寫率報告中,去除它們對整個覆寫率的影響,進而使報告更具針對性。其基本 POM 标簽設定及解析如清單 3 中所示。

清單 3. POM 中剔除包和類的設定示例

            Java  

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <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 所示。

圖 2. 包、類過濾效果
使用 Cobertura 和反射機制提高 Java 單元測試中的代碼覆寫率

過濾類中的函數

最新版本中的 Cobertura 隻能支援到類級别的過濾,而對于類中方法的過濾是不支援的。是以我們需要通過修改 Cobertura 源碼,使 Cobertura 支援對類中方法的過濾。

對 Cobertura 及其插件改動所依據的主要原理是 : 修改 Cobertura-maven-plugin 項目中的 InstrumentationTask 類,增加 Ignoretrival,IgnoreMethod 等新增 POM 參數。配制正規表達式,修改 Cobertura 核心,在标注(instrumentation) 階段周遊函數名時,檢測函數名是否比對傳入的正規表達式,過濾函數體代碼,進而把這些函數代碼排除在代碼覆寫統計之外,節省開發人員對這類代碼的測試精力。

清單 4 至清單 6 是對 Cobertura 的幾處核心改動,僅供讀者參考。

清單 4. 對 Cobertura 核心代碼的改動之一

            Java  

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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 ;      } }
 清單 5. 對 Cobertura Maven plugin 的改動

          Java  

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 private String ignoreMethodAnnotation ; private String ignoreTrivial ; public ConfigInstrumentation ( ) public void setIgnoreMethodAnnotation ( String ignoreMethodAnnotation ) { this . ignoreMethodAnnotation = ignoreMethodAnnotation ; } public String getIgnoreTrivial ( ) { return ignoreTrivial ; } public void setIgnoreTrivial ( String ignoreTrivial ) { this . ignoreTrivial = ignoreTrivial ; }
清單 6. POM 檔案中使用修改後的 Cobertura 過濾類中的方法

以上修改都完成之後, 就可以運作“mvn:site”指令得到報告。圖 4 是使用沒有被修改的 Cobertura 産生的結果報告,無函數過濾效果。圖 5 是使用被修改後的 Cobertura 産生的結果報告,可以從中看出,幾個 set 與 get 方法已被排除在統計範圍之外。

圖 3. 無函數名過濾效果
使用 Cobertura 和反射機制提高 Java 單元測試中的代碼覆寫率
 圖 4. 增加函數過濾效果
使用 Cobertura 和反射機制提高 Java 單元測試中的代碼覆寫率

 利用 Java 反射(Reflection) 機制提高代碼的行覆寫率

不同的人對反射有不同的了解,大部分人認同的一種觀點是:反射使得程式可以檢查自身結構以及軟體環境,并且根據程式檢測到的實際情況改變行為。

為了實作自檢,一段程式需要有些資訊來表示自身,這些資訊就稱為中繼資料(metadata)。Java 運作過程中對這些中繼資料的自檢稱為内省(introspection)。内省過程之後往往進行行為改變。總的來說,反射 API 利用以下三種技術來實作行為改變:

  • 直接修改中繼資料。
  • 利用中繼資料進行操作。
  • 調解(Intercession), 代碼被允許在程式各種運作期進行調整。

Java 語言反射機制提供一組豐富的 API 函數來操作中繼資料,且提供了少部分重要的 API 來實作 Intercession 能力。

實際項目中,為了保證軟體代碼的整體品質,單元測試不僅要覆寫類的公有成員,還要覆寫重要的私有成員。而有些私有成員的調用,會被放入到極為複雜的條件分支中。而構造進入這個私有方法的相關條件,可能需要開發人員編寫大量測試代碼及測試資料。這無疑增加了單元測試的成本。有時為了節省成本,該類私有方法便跳過不測,進而在無形中降低了代碼的行覆寫率,影響了軟體的整體品質。

而利用反射的一系列特性,我們可以在不改變源代碼的情況下,直接對複雜的私有方法進行單元測試,無需增加行覆寫檢查中被覆寫的代碼行數,進而可以在不增加單元測試成本的前提下,提高代碼的行覆寫率與單元測試的整體品質。

清單 7 給出了一段簡單的目标測試代碼示例。

清單 7. 目标測試代碼示例

          Java  

1 2 3 4 5 6 7 8 9 10 11 12 13 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 的測試代碼為:

清單 8. 利用反射中繼資料操作 API 的測試代碼

            Java  

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31                            @ 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 所示。

圖 5. 非公有函數測試效果
使用 Cobertura 和反射機制提高 Java 單元測試中的代碼覆寫率

從圖中我們可以看到 Customer 成員的私有方法 sayHello 被測試代碼覆寫到。是以,當一些代碼函數複雜度過高,到通過構造測試資料或測試用例的方法很難使非公有成員得到運作時,我們就可以利用 Java 反射機制,直接在測試類中調用和測試目标類的非公有成員,進而提高覆寫率。

 結語

本文使用兩種方法,從兩個不同的角度對單元測試中的代碼覆寫率進行了增強。改進 Cobertura 來提高單元測試代碼覆寫率,主要從縮小參與測試的代碼總範圍的角度入手,适用于代碼總數龐大而被測代碼數量不多的情況。而使用 Java 反射機制提高單元測試代碼覆寫率,主要從提高被測代碼數量的角度入手,适用于被測代碼私有成員多且觸發條件苛刻的情況。針對項目中對單元測試的不同需求,選取合适的技術來增強單元測試,才能真正提高代碼以至項目的總體品質。

http://blog.jobbole.com/49717/