如何使⽤JaCoCo分析java单元测试覆盖率
前⾔
随着敏捷开发的流⾏,编写单元测试已经成为业界共识。但如何来衡量单元测试的质量呢?有些管理者⽚⾯追求单元测试的数量,导致底下的开发⼈员投机取巧,编写出⼤量的重复测试,数量上去了,质量却依然原地踏步。相⽐单纯追求单元测试的数量,分析单元测试的代码覆盖率是⼀种更为可⾏的⽅式。
JaCoCo(Java Code Coverage)就是⼀种分析单元测试覆盖率的⼯具,使⽤它运⾏单元测试后,可以给出代码中哪些部分被单元测试测到,哪些部分没有没测到,并且给出整个项⽬的单元测试覆盖情况百分⽐,看上去⼀⽬了然。
EclEmma 是基于 JaCoCo 的⼀个 Eclipse 插件,开发⼈员可以⽅便的和其交互。因此,本⽂先从 EclEmma ⼊⼿,给读者⼀个直观的体验。
使⽤ EclEmma 在 Eclipse 中查看单元测试覆盖率
EclEmma 是基于 JaCoCo 的 Eclipse 插件,使⽤它,开发⼈员可以直观地看到单元测试的覆盖情况。
安装 EclEmma
打开 Eclipse 的软件市场,在其中搜索 EclEmma,到后完成安装,如下图所⽰:
图 1. 安装 EclEmma
安装完成后,Eclipse 的⼯具条⾥会多出下⾯这样⼀个图标:
图 2. Coverage 图标
分析单元测试覆盖率
成功安装 EclEmma 后,就可以试着⽤它来分析项⽬的单元测试覆盖率了。为了⽅便演⽰,我们使⽤ Eclipse 创建了⼀个标准 Java ⼯程。其中包含⼀个数学⼯具类,⽤来计算三个数中的最⼤值,代码如下:
清单 1. 数学⼯具类
package com.dw.math;
public class MathUtil {
public static int max(int a, int b, int c){
if(a > b){
if(a > c){
return a;
}else{
return c;
}
}else{
if(b > c){
return b;
}else{
return c;
java64位
}
}
}
}
可以看到,这⾥的算法稍微有点复杂,使⽤到了多个条件判断分⽀,因此,特别适合为其编写单元测试。第⼀个版本的单元测试如下:
清单 2. 第⼀个版本的单元测试
package com.dw.math;
import static org.junit.Assert.*;
import org.junit.Test;
public class MathUtilTest {
@Test
public void test_max_1_2_3() {
assertEquals(3, MathUtil.max(1, 2, 3));
}
}
试着运⾏⼀下单元测试覆盖率分析⼯具:40.0%!似乎不太理想。展开分析报告,双击后在编辑器⾥可以看到覆盖情况被不同的颜⾊标识出来,其中绿颜⾊表⽰代码被单元测试覆盖到,黄⾊表⽰部分覆盖,红⾊则表⽰完全没有覆盖到,如下图所⽰:
图 3. 单元测试覆盖率报告
让我们尝试多加⼀些单元测试,来改善这种情况,请看下⾯第⼆个版本的单元测试:
清单 3. 第⼆个版本的单元测试
package com.dw.math;
import static org.junit.Assert.*;
import org.junit.Test;
public class MathUtilTest {
@Test
public void test_max_1_2_3() {
assertEquals(3, MathUtil.max(1, 2, 3));
}
@Test
public void test_max_10_20_30() {
assertEquals(30, MathUtil.max(10, 20, 30));
}
@Test
public void test_max_100_200_300() {
assertEquals(300, MathUtil.max(100, 200, 300));
}
}
测试覆盖率还是 40.0%!虽然我们额外再加了两个测试,但覆盖率没有半点提升,这些单元测试其实是重复的,它们在重复测试同⼀段代码。如果单纯追求单元测试的数量,那么这⽆疑会给管理者造成错觉,他们觉得单元测试的数量增加了,软件的质量更有保证了;⽽对于那些喜欢偷懒的程序员,也蒙混过关,但却给软件质量埋下了隐患。让我们删掉这些重复的单元测试,重新思考⼀下怎么测试这个⽅法。
⾸先我们要测试正常情况,这其中⼜包含 3 种情况:第⼀个参数最⼤,第⼆个参数最⼤,以及最后⼀个参数最⼤。然后我们还需测试⼏种特殊情况,⽐如三个参数相同,三个参数中,其中两个相同。让我们照此思路重新编写单元测试:
清单 4. 第三个版本的单元测试
package com.dw.math;
import static org.junit.Assert.*;
import org.junit.Test;
public class MathUtilTest {
@Test
public void test_max_1_2_3() {
assertEquals(3, MathUtil.max(1, 2, 3));
}
@Test
public void test_max_1_3_2() {
assertEquals(3, MathUtil.max(1, 3, 2));
}
@Test
public void test_max_3_2_1() {
assertEquals(3, MathUtil.max(3, 2, 1));
}
@Test
public void test_max_0_0_0(){
assertEquals(0, MathUtil.max(0, 0, 0));
}
@Test
public void test_max_0_1_0(){
assertEquals(1, MathUtil.max(0, 1, 0));
}
}
再次运⾏单元测试分析⼯具:75.0%!这次⽐以前有了很⼤提升,但是结果还不能令⼈满意,打开分析报告可以看到,有⼀个分⽀还是没有覆盖到,如图所⽰:
图 4. 单元测试覆盖率报告
阅读代码可以看出,这种情况是指第⼀个参数⼤于第⼆个参数,却⼩于第三个参数,因此我们再增加⼀个单元测试:
清单 5. 再增加⼀个单元测试
@Test
public void test_max_2_1_3() {
assertEquals(3, MathUtil.max(2, 1, 3));
}
再运⾏⼀遍单元测试分析⼯具:100.0%!终于我们的单元测试达到了全覆盖,这样我们对⾃⼰开发的代码更有信⼼了。当然,我们在这⾥并不是为了单纯的追求这个数字,在增加单元测试覆盖率的诱导下,我们重新理清了测试的步骤,写出了更有意义、更全⾯的单元测试。⽽且根据单元测试分析⼯具给的反馈,我们还发现了先前没有想到的情形。因此,单元测试的覆盖率并不只是⼀个为了取悦管理者的数据,它实实在在地帮助我们改善了代码的质量,增加了我们对所编写代码的信⼼。
给管理者的单元测试覆盖率报告
管理者天⽣喜欢阅读报告。他们不会屈尊坐在你的办公桌前,让你向他展⽰ Eclipse 中这⼀⽚花花绿绿的东西。⽽且这份报告对他们⾄关重要,他们需要⽤它向上级汇报;年底回顾时,他们也可以兴奋地宣称产品的单元测试覆盖率增加了多少。作为⼀名开发⼈员,我们很⼤⼀部分⼯作量就在于满⾜管理者的这种需求。因此,本节我们讨论如何将 JaCoCo 集成到 Ant 脚本中,⽣成漂亮的单元测试覆盖率报告。
准备⼯作
在集成 JaCoCo 前,必须先确保你的 Java ⼯程有⼀个可执⾏的 Ant 构建脚本。⼀个简单的 Ant 构建脚本⼀般会执⾏如下任务:编译(包括编译⼯程代码和测试代码)、打包和执⾏单元测试。下⾯是本⽂⽰例 Java 项⽬所⽤的 Ant 构建脚本,读者可结合⾃⼰的项⽬及⽂件路径,在此基础之上进⾏修改。
清单 6. l
<project name="math" basedir="." default="junit">
<!--预定义的属性和 classpath -->
<property name="src.dir" value="src" />
<property name="test.dir" value="test" />
<property name="build.dir" value="build" />
<property name="classes.dir" value="${build.dir}/classes" />
<property name="tests.dir" value="${build.dir}/tests" />
<property name="jar.dir" value="${build.dir}/jar" />
<property name="lib.dir" value="lib" />
<path id="classpath">
<fileset dir="${lib.dir}" includes="**/*.jar" />
</path>
<!--清除上次构建 -->
<target name="clean">
<delete dir="${build.dir}" />
</target>
<!--编译代码,包括单元测试 -->
<target name="compile" depends="clean">
<mkdir dir="${classes.dir}" />
<mkdir dir="${tests.dir}" />
<javac srcdir="${src.dir}" destdir="${classes.dir}" />
<javac srcdir="${test.dir}" destdir="${tests.dir}">
<classpath>
<path refid="classpath" />
<path location="${classes.dir}" />
</classpath>
</javac>
</target>
<!--打包 -->
<target name="jar" depends="compile">
<mkdir dir="${jar.dir}" />
<jar destfile="${jar.dir}/${ant.project.name}.jar" basedir="${classes.dir}">
</jar>
</target>
<!--运⾏单元测试 -->
<target name="junit" depends="jar">
<junit printsummary="yes">
<classpath>
<path refid="classpath"/>
<path location="${classes.dir}" />
<path location="${tests.dir}" />
</classpath>
<batchtest fork="yes">
<fileset dir="${test.dir}" includes="**/*Test.java"/>
</batchtest>
</junit>
</target>
</project>
集成 JaCoCo
⾸先需要从然后就是使⽤ JaCoCo 官⽹下载需要的版本,然后将下载得到的压缩⽂件解压,将其中的 jacocoant.jar 拷贝⾄ Java ⼯程下存放第三⽅jar 包的⽬录,在⽰例⼯程⾥,我有⼀个和 src 平级的 lib ⽬录,jacocoant.jar 就放到了这个⽬录底下,读者可根据⾃⼰的项⽬组织结构做相应调整。然后我们需要在 Ant 构建脚本中定义新的任务:
清单 7. 定义新的构建任务
<taskdef uri="antlib:org.jacoco.ant" resource="org/jacoco/l">
<classpath refid="classpath" />
</taskdef>
现在就可以在 Ant 构建脚本中使⽤ JaCoCo 了。需要注意的是,为了避免命名冲突,需要给 Ant 构建脚本加⼊新的 XML 命名空间:
清单 8. 加⼊新的 JaCoCo 命名空间
<project name="math" basedir="." xmlns:jacoco="antlib:org.jacoco.ant" default="junit">
我们主要使⽤ JaCoCo 的两个任务:⾸先是jacoco:coverage,⽤来⽣成单元测试覆盖率数据,这是⼀个⼆进制⽂件,为了⽣成从该⽂件⽣成报表,我们还要调⽤另外⼀个任务jacoco:report,它的输⼊为jacoco:coverage⽣成的⼆进制⽂件,输出报表。报表有多种格式可选,可以是 HTML、XML、CSV 等。具体的脚本如下:
清单 9. 使⽤ JaCoCo ⽣成测试覆盖率和报表
<jacoco:coverage destfile="${build.dir}/">
<junit fork="true" forkmode="once" printsummary="yes">
<classpath>
<path refid="classpath" />
<path location="${classes.dir}" />
<path location="${tests.dir}" />
</classpath>
<batchtest fork="yes">
<fileset dir="${test.dir}" includes="**/*Test.java"/>
</batchtest>
</junit>
</jacoco:coverage>
<jacoco:report>
<executiondata>
<file file="${build.dir}/"/>
</executiondata>
<structure name="dw demo">
<classfiles>
<fileset dir="${classes.dir}"/>
</classfiles>
<sourcefiles encoding="UTF-8">
<fileset dir="${src.dir}"/>
</sourcefiles>
</structure>
<html destdir="${build.dir}"/>
</jacoco:report>
JaCoCo 的任务定义⾮常清晰,在这⾥略作说明。⾸先需要将原来的junit任务嵌⼊jacoco:coverage,⽽且需要指定fork="true",代表单元测试需要另起⼀个 JVM 执⾏,否则 JaCoCo 就会执⾏失败。destfile="${build.dir}/"指定⽣成的测试覆盖率⽂件输出到什么地⽅,后⾯⽣成报告的时候需要输⼊该⽂件的地址。
然后就是使⽤ jacoco:report ⽣成报告,指定前⾯任务⽣成的单元测试覆盖率⽂件、编译好的类⽂件以及源代码,最后选择⼀种格式,这⾥使⽤html,⽣成报告。打开报告的存放路径,就可以看到如下所⽰的单元测试覆盖率报告:
图 5. HTML 版的单元测试覆盖率报告
和同类产品⽐较
市⾯上流⾏的单元测试覆盖率⼯具还有 Clover 和 Cobertura。和它们相⽐,JaCoCo 有如下优势:
JaCoCo 拥有友好的授权形式。JaCoCo 使⽤了 Eclipse Public License,⽅便个⼈⽤户和商业⽤户使⽤。⽽ Clover 对于商业⽤户是收费的。JaCoCo 被良好地集成进各种⼯具中。在 Java 社区⾥,很多流⾏的⼯具都可以集成 JaCoCo,⽐如 SonarQube、Jenkins、Netbeans、Eclipse、IntelliJ IDEA、Gradle 等。
JaCoCo 社区⾮常活跃,它是⽬前唯⼀⽀持 Java 8 的单元测试覆盖率⼯具。⽽且关于 JaCoCo 的⽂档相对较多,降低了学习门槛。
结束语
本⽂为⼤家介绍了如何使⽤ JaCoCo 分析项⽬的单元测试覆盖率,⽂章先从 JaCoCo 的 Eclipse 插件 EclEmma 开始,直观地介绍了如何⼀步步提⾼单元测试质量,最终达到对代码的全覆盖;然后为⼤家介绍了如何将 JaCoCo 集成到 Ant 构建脚本中,⽣成漂亮的单元测试覆盖率报告。但是使⽤ JaCoCo 只是第⼀步,重要的是开发⼈员能够根据⼯具所给的反馈,不断改进⾃⼰的单元测试,写出⾼质量的代码。
以上就是本⽂的全部内容,希望对⼤家的学习有所帮助,也希望⼤家多多⽀持。