愚弄dnn_不要被覆盖率报告愚弄

您还记得大多数开发人员加入代码质量潮流之前的情况吗? 在那些日子里,熟练地放置main()方法被认为既敏捷又足以进行测试。 kes ! 从那时起,我们已经走了很长一段路。 首先,我非常感谢自动化测试现已成为以质量为中心的代码开发的重要方面。 这不是我要感谢的全部。 Java™开发人员拥有大量工具,可通过代码指标,静态分析等来衡量代码质量。 哎呀,我们甚至设法将重构归为一组便捷的模式!

所有这些新工具使确保代码质量比以往更加容易,但是您必须知道如何使用它们。 在本系列文章中,我将重点介绍确保代码质量的有时有些不可思议的细节。 除了使您熟悉可用于代码质量保证的各种工具和技术之外,我还将向您展示如何:

  • 定义并有效衡量影响质量的代码方面。
  • 设定质量保证目标并相应地计划您的开发工作。
  • 确定哪些代码质量工具和技术真正满足您的需求。
  • 实施最佳实践(并淘汰不良实践),以确保及早确保代码质量, 并且通常成为开发实践中无痛有效的方面。

我将从这个月开始,看看Java开发人员的质量保证工具包中最流行,最简单的功能之一:测试覆盖率测量。

当心愚人金

每天晚上进行建造之后的早晨,每个人都站在水冷却器周围。 当开发人员和管理人员得知一些经过特殊测试的课程的覆盖率高达90年代时,正在背面交换大胆的NFL风格的拍子! 团队的集体信心空前高涨。 “放弃吧!” 可以在远处听到,因为缺陷已成为遥远的记忆和弱者和劣等者的责任。 但是,有一个小小的不同声音说:

女士们,先生们:不要被报道蒙骗了 。

现在,请不要误会我的意思:使用测试覆盖率工具没有什么愚蠢的。 它们是单元测试范例的一个很好的补充。 重要的是一旦获得信息就如何综合信息,这是一些开发团队犯下的第一个错误。

高覆盖率仅意味着要执行大量代码。 高覆盖率并不意味着代码执行得很好 。 如果您专注于代码质量,则需要准确了解测试覆盖率工具的工作原理以及它们如何工作; 然后您将知道如何使用这些工具来获取有价值的信息,而不仅仅是像许多开发人员一样,为实现高覆盖率目标而定。

测试覆盖率测量

测试覆盖率工具通常很容易添加到已建立的单元测试过程中,并且结果可以放心。 只需下载一个可用的工具,略微修改您的Ant或Maven构建脚本,您和您的同事就可以在水冷却器周围谈论一种新的报告: “测试覆盖率报告” 。 当诸如foobar类的程序包显示出惊人的高覆盖率时,这可能是一种极大的安慰;当您认为代码的至少一部分可以证明是“无错误的”时,尝试放松就容易了。 但是这样做将是一个错误。

覆盖率度量有不同的类型,但是大多数工具都关注行覆盖率 ,也称为语句覆盖率 。 另外,一些工具报告分支机构覆盖率 。 通过使用测试工具使用代码库并捕获与在整个测试过程的生命周期中“被触摸”的代码相对应的数据,可以获得测试覆盖率的度量。 然后将数据合成以生成覆盖率报告。 在Java商店中,测试工具通常是JUnit,而覆盖率工具通常是诸如Cobertura,Emma或Clover之类的工具。

行覆盖率只是表明已执行了特定的代码行。 如果某个方法长10行,并且在测试运行中使用了8行,则该方法的行覆盖率为80%。 此过程也适用于汇总级别:如果一个班级有100条线,其中有45条线被触摸,则该班级的线覆盖率为45%。 同样,如果一个代码库包含10,000条未注释的代码行,并且其中3500条是在特定的测试运行中执行的,则该代码库的行覆盖率为35%。

报告分支覆盖率的工具会尝试测量决策点的覆盖率,例如包含逻辑ANDOR的条件块。 就像行覆盖率一样,如果特定方法中有两个分支并且都通过测试覆盖,那么您可以说该方法具有100%的分支覆盖率。

问题是,这些测量有多有用? 显然,所有这些信息都很容易获得,但是要由您来辨别如何合成这些信息。 一些例子阐明了我的观点。

