本文将介绍两种开发实践,用于提高 Java 单元测试中的代码覆盖率。代码覆盖率 = (被测代码 / 代码总数)* 100%。提高被测代码数量或降低代码总数,均可达到提高代码覆盖率的效果。在本文中,您将看到如何通过使用反射机制,在外部直接对目标类中的不可访问成员进行测试,以提高被测代码数量;以及通过修改 Cobertura 源码,使其支持通过正则表达式来过滤不需要进行单元测试的代码,以降低代码总数。代码覆盖率的提高,减少了单元测试过程中未被覆盖的代码数量,降低了开发人员编写或修改单元测试用例的时间成本,从而提高了整个单元测试的效率。

引言

单元测试是软件开发过程中重要的质量保证环节。单元测试可以减少代码中潜在的错误,使缺陷更早地被发现,从而降低了软件的维护成本。软件代码的质量由单元测试来保证,而单元测试自身的质量与效率问题也不容忽视。提高单元测试的质量与效率,不仅能够使软件代码更加有保证,而且能够节省开发人员编写或者修改单元测试代码的时间。衡量单元测试质量与效率的指标多种多样,代码覆盖率是其中一个极为重要的指标。一般而言,代码覆盖率越高,单元测试覆盖的范围就越大,代码中潜在错误的数量就越少,软件质量就越高。本文首先介绍代码覆盖率的统计指标类型及常用统计工具,然后重点选取具有代表性的行覆盖率进行分析,介绍两种方法用于提高代码的行覆盖率。

代码覆盖率的统计指标

代码覆盖率指的是一种衡量代码覆盖程度的方式,通常会对以下几种方式进行统计分析:

  • 行覆盖。它又被称作语句覆盖或基本块覆盖。这是一种较为常用且具有代表性的指标,度量的是被测代码中每个可执行语句是否被执行到。
  • 条件覆盖。它度量的是当代码中存在分支时,是否能覆盖进入分支和不进入分支这两种情况。这要求开发人员编写多个测试用例以分别满足进入分支与不进入分支这两种情况。
  • 路径覆盖。它度量的是当代码中存在多个分支时,是否覆盖到分支之间不同组合方式所产生的全部路径。这是一种力度最强的覆盖检测,相对而言,条件覆盖只是路径覆盖中的一部分。

在这三种覆盖指标中,行覆盖简单,适用性广,但可能会被认为是“最弱的覆盖”,其实不然。行覆盖相对于条件或路径覆盖,可以使开发人员通过尽可能少的测试数据和用例,覆盖尽可能多的代码。通常情况下,是先通过工具检测一遍整个工程单元测试的行覆盖情况,然后针对没有被覆盖到的代码,分析其没有被覆盖到的原因。如果是由于该代码所在分支由于不满足进入该分支的条件而没有被覆盖,那么开发人员才会进一步修改或增加测试代码,完成该部分的条件或路径覆盖。

可见,高效高质量的行覆盖是有效进行条件覆盖与路径覆盖的前提。行覆盖率越高,说明没有被覆盖到的代码越少,这样开发人员便会集中精力修改测试用例,覆盖这些数量不多的代码。相反,如果行覆盖率低,开发人员需要逐个检查没有被覆盖到的代码,精力被分散,因此很难提高剩余代码单元测试的质量。

代码覆盖率 = 被测代码行数 / 参测代码总行数 * 100%。 从代码覆盖率的计算方式中可以看出,要提高代码覆盖率,可通过提高被测代码行数,或减少参测代码总行数的方式进行。以下将会从这两个角度分别入手,分析如何提高被测代码行数及减少参测代码总行数。

使用 Cobertura 统计并提高代码的行覆盖率

Cobertura 是一款优秀的开源测试覆盖率统计工具,它与单元测试代码结合,标记并分析在测试包运行时执行了哪些代码和没有执行哪些代码以及所经过的条件分支,来测量测试覆盖率。除了找出未测试到的代码并发现 bug 外,Cobertura 还可以通过标记无用的、执行不到的代码来优化代码,最终生成一份美观详尽的 HTML 覆盖率检测报告。

Cobertura 基本工具包里有四个基本过程及对应的工具:cobertura-check, cobertura-instrument, cobertura-merge, cobertura-report; 这个脚本独立使用较为繁琐,不方便也不利于自动化。不过, Cobertura 在 Maven 编译平台上有相应的 cobertura-maven-plugin 插件,使代码编译、检测、集成等各个周期可以流水线式自动化完成。

