Jacoco Code Coverage

Java Jacoco Ant Maven

近期因工作需要,需對(duì)代碼覆蓋率進(jìn)行統(tǒng)計(jì),所以這篇就當(dāng)做對(duì)這段時(shí)間學(xué)習(xí)的總結(jié)。
總得來說網(wǎng)上找到的資料都不系統(tǒng),不適合新手理解和參考,下面我就以我一個(gè)小白的親身體驗(yàn),將我
踩到的那些坑和遇到的那些疑惑記錄下來
(作為一名初學(xué)者,文章中可能會(huì)有錯(cuò)誤或者理解偏差的地方,歡迎各位批評(píng)指正)

代碼覆蓋率工具調(diào)研信息如下:

  • 市場(chǎng)上主要代碼覆蓋率工具:
    • Emma
    • Cobertura
    • Jacoco
    • Clover(商用)

具體見下表:

工具 Jacoco Emma Cobertura
原理 使用 ASM 修改字節(jié)碼 修改 jar 文件,class 文件字節(jié)碼文件 基于 jcoverage,基于 asm 框架對(duì) class 文件插樁
覆蓋粒度 行,類,方法,指令,分支 行,類,方法,基本塊,指令,無分支覆蓋 項(xiàng)目,包,類,方法的語句覆蓋/分支覆蓋
插樁 on the fly、offline on the fly、offline offline,把統(tǒng)計(jì)代碼插入編譯好的class文件中
生成結(jié)果 在 Tomcat 的 catalina.sh 配置 javaangent 參數(shù),指出需要收集覆蓋率的文件,shutdown 時(shí)才收集,只能使用 kill 命令關(guān)閉 Tomcat,不要使用 kill -9 html、xml、txt,二進(jìn)制格式報(bào)表 html,xml
缺點(diǎn) 需要源代碼 1、需要 debug 版本,并打來 build.xml 中的 debug 編譯項(xiàng); 2、需要源代碼,且必須與插樁的代碼完全一致 1、不能捕獲測(cè)試用例中未考慮的異常; 2、關(guān)閉服務(wù)器才能輸出覆蓋率信息(已有修改源代碼的解決方案,定時(shí)輸出結(jié)果;輸出結(jié)果之前設(shè)置了 hook,會(huì)與某些服務(wù)器的 hook 沖突,web 測(cè)試中需要將 cobertura.ser 文件來回 copy
性能 小巧 插入的字節(jié)碼信息更多
執(zhí)行方式 maven,ant,命令行 命令行 maven,ant
Jenkins 集成 生成 html 報(bào)告,直接與 hudson 集成,展示報(bào)告,無趨勢(shì)圖 無法與 hudson 集成 有集成的插件,美觀的報(bào)告,有趨勢(shì)圖
報(bào)告實(shí)時(shí)性 默認(rèn)關(guān)閉,可以動(dòng)態(tài)從 jvm dump 出數(shù)據(jù) 可以不關(guān)閉服務(wù)器 默認(rèn)是在關(guān)閉服務(wù)器時(shí)才寫結(jié)果
維護(hù)狀態(tài) 持續(xù)更新中 停止維護(hù) 停止維護(hù)

Tip:Jacoco 也是 Emma 團(tuán)隊(duì)開發(fā)的



JaCoCo Java Code Coverage Library

Jacoco 是一個(gè)開源的覆蓋率工具。Jacoco 可以嵌入到 Ant 、Maven 中,并提供了 EclEmma Eclipse 插件,也可以使用 Java Agent 技術(shù)監(jiān)控 Java 程序。很多第三方的工具提供了對(duì) Jacoco 的集成,如:Sonar、Jenkins、IDEA.

Java Counters

Jacoco 包含了多種尺度的覆蓋率計(jì)數(shù)器,包含指令級(jí)(Instructions,C0 coverage),分支(Branches,C1 coverage)、圈復(fù)雜度(Cyclomatic Complexity)、行(Lines)、方法(Non-abstract Methods)、類(Classes)。

? Instructions:Jacoco 計(jì)算的最小單位就是字節(jié)碼指令。指令覆蓋率表明了在所有的指令中,哪些被執(zhí)行過以及哪些沒有被執(zhí)行。這項(xiàng)指數(shù)完全獨(dú)立于源碼格式并且在任何情況下有效,不需要類文件的調(diào)試信息。

? Branches:Jacoco 對(duì)所有的 if 和 switch 指令計(jì)算了分支覆蓋率。這項(xiàng)指標(biāo)會(huì)統(tǒng)計(jì)所有的分支數(shù)量,并同時(shí)支出哪些分支被執(zhí)行,哪些分支沒有被執(zhí)行。這項(xiàng)指標(biāo)也在任何情況都有效。異常處理不考慮在分支范圍內(nèi)。

      在有調(diào)試信息的情況下,分支點(diǎn)可以被映射到源碼中的每一行,并且被高亮表示。
      紅色鉆石:無覆蓋,沒有分支被執(zhí)行。
      黃色鉆石:部分覆蓋,部分分支被執(zhí)行。
      綠色鉆石:全覆蓋,所有分支被執(zhí)行。

? Cyclomatic Complexity:Jacoco 為每個(gè)非抽象方法計(jì)算圈復(fù)雜度,并也會(huì)計(jì)算每個(gè)類、包、組的復(fù)雜度。根據(jù) McCabe 1996 的定義,圈復(fù)雜度可以理解為覆蓋所有的可能情況最少使用的測(cè)試用例數(shù)。這項(xiàng)參數(shù)也在任何情況下有效。

? Lines:該項(xiàng)指數(shù)在有調(diào)試信息的情況下計(jì)算。

      因?yàn)槊恳恍写a可能會(huì)產(chǎn)生若干條字節(jié)碼指令,所以我們用三種不同狀態(tài)表示行覆蓋率
      紅色背景:無覆蓋,該行的所有指令均無執(zhí)行。
      黃色背景:部分覆蓋,該行部分指令被執(zhí)行。
      綠色背景:全覆蓋,該行所有指令被執(zhí)行。

