思步网

查看: 13496|回复: 16
打印 上一主题 下一主题

软件测试中不要被覆盖报告所迷惑

[复制链接]
测试覆盖工具对单元测试具有重要的意义,但是经常被误用。这个月,Andrew Glover 会在他的新系列 —— 追求代码质量 中向您介绍值得参考的专家意见。第一部分深入地介绍覆盖报告中数字的真实含义。然后他会提出您可以尽早并经常地利用覆盖来确保代码质量的三个方法。  您还记得以前大多数开发人员是如何追求代码质量的吗。在那时,有技巧地放置 main() 方法被视为灵活且适当的测试方法。经历了漫长的道路以后,现在自动测试已经成为高质量代码开发的基本保证,对此我很感谢。但是这还不是我所要感谢的全部。Java? 开发人员现在拥有很多通过代码度量、静态分析等方法来度量代码质量的工具。我们甚至已经设法将重构分类成一系列便利的模式!
  所有的这些新的工具使得确保代码质量比以前简单得多,不过您还需要知道如何使用它们。在这个系列中,我将重点阐述有关保证代码质量的一些有时看上去有点神秘的东西。除了带您一起熟悉有关代码质量保证的众多工具和技术之外,我还将为您说明:
定义并有效度量最影响质量的代码方面。
设定质量保证目标并照此规划您的开发过程。
确定哪个代码质量工具和技术可以满足您的需要。
实现最佳实践(清除不好的),使确保代码质量及早并经常地 成为开发实践中轻松且有效的方面。
  在这个月,我将首先看看 Java 开发人员中最流行也是最容易的质量保证工具包:测试覆盖度量。
  谨防上当
  这是一个晚上鏖战后的早晨,大家都站在饮水机边上。开发人员和管理人员们了解到一些经过良好测试的类可以达到超过 90% 的覆盖率,正在高兴地互换着 NFL 风格的点心。团队的集体信心空前高涨。从远处可以听到 “放任地重构吧” 的声音,似乎缺陷已成为遥远的记忆,响应性也已微不足道。但是一个很小的反对声在说:
  女士们,先生们,不要被覆盖报告所愚弄。
  现在,不要误解我的意思:并不是说使用测试覆盖工具是愚蠢的。对单元测试范例,它是很重要的。不过更重要的是您如何理解所得到的信息。许多开发团队会在这儿犯第一个错。
  高覆盖率只是表示执行了很多的代码,并不意味着这些代码被很好地 执行。如果您关注的是代码的质量,就必须精确地理解测试覆盖工具能做什么,不能做什么。然后您才能知道如何使用这些工具去获取有用的信息。而不是像许多开发人员那样,只是满足于高覆盖率。
  测试覆盖度量
  测试覆盖工具通常可以很容易地添加到确定的单元测试过程中,而且结果可靠。下载一个可用的工具,对您的 Ant 和 Maven 构建脚本作一些小的改动,您和您的同事就有了在饮水机边上谈论的一种新报告:测试覆盖报告。当 foo 和 bar 这样的程序包令人惊奇地显示高 覆盖率时,您可以得到不小的安慰。如果您相信至少您的部分代码可以保证是 “没有 BUG” 的,您会觉得很安心。但是这样做是一个错误。
  存在不同类型的覆盖度量,但是绝大多数的工具会关注行覆盖,也叫做语句覆盖。此外,有些工具会报告分支覆盖。通过用一个测试工具执行代码库并捕获整个测试过程中与被 “触及” 的代码对应的数据,就可以获得测试覆盖度量。然后这些数据被合成为覆盖报告。在 Java 世界中,这个测试工具通常是 JUnit 以及名为 Cobertura、Emma 或 Clover 等的覆盖工具。
  行覆盖只是指出代码的哪些行被执行。如果一个方法有 10 行代码,其中的 8 行在测试中被执行,那么这个方法的行覆盖率是 80%。这个过程在总体层次上也工作得很好:如果一个类有 100 行代码,其中的 45 行被触及,那么这个类的行覆盖率就是 45%。同样,如果一个代码库包含 10000 个非注释性的代码行,在特定的测试运行中有 3500 行被执行,那么这段代码的行覆盖率就是 35%。
  报告分支覆盖 的工具试图度量决策点(比如包含逻辑 AND 或 OR 的条件块)的覆盖率。与行覆盖一样,如果在特定方法中有两个分支,并且两个分支在测试中都被覆盖,那么您可以说这个方法有 100% 的分支覆盖率。
  问题是,这些度量有什么用?很明显,很容易获得所有这些信息,不过您需要知道如何使用它们。一些例子可以阐明我的观点。
  代码覆盖在活动
  我在清单 1 中创建了一个简单的类以具体表述类层次的概念。一个给定的类可以有一连串的父类,例如 Vector,它的父类是 AbstractList,AbstractList 的父类又是 AbstractCollection,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 实例以及它的父类的集合。清单 2 中的 HierarchyBuilder 通过两个复制