Cobertura-maven-plugin 官方版有五个主要目标指令 (goal),如表 1:

表 1. Cobertura 目标指令及作用解释

目标指令 作用解释
Cobertura:check 检查最后一次标注(instrumentation) 正确与否
Cobertura:clean 清理插件生产的中间及最终报告文件
Cobertura:dump-datafile Cobertura 数据文件 dump 指令 , 不常用
Cobertura:instrument 标注编译好的 javaclass 文件
Cobertura:cobertura 标注、运行测试并产生 Cobertura 覆盖率报告

Cobertura 通常会与 Maven 一起使用。因此工程目录结构如果遵循 Maven 推荐的标准的话,一个集成 Cobertura 的基本 POM 文件如清单 1 所示:

清单 1. POM 文件的基本结构
Java
1
2
3
4
5
6
7
8
9
10
11
12

<project>
  <reporting>
     <plugins>
        <plugin>
          <!--此处用于将Cobertura插件集成到Maven中-->
          <groupId>org.codehaus.mojo</groupId>
                <artifactId>cobertura-maven-plugin</artifactId>
                <version>2.5.2</version>
         </plugin>
      </plugins>
   </reporting>
</project>

如果工程目录结构没有采用 Maven 推荐标准,则需要进行如下额外设置:

清单 2. 适合 Maven 的工程目录结构配置
Java
1
2
3
4
5
6
7
8
9
10
11
12

<build>
     <!--Java源代码的路径配置-->
     <sourceDirectory>src/main/java</sourceDirectory>
     <scriptSourceDirectory>src/main/scripts</scriptSourceDirectory>
     <!--测试代码的路径配置-->
     <testSourceDirectory>src/test/java</testSourceDirectory>
     <!--源码编译后的class文件的路径配置-->
     <outputDirectory>target/classes</outputDirectory>
     <!--测试源码编译后的class文件的路径配置-->
     <testOutputDirectory>target/test-classes</testOutputDirectory>
     <plugin>....</plugin>
</build>

单元测试代码编写完成,所有设置配制好后,在工程根目录运行“mvn cobertura:cobertura”Maven 就会对代码进行编译。编译完成之后,就会在项目中运行测试代码并输出测试报告结果到目录 project_base$\target\site\cobertura\index.html,效果如图 1 所示。

图 1. Cobertura 覆盖分析报告

从以上报告中可见,

  • 代码整体的行覆盖率并不高,有些包或类覆盖率很低,甚至为 0。考虑到这些包或类的特殊性(例如它们已被其他类取代),无需对它们进行单元测试,因此需要从整个测试范围中剔除。
  • 部分类的行覆盖率虽然已接近 100%,但仍存在一些方法(如 set 和 get 方法)由于没有测试的必要却被列入了统计范围,这些方法需要被过滤掉。

针对上述两种改进措施,都可以使用 Cobertura 进行实现。第一种改进措施 Cobertura 可以支持,而第二改进措施则需要对 Cobertura 源码进行修改,重编译后方可支持。下面将详细介绍如何使用 Cobertura 对上述问题进行优化。

过滤不需进行单元测试的包和类

针对项目中不需进行单元测试的包和类,我们可以利用 POM 文件中 Cobertura 的标注 (instrument) 设置,对相应的包和类进行剔除 (exclude) 或筛选 (include),使之不体现在覆盖率报告中,去除它们对整个覆盖率的影响,从而使报告更具针对性。其基本 POM 标签设置及解析如清单 3 中所示。

清单 3. POM 中剔除包和类的设置示例
Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

<configuration>
    <instrumentation>
    <excludes>
    <!--此处用于指定哪些类会从单元测试的统计范围中被剔除-->
                <exclude>exs/res/process/egencia/Mock*.class</exclude>
                <exclude>exs/res/process/test/**/*Test.class</exclude></excludes>
    </instrumentation>
    </configuration>
    <executions>
          <execution>
                <goals>
                     <goal>clean</goal>
                </goals>
           </execution>
   </executions>

