首页 文章 JUnit Java理论与实践: 平衡测试,第1部分

邮件订阅

Java理论与实践: 平衡测试,第1部分 E-mail
用户评价: / 0
好 
作者:Administrator   
2009-02-21 01:34

   【IT168 技术文章】

         典型的单元测试场景

  在发现 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 方法

1 public class ShoppingCart {
2     private BigDecimal totalCost;
3
4     private boolean qualifiesForFreeShipping() { ... }
5     private BigDecimal getShippingCost() { ... }
6
7     public void checkout() {
8         ...
9         if (!qualifiesForFreeShipping())
10             totalCost.add(getShippingCost()); //WRONG!
11     }
12 }
13

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

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

  花费一点时间为 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 模式(我们想为它构建一个检测器)的代码

1 public class BadClass {
2     public void doBadStuff() {
3         System.gc();
4     }
5 }
6

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

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

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

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

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

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

1 public class CallSystemGC extends BytecodeScanningDetector {
2     private BugReporter bugReporter;
3
4     public CallSystemGC(BugReporter bugReporter) {
5         this.bugReporter = bugReporter;
6     }
7     
8     public void sawOpcode(int seen) {
9         if (seen == INVOKESTATIC) {
10             if (getClassConstantOperand().equals("java/lang/System")
11                     && getNameConstantOperand().equals("gc")) {
12                 bugReporter.reportBug(new BugInstance("SYSTEM_GC", NORMAL_PRIORITY)
13                         .addClassAndMethod(this)
14                         .addSourceLine(this));
15             }
16         }
17     }
18 }
19

  将检测器包装为插件

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

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

1 <FindbugsPlugin xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
2                    xsi:noNamespaceSchemaLocation="findbugsplugin.xsd"
3                 pluginid="com.briangoetz.findbugs.plugin"
4                 defaultenabled="true"
5                 provider="Brian Goetz"
6                 website="http://www.briangoetz.com">
7
8   <Detector class="com.briangoetz.findbugs.plugin.CallSystemGC"
9             speed="fast"
10             reports="SYSTEM_GC" />
11
12   <BugPattern abbrev="GC" type="SYSTEM_GC" category="PERFORMANCE" />
13 </FindbugsPlugin>
14 <

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

1 <MessageCollection xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
2                   xsi:noNamespaceSchemaLocation="messagecollection.xsd">
3
4   <Plugin>
5     <ShortDescription>Brian's plugin</ShortDescription>
6     <Details></Details>
7   </Plugin>
8
9   <Detector class="com.briangoetz.findbugs.plugin.CallSystemGC">
10     <Details>
11 <![CDATA[
12 Finds calls to System.gc().
13 ]]>
14     </Details>
15   </Detector>
16
17   <BugPattern type="SYSTEM_GC">
18     <ShortDescription>Method calls System.gc()</ShortDescription>
19     <LongDescription>Call to System.gc() method in {1}</LongDescription>
20     <Details>
21 <![CDATA[
22 Library code should not call System.gc()
23 ]]>
24     </Details>
25   </BugPattern>
26
27   <BugCode abbrev="GC" >Garbage collection</BugCode>
28 </MessageCollection>
29

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

   结束语

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

  代码下载: j-jtp06206fb_plugin_src.jar

最后更新于: 2009-02-21 01:34
 

欢迎转载

本站文章欢迎转载,但请注明出处(http://www.javajia.com,Java家)

其他相关文章