一、Why
隨着敏捷開發的流行,版本快速迭代,開發人員由於時間緊迫,在一定程度上也會造成送測代碼質量降低,因此編寫單元測試已經成為業界共識,良好的單元測試不僅能提升編碼質量,也能在整個測試周期的最開始階段減少很大一部分的缺陷,但如何來度量保證單元測試的質量呢?相比單純追求單元測試用例的數量,分析單元測試的代碼覆蓋率是一種更為可行的方式。JaCoCo(Java Code Coverage)就是一種分析單元測試覆蓋率的工具,使用它運行單元測試后,可以給出代碼中哪些部分被單元測試測到,哪些部分沒有沒測到,並且給出整個項目的單元測試覆蓋情況百分比,看上去一目了然。
二、What
Jacoco 是一個開源的覆蓋率工具。Jacoco 可以嵌入到 Ant 、Maven 中,並提供了 EclEmma Eclipse 插件,也可以使用 Java Agent 技術監控 Java 程序。很多第三方的工具提供了對 Jacoco 的集成,如:Sonar、Jenkins、IDEA。
Jacoco 包含了多種尺度的覆蓋率計數器,包含指令級(Instructions,C0 coverage),分支(Branches,C1 coverage)、圈復雜度(Cyclomatic Complexity)、行(Lines)、方法(Non-abstract Methods)、類(Classes)
→ Instructions:Jacoco 計算的最小單位就是字節碼指令。指令覆蓋率表明了在所有的指令中,哪些被執行過以及哪些沒有被執行。這項指數完全獨立於源碼格式並且在任何情況下有效,不需要類文件的調試信息。
→ Branches:Jacoco 對所有的 if 和 switch 指令計算了分支覆蓋率。這項指標會統計所有的分支數量,並同時支出哪些分支被執行,哪些分支沒有被執行。這項指標也在任何情況都有效。異常處理不考慮在分支范圍內。
在有調試信息的情況下,分支點可以被映射到源碼中的每一行,並且被高亮表示。
紅色鑽石:無覆蓋,沒有分支被執行。
黃色鑽石:部分覆蓋,部分分支被執行。
綠色鑽石:全覆蓋,所有分支被執行。
→ Cyclomatic Complexity:Jacoco 為每個非抽象方法計算圈復雜度,並也會計算每個類、包、組的復雜度。根據 McCabe 1996 的定義,圈復雜度可以理解為覆蓋所有的可能情況最少使用的測試用例數。這項參數也在任何情況下有效。
→ Lines:該項指數在有調試信息的情況下計算。
因為每一行代碼可能會產生若干條字節碼指令,所以我們用三種不同狀態表示行覆蓋率
紅色背景:無覆蓋,該行的所有指令均無執行。
黃色背景:部分覆蓋,該行部分指令被執行。
綠色背景:全覆蓋,該行所有指令被執行。
→ Methods:每一個非抽象方法都至少有一條指令。若一個方法至少被執行了一條指令,就認為它被執行過。因為 Jacoco 直接對字節碼進行操作,所以有些方法沒有在源碼顯示(比如某些構造方法和由編譯器自動生成的方法)也會被計入在內。
→ Classes:每個類中只要有一個方法被執行,這個類就被認定為被執行。有些沒有在源碼聲明的方法被執行,也認定該類被執行。
三、Where
如何快速了解代碼覆蓋率以及jacoco
2.Java 覆蓋率 Jacoco 插樁的不同形式總結和踩坑記錄
四、How
覆蓋率工具工作流程
1. 對Java字節碼進行插樁,On-The-Fly和Offine兩種方式。
2. 執行測試用例,收集程序執行軌跡信息,將其dump到內存。
3. 數據處理器結合程序執行軌跡信息和代碼結構信息分析生成代碼覆蓋率報告。
4. 將代碼覆蓋率報告圖形化展示出來,如html、xml等文件格式。
插樁原理
主流代碼覆蓋率工具都采用字節碼插樁模式,通過鈎子的方式來記錄代碼執行軌跡信息。其中字節碼插樁又分為兩種模式On-The-Fly和Offine。On-The-Fly模式優點在於無需修改源代碼,可以在系統不停機的情況下,實時收集代碼覆蓋率信息。Offine模式優點在於系統啟動不需要額外開啟代理,但是只能在系統停機的情況下才能獲取代碼覆蓋率。 基於以上特性,同時由於公司使用JDK8,我們采用Jacoco來獲取集成測試代碼覆蓋率,單元測試使用Cobertura。
On-The-Fly插樁 Java Agent
- JVM中通過-javaagent參數指定特定的jar文件啟動Instrumentation的代理程序
- 代理程序在每裝載一個class文件前判斷是否已經轉換修改了該文件,如果沒有則需要將探針插入class文件中。
- 代碼覆蓋率就可以在JVM執行代碼的時候實時獲取。
- 典型代表:Jacoco
On-The-Fly插樁 Class Loader
- 自定義classloader實現自己的類裝載策略,在類加載之前將探針插入class文件中
- 典型代表:Emma
Offine插樁
- 在測試之前先對文件進行插樁,生成插過樁的class文件或者jar包,執行插過樁的class文件或者jar包之后,會生成覆蓋率信息到文件,最后統一對覆蓋率信息進行處理,並生成報告。
- Offline插樁又分為兩種:
- Replace:修改字節碼生成新的class文件
- Inject:在原有字節碼文件上進行修改
- 典型代表:Cobertura
On-The-Fly和Offine比較
- On-The-Fly模式更加方便的獲取代碼覆蓋率,無需提前進行字節碼插樁,可以實時獲取代碼覆蓋率信息
- Offline模式適用於以下場景:
- 運行環境不支持java agent
- 部署環境不允許設置JVM參數
- 字節碼需要被轉換成其他虛擬機字節碼,如Android Dalvik VM
- 動態修改字節碼過程中和其他agent沖突
- 無法自定義用戶加載類
五、示例
本文將通過idea maven插件啟動的方式、war包ant構建的方式逐一演示jacoco是如何進行代碼覆蓋率采集的。在此之前運行環境准備工作有:
- jdk 1.8,且配置好環境變量
- maven, 且配置好環境變量,且保證能從遠程倉庫拉取dependency
- ant,且配置好環境變量
- tomcat 8
- jenkins,且保證插件能正常安裝,通常能直接訪問外網的情況下是能正常安裝的
- idea
A.idea+maven+testng
Step 1:創建maven工程,並在pom.xml中配置maven插件和testng依賴
<dependencies>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>6.11</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.7.9</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>prepare-package</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
</plugins>
</build>
Step 2:編寫示例代碼,以及單元測試代碼
可以看到編寫單元測試用例對示例的Count類的Add和Sub方法進行了調用,而Login類的login方法無任何地方調用
示例項目的目錄結構
示例代碼
Count.java
public class Count {
public int add(int x, int y){
return x + y;
}
public int sub(int x, int y){
return x - y;
}
}
login.java
public class Login {
public int login(String username, String password){
if("123".equals(username) && "123".equals(password)){
return 1;
} else{
return 0;
}
}
}
CountTest.java
import org.testng.Assert;
import org.testng.annotations.Test;
public class CountTest {
@Test
public void testAdd(){
Count count = new Count();
Assert.assertEquals(count.add(1, 2), 3);
}
@Test
public void testSub(){
Count count = new Count();
Assert.assertEquals(count.sub(2, 1), 1);
}
}
Step 3:編譯
1.將idea切換至控制台窗口
2.輸入命令:mvn install,如圖構建成功
3.稍等片刻,進入/target/site/jacoco/jacoco-resources/index.html查看匯總報告
Step 4:查看代碼覆蓋率檢測報告
打開/target/site/jacoco/jacoco-resources/index.html,我們可以看到覆蓋率計數器,包含指令級(Instructions,C0 coverage),分支(Branches,C1 coverage)、圈復雜度(Cyclomatic Complexity)、行(Lines)、方法(Non-abstract Methods)、類(Classes),綠色代表覆蓋,紅色代表未覆蓋
可查看具體的分支覆蓋情況
B.war包+tomcat+ant
Step 1:創建一個java web工程,並打包成war包
1.編寫一個示例servlet
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/TestServlet")
public class TestServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("123");
if (1==1){
System.out.println("1==1");
}
response.getWriter().println("123");
}
}
2.打包成war包(具體如何打包請移步百度)
Step 2:配置tomcat catalina.bat啟動參數,並啟動web項目,注意%TOMCAT_HOME%值得是tomcat目錄,根據你的安裝目錄替換
1.下載jacoco.zip,jacoco.zip
2.將Step 1中的war包移動至%TOMCAT_HOME%\webapps目錄下,屆時啟動后tomcat將自行解包,另外默認啟動端口為8080,若不想被占用,可進入%TOMCAT_HOME%\conf\server.xml中修改
3.打開%TOMCAT_HOME%\bin\catalina.bat,添加set "JAVA_OPTS=%JAVA_OPTS% -javaagent:D:\jacoco\lib\jacocoagent.jar=includes=*,output=tcpserver,port=7777,address=192.168.2.133",如果你不知道加在哪,請搜索set "JAVA_OPTS=%JAVA_OPTS% -Djava.protocol.handler.pkgs=org.apache.catalina.webresources"追加至后面就行
- 啟動參數的含義:
-
jacocoAgentPath:
是放jacocoagent.jar文件的目錄路徑;那么包的路徑就是在准備工作里下載下來的zip包,解壓之后的lib目錄下,如:D:\jacoco\lib\jacocoagent.jar -
includes:是指要收集哪些類(注意不要光寫包名,最后要寫.*),不寫的話默認是*,會收集應用服務上所有的類,包括服務器和其他中間件的類,一般要過濾(當然如果你願意寫*也完全沒有問題,如:`includes=com.*` or `includes=*`)
-
output:有4個值,分別是file、tcpserver、tcpclient、mbean,默認是 file。使用 file 的方式只有在停掉應用服務的時候才能產生覆蓋率文件,而使用 tcpserver 的方式可以在不停止應用服務的情況下下載覆蓋率文件,后面會介紹如何使用 dump 方法來得到覆蓋率文件
-
address:ip地址,就是tomcat 服務器的機器的IP
-
port:端口地址
4.啟動%TOMCAT_HOME%\bin\startup.bat,並確保窗口不會關閉
5.瀏覽器打開http://localhost:8080/untitled_war exploded2 archive/TestServlet,可正常訪問
Step 3:配置jacoco ant dump文件build_dump.xml