通过在配置文件中使用 Include 与 Exclude,可以显式地指定哪些包和类被列入单元测试的统计范围,哪些包和类被剔除在此范围之外。正则表达式支持丰富的匹配条件,可以满足大多数项目对单元测试范围的要求。以上代码将 exs.res.process.egencia 下面所有的名称 Mock 开头的类,以及 exs.res.process.egencia.test 包下面以 Test 结尾的类都剔除在测试范围以外。在使用这种配置之后,代码整体的范围被缩小,因此在被覆盖到的代码数量不变的基础上,整个代码覆盖率会较以前提高。输出结果如图 2 所示。

图 2. 包、类过滤效果

过滤类中的函数

最新版本中的 Cobertura 只能支持到类级别的过滤,而对于类中方法的过滤是不支持的。因此我们需要通过修改 Cobertura 源码,使 Cobertura 支持对类中方法的过滤。

对 Cobertura 及其插件改动所依据的主要原理是 : 修改 Cobertura-maven-plugin 项目中的 InstrumentationTask 类,增加 Ignoretrival,IgnoreMethod 等新增 POM 参数。配制正则表达式,修改 Cobertura 核心,在标注(instrumentation) 阶段遍历函数名时,检测函数名是否匹配传入的正则表达式,过滤函数体代码,从而把这些函数代码排除在代码覆盖统计之外,节省开发人员对这类代码的测试精力。

清单 4 至清单 6 是对 Cobertura 的几处核心改动,仅供读者参考。

清单 4. 对 Cobertura 核心代码的改动之一
Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

privatevoidcheckForTrivialSignature(){
     Type[]args=Type.getArgumentTypes(myDescriptor);
     Type ret=Type.getReturnType(myDescriptor);
     if(myName.equals("<init>")){
         isInit=true;mightBeTrivial=true;return;
     }
     if(myName.startsWith("set")&&args.length==1&&ret.equals(Type.VOID_TYPE)){
          isSetter=true;
          mightBeTrivial=true;
          return;
     }
     if((myName.startsWith("get")||myName.startsWith("is")||myName.startsWith("has"))
     &&args.length==0&&!ret.equals(Type.VOID_TYPE)){
          isGetter=true;
          mightBeTrivial=true;
          return;
     }
}

清单 5. 对 Cobertura Maven plugin 的改动
Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

privateStringignoreMethodAnnotation;
privateStringignoreTrivial;
/**
  * 创建一个新的对象,用于进行配置。
  */
publicConfigInstrumentation()
/**
* * 该方法用于设置annotation的名字以用于过滤类内部的方法
* @param ignoreMethodAnnotation
*/
publicvoidsetIgnoreMethodAnnotation(StringignoreMethodAnnotation){
this.ignoreMethodAnnotation=ignoreMethodAnnotation;
}
publicStringgetIgnoreTrivial(){
returnignoreTrivial;
}
/**
* 该方法用于标识测试类中的方法是否无关紧要不需要测试。
* @param ignoreTrivial
*/
publicvoidsetIgnoreTrivial(StringignoreTrivial){
this.ignoreTrivial=ignoreTrivial;
}

清单 6. POM 文件中使用修改后的 Cobertura 过滤类中的方法

以上修改都完成之后, 就可以运行“mvn:site”命令得到报告。图 4 是使用没有被修改的 Cobertura 产生的结果报告,无函数过滤效果。图 5 是使用被修改后的 Cobertura 产生的结果报告,可以从中看出,几个 set 与 get 方法已被排除在统计范围之外。

图 3. 无函数名过滤效果

图 4. 增加函数过滤效果

利用 Java 反射(Reflection) 机制提高代码的行覆盖率

不同的人对反射有不同的理解,大部分人认同的一种观点是:反射使得程序可以检查自身结构以及软件环境,并且根据程序检测到的实际情况改变行为。

为了实现自检,一段程序需要有些信息来表示自身,这些信息就称为元数据(metadata)。Java 运行过程中对这些元数据的自检称为内省(introspection)。内省过程之后往往进行行为改变。总的来说,反射 API 利用以下三种技术来实现行为改变:

  • 直接修改元数据。
  • 利用元数据进行操作。
  • 调解(Intercession), 代码被允许在程序各种运行期进行调整。

Java 语言反射机制提供一组丰富的 API 函数来操作元数据,且提供了少部分重要的 API 来实现 Intercession 能力。

