java代码覆盖率jacoco原理及案例分析
目录:https://blog.csdn.net/liuxueyi521/article/details/87898581
实战案例:视频内容1:https://edu.csdn.net/course/detail/19104
实战案例:视频内容2:http://edu.51cto.com/course/17028.html?source=so
1、jacoco 顺序时插入的探针:
2、if分支时的字节码探针
探针插入策略
探针是可以在现有指令之间插入的附加指令。它们不会改变方法的行为,但会记录它们已被执行的事实。可以认为探针放置在控制流图的边缘上。从理论上讲,我们可以在控制流图的每个边缘插入一个探针。由于探测器实现本身需要多个字节码指令,这会多次增加类文件的大小,并显着降低已检测类的执行速度。幸运的是,这不是必需的,实际上我们每个方法只需要一些探针,具体取决于方法的控制流程。例如,没有任何分支的方法仅需要单个探测。
如果已经执行了探测,我们知道已经访问了相应的边缘。从这个边缘我们可以得出其他前面的节点和边:
- 如果访问了边,我们知道该边的源节点已被执行。
- 如果节点已被执行且节点仅是一个边缘的目标,则我们知道已经访问了该边缘。
递归地应用这些规则允许确定方法的所有指令的执行状态 - 假设我们在正确的位置具有探针。因此,JaCoCo插入探针
- 在每个方法退出(返回或抛出)和
- 在目标指令是多个边缘的目标的每个边缘。
我们记得,探针只是一小部分需要在控制流边缘插入的附加指令。下表说明了在不同边缘类型的情况下如何添加此额外指令。
类型 | 之前 | 后 | 备注 |
序列 | ![]() | ![]() | 在简单序列的情况下,将探针简单地插入两个指令之间。 |
JUMP(无条件) | ![]() | ![]() | 由于在任何情况下都执行无条件跳转,我们也可以在GOTO指令之前插入探针。 |
JUMP(有条件的) | ![]() | ![]() | 向条件跳转添加探测器有点棘手。我们反转操作码的语义,并在条件跳转后立即添加探测。随后的GOTO 指令我们跳转到原始目标。请注意,此方法不会引入向后跳转,如果堆栈上有未初始化的对象,则会导致Java验证程序出现问题。 |
出口 | ![]() | ![]() | 实际上离开方法的RETURN和THROW语句的本质是我们在这些语句之前添加探测。 |
现在让我们看看这个规则如何应用于上面的示例代码段。我们看到该 INVOKE d()
指令是唯一具有多个传入边缘的节点。因此,我们需要在这些边上放置探针,并在唯一的出口节点上放置另一个探针。结果显示在上图的右侧框中。
线之间的额外探测
到目前为止描述的探针插入策略不考虑例如从调用的方法抛出的隐式异常。如果两个探测器之间的控制流被未使用throw
语句显式创建的异常中断,则其间的所有指令都被视为未被覆盖。这会导致意外的结果,尤其是当指令块跨越多行源代码时。
因此,只要后续行包含至少一个方法调用,JaCoCo就会在两行的指令之间添加额外的探测。这限制了从方法调用到单行源的隐式异常的影响。该方法仅适用于使用调试信息(行号)编译的类文件,并且不考虑除方法调用(例如NullPointerException
或ArrayIndexOutOfBoundsException
)之外的其他指令的隐式异常 。
探索实施
代码覆盖率分析是一个运行时指标,它提供被测软件的执行细节。这需要详细记录已执行的指令(指令覆盖)。对于分支覆盖,还必须记录决策的结果。在任何情况下,执行数据都由所谓的探测器收集:
甲探针是可以被插入到一个Java字节码方法的指令序列。执行探测时,会记录此事实,并可由coverage运行时报告。探针不得更改原始代码的行为。
探测的唯一目的是记录它至少执行过一次。探测器不记录它被调用的次数或收集任何时间信息。后者超出了代码覆盖率分析的范围,更多的是在性能分析工具的目标中。通常需要将多个探针插入每个方法中,因此需要识别探针。此外,探针实现和它依赖的存储机制需要是线程安全的,因为多线程执行是java应用程序的常见场景(尽管不适用于普通单元测试)。探针不得对方法的原始代码产生任何副作用。他们也应该增加最小的开销。
因此,总结执行探针的要求:
- 记录执行
- 鉴定不同的探针
- 线程安全
- 对应用程序代码没有副作用
- 最小的运行时开销
JaCoCo使用boolean[]
每个类的数组实例实现探测。每个探针对应于此数组中的条目。无论何时执行探测,都将条目设置为true
以下四个字节码指令:
<span style="color:#000000"><span style="color:#000000">ALOAD探针
阵列xPUSH
探针
ICONST_1 BASTORE</span></span>
请注意,此探测代码是线程安全的,不会修改操作数堆栈或修改局部变量,也是线程安全的。它也不会通过外部调用离开该方法。唯一的先决条件是探测器阵列可用作局部变量。为此,在每个方法的开头,需要添加额外的检测代码以获取与所属类关联的数组实例。为了避免代码重复,初始化被委托给一个静态私有方法$jacocoinit()
,该方法 被添加到每个非接口类中。