上一篇:(原创)如何写好测试用户操作手册
下一篇:使用LoadRunner 编写JAVA 测试脚本
分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
收藏收藏 转播转播 分享分享 分享淘帖 支持支持 反对反对
回复 论坛版权

使用道具 举报

buildHierarchy 的重载的 static 方法创建了 Hierarchy 类。

清单 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 测试类,它将试图执行 Hierarchy 类和 HierarchyBuilder 类:

清单 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,因为它的报告很友好。而且,Corbertura 是开放源码项目,它派生出了 JCoverage 项目的前身。

Cobertura 的报告

  运行 Cobertura 这样的工具和运行您的 JUnit 测试一样简单,只是有一个用专门逻辑在测试时检查代码以报告覆盖率的中间步骤(这都是通过工具的 Ant 任务或 Maven 的目标完成的)。
这样看来,我的第一次覆盖测试是失败的。首先,带有 String 参数的 buildHierarchy() 方法根本没有被测试。其次,另一个 buildHierarchy() 方法中的两个条件都没有被执行。有趣的是,所要关注的正是第二个没有被执行的 if 块。

  因为我所需要做的只是增加一些测试案例,所以我并不担心这一点。一旦我到达了所关注的区域,我就可以很好地完成工作。注意我这儿的逻辑:我使用测试报告来了解什么没有 被测试。现在我已经可以选择使用这些数据来增强测试或者继续工作。在本例中,我准备增强我的测试,因为我还有一些重要的区域未覆盖。

  Cobertura:第二轮

  清单 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() { zo        
     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() 方法,也处理了另一个 buildHierarchy() 方法中的两个 if 块。然而,因为 HierarchyBuilder 的构造器是 private 类型的,所以我不能通过我的测试类测试它(我也不关心)。因此,我的行覆盖率仍然只有 88%。

[ 本帖最后由 流浪开心果 于 2008-11-6 14:40 编辑 ]
正如您看到的,使用一个代码覆盖工具可以 揭露重要的没有相应测试案例的代码。重要的事情是,在阅读报告(特别 是覆盖率高的)时需要小心,它们也许隐含危险的信息。让我们看看两个例子,看看在高覆盖率后面隐藏着什么。

条件带来的麻烦

  正如您已经知道的,代码中的许多变量可能有多种状态;此外,条件的存在使得执行有多条路径。在留意这些问题之后,我将在清单 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 中有一个隐藏的缺陷呢?如果没有,不要担心,我会在清单 6 中写一个测试案例来执行 pathExample() 方法并确保它正确地工作:

清单 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);
  }
}
      
测试覆盖率达到了 100%!
认真检查清单 5 会发现,如果 condition 为 false,那么第 13 行确实会抛出 NullPointerException。Yeesh,这儿发生了什么?

  这表明行覆盖的确不能很好地指示测试的有效性。

  路径的恐怖

  在清单 7 中,我定义了另一个包含 indirect 的简单例子,它仍然有不能容忍的缺陷。请注意 branchIt() 方法中 if 条件的后半部分。(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!
  }   
}
      



  呀!清单 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 比较更复杂,那么您 需要写多少测试案例才能满意?
转贴,也挺好!
可以好好看看一下!
老扣分数
其实,很多情况下都是这样的,习惯就好。
很有借鉴意义,先收藏了,谢谢楼主。
很有借鉴意义,先收藏了,谢谢楼主。
不错 支持一个了
向楼主学习
打酱油的人拉,顺便赚点金币
您需要登录后才可以回帖 登录 | 注册

本版积分规则



思步组织思步科技|思步网|火花学堂|思步文库|思步问答|思步英才|天下心
© 2007 思步网 浙ICP备10212573号-4(首次备案号:浙ICP备07035264号)|邮箱:service#step365.com(将#换成@)|服务热线:0571-28827450
在线培训课程|求职招聘|思步文库|官方微信|手机APP|思步问答|微博平台|官方QQ群|交流论坛|软件工程透析|关于我们|申请友链|
点击这里给我发消息     点击这里给我发消息
思步 step365 过程改进 CMMI中文 质量保证 质量管理 流程体系 需求跟踪矩阵 敏捷开发 Scrum 软件度量 项目评审 全员改进 流程管理 人力资源 6sigma 信息安全 ISO27001认证 IT服务管理 ISO20000认证 ISO9000认证 软件测试 SQA 配置管理 IPD 软件工程 PMP认证 PMP试题 PMBOK中文 精益研发 agile 顾问式管理培训
返回顶部