实际项目中,为了保证软件代码的整体质量,单元测试不仅要覆盖类的公有成员,还要覆盖重要的私有成员。而有些私有成员的调用,会被放入到极为复杂的条件分支中。而构造进入这个私有方法的相关条件,可能需要开发人员编写大量测试代码及测试数据。这无疑增加了单元测试的成本。有时为了节省成本,该类私有方法便跳过不测,从而在无形中降低了代码的行覆盖率,影响了软件的整体质量。

而利用反射的一系列特性,我们可以在不改变源代码的情况下,直接对复杂的私有方法进行单元测试,无需增加行覆盖检查中被覆盖的代码行数,从而可以在不增加单元测试成本的前提下,提高代码的行覆盖率与单元测试的整体质量。

清单 7 给出了一段简单的目标测试代码示例。

清单 7. 目标测试代码示例
Java
1
2
3
4
5
6
7
8
9
10
11
12
13

packageexs.res.util;
publicclassCustomer{    
    privateStringmessage;    
    publicStringgreet;    
    privateStringsayHello()
    {
        return"Hello";
    }    
    publicStringpHello()
    {
        return"pHello";
    }
}

为了测试私有函数 sayHello(),利用反射元数据操作 API 的测试代码为:

清单 8. 利用反射元数据操作 API 的测试代码
Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

                           @Test
publicvoidprivateMethodTest(){
  finalMethod methods[]=Customer.class.getDeclaredMethods();
    for(inti=0;i<methods.length;++i){
        if("sayHello".equals(methods[i].getName())){
            //这里会将 sayHello 方法由 private 变为 public,从而可以直接被外部对象访问
            methods[i].setAccessible(true);
            try{
                StringanotherString=(String)methods[i].invoke(newCustomer(),newObject[0]);
                assertTrue("Hello".equalsIgnoreCase(anotherString));
            }catch(Exceptione){
                e.printStackTrace();
            }
            break;
         }
     }
}
    @Test
    publicvoidprivateFieldTest()
                     throwsNoSuchFieldException,SecurityException{
        try{
        Field message=Customer.class.getDeclaredField("message");
        Customer testCustomer=newCustomer();
         //这里会将 message 属性由 private 变为 public,从而可以直接被外部对象访问
        message.setAccessible(true);
        message.set(testCustomer,"newMessage");    
        assertTrue("newMessage".equalsIgnoreCase((String)message.get(testCustomer)));
        }catch(Exceptione){
            e.printStackTrace();
        }        
}

运行以上单元测试用例来分别对 Customer 的私有方法 sayHello 以及私有属性 message 进行直接访问,结果如图 6 所示。

图 5. 非公有函数测试效果

从图中我们可以看到 Customer 成员的私有方法 sayHello 被测试代码覆盖到。所以,当一些代码函数复杂度过高,到通过构造测试数据或测试用例的方法很难使非公有成员得到运行时,我们就可以利用 Java 反射机制,直接在测试类中调用和测试目标类的非公有成员,从而提高覆盖率。

结语

本文使用两种方法,从两个不同的角度对单元测试中的代码覆盖率进行了增强。改进 Cobertura 来提高单元测试代码覆盖率,主要从缩小参与测试的代码总范围的角度入手,适用于代码总数庞大而被测代码数量不多的情况。而使用 Java 反射机制提高单元测试代码覆盖率,主要从提高被测代码数量的角度入手,适用于被测代码私有成员多且触发条件苛刻的情况。针对项目中对单元测试的不同需求,选取合适的技术来增强单元测试,才能真正提高代码以至项目的总体质量。

http://blog.jobbole.com/49717/