实际的代码覆盖率

我在清单1中创建了一个简单的类来体现类层次结构的概念。 给定的类可以具有一系列超类-例如Vector ,其父级是AbstractList ,其父级是AbstractCollection ,其父级是Object

清单1.代表类层次结构的类
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

清单2.类层次构建器
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测试类,其中包含三个测试用例,它们试图同时使用HierarchyHierarchyBuilder类:

清单3.测试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报告

运行像Cobertura这样的工具就像运行JUnit测试一样简单,只有中间步骤,即使用专门的逻辑对被测代码进行检测以报告覆盖率(这全部通过该工具的Ant任务或Maven的目标进行处理)。

正如你在图1中看到,用于覆盖报告HierarchyBuilder说明了未行使的代码几节。 实际上,Cobertura声称HierarchyBuilder具有59%的行覆盖率和75%的分支覆盖率。

图1. Cobertura报告
Cobertura报告

因此,我在覆盖率测试中的第一枪未能测试很多东西。 首先, buildHierarchy()没有测试将String作为参数的buildHierarchy()方法。 其次,另一个buildHierarchy()方法中的两个条件也未执行。 有趣的是, if担心,这是第二个未经锻炼的问题。

我现在不担心,因为我要做的就是添加更多测试用例。 一旦到达这些令人关注的领域,我应该会很好。 在这里注意我的逻辑:我使用覆盖率报告了解未测试的内容。 现在,我可以选择使用这些数据来增强测试或继续前进。 在这种情况下,我将增强测试,因为我发现了一些重要的领域。

Cobertura:第2轮

清单4是更新后的JUnit测试用例,其中添加了一些其他测试用例,以尝试完全使用HierarchyBuilder

清单4.更新的JUnit测试用例
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){}
  }
}

当我使用新的测试用例再次运行测试覆盖过程时,我得到了更加完整的报告,如图2所示。我现在介绍了未经测试的buildHierarchy()方法,并在另一个buildHierarchy()同时击中了两个if块。 buildHierarchy()方法。 但是, HierarchyBuilder的构造函数是private ,因此我无法通过我的测试类对其进行测试(也不关心); 因此,我的线路覆盖率仍然徘徊在88%。

图2.谁说没有第二次机会?
谁说没有第二次机会?

如您所见,使用代码覆盖率工具可以发现没有相应测试用例的重要代码。 重要的是在查看报告( 尤其是具有较高价值的报告)时要格外小心,因为它们可能掩盖邪恶的微妙之处。 让我们看几个隐藏在高覆盖率背后的代码问题示例。

有条件的麻烦

您可能已经知道,在代码中找到的许多变量可以具有多个状态。 此外,条件的存在会创建多个执行路径。 考虑到这些警告,我使用清单5中的一种方法定义了一个荒谬的简单类:

清单5.您看到下面的缺陷了吗?
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中正常工作:

清单6.抢救JUnit!
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);
  }
}

我的测试用例运行无懈可击,而我方便的代码覆盖率报告(如图3所示)使我看起来像超级明星,具有100%的测试覆盖率!

图3.摇滚明星的报道,宝贝!
摇滚明星报道,宝贝!

我想是时候该在水冷却器旁闲逛了,但是等等-我是否怀疑该代码中的缺陷? 清单5的仔细检查表明,如果conditionfalse ,则第13行确实将抛出NullPointerException 。 是的 ,这里发生了什么?

事实证明,线路覆盖率并不是测试有效性的很好指标。

恐怖之路

在清单7中,我定义了另一个简单的示例,它具有间接但明显的缺陷。 请注意在branchIt()方法中找到的if条件的branchIt() 。 ( HiddenObject类在清单8中定义。)

清单7.此代码很简单
package com.vanward.coverage.example02;

import com.acme.someotherpackage.HiddenObject;

public class AnotherBranchCoverage {
   
  public void branchIt(int value){
    if((value > 100) || (HiddenObject.doWork() == 0)){
      this.dontDoIt();
    }else{
      this.doIt();
    }
  }                             

  private void dontDoIt(){
    //don't do something...
  }

  private void doIt(){
    //do something!
  }   
}

kes! 清单8中的HiddenObject是邪恶的 。 像清单7一样调用doWork()方法会产生RuntimeException