? Methods:每一個(gè)非抽象方法都至少有一條指令。若一個(gè)方法至少被執(zhí)行了一條指令,就認(rèn)為它被執(zhí)行過。因?yàn)?Jacoco 直接對(duì)字節(jié)碼進(jìn)行操作,所以有些方法沒有在源碼顯示(比如某些構(gòu)造方法和由編譯器自動(dòng)生成的方法)也會(huì)被計(jì)入在內(nèi)。

? Classes:每個(gè)類中只要有一個(gè)方法被執(zhí)行,這個(gè)類就被認(rèn)定為被執(zhí)行。同 5 一樣,有些沒有在源碼聲明的方法被執(zhí)行,也認(rèn)定該類被執(zhí)行。

Jacoco 原理

參考資料:

  1. 淺談代碼覆蓋率
  2. Jacoco 的原理
  3. Java 代碼覆蓋率工具 JaCoCo 原理篇


好了,廢話不多說,咱們直奔主題,大家只要按照操作步驟執(zhí)行就可以

Jacoco 收集集成測(cè)試代碼覆蓋率

什么是集成測(cè)試?
  • 準(zhǔn)備工作

  • 第一步:將下載下來的 zip 包與 Tomcat 服務(wù)放在一臺(tái)機(jī)器上

  • 第二步:在 [yourTomcatPath]/bin/catalina.sh 添加 Jacoco 插件,指令如下??

     JAVA_OPTS="-javaagent:[yourPath/]jacocoagent.jar=includes=com.companyName.*,output=tcpserver,port=8044,address=100.44.44.144,append=true -Xverify:none"
    

    Tip:添加插件之前,須將的 Tomcat 服務(wù)停掉之后再添加,添加完之后,再啟動(dòng) Tomcat 服務(wù)

    參數(shù)說明:
       1. yourPath 是放 jacocoagent.jar 文件的目錄路徑;那么 `jacocoagent.jar` 這個(gè) `jar` 包的路徑就是在準(zhǔn)備工作里下載下來的 `zip` 包,解壓之后的 `lib` 目錄下,如:'/jacoco-0.7.9/lib/jacocoagent.jar'
       2. includes 是指要收集哪些類(注意不要僅寫包名,最后要寫.*),不寫的話默認(rèn)是*,會(huì)收集應(yīng)用服務(wù)上所有的類,包括服務(wù)器和其他中間件的類,一般要過濾(當(dāng)然如果你愿意寫*也完全沒有問題,如:`includes=com.*` or `includes=*`),如果要指定多個(gè)的話,即這樣寫 `includes=com.package.1:com.package.2`(切記指定多個(gè)時(shí)中間用英文 `:` 隔開);
       3. output 有 4 個(gè)值,分別是 file、tcpserver、tcpclient、mbean,默認(rèn)是 file。使用 file 的方式只有在停掉應(yīng)用服務(wù)的時(shí)候才能產(chǎn)生覆蓋率文件,而使用 tcpserver 的方式可以在不停止應(yīng)用服務(wù)的情況下下載覆蓋率文件,后面會(huì)介紹如何使用 dump 方法來得到覆蓋率文件。
       4. address 是 IP 地址,IP 就是 Tomcat 服務(wù)器的機(jī)器的 IP,至于是寫 `服務(wù)器本機(jī)的 IP` 還是寫 `127.0.0.1` 要看情況
           1) 如果是在 Tomcat 服務(wù)器上執(zhí)行 `ant dump` 的話,就直接寫 `address=127.0.0.1`
           2) 如果執(zhí)行 `ant dump` 不是在 Tomcat 服務(wù)器上執(zhí)行的,就得寫服務(wù)器本機(jī)的IP(切記)
       5. port 是端口(端口比較隨便,找個(gè)能用的端口就行,直接我為什么將端口寫成 `8044`,我的想法是 `BUG 死死` 與 `8044` 挺配的,所以就用它作為端口號(hào)了)
    (`address` 和 `port` 是使用 tcpserver 方式需要的 2 個(gè)參數(shù),也是執(zhí)行 ant dump 方法必須要用到的。)
       6. append 表示覆蓋率數(shù)據(jù)的追加方式,默認(rèn)為 true。客戶端在執(zhí)行 dump 操作時(shí),如果該 exec 覆蓋率文件已存在,那么該輪的覆蓋率數(shù)據(jù)會(huì)直接在文本末尾進(jìn)行追加,因此會(huì)導(dǎo)致覆蓋率數(shù)據(jù)文件越來越大。如果改為 false,則客戶端執(zhí)行 dump 操作時(shí)會(huì)直接清空原覆蓋率文件的內(nèi)容,保證該覆蓋率文件只有該輪的覆蓋率數(shù)據(jù)。
       7. `-Xverify:none`:這個(gè)參數(shù)是防止啟動(dòng)主程序異常才加的(非強(qiáng)制,可以不加)
    

    Tip:更多參數(shù)說明,請(qǐng)點(diǎn)擊 這里

