Jacoco是一個開源的覆蓋率工具糯崎,針對java語言队询。
一往踢、覆蓋率計(jì)數(shù)器
1. 行覆蓋
所有類文件均攜帶debug信息編譯慰技,則每行的覆蓋率可計(jì)算椭盏。當(dāng)至少一個指令被指定到源碼行且已執(zhí)行時(shí),該源碼行被認(rèn)為已執(zhí)行吻商。
**全部未覆蓋:該行中指令均未執(zhí)行掏颊,紅色標(biāo)志
**部分覆蓋:該行中部分指令執(zhí)行,黃色標(biāo)志
**全覆蓋:該行中所有指令已執(zhí)行艾帐,綠色標(biāo)志
2. 類覆蓋
當(dāng)類中至少有一個方法已執(zhí)行乌叶,則該類被認(rèn)為已執(zhí)行。Jacoco中認(rèn)為構(gòu)造函數(shù)和靜態(tài)初始化方法也當(dāng)作被執(zhí)行過的方法柒爸。Java接口類型若包含靜態(tài)初始化方法枉昏,這種接口也被認(rèn)為是可執(zhí)行的類。
3. 方法覆蓋
每個非抽象方法至少包含一個指令揍鸟。當(dāng)至少一個指令被執(zhí)行兄裂,該方法被認(rèn)為已執(zhí)行。由于Jacoco基于字節(jié)碼級別的阳藻,構(gòu)造函數(shù)和靜態(tài)初始化方法也被當(dāng)作方法計(jì)算晰奖。其中有些方法,可能無法直接對應(yīng)到源碼中腥泥,比如默認(rèn)構(gòu)造器或常量的初始化命令匾南。
4. 分支覆蓋
Jacoco為if和switch語句計(jì)算分支覆蓋率。這個指標(biāo)計(jì)算一個方法中的分支總數(shù)蛔外,并決定已執(zhí)行和未執(zhí)行的分支的數(shù)量蛆楞。分支覆蓋率在class文件中缺少debug信息時(shí)也可使用溯乒。異常處理不在分支覆蓋的統(tǒng)計(jì)范圍內(nèi)。
**全部未覆蓋:所有分支均未執(zhí)行豹爹,紅色標(biāo)志
**部分覆蓋:只有部分分支被執(zhí)行裆悄,黃色標(biāo)志
**全覆蓋:所有分支均已執(zhí)行,綠色標(biāo)志
5. 指令覆蓋
Jacoco計(jì)數(shù)的最小單元是Java字節(jié)碼指令臂聋,它為執(zhí)行/未執(zhí)行代碼提供了大量的信息光稼。這個指標(biāo)完全獨(dú)立于源格式,在類文件中缺少debug信息時(shí)也可以使用孩等。
6. 圈復(fù)雜度
Jacoco對每個非抽象方法計(jì)算圈復(fù)雜度艾君,總結(jié)類、包肄方、組的復(fù)雜性冰垄。
圈復(fù)雜度:在(線性)組合中,計(jì)算在一個方法里面所有可能路徑的最小數(shù)目权她。所以復(fù)雜度可以作為度量單元測試是否有完全覆蓋所有場景的一個依據(jù)虹茶。在沒有debug信息的時(shí)候也可以使用。
**圈復(fù)雜度V(G)是基于方法的控制流圖的有向圖表示:V(G) = E - N + 2
**E是邊界數(shù)量伴奥,N是節(jié)點(diǎn)數(shù)量写烤。
**Jacoco基于下面方程來計(jì)算復(fù)雜度翼闽,B是分支數(shù)量拾徙,D是決策點(diǎn)數(shù)量:
**V(G) = B - D + 1
基于每個分支的被覆蓋情況,Jacoco也未每個方法計(jì)算覆蓋和缺失的復(fù)雜度感局。缺失復(fù)雜度同樣表示測試案例沒有完全覆蓋到這個模塊尼啡。注意Jacoco不將異常處理作為分支,try/catch塊也同樣不增加復(fù)雜度询微。
二崖瞭、Jacoco原理
Jacoco使用插樁的方式來記錄覆蓋率數(shù)據(jù),是通過一個probe探針來注入撑毛。
插樁模式有兩種:
1. on-the-fly模式
JVM通過 -javaagent參數(shù)指定jar文件啟動代理程序书聚,代理程序在ClassLoader裝載一個class前判斷是否修改class文件,并將探針插入class文件藻雌,探針不改變原有方法的行為雌续,只是記錄是否已經(jīng)執(zhí)行。
2. offline模式
在測試之前先對文件進(jìn)行插樁胯杭,生成插過樁的class或jar包驯杜,測試插過樁的class和jar包,生成覆蓋率信息到文件做个,最后統(tǒng)一處理鸽心,生成報(bào)告滚局。
on-the-fly和offline對比
on-the-fly更方便簡單,無需提前插樁顽频,無需考慮classpath設(shè)置問題藤肢。
以下情況不適合使用on-the-fly模式:
(1)不支持javaagent
(2)無法設(shè)置JVM參數(shù)
(3)字節(jié)碼需要被轉(zhuǎn)換成其他虛擬機(jī)
(4)動態(tài)修改字節(jié)碼過程和其他agent沖突
(5)無法自定義用戶加載類
Java方法的控制流分析
官方文檔在這里:https://www.jacoco.org/jacoco/trunk/doc/flow.html
1. 探針插入策略
探針可以在現(xiàn)有指令之間插入附加指令,他們不改變已有方法行為冲九,只是去記錄是否已經(jīng)執(zhí)行谤草。可以認(rèn)為探針放置在控制流圖的邊緣上莺奸,理論上講丑孩,我們可以在控制流圖的每個邊緣插入一個探針,但這樣會增加類文件大小灭贷,降低執(zhí)行速度温学。事實(shí)上,我們每個方法只需要一些探針甚疟,具體取決于方法的控制流程仗岖。
如果已經(jīng)執(zhí)行了探測,我們知道已經(jīng)訪問了相應(yīng)的邊緣览妖,從這個邊緣我們可以得出其他前面的節(jié)點(diǎn)和邊:
(1)如果訪問了邊轧拄,我們知道該邊的源節(jié)點(diǎn)已經(jīng)被執(zhí)行。
(2)如果節(jié)點(diǎn)已經(jīng)被執(zhí)行且節(jié)點(diǎn)是一個邊緣的目標(biāo)節(jié)點(diǎn)讽膏,則我們知道已經(jīng)訪問了該邊檩电。
2. 探針的實(shí)現(xiàn)
探針需要滿足如下幾點(diǎn)要求:
(1)記錄執(zhí)行
(2)識別不同的探針
(3)線程安全
(4)對應(yīng)用程序無影響
(5)最小的運(yùn)行時(shí)開銷
Jacoco給每個類一個boolean[]數(shù)組實(shí)例烹卒,每個探針對應(yīng)該數(shù)組中的一個條目。無論何時(shí)執(zhí)行弯洗,都用下面4條字節(jié)碼指令將條目設(shè)置為true旅急。
ALOAD probearray
xPUSH probeid
ICONST_1
BASTORE
三、Jacoco的使用方式
- 不詳細(xì)介紹了=》ant
- 不詳細(xì)介紹了=》maven
3.不詳細(xì)介紹了=》offline -
Java agent
Jacoco的使用分為三部分涂召,第一部分是注入并采集坠非,第二部分是導(dǎo)出,第三部分是生成報(bào)告果正,三部分可以分開執(zhí)行炎码。
(1)首先在被測程序的啟動命令行中加上-javaagent選項(xiàng)盟迟,指定jacocoagent.jar作為代理程序。
Jacoco agent搜集執(zhí)行信息并且在請求或者JVM退出的時(shí)候?qū)С鰯?shù)據(jù)潦闲。有三種不同的導(dǎo)出數(shù)據(jù)模式:
- 文件系統(tǒng):JVM停止時(shí)攒菠,數(shù)據(jù)被導(dǎo)出到本地文件
- TCP socket Server:監(jiān)聽端口連接,通過socket連接獲取到執(zhí)行數(shù)據(jù)歉闰。在VM退出時(shí)辖众,可選擇進(jìn)行數(shù)據(jù)重置和數(shù)據(jù)導(dǎo)出。
- TCP socket Client:啟動時(shí)和敬,Jacoco agent連接到一個給定的TCP端凹炸,請求時(shí)執(zhí)行數(shù)據(jù)寫到socket,在VM退出時(shí)昼弟,可選擇進(jìn)行數(shù)據(jù)重置和數(shù)據(jù)導(dǎo)出啤它。
-javaagent:[yourpath/]jacocoagent.jar=[option1]=[value1],[option2]=[value2]
(2)導(dǎo)出數(shù)據(jù),假如指定導(dǎo)出模式為tcpserver舱痘,那么我們需要啟動一個client來請求覆蓋率文件數(shù)據(jù)变骡。
- 代碼導(dǎo)出
Jacoco給出的example示例如下:
/*******************************************************************************
* Copyright (c) 2009, 2019 Mountainminds GmbH & Co. KG and Contributors
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Marc R. Hoffmann - initial API and implementation
*
*******************************************************************************/
package org.jacoco.examples;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import org.jacoco.core.data.ExecutionDataWriter;
import org.jacoco.core.runtime.RemoteControlReader;
import org.jacoco.core.runtime.RemoteControlWriter;
/**
* This example connects to a coverage agent that run in output mode
* <code>tcpserver</code> and requests execution data. The collected data is
* dumped to a local file.
*/
public final class ExecutionDataClient {
private static final String DESTFILE = "jacoco-client.exec";
private static final String ADDRESS = "localhost";
private static final int PORT = 6300;
/**
* Starts the execution data request.
*
* @param args
* @throws IOException
*/
public static void main(final String[] args) throws IOException {
final FileOutputStream localFile = new FileOutputStream(DESTFILE);
final ExecutionDataWriter localWriter = new ExecutionDataWriter(
localFile);
// Open a socket to the coverage agent:
final Socket socket = new Socket(InetAddress.getByName(ADDRESS), PORT);
final RemoteControlWriter writer = new RemoteControlWriter(
socket.getOutputStream());
final RemoteControlReader reader = new RemoteControlReader(
socket.getInputStream());
reader.setSessionInfoVisitor(localWriter);
reader.setExecutionDataVisitor(localWriter);
// Send a dump command and read the response:
writer.visitDumpCommand(true, false);
if (!reader.read()) {
throw new IOException("Socket closed unexpectedly.");
}
socket.close();
localFile.close();
}
private ExecutionDataClient() {
}
}
-
命令行導(dǎo)出
(3)到此,已經(jīng)生成了exec文件芭逝,那我們的報(bào)告呢塌碌?
- 代碼生成報(bào)告
官方示例如下:
/*******************************************************************************
* Copyright (c) 2009, 2019 Mountainminds GmbH & Co. KG and Contributors
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Brock Janiczak - initial API and implementation
*
*******************************************************************************/
package org.jacoco.examples;
import java.io.File;
import java.io.IOException;
import org.jacoco.core.analysis.Analyzer;
import org.jacoco.core.analysis.CoverageBuilder;
import org.jacoco.core.analysis.IBundleCoverage;
import org.jacoco.core.tools.ExecFileLoader;
import org.jacoco.report.DirectorySourceFileLocator;
import org.jacoco.report.FileMultiReportOutput;
import org.jacoco.report.IReportVisitor;
import org.jacoco.report.html.HTMLFormatter;
/**
* This example creates a HTML report for eclipse like projects based on a
* single execution data store called jacoco.exec. The report contains no
* grouping information.
*
* The class files under test must be compiled with debug information, otherwise
* source highlighting will not work.
*/
public class ReportGenerator {
private final String title;
private final File executionDataFile;
private final File classesDirectory;
private final File sourceDirectory;
private final File reportDirectory;
private ExecFileLoader execFileLoader;
/**
* Create a new generator based for the given project.
*
* @param projectDirectory
*/
public ReportGenerator(final File projectDirectory) {
this.title = projectDirectory.getName();
this.executionDataFile = new File(projectDirectory, "jacoco.exec");
this.classesDirectory = new File(projectDirectory, "bin");
this.sourceDirectory = new File(projectDirectory, "src");
this.reportDirectory = new File(projectDirectory, "coveragereport");
}
/**
* Create the report.
*
* @throws IOException
*/
public void create() throws IOException {
// Read the jacoco.exec file. Multiple data files could be merged
// at this point
loadExecutionData();
// Run the structure analyzer on a single class folder to build up
// the coverage model. The process would be similar if your classes
// were in a jar file. Typically you would create a bundle for each
// class folder and each jar you want in your report. If you have
// more than one bundle you will need to add a grouping node to your
// report
final IBundleCoverage bundleCoverage = analyzeStructure();
createReport(bundleCoverage);
}
private void createReport(final IBundleCoverage bundleCoverage)
throws IOException {
// Create a concrete report visitor based on some supplied
// configuration. In this case we use the defaults
final HTMLFormatter htmlFormatter = new HTMLFormatter();
final IReportVisitor visitor = htmlFormatter
.createVisitor(new FileMultiReportOutput(reportDirectory));
// Initialize the report with all of the execution and session
// information. At this point the report doesn't know about the
// structure of the report being created
visitor.visitInfo(execFileLoader.getSessionInfoStore().getInfos(),
execFileLoader.getExecutionDataStore().getContents());
// Populate the report structure with the bundle coverage information.
// Call visitGroup if you need groups in your report.
visitor.visitBundle(bundleCoverage, new DirectorySourceFileLocator(
sourceDirectory, "utf-8", 4));
// Signal end of structure information to allow report to write all
// information out
visitor.visitEnd();
}
private void loadExecutionData() throws IOException {
execFileLoader = new ExecFileLoader();
execFileLoader.load(executionDataFile);
}
private IBundleCoverage analyzeStructure() throws IOException {
final CoverageBuilder coverageBuilder = new CoverageBuilder();
final Analyzer analyzer = new Analyzer(
execFileLoader.getExecutionDataStore(), coverageBuilder);
analyzer.analyzeAll(classesDirectory);
return coverageBuilder.getBundle(title);
}
/**
* Starts the report generation process
*
* @param args
* Arguments to the application. This will be the location of the
* eclipse projects that will be used to generate reports for
* @throws IOException
*/
public static void main(final String[] args) throws IOException {
for (int i = 0; i < args.length; i++) {
final ReportGenerator generator = new ReportGenerator(new File(
args[i]));
generator.create();
}
}
}
-
命令行生成報(bào)告
到此,我們就學(xué)會了on-the-fly模式的Jacoco使用旬盯。