跳转到主要内容

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

当您初次登录到 developerWorks 时,将会为您创建一份概要信息。您在 developerWorks 概要信息中选择公开的信息将公开显示给其他人,但您可以随时修改这些信息的显示状态。您的姓名(除非选择隐藏)和昵称将和您在 developerWorks 发布的内容一同显示。

所有提交的信息确保安全。

  • 关闭 [x]

当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

所有提交的信息确保安全。

  • 关闭 [x]

Java 理论与实践: 平衡测试,第 1 部分

不要仅编写测试,还要编写 bug 检测器

Brian Goetz (brian@quiotix.com), 首席顾问, Quiotix
Brian Goetz 在过去的 18 年里一直是一名专业软件开发人员。他是 Quiotix 的首席顾问,该公司是位于加利福尼亚Los Altos 的软件开发和咨询公司。他还在几个 JCP 专家组任职。Brian 的新书 Java Concurrency In Practice 将于 2006 年 5 月由 Addison-Wesley 出版。请参阅 Brian 在流行的业界出版物上 已发表和即将发表的文章

简介: 在 2004 年 6 月,经验丰富的清除专家 Brian Goetz 引进了 FindBugs 静态代码分析工具,即使在测试良好的软件中,该工具也可以检测 bug。本月他将重拾该主题,查看静态分析工具如何通过开发存在所有种类的 bug 的资源(而不是特定实例)来改进管理软件质量的方式。

查看本系列更多内容

发布日期: 2006 年 7 月 10 日
级别: 中级
访问情况 : 2925 次浏览
评论: 


对于许多团队来说,单元测试现在是开发过程的一个主要部分;JUnit 之类的框架可以进行无损测试,尽管我们并不喜欢它,宁愿为某些 代码编写某些 测试。单元测试运行效率很低,只能测试单个代码片段,并且,一般情况下,测试代码的重用性通常很也低 —— 昨天为组件 A 编写的测试不能很好地用于测试组件 B(示例代码除外)。

典型的单元测试场景

在发现 bug 时,要做的第一件事是什么?您可能只是想去修复它,但是,在长时间的运行中,这不是一个最有效的方法。 在许多开发部门中,处理 bug 的过程如下:

  • 针对 bug 编写测试用例
  • 确保测试用例在遇到 bug 时运行失败
  • 修复 bug
  • 确保测试用例通过
  • 确保其他测试套件仍能通过
  • 检查修正和测试用例,形成版本控制
  • 将修正记录在 bug 跟踪系统中

尽管此方法在短期内比仅修复 bug 要多做许多工作,但它提供了许多更有价值的东西:获得修复 bug 的更多信心,因为您已经对它进行了测试;获得 bug 将不会再出现的更多信心,因为测试用例是回归测试套件的一部分。在版本控制系统和 bug 跟踪系统之间,还可以获得一个记录,该记录描述了 bug 是什么以及如何修复它 —— 这是非常有用的信息,其他人会从中受益。

如果进取心较强,那么可以思考一下 bug 是怎样出现的,并在其他位置查找同一错误。如果在别处发现同一错误,那么可以对这些 bug 进行测试和修复。单元测试作为质量管理工具的主要弱点是每个测试用例只能测试一个代码片段。因为测试用例是专为每个组件和每个潜在错误模式设计的,所以只有编写足够多的单元测试才能测试大量的产品,这非常耗时并且代价高昂。


QA 经济

测试是一种基本的质量管理工具,我们知道仅有多组测试用例还不足以找出复杂软件片段中的所有 bug。事实上,对于任何优秀程序而言,“查找所有 bug” 是不可能实现的目标。据估计,NASA 向每个开发人员提供了 20 个测试程序(大大超过任何商业实体)来负责质量评价 (QA) —— 但软件仍有缺陷。因此,质量评价的目标不应是查找所有的 bug,因为这是不可能的。相反,质量评价的目标应该是提高代码运行良好的信心,从而最大程度地提供可用资源。

