目录
1、什么是jacoco?
jacoco是一个开源的代码覆盖率工具,针对java语言,其使用方法很灵活,可以嵌入到Ant、Maven中;可以作为Eclipse插件,可以使用其JavaAgent技术监控Java程序等等。代码覆盖率一般又分为单元测试覆盖率和功能测试覆盖率,对于开发人员,一般比较关注单元测试覆盖率,而对于测试人员,一般更关注的是功能测试覆盖率。
2、为什么要做代码覆盖率统计?
代码覆盖率是衡量测试质量的一个重要指标,在进行了一轮或者多轮各种类型的测试之后,如何能够比较直观地看出,我们当前迭代的测试工作都覆盖了哪些功能点,又有哪些功能点被遗漏掉了。通过对代码的覆盖率进行统计,能够比较直观地看出哪些代码在测试的时候有被覆盖到,哪些代码被遗漏了。虽然覆盖了并不能代表逻辑一定正确,但如果测试覆盖到了绝大部分的代码,那么我们对版本的质量保障就会有一个较为合理的信心。
3、jacoco原理简析
1)java代码的运行原理
在了解jacoco原理之前,我们先来了解一下java代码的运行原理:
我们都知道java代码是运行在java虚拟机(JVM)上的,JVM相当于一个虚拟的计算机,符合约定的指令均可在上面执行。java编译后的class文件就是一种符合JVM的字节码指令集合,所以可以在JVM上执行。所以JVM其实指的不是运行java代码的虚拟机,它并不关心字节码是由哪种语言编译而来的,而是只要符合该虚拟机指令的文件均可在上面执行,如Kotlin、Groovy、JRuby、Jython、Scala等编译后也可以在JVM上执行。所以java代码在JVM里运行的时候,实际运行的是编译后的class文件字节码指令流,如果想要改变类的行为,分析类的信息等,只需要修改对应的字节码即可。
2)jacoco原理介绍
jacoco即是通过修改class文件的字节码来进行代码覆盖率统计的。即,在原有class字节码中的指定位置插入探针字节码,形成新的字节码指令流。jacoco使用的是ASM字节码框架对字节码进行修改的(关于ASM框架,可以参考这篇文章:https://www.cnblogs.com/liuling/archive/2013/05/25/asm.html)。jacoco的探针实际是一个布尔值,当代码执行到探针位置时,将其置为true,该探针前面的代码会被认为执行过,然后对该部分代码对应的html文件中的css样式进行染色(红色表示未覆盖,绿色表示已覆盖,黄色表示部分覆盖),形成最终的覆盖率报告。
3)jacoco的插桩(插入探针)模式
offline模式:编译时插桩,在测试前先对文件进行插桩,然后生成插过桩的class或jar包,测试插过桩 的class和jar包后,会生成动态覆盖信息到文件,最后统一对覆盖信息进行处理,并生成报告。
on-the-fly模式:运行时插桩,JVM中通过-javaagent参数指定特定的jar文件启动Instrumentation的代理程序,启动jvm实例会调用程序里面的premain方法,通过Class Loader装载一个class前判断是否转换修改class文件,将统计代码插入class,测试覆盖率分析可以在JVM执行测试代码的过程中完成。addTransformer 方法并没有指明要转换哪个类,转换发生在 premain 函数执行之后,main 函数执行之前(转换发生在JVM定义类之前),这时每装载一个类,transform 方法就会执行一次,看看是否需要转换。
4)jacoco启动过程
a. 初始化Agent
- 最主要的是初始化一个RuntimeData,底层是new了一个ExecutionDataStore对象来存储覆盖率数据。
- 根据启动参数来初始化一个IAgentOutput,用来把生成的覆盖率数据传递给外部系统。
b. 创建IRuntime对象,并且启动。(需要传入上面的RuntimeData对象)。
c. 插桩。 调用ClassFileTransformer的addTransformer 入参ClassFileTransformer
5)插桩前后比较
4、jacoco安装和使用
jacoco的安装和使用可参考我之前写的一篇文章,这里不再赘述。文章链接:jacoco原理和实践
5、新增代码覆盖率统计
该新增代码覆盖率统计是在原jacoco全量覆盖率统计报告的基础上进行的,根据git diff进行新增代码覆盖率的统计。
1)获取diff文件
diff文件格式大体如下:
diff --git a/pom.xml b/pom.xml
index 33e2670..cb9ac97 100644
--- a/pom.xml
+++ b/pom.xml
@@ -19,8 +19,8 @@
<argLine> -XX:MaxPermSize=256M -Xmx1024m -Dfile.encoding=UTF-8 -da </argLine>
<sonar-project-artifactId>${project.artifactId}</sonar-project-artifactId>
<jacocoCoverageDir>**</jacocoCoverageDir>
- <ykt-biz-dependencies.version>1.0.150</ykt-biz-dependencies.version>
- <ykt-infrastructure-dependencies.version>1.0.48</ykt-infrastructure-dependencies.version>
+ <ykt-biz-dependencies.version>1.0.153</ykt-biz-dependencies.version>
+ <ykt-infrastructure-dependencies.version>1.0.59</ykt-infrastructure-dependencies.version>
<ykt-third-party-dependencies.version>1.0.31</ykt-third-party-dependencies.version>
<spring-cloud-sleuth.version>2.0.0.M7</spring-cloud-sleuth.version>
</properties>
解释一下:
1) 第一行表示结果为git格式的diff.
diff –git a/pom.xml b/pom.xml
进行比较的是,a版本的pom.xml(即变动前)和b版本的pom.xm(即变动后).
2) 第二行表示两个版本的git哈希值(index区域的33e2670对象,与工作目录区域的cb9ac97对象进行比较),最后的六位数字是对象的模式(普通文件,644权限).
3)第三四行表示进行比较的两个文件.
— a/pom.xml
+++ b/pom.xml
“—“表示变动前的版本,”+++”表示变动后的版本.
4)第五行描述文件变动的位置,用两个@@作为起始和结尾@@ -19,8 +19,8 @@
前面的“-19,8”:减号表示变动前文件,19表示从第19行开始,“8”表示连续8行
后面的“+19,8 ”:加号表示变动后文件,19表示从第19行开始,“8表示连续8行”
合在一起就表示下面是变动前文件从19行开始的连续8行,是变动后文件从19行开始的连续8行
5)6-15行是变动的具体内容,前面减号表示删除的内容,前面+号表示新增的内容
2)根据上面的diff文件,按照文件格式,正则匹配提取出新增的代码行号,将文件对应的新增代码行号存入字典中
for line in diff:
if line.startswith('diff --git'):
# 进入新的block
if file_name != "":
ret[file_name] = diff_lines
file_name = re.findall('b/(\S+)$', line)[0]
diff_lines = []
current_line = 0
elif re.match('@@ -\d+,\d+ \+(\d+),\d+ @@', line):
match = re.match('@@ -\d+,\d+ \+(\d+),\d+ @@', line)
current_line = int(match.group(1)) - 1
elif line.startswith("-"):
continue
elif line.startswith("+") and not line.startswith('+++'):
current_line += 1
diff_lines.append(current_line)
else:
current_line += 1
ret[file_name] = diff_lines
3)根据变更的行号,修改原覆盖率html报告中对应行的css样式
在原有style的class中增加“diff”,然后对加了diff样式的行前面增加一个蓝色的方块,表示为一个新增行,并且统计新增代码总行数和覆盖的总行数,方便进行新增代码覆盖率计算;
for i in range(1, len(content)):
if i + 1 in diff_lines:
match = re.search('class="([^"]+)"', content[i])
if match:
content[i] = re.sub('class="([^"]+)"', lambda m: 'class="{}-diff"'.format(m.group(1)), content[i])
css_class = match.group(1)
new_line_count += 1
if css_class.startswith("fc") or css_class.startswith("pc"):
cover_line_count += 1
新增代码标记如图所示:
4)更新index.html文件,增加新增代码覆盖率报告内容:
a、更新新增覆盖率红绿区域占比
if totle_new_line_count != 0:
red_count = int(119*(totle_new_line_count-totle_cover_line_count)/totle_new_line_count)
green_count = int(119*totle_cover_line_count/totle_new_line_count)
else:
red_count = 119
green_count = 0
str5 = '<td class="bar" id="new_add_b0">' \
'<img src="jacoco-resources/redbar.gif" width="{}" height="10" title="{}" alt="{}" />' \
'<img src="jacoco-resources/greenbar.gif" width="{}" height="10" title="{}" alt="{}" /></td>\n'.format(red_count,totle_new_line_count,totle_new_line_count,green_count,totle_cover_line_count,totle_cover_line_count)
str6 = '<td class="ctr2" id="new_add_c0">{}%</td>\n'.format(bfb)
b、展示新增代码所在类的列表并配置超链接
str7 = '<style type="text/css">.added_div{margin-top:50px;margin-bottom:20px;}' \
'.added_table{background-color:green;text-align:center;width:1200px; height:200px; font-size:20px;}.add_table_title{background-color:grey;}' \
'.added_class{text-align:left;}</style><div class="added_div" >' \
'新增代码列表如下:<p>current_branch:'+ str(self.repo.active_branch) + '</p><p>old_branch:' + str(self.old_version) + '</p>' \
'<table border="1" cellspacing="0" class="added_table">' \
'<tr class="add_table_title"><td>类名</td><td>新增(行)</td><td>覆盖(行)</td></tr>'
str9 = '</table></div>'
content1[0] = content1[0].replace('>',">\n")
with open(html_file_name, 'w') as fpw:
fpw.write("".join(content1[0]))
with open(html_file_name,'r') as fpr:
content1 = fpr.readlines()
for i in range(0,len(content1)):
match1 = content1[i].find('Element')
match2 = content1[i].find('Total')
match3 = content1[i].find('id="a0"')
match4 = content1[i].find('</table>')
content2.append(content1[i])
if match1 >= 0:
content2.append(str1)
content2.append(str2)
if match2 >= 0:
content2.append(str3)
content2.append(str4)
if match3 >= 0:
content2.append(str5)
content2.append(str6)
if totle_new_line_count != 0:
if match4 >= 0:
content2.append(str7)
for p in range(0,len(print_str)):
package_class_info = print_str[p]
package_class = package_class_info["package"] + "." + package_class_info["class"]
str8 = '<tr><td class="added_class"><a href="./{}_coverage/{}/{}.java.html">{}</a></td><td>{}</td><td>{}</td></tr>'.format(self.report_name,package_class_info['package'],package_class_info['class'],package_class,package_class_info['new_line'],package_class_info['cover_line'])
content2.append(str8)
content2.append(str9)
结果如下:
至此,即可完成新增代码覆盖率的统计。
6、新增代码覆盖率在项目中的实践
在项目中,我们通常用于评估当前迭代接口自动化的覆盖情况,流程如下:
接口自动化用例编写 —> 将当前迭代分支部署在自动化环境 —> 在自动化环境执行当前迭代的接口自动化用例 —> 执行新增代码覆盖率统计
通过新增代码覆盖率能辅助发现一些问题,如在最近一次迭代的执行过程中,通过新增代码覆盖率发现了如下问题:
1)当前迭代的分支即将上线,但仍没合master
迭代涉及的接口只有三个,且已编写接口自动化用例进行覆盖,但统计出来的新增覆盖率较低,查看下面新增代码的列表发现:新增的代码并不是当前迭代的,而前一天晚上刚上线的内容。故可知,当前分支没有合master代码,于是通知开发同学合并master代码,并重新进行验证;
2)接口覆盖的场景不够全面
由上图发现,当前迭代的新增代码的接口自动化覆盖率为86.2%,仍有部分未覆盖,通过查看具体代码发现:
如传入的pageSize小于0或者大于100的情况下的校验、上传文件过大等未覆盖到,于是就根据未覆盖的代码点,进行接口用例的补充。通过补充相关用例后,接口覆盖率得到较为明显的提升:
3)发现一些异常的bug
等校验上传文件过大接口时发现,当我上传了一个较大文件时,接口直接报内部异常了,并没有走到上传文件过大的提示中去,后来经多次尝试,发现当记录数大于10000的时候,会报错,告知开发后,开发同学忽然想起来自己有设置10000的上限,但设置上限应该也不会有问题,因为代码中有对上传记录过多时的校验提示,如下图
但实际并没有提示,后来发现在抛出异常时,异常的类型写错,不应该是RuntimeException,而应该是通知到前端的FrontNotifiableRuntimeException,经过修改后,接口可正常运行。
4)发现配置问题
在自动化环境执行“上传大文件”相关用例时失败,但是在提测环境执行成功,服务器日志显示 /home/appops/uploadTmpDir/upload__242d4408_1742e78a540__7ffe_00000025.tmp 这个文件找不到,然后在服务器的/home/appops/路径下手动创建了uploadTmpDir文件夹,再次运行就成功了。反馈给开发同学排查发现,提测环境之前有人手动创建过这个目录,所以正常,而包括线上环境在内的其他环境的机器没有这个目录,如果不事先手动创建,上线后大文件上传会直接报错。
以上问题虽然通过手工的方式也能发现,但是对于刚入职不久等经验不太丰富的测试同学来说,可能会遗漏掉一些情况(上述几个问题即为迭代测试同学遗漏的测试点),所以这种方式也是保障我们迭代的交付质量的一种比较有效的方式。
今天的文章jacoco 原理_cyquant原理分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/78518.html