代码覆盖率-Jacoco

发布时间 2023-08-08 10:16:11作者: 、阿红吖

Jacoco

1、什么是jacoco

jacoco是一个免费、开源java代码覆盖率工具。

2、什么是代码覆盖率

覆盖率是用来衡量测试代码对功能代码的测试情况,通过统计测试中对功能代码中行、分支、类等模拟场景数量,来量化说明测试的充分度。代码覆盖率=代码的覆盖程度,一种度量方式

覆盖率简单说:跑了一个测试用例,项目代码中哪些模块、文件、类、方法、行 执行了。

其中行覆盖率是最细粒度,其他覆盖率可以从行覆盖率情况计算出来。

1、行覆盖

当至少一个指令被指定源码行执行时,该源码被认为已执行。

2、分支覆盖

if和switch语句算作分支覆盖率,这个指标计算一个方法中的分支总数,并决定已执行和未执行的分支的数量

全部未覆盖:所有分支均未执行,红色标志

部分覆盖:部分分支被执行,黄色标志

全覆盖:所有分支均已执行,绿色标志

3、方法覆盖

当方法中至少有一个指令被执行,该方法被认为已执行,包括构造函数和静态初始化方法。

4、类覆盖

当一个类至少有一个方法已执行,则该类被认为已执行,包括构造函数和静态初始化方法。

3、代码覆盖率意义

分析未覆盖部分的代码,反推测试设计是否充分,没有覆盖到的代码是否存在测试设计盲点。

4、覆盖率的误区

若代码如下:

if (i>100)
	j = 10 / i      // 没有除零错误
else
	j=10 / (i + 2)  // 1==-2除零错误

覆盖两个分支只需i101和i1,但对于i==-2没有作用。

所以:

  • 不要简单的追求高的代码覆盖率
  • 高覆盖率测试用例不等于测试用例有效
  • 没覆盖的分支相当于该分支上的任何错误肯定都测不到

5、jacoco原理

jacoco使用插桩的方式来记录覆盖率数据,是通过一个probe探针来注入。

Jacoco是通过修改class文件的字节码来进行代码覆盖率统计。在原有class字节码中的指定位置插入探针字节码,形成新的字节码指令流。jacoco使用的是ASM字节码框架对字节码进行修改的。jacoco的探针实际是一个布尔值,当代码执行到探针位置时,将其置为true,该探针前面的代码会被认为执行过,然后对该部分代码对应的html文件中的css样式进行染色(红色表示未覆盖,绿色表示已覆盖,黄色表示部分覆盖),形成最终的覆盖率报告。

插桩模式有两种:

1、on-the-fly模式

JVM中通过-javaagent参数指定特定的jar文件启动Instrumentation的代理程序,代理程序在通过Class Loader装载一个class前判断是否转换修改class文件,将统计代码插入class,测试覆盖率分析可以在JVM执行测试代码的过程中完成。

2、offline模式

在测试之前先对文件进行插桩,生成插过桩的class或jar包,测试插过桩的class和jar包,生成覆盖率信息到文件,最后统一处理,生成报告。

3、on-the-fly和offline对比

on-the-fly更方便简单,无需提前插桩,无需考虑classpath设置问题。

存在一下情况不适合使用on-the-fly模式:

  • 不支持javaagent

  • 无法设置JVM参数

  • 字节码需要被转换成其他虚拟机

  • 动态修改字节码过程和其他agent冲突

  • 无法自定义用户加载类

6、jacoco应用

1、下载jacoco

官网:https://www.jacoco.org/jacoco/index.html

2、拷贝jar包

3、启动jacocoagent

打开cmd窗口启动项目

java -javaagent:jacocoagent.jar=includes=*,output=tcpserver,port=6300,address=localhost,append=true -jar demo-001.jar
# 使用-javaagent启动,includes在哪里插桩,output覆盖率以什么方式输出,demo-001.jar是被测项目的jar包
4、cli包dump生成exec文件

重新打开cmd窗口,生成exec文件