要高效运行质量评估量 (QA),则需要对可用 QA 方法中的可用资源做预算,这样才能最大限度地提高信心。覆盖范围大的测试套件可以提高我们对代码使用的信心,因为它进行了一次彻底的代码审查。执行两次比执行一次较果好,因为每次都会发现另一次可能错过的错误。两次同样遵循收益递减规则,所以测试价值为 X 美元和代码审查价值为 Y 的 QA 计划要比价值为 X+Y 的任何一次测试或代码审查的效果好。

添加静态分析

静态分析是在不运行代码的情况下对其进行分析的过程,它与进行前面的代码审查时我们执行的操作非常相似,或者与标记可疑结构时 IDE 执行的操作非常相似。静态分析是添加到 QA 混合(QA mix)中的一项优良技术,因为它擅长查找其他方法(如测试和代码审查)可能错过的错误。静态分析相对比较容易一些,不像单元测试那样必须为要测试的每个类重新编写测试,您可以在任何代码上运行静态分析工具。

FindBugs 是一种开放源码的静态分析工具,它包含用于许多常见 bug 模式的 bug 模式检测器,令人惊讶的是,即使在测试良好的软件中,FindBugs 也常常会发现一些 “沉默” 的 bug,但是单元测试和专业代码审查都可能错过这些 bug。FindBugs 还允许编写新的 bug 模式检测器,并将它们包装为插件,所以如果一组标准的检测器不能按您的需要执行,那么您可以很容易地编写自已的检测器。此扩展性使 FindBugs 成为非常强大的质量管理工具,因为当发现新类型的错误时,可以针对该错误编写检测器,并在整个代码基址中搜索该错误。

静态分析的主要作用是分析输出,并确定报告的条目是真的 bug 还是假警报。编写的部分优秀分析工具或 bug 模式检测器会管理误报率;核心 FindBugs 包中的检测器已经进行了调优,目的是使误报率不超过 50 %,这样分析输出时不会有太多的烦麻。(将此阈值与针对 C 的 lint-like 工具进行比较,后者常常发出许多假警报,使用时相当耗时。)

将它提升一个级别

前面描述修复 bug 的方法(首先编写测试用例,然后检查修复和测试用例)反映了这样一个愿望:不仅要修复 bug,还要提高修复它的信心,并记录如何修复它,以及何时修复它。此方法比仅修复 bug 要多做许多工作,但是它给我们提供了更多的信心,我们的代码在经过多个开发人员的不断修改后可以继续使用。不过,仅为所发现的 bug 编写测试用例是一种消极方法。在代码失败之前,我们希望尽可能以最佳实践分析代码。

清单 1 通过 BigDecimal 类说明了常见的 bug。BigDecimal 是固定不变的, 所以算术方法(如 add())会返回一个新的 BigDecimal 作为其结果,而不修改调用它们的对象。清单 1 中的代码显然被假定为有条件地将运输费用添加到总体订购价格中,但是,实际上不能随意添加任何内容,因为 add() 的返回值被丢弃了:


清单 1. 典型的 bug 模式 —— 使用 mutator 方法配置 factory 方法

public class ShoppingCart { 
    private BigDecimal totalCost;

    private boolean qualifiesForFreeShipping() { ... }
    private BigDecimal getShippingCost() { ... }

    public void checkout() {
        ...
        if (!qualifiesForFreeShipping())
            totalCost.add(getShippingCost()); //WRONG!
    }
}

清单 1 中的错误是一种常见的错误,它忘记了对象是不可变的,从而将 factory 方法误认为 mutator 方法。如果在代码中查找此类错误,就会发现存在同一错误多次发生的情况,因为它来源于对特定库类工作方式的误解。对于查找此 bug,负责任的开发人员可能会搜索整个代码基址来查找对 BigDecimal.add()subtract() 等方法的调用,并寻找忽略返回值的其他实例。

此策略是一个好的开头,但我们可以做得更好。在这里识别 bug 模式是非常容易的 —— 忽略不可变对象上的求值方法(value-bearing method)的结果。识别出该模式后,构建识别此模式的检测器是相对简单的一件事件。(FindBugs 在核心检测器集中有这样一个检测器。) 此技术不仅可以应用于 BigDecimal,还可以应用于其他不可变类(如 BigIntegerStringColor)中。