啟動(dòng) Tomcat 服務(wù)之后,ps 一下,如果在 Tomcat 服務(wù)中有 jacocoagent 這個(gè)服務(wù)的話
那么恭喜你,你成功了!!!
  • 第三步:獲取報(bào)告 ant dump(也是就上文中提到的,特別提醒:這里使用 ant 命令,和你的代碼工程使用什么編譯工具編譯的沒有一點(diǎn)關(guān)系,不要混淆)
    build.xml 文件內(nèi)容如下??
<?xml version="1.0" encoding="UTF-8"?>
<project name="Jacoco" xmlns:jacoco="antlib:org.jacoco.ant" default="jacoco">   
    <property name="jacocoantPath" value="[yourPath/]jacocoant.jar"/>
    <property name="integrationJacocoexecPath" value="./jacoco-integration.exec"/>
    
    <taskdef uri="antlib:org.jacoco.ant" resource="org/jacoco/ant/antlib.xml">
      <classpath path="${jacocoantPath}" />
    </taskdef>
    
    <target name="dump">
        <jacoco:dump address="100.44.44.144" port="8044" reset="false" destfile="${integrationJacocoexecPath}" append="true"/>
    </target>
</project>
`.exec`:二進(jìn)制文件,Jacoco 就是根據(jù)這個(gè)文件生成最終的報(bào)告
`destfile`:是指生成的覆蓋率文件路徑

