拓胜(广州)计算机技术服务有限公司1/28对于许多团队来说,单元测试现在是开发过程的一个主要部分;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方法中的可用资源做预算,这样才能最大限度地提高信心。覆盖范围大的测试套件可以提高我们对代码使用的信心,因为它进行了一次彻底的代码审查。执行两次比执行一次较果好,因为每次都会发现另一次可能错过的错误。两拓胜(广州)计算机技术服务有限公司2/28次同样遵循收益递减规则,所以测试价值为X美元和代码审查价值为Y的QA计划要比价值为X+Y的任何一次测试或代码审查的效果好。添加静态分析静态分析是在不运行代码的情况下对其进行分析的过程,它与进行前面的代码审查时我们执行的操作非常相似,或者与标记可疑结构时IDE执行的操作非常相似。静态分析是添加到QA混合(QAmix)中的一项优良技术,因为它擅长查找其他方法(如测试和代码审查)可能错过的错误。静态分析相对比较容易一些,不像单元测试那样必须为要测试的每个类重新编写测试,您可以在任何代码上运行静态分析工具。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方法publicclassShoppingCart{privateBigDecimaltotalCost;privatebooleanqualifiesForFreeShipping(){...}privateBigDecimalgetShippingCost(){...}拓胜(广州)计算机技术服务有限公司3/28publicvoidcheckout(){...if(!qualifiesForFreeShipping())totalCost.add(getShippingCost());//WRONG!}}清单1中的错误是一种常见的错误,它忘记了对象是不可变的,从而将factory方法误认为mutator方法。如果在代码中查找此类错误,就会发现存在同一错误多次发生的情况,因为它来源于对特定库类工作方式的误解。对于查找此bug,负责任的开发人员可能会搜索整个代码基址来查找对BigDecimal.add()、subtract()等方法的调用,并寻找忽略返回值的其他实例。此策略是一个好的开头,但我们可以做得更好。在这里识别bug模式是非常容易的——忽略不可变对象上的求值方法(value-bearingmethod)的结果。识别出该模式后,构建识别此模式的检测器是相对简单的一件事件。(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模式(我们想为它构建一个检测器)的代码拓胜(广州)计算机技术服务有限公司4/28publicclassBadClass{publicvoiddoBadStuff(){System.gc();}}清单3显示了运行示例类时javap-c的输出:清单3.清单2中代码的字节码清单publicvoiddoBadStuff();Code:0:invokestatic#2;//Methodjava/lang/System.gc:()V3:return我们很快知道静态方法是通过invokestaticJVM指令调用的,invokestatic的操作数是java/lang/system类的gc:()V方法。字节码中的方法签名和类型名称与源代码中的略有不同,但它很容易用于字节码使用的编码。使用bug模式示例编写FindBugs检测器非常简单。清单4显示了扩展BytecodeScanningDetector基础类并重写sawOpcode()方法的检测器。当它遇到invokestatic指令时,它会检查被调用方法的类和名称,如果是System.gc()指令,它会报告bug实例。清单4.查找调用System.gc()的Bug检测器publicclassCallSystemGCextendsBytecodeScanningDetector{privateBugReporterbugReporter;publicCallSystemGC(BugReporterbugReporter){this.bugReporter=bugReporter;}publicvoidsawOpcode(intseen){if(seen==INVOKESTATIC){if(getClassConstantOperand().equals(java/lang/System)&&getNameConstantOperand().equals(gc)){bugReporter.reportBug(newBugInstance(SYSTEM_GC,NORMAL_PRIORITY).addClassAndMethod(this)拓胜(广州)计算机技术服务有限公司5/28.addSourceLine(this));}}}}将检测器包装为插件创建新的bug检测所需的最后一步是将其打包为一个插件。FindBugs插件包含一个或多个bug检测器、一个部署描述符和一个资源文件,它们被打包成一个JAR文件,放在FindBugs安装的插件目录中。称为findbugs.xml的部署描述符将定义已知的bug检测器和它报告的错误。称为messages.xml(对于本地化版本称为messages_xx.xml)的资源文件定义特定于语言的、将由FindBugsGUI使用的字符串,用它描述所报告的bug。清单5和清单6显示了示例bug检测器的部署描述符和资源文件。插件JAR中可以包括多个资源文件的本地版本;部署描述符和资源文件放置在插件JAR的顶级目录中。清单5.示例bug检测器的部署描述符FindbugsPluginxmlns:xsi=:noNamespaceSchemaLocation=findbugsplugin.xsdpluginid=com.briangoetz.findbugs.plugindefaultenabled=trueprovider=BrianGoetzwebsite==com.briangoetz.findbugs.plugin.CallSystemGCspeed=fastreports=SYSTEM_GC/BugPatternabbrev=GCtype=SYSTEM_GCcategory=PERFORMANCE//FindbugsPlugin清单6.示例bug检测器的资源文件MessageCollectionxmlns:xsi=