清单8.哦,哦!
package com.acme.someotherpackage.HiddenObject;

public class HiddenObject {

  public static int doWork(){
    //return 1;
    throw new RuntimeException("surprise!");
  }
}

但可以肯定的是,我可以通过一个漂亮的测试来捕获异常! 在清单9中,我编写了另一个晴天测试,试图赢得回到摇滚明星的方式:

清单9.使用JUnit规避风险
package test.com.vanward.coverage.example02;

import junit.framework.TestCase;
import com.vanward.coverage.example02.AnotherBranchCoverage;

public class AnotherBranchCoverageTest extends TestCase {
    
  public final void testBranchIt() {
    AnotherBranchCoverage clzzUnderTst = new AnotherBranchCoverage();
    clzzUnderTst.branchIt(101);
  }    
}

您如何看待这个测试用例? 您可能会编写比我更多的测试用例,但是可以想象清单7中的那个可疑条件是否有多个短路操作。 试想一下,如果在上半年的逻辑有点不是一个简单的大脑更int比较- 你需要多少测试用例编写你满意过吗?

给我数字

现在,清单7、8和9的测试覆盖率分析结果对您来说并不令人感到惊讶。 图4中的报告显示,我已经实现了75%的行覆盖率和100%的分支覆盖率。 最重要的是,我锻炼了第10行!

图4.愚人奖赏
愚人奖赏

男孩令我感到骄傲,至少在第一次反思时。 但是,您看到此报告有什么误导之处吗? 粗略的外观可能会使您相信该代码已经过测试 。 基于此,您可能会假设发生缺陷的风险非常低。 该报告几乎没有帮助您确定短路or短路的后半部分是定时炸弹!

质量测试

我再说一遍:您可以(并且应该)将测试覆盖率工具用作测试过程的一部分,但不要被覆盖率报告所迷惑 。 关于覆盖率报告的主要理解是,它们最好用于暴露未经充分测试的代码。 查看覆盖率报告时,请找出较低的值,并了解为什么未对特定代码进行完整测试。 知道了这一点,开发人员,经理和质量检查专业人员可以使用他们真正认为有用的测试覆盖率工具-即针对三种常见情况:

  • 估计修改现有代码的时间
  • 评估代码质量
  • 评估功能测试

现在,我已经建立了一些测试覆盖率报告可以使您误入歧途的方法,请考虑使用这些最佳实践以使您受益。

1.估计修改现有代码的时间

针对代码编写测试用例自然会提高开发团队的集体信心。 与没有相应测试用例的代码相比,经过测试的代码更易于重构,维护和增强。 测试用例也可以作为熟练的文档,因为它们隐式演示了被测代码的工作方式。 而且,如果测试中的代码发生更改,则测试用例通常会并行更改,这与静态代码文档(例如注释和Javadocs)不同。

另一方面,没有相应测试的代码可能更难以理解,并且更难安全修改。 因此,了解代码是否已经过测试,并查看实际的测试覆盖率数字,可以使开发人员和管理人员更准确地预测修改现有代码所需的时间。

回到水冷却器的旅行应该很好地说明了我的观点。

营销部门的Linda表示:“我们希望用户执行交易时,该系统能够执行x 。需要多长时间?我们的客户需要尽快使用此功能。”

经理Jeff:“让我们看看,这些代码是Joe几个月前编写的。这将需要对业务层进行更改,并对UI进行一些更改。Mary应该能够在几天内完成工作。”

琳达:“乔?那是谁?”

杰夫:“哦,乔,是的。我们解雇了他,因为他不知道自己在做什么。”

这种情况听起来有点不祥,不是吗? 杰夫仍然将新功能分配给了玛丽,玛丽也认为她可以在两天内完成它,直到看到代码为止。

玛丽:“乔在编写这段代码时睡着了吗?这是我见过的最糟糕的东西。我什至不确定这是Java代码。我不可能没有改变就改变它!我退出了。”

对于水冷却器团队来说,情况看起来不好,是吗? 但是,让我们看看当我回顾这一系列不幸的事件时会发生什么,这次让Jeff和Mary拥有一份覆盖率报告。 当Linda要求使用新功能时,Jeff要做的第一件事就是查阅上一版本的便利报告。 他注意到需要更改的特定软件包几乎没有覆盖范围,因此他与Mary协商。

