目录
9.4 cli包dump生成exec文件(注意一定要测试完毕之后)
一、些常用工具:
- Java: Atlassian Clover, Cobertura, JaCoCo
- Javascript: istanbul, Blanket.js
- PHP: PHPUnit
- Python: Coverage.py
- Ruby: SimpleCov
有些工具(如istanbul)会将结果直接输出到终端,而另一些工具可以生成完整的HTML报告,让您了解代码中哪些部分缺少覆盖。
二、接口自动化的意义&代码覆盖率的意义
假如我写了很多接口自动化case,已经把被测系统的所有接口都覆盖到了,那这是不是就说明我的自动化case已经全部写完了,不用再添加新的自动化case了呢,是不是就说明我的自动化测试已经做得非常完备了
答案是否定的
因为我们缺少数据来衡量自动化case的完备程度,那该怎么解决呢
业界一般是通过代码覆盖率来输出自动化case的覆盖数据,衡量接口自动化测试的完备程度,来指导后续要增加、完善case的方向。另一方面,它还可以反映服务端功能测试的全面性,用来评估服务端手工测试是否全面。
作为一个测试人员,保证产品的软件质量是其工作首要目标,为了这个目标,测试人员常常会通过很多手段或工具来加以保证,覆盖率就是其中一环比较重要的环节
我们通常会将测试覆盖率分为两个部分,即需求覆盖率和代码覆盖率
需求覆盖:指的是测试人员对需求的了解程度,根据需求的可测试性来拆分成各个子需求点,来编写相应的测试用例,最终建立一个需求和用例的映射关系,以用例的测试结果来验证需求的实现,可以理解为黑盒覆盖
代码覆盖:为了更加全面的覆盖,我们可能还需要理解被测程序的逻辑,需要考虑到每个函数的输入与输出,逻辑分支代码的执行情况,这个时候我们的测试执行情况就以代码覆盖率来衡量,可以理解为白盒覆盖。例如,如果源代码具有一个简单的if…else循环,则如果测试代码可以覆盖这两种情况(即if&else),则代码覆盖率将为100%
测量代码覆盖率:
- 了解我们的测试用例对源代码的测试效果
- 了解我们是否进行了足够的测试
- 在软件的整个生命周期内保持测试质量
-
分析未覆盖部分的代码,从而反推在前期黑盒测试设计是否充分,没有覆盖到的代码是否是测试设计的盲点,为什么没有考虑到?需求/设计不够清晰,测试设计的理解有误,工程方法应用后的造成的策略性放弃等等,之后进行补充测试用例设计。
-
检测出程序中的废代码,可以逆向反推在代码设计中思维混乱点,提醒设计/开发人员理清代码逻辑关系,提升代码质量。
-
代码覆盖率高不能说明代码质量高,但是反过来看,代码覆盖率低,代码质量不会高到哪里去,可以作为测试自我审视的重要工具之一。
-
代码覆盖率可以度量单元/自动化测试用例,提供覆盖率统计情况,可以通过分析覆盖率报告,完善用例。
-
代码覆盖率利于精准回归,通过构建代码调用关系,精准的确定回归测试范围,避免了全量回归造成的测试资源浪费。
以下是从博客里摘抄过来的,感觉说的挺好的。。。
接口自动化测试的意义有下面几点:
- 微服务升级、框架升级、各种升级和改版的时候做 diff 用,快速验证是否影响老的逻辑
- 业务造数,通过 API 组合来快速构造测试数据
- 提测准入验证,因为执行时间很短,可以结合 CI 做快速检查提测版本的功能
- 批量回归测试
- 辅助性能测试和开发自测
代码覆盖率的意义:
- 删除冗余代码
- 辅助测试人员衡量测试覆盖度,尤其是新增的行覆盖率、branch 覆盖率、method 覆盖率,可以非常有效的提升新增业务逻辑的覆盖场景
- 快节奏项目下,评估测试范围和影响的利器
- 海量用例下,评估需要使用的 case,而不是无脑的回归(精准化测试)
- 针对新增 diff 数据未覆盖的代码进行用例补充
- 有效补充上面的 API 自动化 case
- 通过可视化、可度量的方式,促进测试进行自我检视,衡量研发自测效果和水平
三、Jacoco简介:
JaCoCo是一个开源的Java代码覆盖率工具。
JaCoCo 只是统计了全量代码的覆盖率,在代码迭代过程中,比如修改了bug,又重新发布了一版代码。JaCoCo 的统计就被清空了,需要从头执行触发代码覆盖。
这种场景,我们只需要测试增量代码就可以了,这个时候需要对jacoco进行二次开发。
jacoco 统计代码覆盖率,一般与自动化相结合,比如jacoco与接口自动化相结合。当时我们也可以手工执行case,来统计代码覆盖率。
可以嵌入到Ant、Maven中;可以作为Eclipse插件,可以使用其JavaAgent技术监控Java程序等等。
很多第三方的工具提供了对JaCoCo的集成,如sonar、Jenkins等。
四、代码覆盖率介绍:
覆盖率是用来衡量测试代码对功能代码的测试情况。通过统计测试代码中对功能代码中行、分支、类等模拟场景数量,来量化说明测试的充分度。
代码覆盖率=代码的覆盖程度,一种度量方式。
覆盖率简单说:跑了一个测试用例,项目代码中的那些模块、文件、类、方法 执行了。
其中行覆盖率是最细颗粒度,其他覆盖率都可从行覆盖率情况统计算出来。
2.1 行覆盖
当至少一个指令被指定源码行执行时,该源码行被认为已执行。
通俗的说就是,测试行为,触发了这某一行代码,则这一行代码就被覆盖了。
2.2 分支覆盖
if 和switch 语句算作分支覆盖率,这个指标计算一个方法中的分支总数,并决定已执行和未执行的分支的数量。
全部未覆盖:所有分支均未执行,红色标志
部分覆盖:部分分支被执行,黄色标志
全覆盖:所有分支均已执行,绿色标志。
2.3 方法覆盖
当方法中至少有一个指令被执行,该方法被认为已执行,包括构造函数和静态初始化方法。
2.4 代码覆盖率意义
分析未覆盖部分的代码,反推测试设计是否充分,没有覆盖到的代码是否存在测试设计盲点。
2.5 覆盖率的误区
若代码如下:
if (i>100)
j = 10/i // 没有除0错误
else
j = 10/(i+2) // i==-2,除0错误
覆盖2个分支,只需要设计i==101 和 i==1,但是对于找到i==-2这个bug点时没有作用的。
所以:
1、不要简单的追求高的代码覆盖率
2、高覆盖率测试用例不等于测试用例有效
3、没覆盖的分支相当于该分支上的任何错误肯定都找不到
五、单元测试覆盖率&功能测试覆盖率及jacoco原理
代码覆盖率:
1、单元测试覆盖率(开发单元测试自测)
2、功能测试覆盖率(测试人员功能测试)
增量覆盖率一般指的是功能测试覆盖率。
jacoco使用插桩的方式来记录覆盖率数据,是通过一个probe探针来注入。jacoco有2种插桩模式
,offline模式和on-the-fly模式。
单元测试offline模式:
对应的是jacoco的offline模式。
offline模式就是在测试之前先对文件进行插桩,生成插过桩的class或jar包,测试插过桩的class和jar包,生成覆盖率信息到文件,最后统一处理,生成报告。
功能测试on-the-fly模式:
对应的是jacoco的on-the-fly模式。
JVM通过 -javaagent参数指定jar文件启动代理程序,代理程序在ClassLoader装载一个class前判断是否修改class文件,并将探针插入class文件,探针不改变原有方法的行为,只是记录是否已经执行。
六、查看覆盖率报告
Jacoco - 代码覆盖率报告分析_10970859的技术博客_51CTO博客
可以下载以下文件,查看报告:
jacoco代码覆盖率的报告,样式。-Java文档类资源-CSDN下载
Jacoco是从代码指令(Instructions, Coverage),分支(Branches, Coverage),圈复杂度(Cyclomatic Complexity),行(Lines),方法(Methods),类(Classes)等维度进行分析的。
序号 | 字段名称 | 名称 | 描述 |
1 | Element | 元素 | 1、最外层展示分组名称,依次为包》类〉方法 |
2 | Missed Instructions cov | 指令覆盖,字节码中指令 | 1、方法里所有的代码行都有覆盖到(都覆盖类并不代表100%覆盖,会存在分支没有覆盖完整的情况) 2、类下面所有方法都有覆盖 3、包下面的类都有覆盖 |
3 | Missed Branches cov | 分支覆盖率 | 1、对所有的if和switch 指令计算类分支覆盖率 2、用钻石表示,分支覆盖不能看行 |
4 | Missed Cxty | 圈复杂度 | |
5 | Missed Methods | 方法 | 每个非抽象方法都至少有一条指令。若一个方法至少被执行了一条指令,就认为它被执行过 |
6 | Missed Classes | 类 | 每个类中只要有一个方法被执行,这个类就被认定为执行过。同5方法一样,有些没有在源码声明的方法被执行,也被认定该类执行。 |
7 | Missed Lines | 代码行 | 用背景色标识的都算是行统计的目标,变量定义不算行,else也不算。 |
注:Missed表示未覆盖
我们只关注行覆盖率和分支覆盖率
分支覆盖率 Missed Branches cov
红色进度条表未覆盖,绿色进度条表示已覆盖,Cov为总体覆盖率。
Total:8表示没有覆盖的分支,14表示总的分支,Cov表示总体覆盖率。
项目结构:
报告的一级目录:package 包名
报告的二级目录:包下的类
这里显示的与项目接口不一致,点击右上角的Source Files 切换视图模式。
切换之后,就与项目结构一致了。
三级目录 - 类名
展示当前分组>包下面所有的类。
类的覆盖率取决于方法的覆盖情况。
四级目录 - 方法名
展示当前分组>包>类下面的所有方法。
方法的覆盖率取决于方法内代码覆盖的情况。
绿色代表已执行, 红色代表未执行, 黄色代表执行了一部分
七、单元测试的代码覆盖率
JaCoCo官方文档:https://www.eclemma.org/jacoco/trunk/doc/index.html
4.1 、Maven设置JaCoCo插件
- 引入依赖Maven插件
<!-- https://mvnrepository.com/artifact/org.jacoco/jacoco-maven-plugin -->
<dependency>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.7</version>
</dependency>
2. 配置该插件的执行标签<executions>
对于运行简单的单元测试,在执行标签中设置的两个目标可以正常工作。最低限度是设置准备代理(prepare-agent)和报告目标(report),配置如下:
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.6</version>
<executions>
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
prepare-agent goal: prepare-agent 目标准备 JaCoCo 运行时代理以记录执行数据。它记录了执行的行数、回溯的行数等。默认情况下,将执行数据写入文件target/jacoco-ut.exec。
report goal: 报告目标根据 JaCoCo 运行时代理记录的执行数据创建代码覆盖率报告。由于我们已经指定了阶段属性,报告将在测试阶段编译后创建。默认从文件中读取执行数据target/jacoco-ut.exec,将代码覆盖率报告写入目录target/site/jacoco/index.html。
所有配置的Goals,详见官网https://www.eclemma.org/jacoco/trunk/doc/maven.html
其中比较常用的是 prepare-agent、report和check
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
<configuration>
<!--定义输出的文件夹-->
<outputDirectory>target/jacoco-report</outputDirectory>
<!--执行数据的文件-->
<dataFile>${project.build.directory}/jacoco.exec</dataFile>
<!--要从报告中排除的类文件列表,支持通配符(*和?)。如果未指定则不会排除任何内容-->
<excludes>**/test/*.class</excludes>
<!--包含生成报告的文件列表,支持通配符(*和?)。如果未指定则包含所有内容-->
<includes></includes>
<!--HTML 报告页面中使用的页脚文本。-->
<footer></footer>
<!--生成报告的文件类型,HTML(默认)、XML、CSV-->
<formats>HTML</formats>
<!--生成报告的编码格式,默认UTF-8-->
<outputEncoding>UTF-8</outputEncoding>
<!--抑制执行的标签-->
<skip></skip>
<!--源文件编码-->
<sourceEncoding>UTF-8</sourceEncoding>
<!--HTML报告的标题-->
<title>${project.name}</title>
</configuration>
</execution>
4. 2 单元测试代码覆盖率实战
配置pom.xml文件.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>testOOM</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>testOOM</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.jacoco/jacoco-maven-plugin -->
<dependency>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.7</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.0.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.22.2</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
<encoding>UTF8</encoding>
</configuration>
<version>3.8.1</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.7</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
<configuration>
<!--定义输出的文件夹-->
<outputDirectory>target/jacoco-report</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
再写2个测试类
package com.example.testoom;
public class MessageBuilder {
public String getMessage(String name) {
StringBuilder result = new StringBuilder();
if (name == null || name.trim().length() == 0) {
result.append("empty!");
} else {
result.append("Hello " + name);
}
return result.toString();
}
public String getMessage1(String name) {
StringBuilder result = new StringBuilder();
if (name == "zhangsan") {
result.append("zhangsan");
} else {
result.append("Hello " + name);
}
return result.toString();
}
public String getMessage2(String name) {
StringBuilder result = new StringBuilder();
if (name == "lisi") {
result.append("lisi");
} else {
result.append("Hello " + name);
}
return result.toString();
}
}
package com.example.testoom;
public class Calculator {
public int add(int a, int b) {
if (a< 10 || b < 10){
int i = a + b;
return i;
} else {
return 0;
}
}
public int subtraction(int a, int b) {
if (a>=b){
int i = a - b;
return i;
} else {
return 0;
}
}
}
再写2个单元测试
package com.example.testoom;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class CalculatorTest {
@Test
public void testCalculator1() {
Calculator calculator = new Calculator();
assertEquals(10,calculator.add(6,4));
}
@Test
public void testCalculator2() {
Calculator calculator = new Calculator();
assertEquals(1,calculator.subtraction(10,9));
}
}
package com.example.testoom;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class MessageBuilderTest {
@Test
public void testGetMessage1() {
MessageBuilder obj = new MessageBuilder();
assertEquals("Hello test", obj.getMessage("test"));
}
@Test
public void testGetMessage2() {
MessageBuilder obj = new MessageBuilder();
assertEquals("zhangsan", obj.getMessage1("zhangsan"));
}
@Test
public void testGetMessage3() {
MessageBuilder obj = new MessageBuilder();
assertEquals("lisi", obj.getMessage2("lisi"));
}
}
然后执行测试计划
mvn clean test
执行完成后,查看target文件夹下,target/jacoco-report/index.html报告
八、功能测试代码覆盖率
生成代码覆盖率,我们需要依赖源码,和字节码。
一、使用启动我们应用服务的时候,需要添加jvm参数 -javaagent,如:
-javaagent:[yourpath/]jacocoagent.jar=[option1]=[value1],[option2]=[value2]
具体实例如下:
java -javaagent:/tmp/jacoco/lib/jacocoagent.jar=includes=*,output=tcpserver,port=6300,address=localhost,append=true -jar demo-0.0.1-SNAPSHOT.jar
关键参数解释:
includes=*
这个代表了,启动时需要进行字节码插桩的包过滤,*代表所有的class文件加载都需要进行插桩。
你可以写成:
includes=com.test.service.*
这个参数我们可以用来做maven多模块的覆盖率,比如我们只想查看service服务层的覆盖率,我们可以通过设置包路径的方式进行只统计当前包的覆盖率
output=tcpserver
output主要四个参数:
1、file: At VM termination execution data is written to the file specified in the destfile attribute.(当jvm停止掉的时候产出dump文件,即服务挂了产出dump文件)
2、tcpserver: The agent listens for incoming connections on the TCP port specified by the address and port attribute. Execution data is written to this TCP connection.(常用模式,将jacocoaget作为服务,每次通过cli包进行dump命令去获取dump包)
3、tcpclient: At startup the agent connects to the TCP port specified by the address and port attribute. Execution data is written to this TCP connection.(将jacocoagent做为客户端,向指定ip和端口的服务推送dump信息)
4、none: Do not produce any output.(不产出任何dump,dump个寂寞,忽略)
注意:
在k8s容器里面由于ip是动态的,tcpserver模式的ip无法固定填写,可以填 0.0.0.0 然后通过实际容器 ip 就可以访问到,而这个实际ip,一般可以从cmdb服务中动态获取
port=98080
这是jacoco开启的tcpserver的端口,请注意这个端口不能被占用。
address=192.168.110.1
这是对外开发的tcpserver的访问地址。可以配置127.0.0.1,也可以配置为实际访问ip
配置为127.0.0.1的时候,dump数据只能在这台服务器上进行dump,就不能通过远程方式dump数据。
配置为实际的ip地址的时候,就可以在任意一台机器上(前提是ip要通,不通都白瞎),通过ant xml或者api方式dump数据。
举个栗子:
我如上配置了192.168.110.1:2014作为jacoco的tcpserver启动服务,
那我可以在任意一台机器上进行数据的dump,比如在我本机windows上用api或者xml方式调用dump。
如果我配置了127.0.0.1:2014作为启动服务器,那么我只能在这台测试机上进行dump,其他的机器都无法连接到这个tcpserver进行dump
如果不知道本机ip地址,可以使用0.0.0.0,这样ip地址会绑定主机ip。
append:true
是执行数据文件已经存在,则覆盖数据将附加到现有文件
二、我们需要使用cli包去获取exec文件
java -jar jacococli.jar dump --address 192.169.110.1 --port 2014 --destfile ./jacoco-demo.exec
使用这个命令会在我们调用方的服务上生成一个exec文件
三、生成report
java -jar jacococli.jar report ./jacoco-demo.exec --classfiles /Users/oukotoshuu/IdeaProjects/demo/target/classes/com --classfiles /Users/oukotoshuu/IdeaProjects/demo/target/classes/com --sourcefiles /Users/oukotoshuu/IdeaProjects/demo/src/main/java --sourcefiles /Users/oukotoshuu/IdeaProjects/demo/src/main/java --html report --xml report.xml
还是通过cli包去生成报告文件,注意这个classfiles和sourcefiles 可以是多个,我们如果是多模块项目通过指定代码路径和编译文件路径去分开做统计。
九、功能测试代码覆盖率 实战
精准测试 & Jacoco 代码覆盖率统计实战 - Juno3550 - 博客园
9.1 环境准备
jacoco下载:
或者,下载地址:
EclEmma - JaCoCo Java Code Coverage Library
java 8
被测应用:
MockServer,使用springboot实现的mock平台。这里仅供相关的一个项目测试用,要结合其他项目使用-Java文档类资源-CSDN下载
将下载的jacoco压缩文件,上传到服务器,新建一个名为jacoco的文件夹,然后将压缩文件放入里面解压。
9.2 启动jacocoagent,监控被测项目
java -javaagent:/usr/local/webserver/jacoco/lib/jacocoagent.jar=includes=*,output=tcpserver,port=6300,address=0.0.0.0,append=true -jar MockServer.jar
java -javaagent:/usr/local/webserver/jacoco/lib/jacocoagent.jar=includes=*,output=tcpserver,port=6300,address=0.0.0.0,append=true -jar AutoApi-0.0.1-SNAPSHOT.jar
我们在启动项目的同时,也在该服务上起了一个端口为6300的jacocoagent服务。将来dump文件,都是通过端口为6300的jacocoagent服务实现的。
9.3 执行手工测试
9.4 cli包dump生成exec文件(注意一定要测试完毕之后)
9.4.1 在当前服务器执行dump命令
java -jar /usr/local/webserver/jacoco/lib/jacococli.jar dump --address 127.0.0.1 --port 6300 --destfile ./AutoApi-0.0.1-SNAPSHOT.exec
# --address 127.0.0.1 --port 6300 指向jacocoagent启动IP和端口
# ./AutoApi-0.0.1-SNAPSHOT.exec 为生成exec文件名,在当前目录下生成该文件
9.4.2 远程执行dump命令
java -jar jacococli.jar dump --address 124.70.87.136 --port 6300 --destfile ./AutoApi-0.0.1-SNAPSHOT.exec
在当前目录下生成文件。
注意:
在执行命令的机器上,也需要装有jacoco,且该机器能正常连接被测的服务器和端口。
9.5 cli包exec生成report报表
java -jar /usr/local/webserver/jacoco/lib/jacococli.jar report ./AutoApi-0.0.1-SNAPSHOT.exec --classfiles /usr/local/webserver/mock-server/target/classes --sourcefiles /usr/local/webserver/mock-server/src/main/java --html html-report --xml report.xml --encoding=utf-8
#--sourcefiles 本地被测项目的源码
#--classfiles 为本地被测项目的字节码路径
执行命令后,在本地生成一个html-report 文件夹
index.html就是报告。
远程生成测试报告:
java -jar jacococli.jar report ./AutoApi-0.0.1-SNAPSHOT.exec --classfiles /Users/zhaohui/IdeaProjects/mock-server/target/classes --sourcefiles /Users/zhaohui/IdeaProjects/mock-server/src/main/java --html html-report --xml report.xml --encoding=utf-8
十、Jacoco增量覆盖
jacoco 增量方案使用说明书_dray_的博客-CSDN博客_jacoco 增量
jacoco增量覆盖率实践_dray_的博客-CSDN博客_jacoco增量覆盖率
增量代码覆盖率
10.1 增量覆盖
增量覆盖:两次提交之间有哪些代码或者分支没有被覆盖。
目的:检测同一个测试用例在修改前后代码上的行覆盖情况
假设两次提交代码变更如下:
if(x>100)
- print(1)
+ print(2)
else
- print(3)
+ print(4)
每行代码有3种状态:+、-、不变
修改前后,跑同一个测试用例,每行有4种状态:修改前覆盖/修改前未覆盖,修改后覆盖/修改后未覆盖。
所以增量覆盖总共有3x4=12种情况,比较重要的有:新增代码没有覆盖,新增代码覆盖了,不变的代码修改前覆盖,修改后未覆盖等等。
增量代码覆盖率实现方式:
获取增量代码,在report阶段去判断方法是否是增量,再去生成报告
上图说明了jacoco测试覆盖率的生成流程,而我们要做的是在report的时候加入我们的逻辑
根据我们的方案,我们需要三个动作
计算出两个版本的差异代码(基于git)
将差异代码在jacoco的report阶段传给jacoco
修改jacoco源码,生成报告时判断代码是否是增量代码,只有增量代码才去生成报告。
下面我们逐步讲解上述步骤
计算差异代码
计算差异代码我实现了一个简单的工程:差异代码获取
主要用到了两个工具类。
<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.github.javaparser/javaparser-core -->
<dependency>
<groupId>com.github.javaparser</groupId>
<artifactId>javaparser-core</artifactId>
</dependency>
org.eclipse.jgit主要用于从git获取代码,并获取到存在变更的文件
javaparser-core是一个java解析类,能将class类文件解析成树状,方便我们去获取差异类。
获取差异类的核心代码:
/**
* 获取差异类
*
* @param diffMethodParams
* @return
*/
public List<ClassInfoResult> diffMethods(DiffMethodParams diffMethodParams) {
try {
//原有代码git对象
Git baseGit = cloneRepository(diffMethodParams.getGitUrl(), localBaseRepoDir + diffMethodParams.getBaseVersion(), diffMethodParams.getBaseVersion());
//现有代码git对象
Git nowGit = cloneRepository(diffMethodParams.getGitUrl(), localBaseRepoDir + diffMethodParams.getNowVersion(), diffMethodParams.getNowVersion());
AbstractTreeIterator baseTree = prepareTreeParser(baseGit.getRepository(), diffMethodParams.getBaseVersion());
AbstractTreeIterator nowTree = prepareTreeParser(nowGit.getRepository(), diffMethodParams.getNowVersion());
//获取两个版本之间的差异代码
List<DiffEntry> diff = nowGit.diff().setOldTree(baseTree).setNewTree(nowTree).setShowNameAndStatusOnly(true).call();
//过滤出有效的差异代码
Collection<DiffEntry> validDiffList = diff.stream()
//只计算java文件
.filter(e -> e.getNewPath().endsWith(".java"))
//排除测试文件
.filter(e -> e.getNewPath().contains("src/main/java"))
//只计算新增和变更文件
.filter(e -> DiffEntry.ChangeType.ADD.equals(e.getChangeType()) || DiffEntry.ChangeType.MODIFY.equals(e.getChangeType()))
.collect(Collectors.toList());
if (CollectionUtils.isEmpty(validDiffList)) {
return null;
}
/**
* 多线程获取旧代码和新代码的差异类及差异方法
*/
List<CompletableFuture<ClassInfoResult>> priceFuture = validDiffList.stream().map(item -> getClassMethods(getClassFile(baseGit, item.getNewPath()), getClassFile(nowGit, item.getNewPath()), item)).collect(Collectors.toList());
return priceFuture.stream().map(CompletableFuture::join).filter(Objects::nonNull).collect(Collectors.toList());
} catch (GitAPIException e) {
e.printStackTrace();
}
return null;
}
获取差异方法的核心代码:
/**
* 获取类的增量方法
*
* @param oldClassFile 旧类的本地地址
* @param mewClassFile 新类的本地地址
* @param diffEntry 差异类
* @return
*/
private CompletableFuture<ClassInfoResult> getClassMethods(String oldClassFile, String mewClassFile, DiffEntry diffEntry) {
//多线程获取差异方法,此处只要考虑增量代码太多的情况下,每个类都需要遍历所有方法,采用多线程方式加快速度
return CompletableFuture.supplyAsync(() -> {
String className = diffEntry.getNewPath().split("\\.")[0].split("src/main/java/")[1];
//新增类直接标记,不用计算方法
if (DiffEntry.ChangeType.ADD.equals(diffEntry.getChangeType())) {
return ClassInfoResult.builder()
.classFile(className)
.type(DiffEntry.ChangeType.ADD.name())
.build();
}
List<MethodInfoResult> diffMethods;
//获取新类的所有方法
List<MethodInfoResult> newMethodInfoResults = MethodParserUtils.parseMethods(mewClassFile);
//如果新类为空,没必要比较
if (CollectionUtils.isEmpty(newMethodInfoResults)) {
return null;
}
//获取旧类的所有方法
List<MethodInfoResult> oldMethodInfoResults = MethodParserUtils.parseMethods(oldClassFile);
//如果旧类为空,新类的方法所有为增量
if (CollectionUtils.isEmpty(oldMethodInfoResults)) {
diffMethods = newMethodInfoResults;
} else { //否则,计算增量方法
List<String> md5s = oldMethodInfoResults.stream().map(MethodInfoResult::getMd5).collect(Collectors.toList());
diffMethods = newMethodInfoResults.stream().filter(m -> !md5s.contains(m.getMd5())).collect(Collectors.toList());
}
//没有增量方法,过滤掉
if (CollectionUtils.isEmpty(diffMethods)) {
return null;
}
ClassInfoResult result = ClassInfoResult.builder()
.classFile(className)
.methodInfos(diffMethods)
.type(DiffEntry.ChangeType.MODIFY.name())
.build();
return result;
}, executor);
}
7.2 增量应用
jacoco 二开: https://gitee.com/Dray/jacoco.git
增量代码获取: https://gitee.com/Dray/dode-diff.git
使用方法:
1、jacoco客户端,收集信息
原文地址:
jacoco 增量方案使用说明书_dray_的博客-CSDN博客_jacoco 增量