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)指正)
- 針對(duì) Jacoco
+
Jenkins+
SonarQube&
SonarQube Scanner 分為四個(gè)部分寫的,建議閱讀的順序?yàn)椋?- Jacoco Code Coverage ?
- Jenkins + Jacoco 持續(xù)集成代碼覆蓋率
- SonarQube & SonarQube Scanner
- Jenkins + SonarQube & SonarQube Scanner
代碼覆蓋率工具調(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 原理
參考資料:
好了,廢話不多說,咱們直奔主題,大家只要按照操作步驟執(zhí)行就可以
Jacoco 收集集成測(cè)試代碼覆蓋率
什么是集成測(cè)試?
-
準(zhǔn)備工作
- 下載 jacoco.zip 包
第一步:將下載下來的 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
成功之后,接下來就是 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)告,如下圖??
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)論