杰夫:“乔编写的代码太可怕了,而且大多数未经测试。您认为需要多长时间才能添加更改以支持琳达的请求?”

玛丽:“代码太乱了。我什至不敢看。马克不能做吗?”

杰夫:“我开除马克是因为他不编写测试。我需要您测试此代码,然后添加更改。告诉我您需要多少时间。”

玛丽:“我至少需要两天的时间来编写测试,然后我想重构代码,然后再添加功能。我考虑总共四天。”

正如他们所说,知识就是力量。 开发人员可以使用覆盖率报告来检查代码质量, 然后再尝试对其进行修改。 同样,管理人员可以使用覆盖率数据来更好地估计开发人员实际完成工作所需的时间。

2.评估代码质量

开发人员测试降低了代码缺陷的风险,因此许多开发团队现在要求将单元测试与新开发或修改的代码一起编写。 但是,如上文Mark所示,单元测试并非总是与编码并行进行,这可能导致较低质量的代码。

监视覆盖率报告可帮助开发团队快速发现正在增长的代码, 而无需进行相应的测试。 例如,在本周初运行覆盖报告,则表明该项目中的关键软件包的覆盖率为70%。 如果一周后该软件包的覆盖率降至60%,则可以推断出:

  • 该软件包的代码行有所增加,但是没有为新代码编写相应的测试(或者新添加的测试不能有效覆盖新代码)。
  • 测试用例被删除。
  • 这两件事同时发生。

美丽之处在于能够观察趋势。 定期查看报告可以更轻松地设置目标(例如获得覆盖率,维护测试用例与代码比率行等),然后监视其进度。 如果您碰巧发现通常没有编写测试,则可以采取主动措施,例如设置开发人员进行培训,指导或伙伴编程。 当客户发现“一生中一次”的缺陷(可能在几个月前通过简单的测试暴露出来)时,或者在管理层发现单元测试没有发生时,必然的惊喜(和愤怒)比消息灵敏得多。尚未完成。

使用覆盖率报告来确保正确的测试是一个好习惯。 诀窍是要有纪律地做到这一点。 例如,作为连续集成过程的一部分,请尝试每晚生成和查看覆盖率报告。

3.评估功能测试

鉴于代码覆盖率报告在不进行适当测试的情况下最能演示代码部分,因此质量保证人员可以使用此数据来评估与功能测试有关的领域。 让我们回到饮水机,看看质量检查负责人Drew对乔的代码要说些什么:

Drew对Jeff说道:“我们正在为下一个版本准备测试用例,我们注意到大量的代码基本上没有代码覆盖率。看来这是与股票交易有关的代码。”

杰夫:“是的,我们在该领域遇到了一些问题。如果我是一名博彩员,那么我将特别关注该功能领域。玛丽正在研究应用程序的其他重大变化-她做得很好。编写单元测试的工作,但是代码绝不是完美的。”

德鲁:“是的,我正在确定资源和努力水平,看来我们将再次短缺。我想我将让团队专注于股票交易。”

同样,知识就是力量。 通过与软件生命周期中的其他利益相关者(例如质量保证)进行仔细协调,您可以使用覆盖率报告提供的见解来促进风险缓解。 在上述情况下,也许Jeff可以为Drew的团队提供一个早期版本,其中可能不包含Mary的所有更改。 但是,Drew的团队可以专注于应用程序的股票交易方面,其中缺陷的风险似乎比带有相应单元测试的代码高。

测试取得成功的地方

测试覆盖率测量工具是对单元测试范例的绝佳补充。 覆盖率测量为既有利的过程又提供了深度和精确度。 但是,您应该谨慎地查看代码覆盖率报告。 仅高覆盖率并不能确保代码的质量。 覆盖率很高的代码不一定没有缺陷,尽管包含缺陷的可能性较小 。

测试覆盖率度量的技巧是使用覆盖率报告以微观级别和宏级别公开未经测试的代码。 通过从顶层分析代码库以及分析各个类的覆盖范围,可以促进更深入的覆盖范围测试。 集成了该原理后,您和您的组织就可以使用覆盖率测量工具,它们可以真正地发挥作用,例如估算项目所需的时间,持续监控代码质量并促进QA协作。


翻译自: https://www.ibm.com/developerworks/java/library/j-cq01316/index.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值