您还记得大多数开发人员踏上代码质量潮流以前的状况吗?在那些日子里,熟练地放置main() 方法被认为既敏捷又足以进行测试。从那时起,咱们已经走了很长一段路。首先,我很是感谢自动化测试现已成为以质量为中心的代码开发的重要方面。这不是我要感谢的所有。Java开发人员拥有大量工具,可经过代码指标,静态分析等来衡量代码质量,咱们甚至设法将重构归为一组便捷的模式!java
全部这些新工具使确保代码质量比以往更加容易,可是您必须知道如何使用它们。在本系列文章中,我将重点介绍确保代码质量的有时有些难以想象的细节。除了使您熟悉可用于代码质量保证的各类工具和技术以外,我还将向您展现如何解决如下问题:编程
使用测试覆盖率工具没有任何欺骗的可能。它们是单元测试范例的一个很好的补充。重要的你在获取到这些信息的时候,如何综合考量并加以推广,这是一些开发团队犯下的第一个错误。segmentfault
高覆盖率仅意味着要执行大量代码。高覆盖率并不意味着代码能够很好地执行。若是您专一于代码质量,则须要准确了解测试覆盖率工具的工做原理以及它们如何工做;而后您将知道如何使用这些工具来获取有价值的信息,而不只仅是像许多开发人员同样,为实现高覆盖率目标而写了大量的测试代码。浏览器
测试覆盖率工具一般很容易添加到已创建的单元测试过程当中,而且结果能够放心。只需下载一个可用工具,略微修改Ant或Maven构建脚本,您和您的同事就能够围绕测试质量提出一种新的报告:“测试覆盖率报告”。当报告显示出惊人的高覆盖率时,这多是一个很大的安慰;当您相信至少一部分代码能够证实是“无错误的”时,就容易放松。可是这样作将是一个错误。安全
覆盖率度量有不一样的类型,可是大多数工具都关注行覆盖率,也称为语句覆盖率。另外,某些工具报告分支机构覆盖率。经过使用测试工具来运行代码库并捕获与在整个测试过程的生命周期中“被执行”的代码相对应的数据,能够得到测试覆盖率的测量结果。而后将数据合成以生成覆盖率报告。在Java经常使用库中,测试工具一般是JUnit,覆盖工具一般是诸如Cobertura,Emma或Clover之类的工具。框架
行覆盖率只是代表已执行了特定的代码行。若是某个方法长10行,而且在测试运行中使用了8行,则该方法的行覆盖率为80%。该过程也适用于汇总级别:若是一个类有100行,其中有45行被触摸,则该类的行覆盖率为45%。一样,若是一个代码库包含10,000条非注释行代码,而且其中3500条是在特定测试运行中执行的,则该代码库的行覆盖率为35%。函数
报告分支覆盖率的工具会尝试测量决策点的覆盖率,例如包含逻辑条件代码块 。就像行覆盖率同样,若是特定方法中有两个分支而且都经过测试覆盖,那么您能够说该方法具备100%的分支覆盖率。工具
问题是,这些测量有用吗?显然,全部这些信息都很容易得到,可是要由您来辨别如何综合这些信息得出合适的结论。一些例子阐明了个人观点。性能
我在清单1中建立了一个简单的类,以体现类层次结构的概念。给定的类能够具备一系列超类-例如 Vector,其父级为AbstractList,其父级为AbstractCollection,其父级为 Object:单元测试
package com.vanward.adana.hierarchy; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; public class Hierarchy { private Collection classes; private Class baseClass; public Hierarchy() { super(); this.classes = new ArrayList(); } public void addClass(final Class clzz) { this.classes.add(clzz); } /** * @return an array of class names as Strings */ public String[] getHierarchyClassNames() { final String[] names = new String[this.classes.size()]; int x = 0; for (Iterator iter = this.classes.iterator(); iter.hasNext();) { Class clzz = (Class) iter.next(); names[x++] = clzz.getName(); } return names; } public Class getBaseClass() { return baseClass; } public void setBaseClass(final Class baseClass) { this.baseClass = baseClass; } }
如您所见,清单1的Hierarchy类包含一个 baseClass实例及其超类的集合。在 HierarchyBuilder清单2中建立 Hierarchy经过两个重载类static 冠以方法buildHierarchy()。
package com.vanward.adana.hierarchy; public class HierarchyBuilder { private HierarchyBuilder() { super(); } public static Hierarchy buildHierarchy(final String clzzName) throws ClassNotFoundException { final Class clzz = Class.forName(clzzName, false, HierarchyBuilder.class.getClassLoader()); return buildHierarchy(clzz); } public static Hierarchy buildHierarchy(Class clzz) { if (clzz == null) { throw new RuntimeException("Class parameter can not be null"); } final Hierarchy hier = new Hierarchy(); hier.setBaseClass(clzz); final Class superclass = clzz.getSuperclass(); if (superclass != null && superclass.getName().equals("java.lang.Object")) { return hier; } else { while ((clzz.getSuperclass() != null) && (!clzz.getSuperclass().getName().equals("java.lang.Object"))) { clzz = clzz.getSuperclass(); hier.addClass(clzz); } return hier; } } }
若是没有测试用例,关于测试覆盖率的文章将会是什么?在清单3中,我定义了一个简单的JUnit测试类,其中包含三个测试用例,它们试图同时使用 Hierarchy和HierarchyBuilder类:
package test.com.vanward.adana.hierarchy; import com.vanward.adana.hierarchy.Hierarchy; import com.vanward.adana.hierarchy.HierarchyBuilder; import junit.framework.TestCase; public class HierarchyBuilderTest extends TestCase { public void testBuildHierarchyValueNotNull() { Hierarchy hier = HierarchyBuilder.buildHierarchy(HierarchyBuilderTest.class); assertNotNull("object was null", hier); } public void testBuildHierarchyName() { Hierarchy hier = HierarchyBuilder.buildHierarchy(HierarchyBuilderTest.class); assertEquals("should be junit.framework.Assert", "junit.framework.Assert", hier.getHierarchyClassNames()[1]); } public void testBuildHierarchyNameAgain() { Hierarchy hier = HierarchyBuilder.buildHierarchy(HierarchyBuilderTest.class); assertEquals("should be junit.framework.TestCase", "junit.framework.TestCase", hier.getHierarchyClassNames()[0]); } }
由于我是一名“认真”的测试人员,因此我天然但愿进行一些覆盖率测试。在Java开发人员可用的代码覆盖工具中,我倾向于使用Cobertura,由于我喜欢它的友好报告。一样,Cobertura是一个开源项目,它是开拓性的JCoverage项目的分支。
运行像Cobertura这样的工具就像运行JUnit测试同样简单,只有中间步骤,使用专门的逻辑对被测代http://pic.automancloud.com码...(这所有经过工具的Ant任务或Maven的目标进行处理)。
正如你在图中看到,用于覆盖报告 HierarchyBuilder说明的代码几行不执行。实际上,Cobertura报告显示其 HierarchyBuilder线路覆盖率为59%,分支覆盖率为75%。
所以,覆盖率测试的第一枪未能测试不少东西。首先,根本没有测试buildHierarchy()以String类型做为参数的方法 。其次,另buildHierarchy()一种方法中的两个条件均未执行。有趣的是,这是第二个未执行的 if条件代码块。
我如今不担忧,由于我要作的就是添加更多测试用例。一旦到达这些使人关注的领域,我应该会很好。在这里注意个人逻辑:我使用覆盖率报告了解未测试的内容。如今,我能够选择使用此数据来加强测试或继续前进。在这种状况下,我将加强测试,由于我发现了一些重要的事情。
清单4是更新后的JUnit测试用例,其中添加了一些其余测试用例,以尝试全面行使HierarchyBuilder:
package test.com.vanward.adana.hierarchy; import com.vanward.adana.hierarchy.Hierarchy; import com.vanward.adana.hierarchy.HierarchyBuilder; import junit.framework.TestCase; public class HierarchyBuilderTest extends TestCase { public void testBuildHierarchyValueNotNull() { Hierarchy hier = HierarchyBuilder.buildHierarchy(HierarchyBuilderTest.class); assertNotNull("object was null", hier); } public void testBuildHierarchyName() { Hierarchy hier = HierarchyBuilder.buildHierarchy(HierarchyBuilderTest.class); assertEquals("should be junit.framework.Assert", "junit.framework.Assert", hier.getHierarchyClassNames()[1]); } public void testBuildHierarchyNameAgain() { Hierarchy hier = HierarchyBuilder.buildHierarchy(HierarchyBuilderTest.class); assertEquals("should be junit.framework.TestCase", "junit.framework.TestCase", hier.getHierarchyClassNames()[0]); } public void testBuildHierarchySize() { Hierarchy hier = HierarchyBuilder.buildHierarchy(HierarchyBuilderTest.class); assertEquals("should be 2", 2, hier.getHierarchyClassNames().length); } public void testBuildHierarchyStrNotNull() throws Exception { Hierarchy hier = HierarchyBuilder. buildHierarchy("test.com.vanward.adana.hierarchy.HierarchyBuilderTest"); assertNotNull("object was null", hier); } public void testBuildHierarchyStrName() throws Exception { Hierarchy hier = HierarchyBuilder. buildHierarchy("test.com.vanward.adana.hierarchy.HierarchyBuilderTest"); assertEquals("should be junit.framework.Assert", "junit.framework.Assert", hier.getHierarchyClassNames()[1]); } public void testBuildHierarchyStrNameAgain() throws Exception { Hierarchy hier = HierarchyBuilder. buildHierarchy("test.com.vanward.adana.hierarchy.HierarchyBuilderTest"); assertEquals("should be junit.framework.TestCase", "junit.framework.TestCase", hier.getHierarchyClassNames()[0]); } public void testBuildHierarchyStrSize() throws Exception { Hierarchy hier = HierarchyBuilder. buildHierarchy("test.com.vanward.adana.hierarchy.HierarchyBuilderTest"); assertEquals("should be 2", 2, hier.getHierarchyClassNames().length); } public void testBuildHierarchyWithNull() { try { Class clzz = null; HierarchyBuilder.buildHierarchy(clzz); fail("RuntimeException not thrown"); } catch (RuntimeException e) { } } }
当我使用新的测试用例再次运行测试覆盖率过程时,我获得了更加完整的报告,如图所示。我如今介绍了未经测试的buildHierarchy()方法以及if在另buildHierarchy()一种方法中都遇到了问题 。 HierarchyBuilder的构造函数是private,因此我没法经过个人测试类对其进行测试(也不关心);所以,个人线路覆盖率仍然徘徊在88%。
如您所见,使用代码覆盖率工具能够发现没有相应测试用例的重要代码。重要的是在查看报告(尤为是具备较高价值的报告)时要格外当心,由于它们可能掩盖错误的微妙之处很难让人发现。让咱们看几个隐藏在高覆盖率背后的代码问题示例。
package com.vanward.coverage.example01; public class PathCoverage { public String pathExample(boolean condition){ String value = null; if(condition){ value = " " + condition + " "; } return value.trim(); } }
清单5中有一个阴险的缺陷-您看到了吗?若是没有,请不用担忧:我将编写一个测试用例来练习该 pathExample()方法,并确保它在清单6中正确运行:
package test.com.vanward.coverage.example01; import junit.framework.TestCase; import com.vanward.coverage.example01.PathCoverage; public class PathCoverageTest extends TestCase { public final void testPathExample() { PathCoverage clzzUnderTst = new PathCoverage(); String value = clzzUnderTst.pathExample(true); assertEquals("should be true", "true", value); } }
个人测试用例运行无懈可击,而我方便的代码覆盖率报告(如图所示)使我看起来像超级明星,具备100%的测试覆盖率!
我想是时候该去喝水了,我是否怀疑该代码中存在缺陷?清单5的仔细检查显示,第13行确实会抛出NullPointerException if conditionis false。是的,这里发生了什么?
事实证实,线路覆盖率并非测试有效性的很好指标。
我再说一遍:您能够(而且应该)在测试过程当中使用测试覆盖率工具,可是不要被覆盖率报告所迷惑。关于覆盖率报告的主要理解是,它们最好用于公开未经充分测试的代码。查看覆盖率报告时,请找出较低的值,并了解为何未对特定代码进行完整测试。知道了这一点,开发人员,经理和质量检查专业人员可使用他们真正认为有用的测试覆盖率工具。即针对三种常见状况:
既然我已经创建了一些测试覆盖率报告可使您避免误入歧途的方法。下面请考虑使用这些最佳实践以使您受益。
针对代码编写测试用例天然会提升开发团队的集体信心。通过测试的代码比没有相应测试用例的代码更易于重构,维护和加强。测试用例也能够做为熟练的文档,由于它们隐式演示了被测代码的工做方式。并且,若是测试中的代码发生更改,则测试用例一般会并行更改,这与静态代码文档(例如注释和Javadocs)不一样。
在另外一方面,没有相应测试的代码可能更难以理解,而且更难安全修改。所以,了解代码是否已通过测试,并查看实际的测试覆盖率数字,可使开发人员和管理人员更准确地预测修改现有代码所需的时间。
开发人员测试下降了代码缺陷的风险,所以许多开发团队如今要求将单元测试与新开发或修改的代码一块儿编写。可是,如上文所示,单元测试并不老是与编码并行进行,这可能致使较低质量的代码。
监视覆盖率报告可帮助开发团队快速发现正在增加的代码,而无需进行相应的测试。例如,在本周初运行覆盖报告,则代表该项目中的关键软件包的覆盖率为70%。若是本周晚些时候该软件包的覆盖率降至60%,则能够推断出:
该软件包的代码行有所增长,可是没有为新代码编写相应的测试(或者新添加的测试不能有效地覆盖新代码)、测试用例被删除、这两件事同时发生。
高明之处在于可以观察趋势。按期查看报告能够更轻松地设置目标(例如得到覆盖率,维护测试用例与代码比率行等),而后监视其进度。若是您碰巧发现一般没有编写测试,则能够采起主动措施,例如设置开发人员进行培训,指导或伙伴编程。当客户发现及其隐藏的缺陷(可能在几个月前经过简单的测试暴露出来)时,或在管理层发现单元测试未免时,不可避免的意外(和愤怒)比之,明智的响应要好得多。
使用覆盖率报告来确保正确的测试是一个好习惯。诀窍是要有纪律地作到这一点。例如,做为可持续集成过程的一部分,请尝试天天生成和查看覆盖率报告。
鉴于代码覆盖率报告在不进行适当测试的状况下最能说明代码部分,所以质量保证人员可使用此数据来评估与功能测试有关的领域。
一样,知识就是力量。经过与软件生命周期中的其余利益相关者(例如质量保证)进行仔细协调,您可使用覆盖率报告提供的看法来促进风险缓解。
测试覆盖率测量工具是对单元测试范例的绝佳补充。覆盖率测量是有效的过程又提供了深度和精确度。可是,您应该谨慎地查看代码覆盖率报告。高覆盖率自己并不能确保代码的质量。覆盖率很高的代码不必定没有缺陷,尽管包含缺陷的可能性确定较小。
测试覆盖率度量的技巧是使用覆盖率报告在微观级别和宏观级别公开未经测试的代码。经过从顶层分析代码库以及分析各个类的覆盖范围,能够促进更深刻的覆盖范围测试。集成了该原理后,您和您的组织就可使用覆盖率测量工具,它们能够真正地发挥做用,例如估算项目所需的时间,持续监控代码质量并促进QA协做。