tags: Java
前陣子使用 Jacoco 進(jìn)行代碼覆蓋率測(cè)試吸耿,由于項(xiàng)目特殊遇到了不少坑祠锣,網(wǎng)上搜到的教程感覺也不夠全面,特此記錄咽安。
所用到的工具軟件的版本信息如下
- Jacoco 版本:0.8.0
- Eclemma 版本:3.0.0
- Eclipse 版本:4.3
- JDK 版本:1.8
- ANT 版本:1.9
1. 工具介紹
JaCoCo伴网,即 Java Code Coverage,是一款開源的 Java 代碼覆蓋率統(tǒng)計(jì)工具妆棒。支持 Ant 澡腾、Maven、Gradle 等構(gòu)建工具糕珊,支持 Jenkins动分、Sonar 等持續(xù)集成工具,支持 Java Agent 技術(shù)遠(yuǎn)程監(jiān)控 Java 程序運(yùn)行情況红选,支持Eclipse澜公、IDEA等IDE,提供HTML喇肋,CSV 等格式的報(bào)表導(dǎo)出坟乾,輕量級(jí)實(shí)現(xiàn),對(duì)外部庫和系統(tǒng)資源的依賴性小蝶防,性能開銷小甚侣。
JaCoCo 支持從 JDK1.0 版本到 JDK1.8 版本 的 Java 類文件。但是间学,JaCoCo 工具所需的JRE 版本最小為 1.5饵筑。另外,1.6及以上版本的測(cè)試中的類文件必須包含有效的堆棧映射幀法瑟。
2. 入門使用
本文將以 tcpserver 模式遠(yuǎn)程獲取應(yīng)用覆蓋率肉津,通過 Ant 腳本執(zhí)行相關(guān)命令,在 Eclipse 上查看源碼覆蓋率情況德谅。
2.1 配置部署
先從官網(wǎng)獲取 Jacoco 的壓縮包, 將其上傳到你要進(jìn)行覆蓋率檢測(cè)的應(yīng)用所在的服務(wù)器上。在解壓后的 lib 目錄下找到 jacocoagent 染苛,將其路徑添加到 JAVA_OPTS 環(huán)境變量中(如果項(xiàng)目中用到了 Tomcat,也可以直接將其添加到 CATALINA_OPTS 的環(huán)境變量中主到,JAVA_OPTS 只是更通用而已)茶行。
如果是 Windows 系統(tǒng),將以下內(nèi)容追加到 JAVA_OPTS 環(huán)境變量登钥。
-javaagent:D:\jacoco-0.7.9\lib\jacocoagent.jar=includes=*,address=10.1.231.168,port=6300,output=tcpserver,append=true;%JAVA_OPTS%
如果是 Linux 系統(tǒng)畔师,可以直接編輯 .bash_profile
export JACOCO="-javaagent:/$your_path/jacocoagent.jar=includes=com.grgbanking.*,output=tcpserver,address=11.111.1.11,port=6300,append=true"
export JAVA_OPTS="$JACOCO":"$JAVA_OPTS"
其中常用選項(xiàng)的含義如下
- javaagent: 指定 jacocoagent 的路徑
- includes: 表示只對(duì)指定包下的類進(jìn)行覆蓋率注入分析,默認(rèn)為 *牧牢,示例中只分析 com.test 包的類看锉。
- output: 表示覆蓋率的輸出方式。在 tcpserver 模式下塔鳍,Jacoco 會(huì)在客戶端執(zhí)行 dump 操作時(shí)將目前收集獲取到的覆蓋率數(shù)據(jù)統(tǒng)一寫到指定的ip和端口伯铣。在 file 模式下,Jacoco 只會(huì)在JVM 終止的時(shí)候才將收集到的覆蓋率數(shù)據(jù)寫入到指定的 exec 文件里去轮纫。注意腔寡,不管是任何模式,應(yīng)用運(yùn)行過程中的臨時(shí)覆蓋率數(shù)據(jù)都是保存在服務(wù)端的內(nèi)存中的掌唾,因此對(duì)于 tcpserver 模式來說放前,如果 JVM 不小心終止了,那么在這個(gè)覆蓋率統(tǒng)計(jì)周期內(nèi)的覆蓋率數(shù)據(jù)都會(huì)丟失糯彬。還有一個(gè) tcpclient 模式則是以客戶端的形式啟動(dòng)凭语,由于目前沒有這個(gè)使用場(chǎng)景,這里不過多討論撩扒。
- address: 只限 tcpserver 與 tcpclient 使用叽粹,表示監(jiān)聽的應(yīng)用服務(wù)器IP地址或主機(jī)名∪匆ǎ可根據(jù)實(shí)際情況自由選擇虫几。
- port: 只限 tcpserver 與 tcpclient 使用,表示監(jiān)聽的應(yīng)用服務(wù)器的端口號(hào)挽拔,一般用默認(rèn)6300即可辆脸。
- 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ù)叨粘。
修改好以后啟動(dòng) Java 應(yīng)用,讀取 JAVA_OPTS 環(huán)境變量的信息瘤睹,Jacoco 被加載進(jìn)升敲。檢查下6300端口如果已監(jiān)聽,說明服務(wù)端 Jacoco 啟動(dòng)成功轰传。
2.2 數(shù)據(jù)獲取
在正常運(yùn)行過程中驴党,服務(wù)器端的 Jacoco 只是將獲取的覆蓋率數(shù)據(jù)保存到內(nèi)存中,我們還需要在客戶端上進(jìn)行操作才能將覆蓋率數(shù)據(jù) dump 到客戶端获茬。
Jacoco 為我們提供了 Ant港庄、Maven、CLI 等多種方式進(jìn)行操作恕曲,其中 CLI 方式唯一的用途就是可以用來執(zhí)行 execinfo 命令鹏氧,這個(gè)命令是 Ant 與 Maven 所沒有的,它可以將 exec 簡(jiǎn)單轉(zhuǎn)成文本格式方便你查看每個(gè)類的覆蓋率百分比码俩。Maven 與 Ant 大同小異,由于項(xiàng)目中使用 Ant 進(jìn)行構(gòu)建歼捏,下文中將以 Ant 為例講解稿存。
在使用 Ant 腳本獲取覆蓋率之前,我們需要先去官網(wǎng)下載好 Ant瓣履,注意安裝過程中要手動(dòng)勾選 “添加到環(huán)境變量” 的相關(guān)選項(xiàng),省得以后要自己添加练俐。
安裝好以后打開 cmd 輸入ant -version
袖迎,如果能顯示相關(guān)的版本信息例如 “ Apache Ant(TM) version 1.9.11 compiled on March 23 2018 ”,則說明 Ant 安裝成功腺晾。
雖然官方也提供了 Ant腳本燕锥,但較為簡(jiǎn)單,部分內(nèi)容沒有說明悯蝉,因此文末會(huì)附上我在項(xiàng)目中使用的完整腳本归形。
2.3 統(tǒng)計(jì)分析
對(duì)于不熟悉 Java 或者對(duì)項(xiàng)目目錄結(jié)構(gòu)不了解的朋友,往往會(huì)由于源碼和字節(jié)碼不匹配或者路徑錯(cuò)誤導(dǎo)致在結(jié)合源碼查看覆蓋率時(shí)反復(fù)折騰鼻由,跑半天不知道生成的 exec 到底有沒有統(tǒng)計(jì)到暇榴。這時(shí)候我們可以使用 CLI 中的 execinfo 命令厚棵,簡(jiǎn)單查看下 exec 文件中的覆蓋率是否為0。
java -jar D:\jacococli.jar execinfo E:\jacoco\igaps1008.exec
這種方式只能查看 exec 文件的概況蔼紧,要想結(jié)合源碼查看詳細(xì)的覆蓋率使用情況婆硬,我們還是需要花點(diǎn)時(shí)間,配置好源碼和字節(jié)碼奸例,這樣才能在 IDE 中查看源碼覆蓋率彬犯。
首先需要在 Eclipse 中安裝 Eclemma 插件,你可以使用 Eclipse 的 MarketPlace 在線安裝哩至,
也可以下載離線安裝包 eclemma-3.0.0.zip躏嚎,分別將里面的 features 和 plugins 文件夾里的 jar 包拷貝到 Eclipse 對(duì)應(yīng)的文件夾中,重啟 Eclipse 后如果有顯示覆蓋率圖標(biāo)或視圖就說明安裝成功了菩貌。
接著下載項(xiàng)目源碼并將項(xiàng)目導(dǎo)入到 Eclipse 中
注意導(dǎo)入前取消 Eclipse 中的自動(dòng)編譯(即 Project - build automatically )卢佣, 然后拷貝服務(wù)器上的字節(jié)碼文件到這個(gè)項(xiàng)目的編譯輸出文件夾中。例如這個(gè)項(xiàng)目的編譯輸出文件夾為根目錄下bin目錄箭阶,那么就把字節(jié)碼文件都拷貝到這個(gè)目錄下虚茶,到這里我們的項(xiàng)目就準(zhǔn)備好了。
在 Eclipse 控制臺(tái) Coverage 視圖窗口的空白位置仇参,右鍵--Import Session嘹叫,在 Coverage Session 窗口,選擇第三個(gè)代理模式诈乒,Agent address 填寫需要監(jiān)控覆蓋率的遠(yuǎn)程服務(wù)器地址罩扇。點(diǎn)擊下一步后,選擇需要查看覆蓋率的源碼怕磨,一般不需要勾選include binary libraries喂饥,再點(diǎn)擊Finish即可查看覆蓋率。
3. 注意事項(xiàng)
官方文檔才是王道
強(qiáng)烈建議在使用 Jacoco 之前閱讀官方文檔肠鲫,雖然是英文员帮,但是內(nèi)容也很簡(jiǎn)單,花1個(gè)小時(shí)大概瀏覽下能對(duì) Jacoco 有個(gè)系統(tǒng)性的了解导饲。這里對(duì) Jacoco 的官方部分 FAQ 進(jìn)行了簡(jiǎn)單翻譯捞高,同時(shí)加入了部分自己在使用過程中遇到的坑。源代碼沒有覆蓋率高亮問題
必須確保使用調(diào)試信息編譯類文件以包含行號(hào)渣锦,如果使用 Ant 編譯腳本硝岗,則需要檢查腳本中 javac 相關(guān)部分是否沒有設(shè)置 debug=true。
源文件必須在報(bào)表生成時(shí)正確提供袋毙。即指定的源文件夾必須是定義Java包的文件夾的直接父級(jí)辈讶。覆蓋率統(tǒng)計(jì)偏差
既然 Jacoco 是依據(jù) class 文件進(jìn)行覆蓋率的統(tǒng)計(jì),那么在用 EclEmma 合并會(huì)話數(shù)據(jù)時(shí)娄猫,應(yīng)該保證多個(gè)會(huì)話的所測(cè)試 class 文件字節(jié)碼內(nèi)容是相同的贱除,即多次測(cè)試過程中被測(cè)試 Java 類的源文件沒有被修改并且重新編譯過生闲。所以在 Eclipse 中,測(cè)試用例開始執(zhí)行執(zhí)行后月幌,應(yīng)該保證 Testee 源文件不被改動(dòng)碍讯。如果修改了被測(cè)試源文件并保存( Eclipse 會(huì)自動(dòng)重新編譯),請(qǐng)將之前的所有測(cè)試用例重新以 Coverage As 模式執(zhí)行一般扯躺,否則合并后的覆蓋率測(cè)試數(shù)據(jù)會(huì)有誤差捉兴。
另外,由于 JaCoCo 分析統(tǒng)計(jì)的是編譯后的 class 文件中字節(jié)碼指令的執(zhí)行情況录语。例如某源文件中有一個(gè)靜態(tài)的方法 someMethod倍啥,但是在編譯時(shí) Javac 會(huì)自動(dòng)為我們的類生成一個(gè)構(gòu)造方法(本例中沒有提供非空的構(gòu)造方法),所以這個(gè)類同時(shí)有 someMethod 和一個(gè)構(gòu)造方法澎埠。由于在執(zhí)行靜態(tài)方法過程中沒有調(diào)用到構(gòu)造函數(shù)虽缕,所以會(huì)顯示覆蓋率不是100%Android應(yīng)用使用覆蓋率
由于Android不能通過JVM停止后自動(dòng)dump覆蓋率數(shù)據(jù),因此當(dāng)Android應(yīng)用進(jìn)程不存在或停止的時(shí)候蒲稳,覆蓋率數(shù)據(jù)不會(huì)生成氮趋。要想獲得Android應(yīng)用的覆蓋率,江耀,必須使用離線插樁模式進(jìn)行覆蓋率分析多源文件目錄
Ant 腳本執(zhí)行起來很方便剩胁,但如果要執(zhí)行 report 命令則需要注意,如果該應(yīng)用編譯后的class 或 jar 分別在幾個(gè)不同的目錄下祥国,那么就需要分別在 Ant 腳本中指定 group昵观,同時(shí)每個(gè) group 也都要指定源文件 sourcefiles 以及 編譯后的類文件 classfiles。同樣的舌稀,如果
項(xiàng)目的源碼存放目錄也沒有統(tǒng)一的入口啊犬,那也需要在一個(gè) sourcefiles 中指定多個(gè) fileset,就如本腳本中分別指定了<fileset dir="${JacocoClassPath}/lib/core"/>
和<fileset dir="${JacocoClassPath}/project"/>
這2個(gè) classfiles 一樣扩借。Eclipse中導(dǎo)入覆蓋率數(shù)據(jù)時(shí)出錯(cuò)
如果在Eclipse的Eclemma插件中導(dǎo)入exec文件時(shí)彈窗椒惨,提示 “Error while loading coverage session (code 5001).”
一般是因?yàn)閑clipse 中導(dǎo)入的項(xiàng)目編譯輸出文件夾目錄結(jié)構(gòu)不合法導(dǎo)致缤至,同時(shí) class 文件必須是從服務(wù)器中獲取的潮罪,不能使用 eclipse 自己的編譯器編譯的 class。由于 Eclipse 默認(rèn)會(huì)開啟自動(dòng)編譯领斥,所以哪怕你沒有手動(dòng)編譯嫉到,在你導(dǎo)入項(xiàng)目的時(shí)候 Eclipse 已經(jīng)幫你編譯了一次了。這里必須刪掉編譯后的 class 然后重新拷貝一份服務(wù)器上的 class 文件
4. 技術(shù)原理
運(yùn)行時(shí)分析 (Runtime Profilling) 技術(shù) 在 PureCoverage 中有使用月洛,他就是通過 JVMTI 來監(jiān)聽 JVM 的相關(guān)事件進(jìn)行覆蓋率數(shù)據(jù)收集何恶,而 Jacoco 則是使用字節(jié)碼注入(Byte Code Instrumentation)的方式,使用 ASM 庫在字節(jié)碼中插入 Probe 探針嚼黔,通過統(tǒng)計(jì)運(yùn)行時(shí)探針的覆蓋情況來統(tǒng)計(jì)覆蓋率信息细层。
On-the-fly 模式:
JVM 中通過 javaagent 參數(shù)指定特定的 jar 文件啟動(dòng) Instrumentation 的代理程序惜辑,代理程序在通過 Class Loader 裝載一個(gè) class 前判斷是否轉(zhuǎn)換修改 class文件,將統(tǒng)計(jì)代碼插入 class疫赎,測(cè)試覆蓋率分析可以在 JVM 執(zhí)行測(cè)試代碼的過程中完成盛撑。
Offline 模式:
在測(cè)試前先對(duì)文件進(jìn)行插樁,然后生成插過樁的 class 或 jar 包捧搞,測(cè)試插過樁的 class 和 jar 包后抵卫,會(huì)生成動(dòng)態(tài)覆蓋信息到文件,最后統(tǒng)一對(duì)覆蓋信息進(jìn)行處理胎撇,并生成報(bào)告介粘。
存在如下情況不適合 on-the-fly,需要采用 offline 提前對(duì)字節(jié)碼插樁:
- 運(yùn)行環(huán)境不支持 java agent晚树。
- 部署環(huán)境不允許設(shè)置 JVM 參數(shù)姻采。
- 字節(jié)碼需要被轉(zhuǎn)換成其他的虛擬機(jī)如 Android Dalvik VM。
- 動(dòng)態(tài)修改字節(jié)碼過程中和其他 agent 沖突题涨。
- 無法自定義用戶加載類偎谁。
5. Ant 腳本
<?xml version="1.0" encoding="UTF-8" ?>
<project default="report" basedir="." xmlns:jacoco="antlib:org.jacoco.ant">
<!-- 定義 Jacoco 相關(guān)變量和庫路徑 -->
<property name="JacocoIP" value="192.168.22.33"/>
<property name="JacocoPort" value="6300" />
<property name="JacocoExec" value="./jacoco/merge-0608.exec" />
<property name="JacocoReport" value="./jacoco/igaps-report.zip" />
<property name="JacocoSrcPath" value="."/>
<property name="JacocoClassPath" value="./igaps/apps"/>
<property name="Encoding" value="UTF-8"/>
<taskdef uri="antlib:org.jacoco.ant" resource="org/jacoco/ant/antlib.xml">
<classpath path="E:/jacoco/lib/jacocoant.jar"/>
</taskdef>
<!-- 1 獲取覆蓋率exec文件 -->
<target name="dump">
<jacoco:dump address="${JacocoIP}" port="${JacocoPort}" reset="false" append="true" destfile="${JacocoExec}" />
</target>
<!-- 2 合并exec文件 -->
<!-- 獲取指定目錄下的所有 exec 文件并將數(shù)據(jù)合并為一個(gè)exec -->
<target name="merge">
<jacoco:merge destfile="./jacoco/merge.exec">
<fileset dir="./jacoco/all" includes="*.exec" />
</jacoco:merge>
</target>
<!-- 3 生成覆蓋率報(bào)告 -->
<target name="report">
<jacoco:report>
<executiondata>
<file file="${JacocoExec}" />
</executiondata>
<structure name="JaCoCo Report">
<group name="Core">
<classfiles>
<fileset dir="${JacocoClassPath}/lib/core" />
</classfiles>
<sourcefiles encoding="${Encoding}">
<fileset dir="${JacocoSrcPath}/src/monitor/timeout"/>
<fileset dir="${JacocoSrcPath}/src/tools/utils"/>
<fileset dir="${JacocoSrcPath}/src/redis/link"/>
<fileset dir="${JacocoSrcPath}/src/redis/util"/>
<fileset dir="${JacocoSrcPath}/src/server/init"/>
<fileset dir="${JacocoSrcPath}/src/grgbpm/core"/>
<fileset dir="${JacocoSrcPath}/src/grgbpm/handler"/>
<fileset dir="${JacocoSrcPath}/src/server/core"/>
<fileset dir="${JacocoSrcPath}/src/server/backend"/>
<fileset dir="${JacocoSrcPath}/src/server/exception"/>
<fileset dir="${JacocoSrcPath}/src/server/audit"/>
<fileset dir="${JacocoSrcPath}/src/server/dao"/>
<fileset dir="${JacocoSrcPath}/src/server/log"/>
<fileset dir="${JacocoSrcPath}/src/server/reload"/>
<fileset dir="${JacocoSrcPath}/src/server/business"/>
<fileset dir="${JacocoSrcPath}/src/component/service/http"/>
<fileset dir="${JacocoSrcPath}/src/component/service/https"/>
<fileset dir="${JacocoSrcPath}/src/component/service/webservice"/>
<fileset dir="${JacocoSrcPath}/src/component/unpack/separativesign"/>
<fileset dir="${JacocoSrcPath}/src/component/pack/separativesign"/>
<fileset dir="${JacocoSrcPath}/src/component/unpack/struct"/>
<fileset dir="${JacocoSrcPath}/src/component/pack/iso8583"/>
<fileset dir="${JacocoSrcPath}/src/component/pack/struct"/>
<fileset dir="${JacocoSrcPath}/src/component/unpack/xml"/>
<fileset dir="${JacocoSrcPath}/src/component/pack/xml"/>
<fileset dir="${JacocoSrcPath}/src/component/unpack/iso8583"/>
<fileset dir="${JacocoSrcPath}/src/component/service/tcp"/>
<fileset dir="${JacocoSrcPath}/src/component/communicate/ftp"/>
<fileset dir="${JacocoSrcPath}/src/component/communicate/http"/>
<fileset dir="${JacocoSrcPath}/src/component/communicate/https"/>
<fileset dir="${JacocoSrcPath}/src/component/communicate/webservice"/>
<fileset dir="${JacocoSrcPath}/src/component/communicate/tcp"/>
<fileset dir="${JacocoSrcPath}/src/component/timeout"/>
<fileset dir="${JacocoSrcPath}/src/component/endflow"/>
<fileset dir="${JacocoSrcPath}/src/component/logic"/>
<fileset dir="${JacocoSrcPath}/src/component/encryptor"/>
<fileset dir="${JacocoSrcPath}/src/component/judge"/>
<fileset dir="${JacocoSrcPath}/src/component/option"/>
<fileset dir="${JacocoSrcPath}/src/component/startflow"/>
<fileset dir="${JacocoSrcPath}/src/component/format"/>
</sourcefiles>
</group>
<group name="Project">
<classfiles>
<fileset dir="${JacocoClassPath}/project"/>
</classfiles>
<sourcefiles encoding="${Encoding}">
<fileset dir="${JacocoSrcPath}/src/project">
<exclude name="config/**" />
</fileset>
</sourcefiles>
</group>
</structure>
<html destfile="${JacocoReport}" encoding="${Encoding}" footer="${ReportFooter}"/>
</jacoco:report>
</target>
</project>