使用 Cobertura 和反射机制提高 Java 单元测试中的代码覆盖率相关推荐

  1. IBM技术论坛:使用 Cobertura 和反射机制提高单元测试中的代码覆盖率

    引言 单元测试是软件开发过程中重要的质量保证环节.单元测试可以减少代码中潜在的错误,使缺陷更早地被发现,从而降低了软件的维护成本.软件代码的质量由单元测试来保证,而单元测试自身的质量与效率问题也不容忽 ...

  2. 【反射机制】Java中的反射机制,使用反射机制创建对象、访问属性、方法、构造方法等

    这篇文章主要是整理了Java中的反射机制,包括:反射机制概念.反射机制访问构造方法.反射机制访问普通方法.反射机制访问属性,反射机制访问修饰符. 目录 一.反射机制概念 二.反射机制使用 (1)加载C ...

  3. 什么叫做类的类?如何获取私有的方法?Java反射机制太强大了,详解Java反射机制【Java养成】

    Java学习打卡:第二十二天 内容导航 Java学习打卡:第二十二天 内容管理 Java反射机制 问题引入---数据库介绍 Java反射的介绍 class的使用 由字符串产生类和对象 获取一个类的所有 ...

  4. Java单元测试(Junit+Mock+代码覆盖率)

    单元测试是编写测试代码,用来检测特定的.明确的.细颗粒的功能.单元测试并不一定保证程序功能是正确的,更不保证整体业务是准备的. 单元测试不仅仅用来保证当前代码的正确性,更重要的是用来保证代码修复.改进 ...

  5. 注解和反射详细笔记。自定义注解,元注解,内置注解。反射机制,Java Reflection,Java内存分析,反射操作注解,java.lang.reflect.Method,Class

    文章目录 注解 什么是注解 内置注解 元注解 自定义注解 反射机制 静态语言 vs 静态语言 Java Reflection 反射相关的主要API Class类 Java内存分析 创建运行时类的对象 ...

  6. java反射机制是什么_java中的反射机制是什么?

    java中的反射机制是什么? 发布时间:2020-05-21 22:45:50 来源:亿速云 阅读:156 作者:鸽子 java:"一切即对象",感觉java语言本身在不断践行着这 ...

  7. 你知道这个提高 Java 单元测试效率的 IDEA 插件吗

    前言 2023年我们公司主抓代码质量,所以单元测试必不可少,而且都写到了年底的绩效目标中了.在考虑如何达成这个目标的过程中,我发现了一个关于单元测试的IDEA插件--SquareTest,它可以帮助我 ...

  8. 反射机制(java)

    反射机制 反射机制可通过在运行时加载类名而获取类,并对其进行操作.工厂模式,动态代理中较常用到. 在实际场景中:由于有好多类具有共同的接口样式,而他们又用的不是很频繁,如果在服务器中保有这些类会占用资 ...

  9. Mock和Java单元测试中的Mock框架Mockito介绍

    什么是Mock? 在面向对象程序设计中,模拟对象(英语:mock object,也译作模仿对象)是以可控的方式模拟真实对象行为的假的对象.程序员通常创造模拟对象(mock object)来测试其他对象 ...

最新文章

  1. Solaris中创建磁盘集报”rpc.metad:Permission denied”错误
  2. WCF部署到IIS异常(详细: 不能加载类型System.ServiceModel.Activation.HttpModule )
  3. 如果您遇到文件或数据库问题,如何重置Joomla
  4. javascript控制台_如何充分利用JavaScript控制台
  5. 单链表头插法与尾插法的c语言实现(回顾)
  6. Java虚拟机--------JVM常见参数
  7. 大话Synchronized及锁升级
  8. SDUST 作业10 Problem D 魔方阵
  9. 六石管理学:考评不要与自己为难
  10. 总结:Oracle 递归查询
  11. 工作遇到问题:通过命令如何控制程控电源的第二个通道
  12. 强大的3D音效增强神器:Boom 3D for Mac中文
  13. 青客公寓挂牌房源分城市爬取
  14. Android 移动网络接入点名称(APN)
  15. android 装苹果系统,安卓机子安装苹果IOS系统?
  16. oracle捕获工具,SQL Monitor(oracle语句追踪工具)
  17. 【Python检测脚本】你知道朋友屏蔽你吗?你知道屏蔽别人的下场吗?(拉黑统统拉黑)
  18. 任正非与美国思想家的咖啡对话全文
  19. 小米usb当前设备已被临时限制3-2
  20. 侍魂胧月传说显示服务器满了,侍魂胧月传说:满物防攻略,仅供参考

热门文章

  1. Matplotlib 2 |折线图| 柱状图| 堆叠图| 面积图| 填图| 饼图| 直方图| 散点图| 极坐标| 图箱型图
  2. 个人如何办理注册商标
  3. 为什么说交换机可以隔离冲突域?
  4. 部署gitlab+gitrunner+nexus镜像仓库CI流水线配置
  5. 茄子用水泡过10分钟后变成蓝色
  6. 大数据平台的软件有哪些?
  7. http://zaojiasys.jianshe99.com 建造师数据泄漏,可以查看全部所有人的信息!
  8. pygame 躲避僵尸
  9. linux关键命令,Linux关键命令
  10. 淘淘商城---8.7