<?xml version="1.0" ?> <project name="coverage" default="dump" xmlns:jacoco="antlib:org.jacoco.ant" > <!--Jacoco的安裝路徑--> <property name="jacocoantPath" value="D:\jacoco\lib\jacocoant.jar"/> <!--最終生成.exec文件的路徑,Jacoco就是根據這個文件生成最終的報告的--> <property name="jacocoexecPath" value="D:\jacoco_output\jacoco.exec"/> <!--生成覆蓋率報告的路徑--> <property name="reportfolderPath" value="D:\jacoco_output\report\"/> <!--遠程tomcat服務的ip地址--> <property name="server_ip" value="192.168.2.133"/> <!--前面配置的遠程tomcat服務打開的端口,要跟上面配置的一樣--> <property name="server_port" value="7777"/> <!--源代碼路徑可以包含多個源代碼 <property name="webSrcpath" value="D://jacoco_output//service//src//main//java//" />--> <property name="webSrcpath" value="C:\Users\10147\IdeaProjects\untitled\src\" /> <!--.class文件路徑可以包含多個,class文件要填寫部署在服務器上的路徑,jar包要解壓>--> <property name="webClasspath" value="D:\tomcat8\apache-tomcat-8.5.54\apache-tomcat-8.5.54\webapps\untitled_war exploded2 archive\WEB-INF\classes" /> <taskdef uri="antlib:org.jacoco.ant" resource="org/jacoco/ant/antlib.xml"> <classpath path="${jacocoantPath}" /> </taskdef> <!--dump任務: 根據前面配置的ip地址,和端口號, 訪問目標tomcat服務,並生成.exec文件。--> <target name="dump"> <jacoco:dump address="${server_ip}" reset="false" destfile="${jacocoexecPath}" port="${server_port}" append="true"/> </target> <!--jacoco任務: 根據前面配置的源代碼路徑和.class文件路徑, 根據dump后,生成的.exec文件,生成最終的html覆蓋率報告。--> <target name="report"> <delete dir="${reportfolderPath}" /> <mkdir dir="${reportfolderPath}" /> <jacoco:report> <executiondata> <file file="${jacocoexecPath}" /> </executiondata> <structure name="JaCoCo Report"> <group name="Launch related"> <classfiles> <fileset dir="${webClasspath}" /> </classfiles> <sourcefiles encoding="gbk"> <fileset dir="${webSrcpath}" /> </sourcefiles> </group> </structure> <html destdir="${reportfolderPath}" encoding="utf-8" /> </jacoco:report> </target> </project>