java -jar jacococli.jar dump --address 127.0.0.1 --port 6300 --destfile jacoco-demo.exec
# --address 127.0.0.1 --port 6300 指向jacocoagent启动IP和端口
# jacoco-demo.exec为生成exec文件名
5、cli包exec生成report报表
java -jar jacococli.jar report jacoco-demo.exec --classfiles D:\code\devops\SBD\target\classes --sourcefiles D:\code\devops\SBD\src\main\java --html html-report --xml report.xml --encoding=utf-8
# --sourcefiles 和 --classfiles 是本地被测项目源码和字节码路径
6、覆盖率报告

  • Instructions:Java 字节指令的覆盖率。执行的最小单位,和代码的格式无关。
  • Branches:分支覆盖率。注意,异常处理不算做分支。
  • Cxty(Cyclomatic Complexity):圈复杂度, Jacoco 会为每一个非抽象方法计算圈复杂度,并为类,包以及组(groups)计算复杂度。圈复杂度简单的说就是为了覆盖所有路径,所需要执行单元测试数量,圈复杂度大说明程序代码可能质量低且难于测试和维护。
  • Line:行覆盖率,只要本行有一条指令被执行,则本行则被标记为被执行。
  • Methods:方法覆盖率,任何非抽象的方法,只要有一条指令被执行,则该方法被计为被执行。
  • Classes:类覆盖率,所有类,包括接口,只要其中有一个方法被执行,则标记为被执行。注意:构造函数和静态初始化块也算作方法。

钻石代表分支覆盖情况:

  • 红钻:这一行没有分支被执行;
  • 黄钻:这一行中只有部分分支被执行;
  • 绿钻:这一行的所有分支都被执行;

背景颜色代表指令覆盖率

  • 红色背景:这一行并没有任何指令被执行;

  • 黄色背景:这一行的部分指令被执行;

  • 绿色背景:这一行的所有指令都被执行了;

7、jacoco增量覆盖

1、增量覆盖

增量覆盖:两次提交之间有哪些代码或者分支没有被覆盖。

目的:检测同一个测试用例在修改前后代码上的行覆盖情况。

假设两次提交代码变更如下:

if (x > 100)
-	print("apple");
+	print("lemon");
else
-	print("12345");
+	print("上山打老虎");

每行代码有三种状态:+ - 不变

修改前后跑同一个用例,每行有四种状态:修改前覆盖、修改前未覆盖,修改后覆盖、修改后未覆盖。

所以增量覆盖率总共有3*4 = 12种情况,比较重要有:新增代码没有覆盖;新增代码覆盖了;不变的代码修改前覆盖,修改后未覆盖等等。

2、增量应用

增量原理

image-20230702205210065

计算增量代码具体步骤:

  • 计算出两个版本的差异代码
  • 将差异代码在report阶段传给jacoco
  • 修改jacoco源码,生成报告时判断代码是否是增量代码,只有增量代码才去生成报告

jacoco二开:https://gitee.com/Dray/jacoco.git

增量代码获取:https://gitee.com/Dray/code-diff.git

使用方法:

1、jacoco客户端收集信息

java - javaagent:jacocoagent.jar=includes=*,output=tcpserver,port=6300,address=localhost,append=true -jar demo-001.jar

2、二开cli包生成exec文件

java -jar cli-0.8.7-diff.jar dump --address 127.0.0.1 --port 6300 --destfile jacoco-demo.exec

3、获取两次提交代码差异

java -jar code-diff.jar  # 启动diff项目
# 网址:http://127.0.0.1:8085/doc.html

4、二开cli包生成report增量报表

java -jar cli-0.8.7-diff.jar report jacoco-demo.exec --classfiles D:\code\devops\SBD\target\classes --sourcefiles D:\code\devops\sBD\src\main\java --html html-report-diff --xml report-diff.xml --diffcode "[{\ "c1assFi1e\":\"me/xz/contro11er/Usercontro1ler\",\"lines\":[{\"endLineNum\":74,\"startLineNum\":69,\"type\":\"INSERT\"}],\"methodInfos\":[{\"methodName\":\"login\",\"parameters\":\"User\"}],\"moduleName\":\"src\",\"type\":\"MODIFY\"}]" --encoding=utf-8
3、增量实战,整合jenkins

1、拉取被测代码

2、启动jacocoagent