花费一点时间为 bug 模式创建一个 bug 检测器,它会为您带来可观的收益。不仅可以用比手工操作更少的工作和更高的信心来审核整个项目,从中寻找 bug,而且还可以在现在和将来将同一检测器应用到其他项目中。您已针对不断恢复、随时可能出现的 bug 类型建立了防御机制,而不是在逐个实例的基础上解决 bug。


示例 bug 检测器

为说明编写 FindBugs 检测器的过程,我们编写了一个简单的检测器,它可以查找对 System.gc() 的调用。(下载此示例检测器代码的 源代码。) 虽然调用的 System.gc() 不一定是 bug,但在实践中,它会带来更多的问题(多于它解决的问题 )。尤其是,如果错误地调用了库中隐藏的 System.gc(),则会降低使用该库的应用程序的性能,开发人员可能会感到很茫然,对性能会如此低下感动很奇怪。

编写 bug 检测器的第一步是识别被检测的 bug 模式。在本例中,该模式非常简单,只需调用 System.gc() 即可。要编写识别字节码中此模式的检测器,则需要知道对应于 bug 模式的字节码是什么。了解此问题的最好方法是编写一个包含 bug 的小程序,对它进行编译,并使用 javap -c 解开 .class 文件。 清单 2 显示了一个展示该 bug 的类:


清单 2. 展示 bug 模式(我们想为它构建一个检测器)的代码

public class BadClass { 
    public void doBadStuff() {
        System.gc();
    }
}

清单 3 显示了运行示例类时 javap -c 的输出:


清单 3. 清单 2 中代码的字节码清单

public void doBadStuff();
  Code:
   0:	invokestatic	#2; //Method java/lang/System.gc:()V
   3:	return

我们很快知道静态方法是通过 invokestatic JVM 指令调用的,invokestatic 的操作数是 java/lang/system 类的 gc:()V 方法。字节码中的方法签名和类型名称与源代码中的略有不同,但它很容易用于字节码使用的编码。

使用 bug 模式示例编写 FindBugs 检测器非常简单。清单 4 显示了扩展 BytecodeScanningDetector 基础类并重写 sawOpcode() 方法的检测器。当它遇到 invokestatic 指令时,它会检查被调用方法的类和名称,如果是 System.gc() 指令,它会报告 bug 实例。


清单 4. 查找调用 System.gc() 的 Bug 检测器

public class CallSystemGC extends BytecodeScanningDetector {
    private BugReporter bugReporter;

    public CallSystemGC(BugReporter bugReporter) {
        this.bugReporter = bugReporter;
    }
    
    public void sawOpcode(int seen) {
        if (seen == INVOKESTATIC) {
            if (getClassConstantOperand().equals("java/lang/System")
                    && getNameConstantOperand().equals("gc")) {
                bugReporter.reportBug(new BugInstance("SYSTEM_GC", NORMAL_PRIORITY)
                        .addClassAndMethod(this)
                        .addSourceLine(this));
            }
        }
    }
}

将检测器包装为插件

创建新的 bug 检测所需的最后一步是将其打包为一个插件。FindBugs 插件包含一个或多个 bug 检测器、一个部署描述符和一个资源文件,它们被打包成一个 JAR 文件,放在 FindBugs 安装的插件目录中。称为 findbugs.xml 的部署描述符将定义已知的 bug 检测器和它报告的错误。称为 messages.xml (对于本地化版本称为 messages_xx.xml)的资源文件定义特定于语言的、将由 FindBugs GUI 使用的字符串,用它描述所报告的 bug。清单 5 和清单 6 显示了示例 bug 检测器的部署描述符和资源文件。 插件 JAR 中可以包括多个资源文件的本地版本;部署描述符和资源文件放置在插件 JAR 的顶级目录中。


清单 5. 示例 bug 检测器的部署描述符

<FindbugsPlugin xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   				xsi:noNamespaceSchemaLocation="findbugsplugin.xsd"
				pluginid="com.briangoetz.findbugs.plugin"
                defaultenabled="true"
                provider="Brian Goetz"
                website="http://www.briangoetz.com">

  <Detector class="com.briangoetz.findbugs.plugin.CallSystemGC"
	        speed="fast"
	        reports="SYSTEM_GC" />

  <BugPattern abbrev="GC" type="SYSTEM_GC" category="PERFORMANCE" />