Tip:
build.xml 只需修改三個(gè)點(diǎn),就可以直接拿去用
第一個(gè)修改點(diǎn):補(bǔ)全 `jacocoant.jar` 路徑。(那么 `jacocoant.jar` 在哪?對(duì)于這個(gè)問題,或許會(huì)有疑問,當(dāng)然,如果細(xì)心的小伙伴就會(huì)很輕易的發(fā)現(xiàn) `jacocoant.jar` 的位置,其實(shí)也就在準(zhǔn)備工作中所下載的 `zip` 包里面,與 `jacocoagent.jar` 在同級(jí)目錄 `lib` 文件夾下)
第二個(gè)修改點(diǎn):修改 IP 地址(IP 須與 `catalina.sh` 中添加的一致)
第三個(gè)修改點(diǎn):修改端口號(hào)(與IP一樣,端口號(hào)須與 `catalina.sh` 中添加的一致)

Frequently Asked Questions:
雖然得到了集成測(cè)試的覆蓋率文件,但是需要應(yīng)用服務(wù)器上的類文件才能產(chǎn)出相應(yīng)的覆蓋率報(bào)告,如果類文件是其他 JVM 編譯的,產(chǎn)出的報(bào)告覆蓋率是 0%。
有 2 種方法可以得到覆蓋率文件所需的 class 文件:
1. 將應(yīng)用服務(wù)部署的包(ear 或 war 或 jar)包下載下來之后解壓,即可得到對(duì)應(yīng)的 class 文件;
2. 在前面做單元測(cè)試之后,可以將 class 文件打成一個(gè) zip 包,然后上傳到服務(wù)器,最后在需要的時(shí)候去服務(wù)器上取。

修改好了,那么我們來測(cè)試一下,終端進(jìn)入 build.xml 所在的目錄,執(zhí)行:ant dump 或者 ant dump -buildfile [yourPath/]build.xml

ant dump

成功之后,接下來就是 Jenkins 集成 jacoco 實(shí)現(xiàn)代碼覆蓋率,詳見:Jenkins + Jacoco
持續(xù)集成代碼覆蓋率

是不是只有上面的這一種方式呢?當(dāng)然不是!
第二種方式(不推薦):
JAVA_OPTS="-javaagent:[yourPath/]jacocoagent.jar=destfile=[storagePath/]jacoco.exec
同樣是加載 cataline.sh 文件中,除了獲取報(bào)告的方式上面的不一樣之前,其余步驟都一樣

