专栏名称: 51Testing软件测试网
51Testing软件测试网,人气最旺的软件测试技术门户,提供软件测试社区交流,软件测试博客,人才服务,测试沙龙,测试杂志,测试资料下载等全方位信息服务,是国内最专业的软件测试就业培训、企业服务供应商...
目录
相关文章推荐
51好读  ›  专栏  ›  51Testing软件测试网

跨了个年回来,身边很多人都在补一门叫“测试覆盖率”的课!

51Testing软件测试网  · 公众号  · 测试  · 2020-01-06 17:30

正文


您还记得大多数开发人员跳上代码质量潮流之前的情况吗?在那些日子里,熟练地放置main() 方法被认为既敏捷又足以进行测试。kes!从那时起,我们已经走了很长一段路。首先,我非常感谢自动化测试现已成为以质量为中心的代码开发的重要方面。这不是我要感谢的全部。Java 开发人员拥有大量工具,可通过代码指标,静态分析等来衡量代码质量。哎呀,我们甚至设法将重构归为一组便捷的模式!
确保您的代码质量
要获得与代码质量有关的问题的答案,请访问由Andrew Glover主持的 “代码质量”讨论论坛。
所有这些新工具使确保代码质量比以往更加容易,但是您必须知道如何使用它们。在本系列文章中,我将重点介绍确保代码质量的有时有些不可思议的细节。除了使您熟悉可用于代码质量保证的各种工具和技术之外,我还将向您展示如何:
  • 定义并有效衡量对代码质量影响最大的方面。

  • 设定质量保证目标并相应地计划您的开发工作。

  • 确定哪些代码质量工具和技术真正满足您的需求。

  • 实施最佳实践(并淘汰不良实践),以便尽早确保代码质量,并且通常成为开发实践中不费力且有效的方面。

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



当心被欺骗


使用测试覆盖率工具没有什么欺骗的。它们是单元测试范例的一个很好的补充。重要的是一旦获得信息就如何对其进行综合,这是一些开发团队犯下的第一个错误。

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



测试覆盖率测量


测试覆盖率工具通常很容易添加到已建立的单元测试过程中,并且结果可以放心。只需下载一个可用工具,略微修改您的Ant或Maven构建脚本,您和您的同事就可以围绕饮水机提出一种新的报告:“测试覆盖率报告”。当软件包喜欢foo并且bar显示出惊人的高覆盖率时,这可能是一个很大的安慰;当您相信至少一部分代码可以证明是“无错误的”时,就容易放松。但是这样做将是一个错误。

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

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

报告分支覆盖率的工具会尝试测量决策点的覆盖率,例如包含逻辑ANDs或ORs的条件块 。就像行覆盖率一样,如果特定方法中有两个分支并且都通过测试覆盖,那么您可以说该方法具有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测试类,其中包含三个测试用例,它们试图同时使用 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,因为我喜欢它的友好报告。同样,Cobertura是一个开源项目,它是开拓性的JCoverage项目的分支。



Cobertura报告


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

正如你在图中看到,用于覆盖报告 HierarchyBuilder说明的代码几节 不行使。实际上,Cobertura声称其 HierarchyBuilder线路覆盖率为59%,分支覆盖率为75%。

因此,我对覆盖率测试的第一枪未能测试很多东西。首先,根本没有测试buildHierarchy()以String类型作为参数的方法 。其次,另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) {
        }
    }
}

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



条件判断的错误


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

清单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 {






请到「今天看啥」查看全文