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

确保您的代码质量

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

当心被忽悠

使用测试覆盖率工具没有任何欺骗的可能。它们是单元测试范例的一个很好的补充。重要的你在获取到这些信息的时候,如何综合考量并加以推广,这是一些开发团队犯下的第一个错误。

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

测试覆盖率测量

测试覆盖率工具通常很容易添加到已建立的单元测试过程中,并且结果可以放心。只需下载一个可用工具,略微修改 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 报告

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

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

覆盖率报告截图

因此,覆盖率测试的第一枪未能测试很多东西。首先,根本没有测试 buildHierarchy() 以 String 类型作为参数的方法 。其次,另 buildHierarchy() 一种方法中的两个条件均未执行。有趣的是,这是第二个未执行的 if 条件代码块。

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

Cobertura:第 2 轮

清单 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。是的,这里发生了什么?

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

质量测试

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

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

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

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

2.评估代码质量

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

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

该软件包的代码行有所增加,但是没有为新代码编写相应的测试(或者新添加的测试不能有效地覆盖新代码)、测试用例被删除、这两件事同时发生。
高明之处在于能够观察趋势。定期查看报告可以更轻松地设置目标(例如获得覆盖率,维护测试用例与代码比率行等),然后监视其进度。如果您碰巧发现通常没有编写测试,则可以采取主动措施,例如设置开发人员进行培训,指导或伙伴编程。当客户发现及其隐藏的缺陷(可能在几个月前通过简单的测试暴露出来)时,或在管理层发现单元测试未免时,不可避免的意外(和愤怒)比之,明智的响应要好得多。

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

3.评估功能测试

鉴于代码覆盖率报告在不进行适当测试的情况下最能说明代码部分,因此质量保证人员可以使用此数据来评估与功能测试有关的领域。

同样,知识就是力量。通过与软件生命周期中的其他利益相关者(例如质量保证)进行仔细协调,您可以使用覆盖率报告提供的见解来促进风险缓解。

测试取得回报的地方

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

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


技术类文章精选

非技术文章精选


↙↙↙阅读原文可查看相关链接,并与作者交流