獲取報(bào)告:
功能測(cè)試或者接口自動(dòng)化后,需要獲取報(bào)告的話,需關(guān)閉 Tomcat 獲取結(jié)果文件 `jacoco.exec`,使用 kill [PID],之后到你保存的路徑下就能看到 `jacoco.exec` 文件(切記不要使用 kill -9 [PID],否則不能生成結(jié)果)
不推薦這種方式的理由:如果使用這種方式的話,不好做持續(xù)集成,因?yàn)?jenkins 服務(wù)器基本上都是和部署代碼的服務(wù)器分開的,所以要從遠(yuǎn)程服務(wù)器取結(jié)果的話還是選擇上面的方式
Q:那現(xiàn)在可能又有同學(xué)會(huì)問,這個(gè)報(bào)告只能在 `Jenkins` 上面生成嗎?
A:當(dāng)然也可以在本地生成了,附上代碼,如下??
<?xml version="1.0" encoding="UTF-8"?>
<project name="Jacoco" xmlns:jacoco="antlib:org.jacoco.ant" default="jacoco">
    <!--Jacoco 的安裝路徑-->
  <property name="jacocoantPath" value="[yourPath/]jacocoant.jar"/>
  <!--最終生成 .exec 文件的路徑,Jacoco 就是根據(jù)這個(gè)文件生成最終的報(bào)告的-->
  <property name="jacocoexecPath" value="[yourPath/]jacoco.exec"/>
    <!--生成覆蓋率報(bào)告的路徑-->
  <property name="reportfolderPath" value="[storageReportPath]"/>
  <!--遠(yuǎn)程 Tomcat 服務(wù)的 ip 地址-->
  <property name="server_ip" value="100.44.44.144"/>
  <!--前面配置的遠(yuǎn)程 Tomcat 服務(wù)打開的端口,要跟上面配置的一樣-->
  <property name="server_port" value="8044"/>
  <!--源代碼路徑-->
  <property name="checkOrderSrcPath" value="[srcPath]" />
  <!--.class 文件路徑-->
  <property name="checkOrderClasspath" value="[classPath]" />

  <!--讓 ant 知道去哪兒找 Jacoco-->
  <taskdef uri="antlib:org.jacoco.ant" resource="org/jacoco/ant/antlib.xml">
      <classpath path="${jacocoantPath}" />
  </taskdef>

  <!--dump 任務(wù):
      根據(jù)前面配置的 ip 地址,和端口號(hào),
      訪問目標(biāo) Tomcat 服務(wù),并生成 .exec 文件。-->
  <target name="dump">
      <jacoco:dump address="${server_ip}" reset="false" destfile="${jacocoexecPath}" port="${server_port}" append="true"/>
  </target>
  
  <!--jacoco 任務(wù):
      根據(jù)前面配置的源代碼路徑和 .class 文件路徑,
      根據(jù) dump 后,生成的 .exec 文件,生成最終的 html 覆蓋率報(bào)告。-->
  <target name="report">
      <delete dir="${reportfolderPath}" />
      <mkdir dir="${reportfolderPath}" />
      
      <jacoco:report>
          <executiondata>
              <file file="${jacocoexecPath}" />
          </executiondata>
              
          <structure name="JaCoCo Report">
              <group name="Check Order related">           
                  <classfiles>
                      <fileset dir="${checkOrderClasspath}">
                          <!-- 過濾不必要的文件 -->
                          <exclude name="**/R.class"/>
                          <exclude name="**/R$*.class"/>
                          <exclude name="**/*$ViewInjector*.*"/>
                          <exclude name="**/BuildConfig.*"/>
                          <exclude name="**/Manifest*.*"/>
                      </fileset>
                  </classfiles>
                  <sourcefiles encoding="UTF-8">
                      <fileset dir="${checkOrderSrcPath}" />
                  </sourcefiles>
              </group>
          </structure>
          <html destdir="${reportfolderPath}" encoding="UTF-8" />
          <csv destfile="${reportfolderPath}/coverage-report.csv" encoding="UTF-8"/>
          <xml destfile="${reportfolderPath}/coverage-report.xml" encoding="UTF-8"/>         
      </jacoco:report>
  </target>
</project>


Jacoco 收集單元測(cè)試代碼覆蓋率

  • pom.xml 配置 plugin
           <plugin>
                <groupId>org.jacoco</groupId>
                <artifactId>jacoco-maven-plugin</artifactId>
                <version>0.7.7.201606060606</version>
                <configuration>
                    <!--指定生成 .exec 文件的存放位置-->
                    <destFile>target/coverage-reports/jacoco-unit.exec</destFile>
                    <!--Jacoco 是根據(jù) .exec 文件生成最終的報(bào)告,所以需指定 .exec 的存放路徑-->
                    <dataFile>target/coverage-reports/jacoco-unit.exec</dataFile>
                </configuration>
                <executions>
                    <execution>
                        <id>jacoco-initialize</id>
                        <goals>
                            <goal>prepare-agent</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>jacoco-site</id>
                        <phase>test</phase>
                        <goals>
                            <goal>report</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
