Jacoco代碼覆蓋率工具


一、覆蓋率計數器

1. 行覆蓋

所有類文件均攜帶debug信息編譯,則每行的覆蓋率可計算。當至少一個指令被指定到源碼行且已執行時,該源碼行被認為已執行。
**全部未覆蓋:該行中指令均未執行,紅色標志
**部分覆蓋:該行中部分指令執行,黃色標志
**全覆蓋:該行中所有指令已執行,綠色標志

2. 類覆蓋

當類中至少有一個方法已執行,則該類被認為已執行。Jacoco中認為構造函數和靜態初始化方法也當作被執行過的方法。Java接口類型若包含靜態初始化方法,這種接口也被認為是可執行的類。

3. 方法覆蓋

每個非抽象方法至少包含一個指令。當至少一個指令被執行,該方法被認為已執行。由於Jacoco基於字節碼級別的,構造函數和靜態初始化方法也被當作方法計算。其中有些方法,可能無法直接對應到源碼中,比如默認構造器或常量的初始化命令。

4. 分支覆蓋

Jacoco為if和switch語句計算分支覆蓋率。這個指標計算一個方法中的分支總數,並決定已執行和未執行的分支的數量。分支覆蓋率在class文件中缺少debug信息時也可使用。異常處理不在分支覆蓋的統計范圍內。
**全部未覆蓋:所有分支均未執行,紅色標志
**部分覆蓋:只有部分分支被執行,黃色標志
**全覆蓋:所有分支均已執行,綠色標志

5. 指令覆蓋

Jacoco計數的最小單元是Java字節碼指令,它為執行/未執行代碼提供了大量的信息。這個指標完全獨立於源格式,在類文件中缺少debug信息時也可以使用。

6. 圈復雜度

Jacoco對每個非抽象方法計算圈復雜度,總結類、包、組的復雜性。
圈復雜度:在(線性)組合中,計算在一個方法里面所有可能路徑的最小數目。所以復雜度可以作為度量單元測試是否有完全覆蓋所有場景的一個依據。在沒有debug信息的時候也可以使用。
**圈復雜度V(G)是基於方法的控制流圖的有向圖表示:V(G) = E - N + 2
**E是邊界數量,N是節點數量。
**Jacoco基於下面方程來計算復雜度,B是分支數量,D是決策點數量:
**V(G) = B - D + 1
基於每個分支的被覆蓋情況,Jacoco也未每個方法計算覆蓋和缺失的復雜度。缺失復雜度同樣表示測試案例沒有完全覆蓋到這個模塊。注意Jacoco不將異常處理作為分支,try/catch塊也同樣不增加復雜度。

二、Jacoco原理

Jacoco使用插樁的方式來記錄覆蓋率數據,是通過一個probe探針來注入。
插樁模式有兩種:

1. on-the-fly模式

JVM通過 -javaagent參數指定jar文件啟動代理程序,代理程序在ClassLoader裝載一個class前判斷是否修改class文件,並將探針插入class文件,探針不改變原有方法的行為,只是記錄是否已經執行。

2. offline模式

在測試之前先對文件進行插樁,生成插過樁的class或jar包,測試插過樁的class和jar包,生成覆蓋率信息到文件,最后統一處理,生成報告。

on-the-fly和offline對比

on-the-fly更方便簡單,無需提前插樁,無需考慮classpath設置問題。
以下情況不適合使用on-the-fly模式:
(1)不支持javaagent
(2)無法設置JVM參數
(3)字節碼需要被轉換成其他虛擬機
(4)動態修改字節碼過程和其他agent沖突
(5)無法自定義用戶加載類

Java方法的控制流分析

官方文檔在這里:https://www.jacoco.org/jacoco/trunk/doc/flow.html

1. 探針插入策略

探針可以在現有指令之間插入附加指令,他們不改變已有方法行為,只是去記錄是否已經執行。可以認為探針放置在控制流圖的邊緣上,理論上講,我們可以在控制流圖的每個邊緣插入一個探針,但這樣會增加類文件大小,降低執行速度。事實上,我們每個方法只需要一些探針,具體取決於方法的控制流程。
如果已經執行了探測,我們知道已經訪問了相應的邊緣,從這個邊緣我們可以得出其他前面的節點和邊:
(1)如果訪問了邊,我們知道該邊的源節點已經被執行。
(2)如果節點已經被執行且節點是一個邊緣的目標節點,則我們知道已經訪問了該邊。

 
image.png
上述探針插入策略沒有考慮到隱式異常,如果兩個探針之間的控制流被未使用throw的語句顯示創建的異常終端,則其間的所有指令都被視為未覆蓋。因此,只要后續行包含至少一個方法調用,Jacoco就會在兩行的指令間添加額外的探測。該方法僅使用於有debug信息的編譯的類文件。且不考慮除方法調用之外的其他指令的隱式異常。

 

2. 探針的實現

探針需要滿足如下幾點要求:
(1)記錄執行
(2)識別不同的探針
(3)線程安全
(4)對應用程序無影響
(5)最小的運行時開銷
Jacoco給每個類一個boolean[]數組實例,每個探針對應該數組中的一個條目。無論何時執行,都用下面4條字節碼指令將條目設置為true。

ALOAD    probearray
xPUSH    probeid
ICONST_1
BASTORE

三、Jacoco的使用方式

  1. 不詳細介紹了=》ant
  2. 不詳細介紹了=》maven
    3.不詳細介紹了=》offline
  3. Java agent
    Jacoco的使用分為三部分,第一部分是注入並采集,第二部分是導出,第三部分是生成報告,三部分可以分開執行。

(1)首先在被測程序的啟動命令行中加上-javaagent選項,指定jacocoagent.jar作為代理程序。

Jacoco agent搜集執行信息並且在請求或者JVM退出的時候導出數據。有三種不同的導出數據模式:

  • 文件系統:JVM停止時,數據被導出到本地文件
  • TCP socket Server:監聽端口連接,通過socket連接獲取到執行數據。在VM退出時,可選擇進行數據重置和數據導出。
  • TCP socket Client:啟動時,Jacoco agent連接到一個給定的TCP端,請求時執行數據寫到socket,在VM退出時,可選擇進行數據重置和數據導出。
  -javaagent:[yourpath/]jacocoagent.jar=[option1]=[value1],[option2]=[value2]
 
image.png

(2)導出數據,假如指定導出模式為tcpserver,那么我們需要啟動一個client來請求覆蓋率文件數據。

  • 代碼導出
    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() { } } 
  •  

     

    命令行導出
     
    image.png

(3)到此,已經生成了exec文件,那我們的報告呢?

  • 代碼生成報告
    官方示例如下:
/******************************************************************************* * 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(); } } } 
  •  

     

    命令行生成報告
     
    image.png

到此,我們就學會了on-the-fly模式的Jacoco使用。



作者:zi萱
鏈接:https://www.jianshu.com/p/a955d274dc9b
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM