单元测试是软件开发过程当中重要的质量保证环节。单元测试能够减小代码中潜在的错误,使缺陷更早地被发现,从而下降了软件的维护成本。软件代码的质量由单元测试来保证,而单元测试自身的质量与效率问题也不容忽视。提升单元测试的质量与效率,不只可以使软件代码更加有保证,并且可以节省开发人员编写或者修改单元测试代码的时间。衡量单元测试质量与效率的指标多种多样,代码覆盖率是其中一个极为重要的指标。通常而言,代码覆盖率越高,单元测试覆盖的范围就越大,代码中潜在错误的数量就越少,软件质量就越高。本文首先介绍代码覆盖率的统计指标类型及经常使用统计工具,而后重点选取具备表明性的行覆盖率进行分析,介绍两种方法用于提升代码的行覆盖率。html
回页首java
代码覆盖率指的是一种衡量代码覆盖程度的方式,一般会对如下几种方式进行统计分析:正则表达式
行覆盖。它又被称做语句覆盖或基本块覆盖。这是一种较为经常使用且具备表明性的指标,度量的是被测代码中每一个可执行语句是否被执行到。并发
条件覆盖。它度量的是当代码中存在分支时,是否能覆盖进入分支和不进入分支这两种状况。这要求开发人员编写多个测试用例以分别知足进入分支与不进入分支这两种状况。maven
路径覆盖。它度量的是当代码中存在多个分支时,是否覆盖到分支之间不一样组合方式所产生的所有路径。这是一种力度最强的覆盖检测,相对而言,条件覆盖只是路径覆盖中的一部分。函数
在这三种覆盖指标中,行覆盖简单,适用性广,但可能会被认为是“最弱的覆盖”,其实否则。行覆盖相对于条件或路径覆盖,可使开发人员经过尽量少的测试数据和用例,覆盖尽量多的代码。一般状况下,是先经过工具检测一遍整个工程单元测试的行覆盖状况,而后针对没有被覆盖到的代码,分析其没有被覆盖到的缘由。若是是因为该代码所在分支因为不知足进入该分支的条件而没有被覆盖,那么开发人员才会进一步修改或增长测试代码,完成该部分的条件或路径覆盖。工具
可见,高效高质量的行覆盖是有效进行条件覆盖与路径覆盖的前提。行覆盖率越高,说明没有被覆盖到的代码越少,这样开发人员便会集中精力修改测试用例,覆盖这些数量很少的代码。相反,若是行覆盖率低,开发人员须要逐个检查没有被覆盖到的代码,精力被分散,所以很难提升剩余代码单元测试的质量。单元测试
代码覆盖率 = 被测代码行数 / 参测代码总行数 * 100%。 从代码覆盖率的计算方式中能够看出,要提升代码覆盖率,可经过提升被测代码行数,或减小参测代码总行数的方式进行。如下将会从这两个角度分别入手,分析如何提升被测代码行数及减小参测代码总行数。测试
回页首优化
Cobertura 是一款优秀的开源测试覆盖率统计工具,它与单元测试代码结合,标记并分析在测试包运行时执行了哪些代码和没有执行哪些代码以及所通过的条件分支,来测量测试覆盖率。除了找出未测试到的代码并发现 bug 外,Cobertura 还能够经过标记无用的、执行不到的代码来优化代码,最终生成一份美观详尽的 HTML 覆盖率检测报告。
Cobertura 基本工具包里有四个基本过程及对应的工具:cobertura-check, cobertura-instrument, cobertura-merge, cobertura-report; 这个脚本独立使用较为繁琐,不方便也不利于自动化。不过, Cobertura 在 Maven 编译平台上有相应的 cobertura-maven-plugin 插件,使代码编译、检测、集成等各个周期能够流水线式自动化完成。
Cobertura-maven-plugin 官方版有五个主要目标指令 (goal),如表 1:
目标指令 | 做用解释 |
---|---|
Cobertura:check | 检查最后一次标注(instrumentation) 正确与否 |
Cobertura:clean | 清理插件生产的中间及最终报告文件 |
Cobertura:dump-datafile | Cobertura 数据文件 dump 指令 , 不经常使用 |
Cobertura:instrument | 标注编译好的 javaclass 文件 |
Cobertura:cobertura | 标注、运行测试并产生 Cobertura 覆盖率报告 |
Cobertura 一般会与 Maven 一块儿使用。所以工程目录结构若是遵循 Maven 推荐的标准的话,一个集成 Cobertura 的基本 POM 文件如清单 1 所示:
<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 推荐标准,则须要进行以下额外设置:
<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 所示。
从以上报告中可见,
代码总体的行覆盖率并不高,有些包或类覆盖率很低,甚至为 0。考虑到这些包或类的特殊性(例如它们已被其余类取代),无需对它们进行单元测试,所以须要从整个测试范围中剔除。
部分类的行覆盖率虽然已接近 100%,但仍存在一些方法(如 set 和 get 方法)因为没有测试的必要却被列入了统计范围,这些方法须要被过滤掉。
针对上述两种改进措施,均可以使用 Cobertura 进行实现。第一种改进措施 Cobertura 能够支持,而第二改进措施则须要对 Cobertura 源码进行修改,重编译后方可支持。下面将详细介绍如何使用 Cobertura 对上述问题进行优化。
针对项目中不需进行单元测试的包和类,咱们能够利用 POM 文件中 Cobertura 的标注 (instrument) 设置,对相应的包和类进行剔除 (exclude) 或筛选 (include),使之不体如今覆盖率报告中,去除它们对整个覆盖率的影响,从而使报告更具针对性。其基本 POM 标签设置及解析如清单 3 中所示。
<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 所示。
最新版本中的 Cobertura 只能支持到类级别的过滤,而对于类中方法的过滤是不支持的。所以咱们须要经过修改 Cobertura 源码,使 Cobertura 支持对类中方法的过滤。
对 Cobertura 及其插件改动所依据的主要原理是 : 修改 Cobertura-maven-plugin 项目中的 InstrumentationTask 类,增长 Ignoretrival,IgnoreMethod 等新增 POM 参数。配制正则表达式,修改 Cobertura 核心,在标注(instrumentation) 阶段遍历函数名时,检测函数名是否匹配传入的正则表达式,过滤函数体代码,从而把这些函数代码排除在代码覆盖统计以外,节省开发人员对这类代码的测试精力。
清单 4 至清单 6 是对 Cobertura 的几处核心改动,仅供读者参考。
private void checkForTrivialSignature() { 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; } }
private String ignoreMethodAnnotation; private String ignoreTrivial; /** * 建立一个新的对象,用于进行配置。 */ public ConfigInstrumentation() /** * * 该方法用于设置annotation的名字以用于过滤类内部的方法 * @param ignoreMethodAnnotation */ public void setIgnoreMethodAnnotation(String ignoreMethodAnnotation) { this.ignoreMethodAnnotation = ignoreMethodAnnotation; } public String getIgnoreTrivial() { return ignoreTrivial; } /** * 该方法用于标识测试类中的方法是否可有可无不须要测试。 * @param ignoreTrivial */ public void setIgnoreTrivial(String ignoreTrivial) { this.ignoreTrivial = ignoreTrivial; }
<reporting> <plugins> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>cobertura-maven-plugin</artifactId> <version>2.5.2</version> <configuration> <ignores> <!--通过修改的 cobertura, 支持方法级别的过滤 --> <ignore>*main*</ignore> <!--以上修改指的是过滤项目中全部类中的方法名中含有 main 的方法 --> </ignores> <IgnoreTrival>true</IgnoreTrival> </configuration> </plugin> </plugins> </reporting>
以上修改都完成以后, 就能够运行“mvn:site”命令获得报告。图 4 是使用没有被修改的 Cobertura 产生的结果报告,无函数过滤效果。图 5 是使用被修改后的 Cobertura 产生的结果报告,能够从中看出,几个 set 与 get 方法已被排除在统计范围以外。
不一样的人对反射有不一样的理解,大部分人认同的一种观点是:反射使得程序能够检查自身结构以及软件环境,而且根据程序检测到的实际状况改变行为。
为了实现自检,一段程序须要有些信息来表示自身,这些信息就称为元数据(metadata)。Java 运行过程当中对这些元数据的自检称为内省(introspection)。内省过程以后每每进行行为改变。总的来讲,反射 API 利用如下三种技术来实现行为改变:
直接修改元数据。
利用元数据进行操做。
调解(Intercession), 代码被容许在程序各类运行期进行调整。
Java 语言反射机制提供一组丰富的 API 函数来操做元数据,且提供了少部分重要的 API 来实现 Intercession 能力。
实际项目中,为了保证软件代码的总体质量,单元测试不只要覆盖类的公有成员,还要覆盖重要的私有成员。而有些私有成员的调用,会被放入到极为复杂的条件分支中。而构造进入这个私有方法的相关条件,可能须要开发人员编写大量测试代码及测试数据。这无疑增长了单元测试的成本。有时为了节省成本,该类私有方法便跳过不测,从而在无形中下降了代码的行覆盖率,影响了软件的总体质量。
而利用反射的一系列特性,咱们能够在不改变源代码的状况下,直接对复杂的私有方法进行单元测试,无需增长行覆盖检查中被覆盖的代码行数,从而能够在不增长单元测试成本的前提下,提升代码的行覆盖率与单元测试的总体质量。
清单 7 给出了一段简单的目标测试代码示例。
package exs.res.util; public class Customer{ private String message; public String greet; private String sayHello() { return "Hello"; } public String pHello() { return "pHello"; } }
为了测试私有函数 sayHello(),利用反射元数据操做 API 的测试代码为:
@Test public void privateMethodTest() { final Method methods[] = Customer.class.getDeclaredMethods(); for (int i = 0; i < methods.length; ++i) { if ("sayHello".equals(methods[i].getName())) { //这里会将 sayHello 方法由 private 变为 public,从而能够直接被外部对象访问 methods[i].setAccessible(true); try{ String anotherString =(String)methods[i].invoke(new Customer(), new Object[0]); assertTrue("Hello".equalsIgnoreCase(anotherString)); }catch(Exception e){ e.printStackTrace(); } break; } } } @Test public void privateFieldTest() throws NoSuchFieldException, SecurityException{ try{ Field message = Customer.class.getDeclaredField("message"); Customer testCustomer = new Customer(); //这里会将 message 属性由 private 变为 public,从而能够直接被外部对象访问 message.setAccessible(true); message.set(testCustomer, "newMessage"); assertTrue("newMessage".equalsIgnoreCase((String)message.get(testCustomer))); }catch(Exception e){ e.printStackTrace(); } }
运行以上单元测试用例来分别对 Customer 的私有方法 sayHello 以及私有属性 message 进行直接访问,结果如图 6 所示。
从图中咱们能够看到 Customer 成员的私有方法 sayHello 被测试代码覆盖到。因此,当一些代码函数复杂度太高,到经过构造测试数据或测试用例的方法很难使非公有成员获得运行时,咱们就能够利用 Java 反射机制,直接在测试类中调用和测试目标类的非公有成员,从而提升覆盖率。
本文使用两种方法,从两个不一样的角度对单元测试中的代码覆盖率进行了加强。改进 Cobertura 来提升单元测试代码覆盖率,主要从缩小参与测试的代码总范围的角度入手,适用于代码总数庞大而被测代码数量很少的状况。而使用 Java 反射机制提升单元测试代码覆盖率,主要从提升被测代码数量的角度入手,适用于被测代码私有成员多且触发条件苛刻的状况。针对项目中对单元测试的不一样需求,选取合适的技术来加强单元测试,才能真正提升代码以致项目的整体质量。