pipeline {
	agent any
	
	environment {
        //jacocoagent.jar、code-diff.jar、c1i-0.8.7-diff.jar  // 下载地址
        agent_ur1 = "https://gitee.com/dzitcast/jacoco-c1i-diff.git"
        //jenkins git token
        git_token = "gitee"
        //被测项目jar名称
        jar_name = "springbootdemo-0.0.1-SNAPSHOT.jar"
        //被测项目名
        project_name = "springbootdemo";
	}
	stages {
		stage("下载最新被测代码"){
            steps{
                build 'springbootdemo'
            }
        }
        stage('获取jacoco agent jar包'){
        	steps{
        		script{
        			agent_file = "${env.WORKSPACE}/jacocoagent.jar"
        			diff_file = "${env.WORKSPACE}/code-diff.jar"
        			c1i_file = "${env.WORKSPACE}/c1i-0.8.7-diff.jar"
        			//可加可不加
					if(fileExists(agent_file) & fileExists(diff_file) & fi1eExists(cli_file)) {
						echo("jar存在")
					}e1se {
                        echo( "jar不存在,git拉取jar")
                        git credentialsId: "${git_token}", ur1: "${agent_ur1}"
					}
        		}
        	}
        }
        stage('启动jacocoagent&启动code-diff'){
            steps{
                script{
                    withEnv(['JENKINS_NODE_COOKIE=background_job']) {
                        sh "nohup java -javaagent:jacocoagent.jar=inc1udes=*,output=tcpserver,port=6300,address=loca1host,append=true -jar ${JENKINS_HOME}/workspace/${project_name}/target/${jar_name} &"
                        sh "nohup java -jar code-diff.jar &"
                    }
                }
            }
        }
	}
}

3、生成报告

pipeline {
	agent any
	environment {
        //增量代码项目接口地址
        diff_ur1 = "http://8.129.52.205:8085"
        //被测代码git地址
        gitur1 = "https://gitee.com/dzitcast/springbootdemo.git"
        //比较git版本号
        baseVersion = "a12beeldee54aa5a2a18d99e43eaec696f5b8931"
        //当前git版本号
        nowVersion = "871fe6ef7942efb1884dc1aa3b0d9b16d1357034"
        //git用户名和密码
        password = "123123"
        username = "123123"
        //jacocoagent项目名,加载jar使用
        jacoco_project_name = "jacoco增量-agent"
        //被测项目名,加载源码和c1ass文件
        test_project_name = "sbd"
        //二开jar包名
        cil_jar = "c1i-0.8.7-diff.jar"
        //读取文件
        diffcode = readFile encoding: 'utf-8 ', file: 'uniqueData'
    }
    stages{
    	stage('获取增量字符串并保存到本地文件'){
            steps{
                script{
                    sh "cur1 -x GET '${diff_ur1}/api/code/diff/git/list?baseversion=${baseVersjon]&gitUr1=${gitUr1]&nowVersion=${nowVersionj&password=${password}&username=${username}’ -H 'Request-Origion:SwaggerBootstrapUi' -H 'accept:*/*’-H 'TOKEN:' | awk -v FS=' \"uniqueData\":' '/uniqueData/{print \$2}' > uniqueData"
                    diffcode = readFile encoding: 'utf-8',file: 'uniqueData'
                }
            }
        }
        stage('生产exec文件'){
            steps{
                script{
                    sh "java -jar ${JENKINS_HOME]}/workspace/${jacoco_project_name}/${cil_jar} dump --address 127.0.0.1 --port 6300 --destfile jacoco-demo.exec"
    			}
            }
        }
		stage('全量报表'){
            steps{
                script{
                    sh "java -jar ${JENKINS_HOME}/workspace/${jacoco_project_name}/${cil_jar} report jacoco-demo.exec --c1assfiles ${(JENKINS_HoME ]}/workspace//${test_project_name}/target/classes --sourcefi1es ${JENKINS_HOME}/workspace/${test_project_name]/src/main/java --htm1 htm1-report --xm1 report.xm1 --encoding=utf-8"
                }
            }
        }
		stage('增量报表'){
            steps{
                script{
                    sh "java -jar
                    ${JENKINS_HOME] /workspace/${jacoco_project_name}/${cil_jar} report jacoco-demo.exec --c1lassfiles ${JENKINS_HOME}/workspace/${test_project_name}/target/classes --sourcefiles ${JENKINS_HOME}/workspace/${test_project_name}/src/main/java --html htm1-report-diff --xm1 report-diff.xm1 --encoding=utf-8 --diffcode ${diffCode]"
                    sh "tar -cvf report.tar htm1-report htm1-report-diff"
                }
            }
        }
        stage('发送邮件'){
            steps{
                emailext attachmentsPattern: 'report.tar ' , body:‘下载附件',subject:
                'jacoco报告',to: '2021938409@qq.com'
            }
		}
    }
}


4、杀死进程(code-diff和jacocoagent)
kill -9 $(ps ax | grep code-diff | grep -v grep | awk '{ print $1 }')

kill -9 $(ps ax l grep jacocoagent l grep -v grep | awk '{ print $1 }')