Jacoco全称是java code coverage,顾名思义它是一个开源的覆盖率工具,它针对的开发语言是java,其使用方法很灵活,可以嵌入到Ant、Maven中。
代码覆盖率的意义
1.了解测试情况
测试过程中覆盖和未覆盖的地方,可能存在的风险。分析未覆盖代码,反推在测试设计是否充分,进一步明确测试设计阶段的问题。
2.发现测试死角、冗余代码、历史废弃代码
有助于发现多个测试用例都覆盖不到的代码,收集方法覆盖率,为废弃的代码提供依据。
3.度量自动化用例
为自动化用例提供覆盖率统计情况,分析覆盖率报告,完善自动化用例。
4.精准回归
构建代码调用关系,精准的确定回归测试范围,避免了全量回归造成测试资源的浪费。
覆盖率统计的指标
Jacoco包含了多种尺度的覆盖率计数器,包含指令级覆盖(Instructions,C0coverage),分支(Branches,C1coverage)、圈复杂度(CyclomaticComplexity)、行覆盖(Lines)、方法覆盖(non-abstract methods)、类覆盖(classes)。
行覆盖率:度量被测程序的每行代码是否被执行,判断标准行中是否至少有一个指令被执行。
类覆盖率:度量计算class类文件是否被执行。
分支覆盖率:度量if和switch语句的分支覆盖情况,计算一个方法里面的总分支数,确定执行和不执行的 分支数量。
方法覆盖率:度量被测程序的方法执行情况,是否执行取决于方法中是否有至少一个指令被执行。
指令覆盖:计数单元是单个java二进制代码指令,指令覆盖率提供了代码是否被执行的信息,度量完全 独立源码格式。
圈复杂度:在(线性)组合中,计算在一个方法里面所有可能路径的最小数目,缺失的复杂度同样表示测 试案例没有完全覆盖到这个模块。
Jacoco工作原理
这个图包含了几种不同的收集覆盖率信息的方法,每种方法的实现方法都不一样,标有旗帜图标的部分是jacoco比较有特色的地方。
Code Coverage | 代码覆盖率 |
---|---|
Runtime Profiling | 运行时分析 |
JVMPI | Java虚拟机监视程序接口 |
JVMTI | Java虚拟机工具接口 |
Instrumentation | 注入 |
Source | 源码注入 |
Byte Code | 字节码注入 |
Offline | 离线模式 |
On-The-Fly | 在线模式 |
Replace | 替换方式 |
Inject | 注入方式 |
Class Loader | 类加载 |
Java Agent | 代理 |
上表红色部分为jacoco支持的,详细的解释一下:
jacoco在Byte Code时使用的ASM技术修改字节码方法,可以修改Jar文件、class文件字节码文件。
JaCoCo同时支持on-the-fly和offline的两种插桩模式。
On-the-fly插桩
JVM中通过-javaagent参数指定特定的jar文件启动Instrumentation的代理程序,代理程序在通过Class Loader装载一个class前判断是否转换修改class文件,将统计代码插入class,测试覆盖率分析可以在JVM执行测试代码的过程中完成。
Offline模式
在测试前先对文件进行插桩,然后生成插过桩的class或jar包,测试插过桩 的class和jar包后,会生成动态覆盖信息到文件,最后统一对覆盖信息进行处理,并生成报告。
On-the-fly和offline比较
On-the-fly模式更方便简单进行代码覆盖分析,无需提前进行字节码插桩,无需考虑classpath 的设置。
存在如下情况不适合on-the-fly,需要采用offline提前对字节码插桩:
运行环境不支持java agent。
部署环境不允许设置JVM参数。
字节码需要被转换成其他的虚拟机如Android Dalvik VM。
动态修改字节码过程中和其他agent冲突。
无法自定义用户加载类。
JaCoCo执行最小的java版本
最小需要Java1.5
字节码处理方式
JaCoCo通过注入来修改和生成java字节码,使用的是ASM库。
那么什么是ASM库呢?
ASM 库是一款基于 Java 字节码层面的代码分析和修改工具。ASM 可以直接生产二进制的 class 文件,也可以在类被加载入 JVM 之前动态修改类行为。
java方法控制流分析
jacoco是如何在字节码注入的?
先看一段java程序:
public static void example() { a(); if(cod()) { b(); } else { c(); } d();}
编译转换成字节码后,内容如下:
public static example()V INVOKESTATIC a()V INVOKESTATIC cond()z IFEQ L1 INVOKESTATIC b()V GOTO L2L1:INVOKESTATIC c()VL2:INVOKESTATIC d()VRETURN
我们知道jacoco是字节码注入方式,它是通过一个Probe探针的方式来注入的,具体如下:
探针是字节指令集插入到java方法中,程序执行后可以被记录,它不会改变原有代码的行为。
我们看看探针前后插入比较:
黄颜色的部分就是探针注入的地方。
JaCoCo是根据控制流Type来采用不同的探针插入策略的。
一个用java字节码定义的java方法的控制流图可能有以下的type,每一个type连接一个源指令与目标指令,type不同探针的注入策略也会不同,如下是type定义:
Type | Source | Target |
---|---|---|
ENTRY | -- | First instruction in method |
SEQUENCE | Instruction,exceppt GOTO,xRETURN,THROW | Subsequent instruction |
JUMP | GOTO,IFx,TABLESWITCH or LOOKUPSWITH instruction | Target instruction |
EXHANDLER | Ant instruction in handler scope | Target instruction |
EXIT | xRETURN or THROW instruction | -- |
EXEXIT | Any instruction | -- |
探针不改变该方法的行为,但记录他们已被执行的事实,从理论上讲,可以在控制流图的每一个边插入一个探针,作为探针实现本身需要多个字节码指令,这将增加几倍的类文件的大小和执行速度。
以上是jacoco插桩原理,如果想深入了解,可以去看看它的源码实现。