Demo 工程下載
  • 下載之后解壓,直接進(jìn)入工程目錄,運(yùn)行 mvn test,接著你將看到如下圖所示的文件
    image.png

其中 jacoco-unit.exec 是二進(jìn)制文件,就不多說了,而 index.html 就是代碼覆蓋率報(bào)告,如下圖??

jacoco.xml
report
report
report
Tip:
綠色部分:完全覆蓋
黃色部分:條件覆蓋
紅色部分:未覆蓋
  • 合并集成測(cè)試代碼覆蓋率和單元測(cè)試代碼覆蓋率,build.xml 代碼如下??
<?xml version="1.0" encoding="UTF-8"?>
    <project name="Jacoco" xmlns:jacoco="antlib:org.jacoco.ant" default="jacoco">
    <property name="baseDir" value="[yourExecFilePath]"/>   
    <property name="jacocoantPath" value="[yourPath/]jacocoant.jar"/>
    <property name="allJacocoexecPath" value="./jacoco-all.exec"/>
    
    <taskdef uri="antlib:org.jacoco.ant" resource="org/jacoco/ant/antlib.xml">
      <classpath path="${jacocoantPath}" />
    </taskdef>

    <target name="merge">
        <jacoco:merge destfile="${allJacocoexecPath}">
        <fileset dir="${baseDir}" includes="*.exec"/>
        </jacoco:merge>
    </target>
</project>

只要將這份 build.xml 放在代碼的根目錄下,執(zhí)行 ant merge 就可將所有以 .exec 文件合并,重新生成名為 jacoco-all.exec 的二進(jìn)制文件,當(dāng)然也可以將文章中的兩份 build.xml 文件合并,代碼如下??

<?xml version="1.0" encoding="UTF-8"?>
<project name="Jacoco" xmlns:jacoco="antlib:org.jacoco.ant" default="jacoco">
    <property name="jacocoantPath" value="[yourpath/]jacocoant.jar"/>
    <property name="baseDir" value="[yourExecFilePath]"/>
    <property name="integrationJacocoexecPath" value="./jacoco-integration.exec"/>
    <property name="allJacocoexecPath" value="./jacoco-all.exec"/>
    
    <taskdef uri="antlib:org.jacoco.ant" resource="org/jacoco/ant/antlib.xml">
      <classpath path="${jacocoantPath}" />
    </taskdef>
    
    <target name="dump">
        <jacoco:dump address="100.44.44.144" port="8044" reset="true" destfile="${integrationJacocoexecPath}" append="false"/>
    </target>

    <target name="merge">
        <jacoco:merge destfile="${allJacocoexecPath}">
        <fileset dir="${baseDir}" includes="*.exec"/>
        </jacoco:merge>
    </target>
</project>
分別執(zhí)行:
    `ant dump` & `ant merge`
          or 
    `ant dump -buildfile [yourpath/]build.xml` & `ant merge -buildfile [yourpath/]build.xml`
這樣生成的代碼覆蓋率報(bào)告中既包含集成測(cè)試代碼覆蓋率,又包含單元測(cè)試代碼覆蓋率的報(bào)告

將 .exec 文件合并之后,參照上文中提到的 Jenkins + Jacoco 持續(xù)集成代碼覆蓋率 這篇文章,將它與 Jenkins 集成。當(dāng)然還可以借助于 Sonar 將靜態(tài)代碼檢查的數(shù)據(jù)與代碼覆蓋率同步到 SonarQube 平臺(tái),詳見:SonarQube & SonarQube Scanner

如果在閱讀或者實(shí)踐的過程中遇到什么問題,歡迎在下方評(píng)論
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容