Jacoco的原理

覆盖率计数器

Jacoco使用一系列的不同的计数器来做覆盖率的度量计算。所有这些计数器都是从java的class文件中获取信息,这些class文件可以(可选)包含调试的信息在里面。即使在没有源码的情况下,这种方法也可以实时有效地对应用程序进行度量和分析。在大部分情况下,收集到的信息可以映射到源码,可视化到每一行代码的粒度。但这种方法还是有一些限制。这些class文件必须使用调试信息来编译,这样才可以计算行的覆盖率和提供出源码的高亮。但不是所有的JAVA语言的结构都可以直接编译成一致的二进制代码。在这种情况下,java 编译器会创建所谓的“合成”代码,会导致产生一些不期望得到的覆盖率结果。

指令(C0 Coverage)

Jacoco最小的计数单元是单个java二进制代码指令。指令覆盖率提供了代码是否被执行的信息。这个度量完全独立源码格式,并且总是可用,即使class文件里面没有调试信息。

分支(C1 Coverage)

Jacoco也计算分支的覆盖率,包括所有的if和switch语句。这个度量计算一个方法里面的总分支数,确定执行和不执行的分支数量。分支覆盖率总是可用的,即使class文件里面没有调试信息。注意异常处理是不在分支度量里面统计的。

如果class文件使用调试信息编译的话,产生的覆盖率可以映射到源码行并且高亮提示:

  • 没有覆盖:在这一行中没有分支被执行(红色方块)
  • 部分覆盖:这一行的分支中只有一部分被执行(黄色方块)
  • 完全覆盖:这一行的所有分支都被执行(绿色方块)

圈复杂度

Jacoco同样可以为每一个非抽象方法计算复杂度,最终计算出类、包和组的复杂度。根据由McCabe1996圈复杂度的定义是,在(线性)组合中,计算在一个方法里面所有可能路径的最小数目。所以复杂度可以作为度量单元测试是否有完全覆盖所有场景的一个依据。复杂度即使是在没有调试信息的情况下也可以计算。

圈复杂度V(G)的正式定义是基于方法的控制流图的有向图表示:

v(G) = E – N + 2

E是边界的数量,N是节点的数量。Jacoco 基于下面的方程来计算复杂度,B是分支的数量,D是决策点的数量:

v(G) = B – D + 1

基于每个分支的被覆盖情况,Jacoco也为每个方法计算覆盖和缺失的复杂度。缺失的复杂度同样表示测试案例没有完全覆盖到这个模块。注意Jacoco不将异常处理作为分支,try/catch块也同样不增加复杂度。

所有的class文件使用debug信息编译之后,就可以计算行的覆盖率信息。一行源代码是否被执行,要看这一行中是否至少有一个指令被执行。

由于实际上一行代码一般被编译成多个二进制代码指令,这样源码在高亮显示时,会显示成3种不同的状态:

  • 没有覆盖:这一行中没有指令被执行(红色背景)
  • 部分覆盖:这一行中只有一部分指令被执行(黄色背景)
  • 完全覆盖:这一行中所有指令都被覆盖(绿色背景

方法

每一个非抽象方法至少包含一个指令。一个方法是否执行取决于方法中是否有至少一个指令被执行。在Jacoco中,构造器和静态初始化同样会像方法一样统计。其中一些方法可能没有可以直接对应的源码,比如默认构造器或常量的初始化命令。

一个方法是否执行取决于类中是否有至少一个方法被执行。注意Jacoco认为构造器和静态初始化都是方法。Java的接口一般包含静态初始化,所以接口也同样被认为是可执行的类。

Comments