<?xml version="1.0" ?> <project name="coverage" default="report" xmlns:jacoco="antlib:org.jacoco.ant" > <!--Jacoco的安裝路徑--> <property name="jacocoantPath" value="D:\jacoco\lib\jacocoant.jar"/> <!--最終生成.exec文件的路徑,Jacoco就是根據這個文件生成最終的報告的--> <property name="jacocoexecPath" value="D:\jacoco_output\jacoco.exec"/> <!--生成覆蓋率報告的路徑--> <property name="reportfolderPath" value="D:\jacoco_output\report\"/> <!--遠程tomcat服務的ip地址--> <property name="server_ip" value="192.168.2.133"/> <!--前面配置的遠程tomcat服務打開的端口,要跟上面配置的一樣--> <property name="server_port" value="7777"/> <!--源代碼路徑可以包含多個源代碼 <property name="webSrcpath" value="D://jacoco_output//service//src//main//java//" />--> <property name="webSrcpath" value="C:\Users\10147\IdeaProjects\untitled\src\" /> <!--.class文件路徑可以包含多個,class文件要填寫部署在服務器上的路徑,jar包要解壓>--> <property name="webClasspath" value="D:\tomcat8\apache-tomcat-8.5.54\apache-tomcat-8.5.54\webapps\untitled_war exploded2 archive\WEB-INF\classes" /> <taskdef uri="antlib:org.jacoco.ant" resource="org/jacoco/ant/antlib.xml"> <classpath path="${jacocoantPath}" /> </taskdef> <!--dump任務: 根據前面配置的ip地址,和端口號, 訪問目標tomcat服務,並生成.exec文件。--> <target name="dump"> <jacoco:dump address="${server_ip}" reset="false" destfile="${jacocoexecPath}" port="${server_port}" append="true"/> </target> <!--jacoco任務: 根據前面配置的源代碼路徑和.class文件路徑, 根據dump后,生成的.exec文件,生成最終的html覆蓋率報告。--> <target name="report"> <delete dir="${reportfolderPath}" /> <mkdir dir="${reportfolderPath}" /> <jacoco:report> <executiondata> <file file="${jacocoexecPath}" /> </executiondata> <structure name="JaCoCo Report"> <group name="Launch related"> <classfiles> <fileset dir="${webClasspath}" /> </classfiles> <sourcefiles encoding="gbk"> <fileset dir="${webSrcpath}" /> </sourcefiles> </group> </structure> <html destdir="${reportfolderPath}" encoding="utf-8" /> </jacoco:report> </target> </project>
build_dump.xml和build_report.xml,除了project標簽下的default屬性不一致,因此屬性不會再贅述
- jacocoantPath:即jacoco.zip解壓后的jacocoant.jar目錄
- jacocoexecPath:執行dump操作后生成exec文件的目錄
- reportfolderPath:執行report操作后生成的report目錄
- server_ip:tomcat的ip
- port:添加至tomcat啟動參數時指定的port
- webSrcpath:未編譯源的文件的目錄,或者說是包含源文件代碼的文件,用於查看報告時,定位代碼
- webClasspath:編譯后的classes文件目錄
Step 4:配置jacoco ant report文件build_report.xml
Step 5:執行檢測
切換至build_dump.xml目錄下,依次執行“ant -buildfile build_dump.xml ”、“ant -buildfile build_report.xml ”命令
Step 6:查看測試報告
進入build_report.xml配置的reportfolderPath屬性的目錄,D:\jacoco_output\report\,查看測試報告index.html