</FindbugsPlugin>
<


清单 6. 示例 bug 检测器的资源文件

<MessageCollection xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                  xsi:noNamespaceSchemaLocation="messagecollection.xsd">

  <Plugin>
    <ShortDescription>Brian's plugin</ShortDescription>
    <Details></Details>
  </Plugin>

  <Detector class="com.briangoetz.findbugs.plugin.CallSystemGC">
    <Details>
<![CDATA[
Finds calls to System.gc().
]]>
    </Details>
  </Detector>

  <BugPattern type="SYSTEM_GC">
    <ShortDescription>Method calls System.gc()</ShortDescription>
    <LongDescription>Call to System.gc() method in {1}</LongDescription>
    <Details>
<![CDATA[
Library code should not call System.gc()
]]>
    </Details>
  </BugPattern>

  <BugCode abbrev="GC" >Garbage collection</BugCode>
</MessageCollection>

根据 JDK 1.4.2 类库构建和包装插件,并运行它,这会为我们带来意想不到的效果:com.sun.imageio 中的几个类(包括 JPEGImageReaderJPEGImageWriter)将调用 System.gc()!此结果还有另一个好处,即静态分析的灵活性:创建 bug 检测器后,它可以在任何地方查找 bug。


结束语

静态分析和自定义 bug 检测器是提高软件质量的非常有效的方法。通过为已知 bug 模式创建检测器,我们不仅可以在特定项目的当前代码基址中搜索 bug 模式,还可以在当前或以后的任何项目中搜索 bug 模式。创建 bug 检测器所付出的额外努力将来会为您带来质量方面的丰厚回报。



下载

描述名字大小下载方法
Source codej-jtp06206fb_plugin_src.jar8KBHTTP

关于下载方法的信息


参考资料

学习

获得产品和技术

  • FindBugs:下载 FindBugs,并在您的代码上试着运行它。

讨论

关于作者

Brian Goetz 在过去的 18 年里一直是一名专业软件开发人员。他是 Quiotix 的首席顾问,该公司是位于加利福尼亚Los Altos 的软件开发和咨询公司。他还在几个 JCP 专家组任职。Brian 的新书 Java Concurrency In Practice 将于 2006 年 5 月由 Addison-Wesley 出版。请参阅 Brian 在流行的业界出版物上 已发表和即将发表的文章

关于报告滥用的帮助

报告滥用

谢谢! 此内容已经标识给管理员注意。


关于报告滥用的帮助

报告滥用

报告滥用提交失败。 请稍后重试。


developerWorks:登录


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 使用条款

 


当您初次登录到 developerWorks 时,将会为您创建一份概要信息。您在 developerWorks 概要信息中选择公开的信息将公开显示给其他人,但您可以随时修改这些信息的显示状态。您的姓名(除非选择隐藏)和昵称将和您在 developerWorks 发布的内容一同显示。

请选择您的昵称:

当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

(长度在 3 至 31 个字符之间)


单击提交则表示您同意developerWorks 的条款和条件。 使用条款.

 


为本文评分

评论

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology, Open source
ArticleID=145386
ArticleTitle=Java 理论与实践: 平衡测试,第 1 部分
publish-date=07102006
author1-email=brian@quiotix.com
author1-email-cc=

标签

Help
使用 搜索 文本框在 My developerWorks 中查找包含该标签的所有内容。

使用 滑动条 调节标签的数量。

热门标签 显示了特定专区最受欢迎的标签(例如 Java technology,Linux,WebSphere)。

我的标签 显示了特定专区您标记的标签(例如 Java technology,Linux,WebSphere)。

使用搜索文本框在 My developerWorks 中查找包含该标签的所有内容。热门标签 显示了特定专区最受欢迎的标签(例如 Java technology,Linux,WebSphere)。我的标签 显示了特定专区您标记的标签(例如 Java technology,Linux,WebSphere)。