|
摘要
一段时间以来,软件开发业一直在推崇单元测试、编程标准执行、指标评估和按合同设计(Design by Contract)等技术。如果能实施这些技术,就能极大地改善软件的可靠性并减少开发时间和成本。但是直到目前为止,由于需要复杂而庞大的工作量,只有很少的开发人员能够实际采用它们。Jtest实现了自动化的解决方案,排除了Java开发人员技术应用上的障碍,使得开发人员能够顺利地采用这些技术。
介绍
成功地开发可靠的Java软件有两个关键方面:
- 通过贯彻执行Java编程标准减少出错概率
- 通过彻底测试每个新写的类将错误扼杀在萌芽状态
Jtest是第一个自动化Java单元测试工具。Jtest自动测试任何Java类或部件,而不需要您写一个测试用例、驱动程序或桩函数。只要点击一个按钮,Jtest自动测试代码构造(白盒测试)、测试代码功能性(黑盒测试)、维护代码完整性(回归测试)和静态分析(编程标准执行和指标度量)。不需要复杂的设置,Jtest能够立即使用并指出问题。如果您使用“按合同设计”技术在代码中加入描述信息,Jtest能够自动建立和执行测试用例验证一个类的功能是否符合其功能描述。
Jtest能够帮助您防止错误,其可定制的静态分析特性让您能够自动执行超过240个软件业认可的编程标准,建立和执行任何数量的定制编程标准,并对它们进行剪裁以适应特定的项目和团队。
本文解释了单元测试和编程标准执行等开发技术如何帮助您防止错误并提高软件可靠性,以及Jtest如何自动化这些技术使得它们能够实际应用到快速开发过程中去。
单元测试
什么是单元测试?
开发人员常常把单元测试当作模块测试。通常模块是指一个大应用的一部分,如一个应用模块或一个子程序。在这里我们有所区别,我们指的单元测试是针对一个应用中的基本单元或部件;在Java中,是测试一个类。
单元测试的好处
单元测试能够极大地改善软件质量,它是在最容易和成本最低的阶段帮助您检测和纠正错误。首先,单元测试最接近错误,它能够有效低帮助您检测在应用级测试中很难找到的错误。图1和图2说明了这一原理。

图1.测试一个应用
图1展示了一个测试包含多个对象的应用的模型。大椭圆表示应用,小椭圆表示对象,箭头表示用户输入,红星表示潜在的错误。
为了发现错误,必须修改输入,对象间的相互作用将迫使某对象引发潜在的错误。这一点无疑是有一定难度的。想象一下你站在一张台球桌前,所有的球围成一个三角形放置在桌子中间,要求你一杆击球将中间的球打入指定的袋中。这是一件多么困难的事情啊!就像要设计一个输入从而发现应用中的一个错误一样。由于其难度,开发人员只能依赖应用软件的运行失败来发现错误,但却仍然没有测试到许多类。因此只做集成测试是一件非常困难的事情。

图2. 测试一个类
如图2所示,类一级测试提供了一种更有效的发现错误的方法。单独测试一个与其它对象分离的类时,由于更接近错误,找到潜在的错误就会变得容易得多。用上述台球的例子来比喻,就像是台面上只有壹两个球时,一杆将一个球击入指定的袋中显然要容易的多。
单元测试简化错误检测的第二个方面是防止错误繁衍出更多的错误,使您避免了总是要穿过重重迷雾去寻找一个简单的错误。因为错误之间是有相互作用的,当您在代码中遗留了一个错误,它可能会导致更多的错误。因此如果您延迟到以后的开发阶段才去测试,您可能不得不去纠正更多的错误,花费更多的时间去发现和改正每个错误,并且要修改更多的代码。如果您在刚开发完一个类的时候测试,发现和纠正一个错误就会容易的多,并且错误繁衍的机会也会降到最低限度。结果是:极大地减少了调试时间和成本。
执行单元测试
手工执行单元测试是困难、乏味和非常耗时的。
执行单元测试的第一步是使得一个类可测。需要两个步骤:
- 设计能够运行这个类的测试驱动程序。
- 设计桩函数,它们为被测类所引用的任何外部资源返回值,这些外部资源当前或者不存在或者无法访问。
建立测试程序涉及到建立一个新类,它只用于测试原始类。测试驱动程序应该包括下列性质:
- 一个指定设置和清除的标准途径。
- 一个选择个别测试和所有测试的方法。
- 一种分析预期或意外输出的手段。
- 一种错误报告的标准形式。
如果您的类引用的任何外部资源(如外部文件、数据库和CORBA对象等)当前尚不存在或无法访问,您必须建立桩函数,它们能够返回相似于实际外部资源能够返回的值。在建立这些桩函数时,您需要选择桩函数的返回值以便测试类的功能性,并完全覆盖整个类。
为了保证对类的测试更彻底和精确,可能需要作若干次修改和重写。一旦建立了测试驱动程序,您必须小心地检查它们以防止它们包含如何错误。测试驱动程序中的错误会破坏测试,但是您却不能孤立地测试一个类,也无法测试驱动程序。
在使得一个类可测后,您需要设计和执行必要的测试用例。理想的情况是您需要测试类的构造(即执行白盒测试),测试类的功能性(即执行黑盒测试),然后在每次代码修改后执行回归测试,保证任何变化不影响类的完整性。
您大致上已经能够看到,手工单元测试需要消耗大量的时间、精力和资源,这就是为什么单元测试不能得到广泛应用的原因。Jtest的自动化测试过程能够极大地加速单元测试,并测试得更彻底更精确。您只需要简单地告诉Jtest一个需要测试的类或项目(一组被测类),然后Jtest能够自动检查每个类,生成相应的测试驱动程序以及任何必要的桩函数,并使用构造、功能和回归测试技术自动测试每个类;还能对所有的.java文件执行静态分析。
白盒测试
白盒测试检查一个类在结构上是否健全。它不根据类的描述测试其行为,而是保证一个类不会垮掉并且通过非预期的输入时运行正确。
白盒测试包括生成和执行测试输入,这些输入数据的设计依据类的内部结构,试图找出是否存在任何对类的使用能够让类垮掉(在Java中等价于抛弃一个未捕捉到的运行时异常),以及是否存在任何代码缺陷可能导致代码出错。成功的白盒测试取决于测试输入的能力,是否能够尽可能全面地覆盖类的方法并发现能够引发类异常的输入。
及早防止和检测结构性问题对Java开发人员来说尤为重要。在大多数编程语言中(如C和C++),一个非法操作通常导致程序的突然终止。而Java提供一个非常简单的机制,捕捉运行时出现的异常,并让程序继续运行,这样可以简化对操作系统和其它服务的调用。另一方面,运行时异常源自非法操作,指出了程序中的一个错误。因此让程序继续运行通常会导致比C++中的突然终止更坏的情形。程序保持运行,就像问题不曾发生一样,但它将进入一种不稳定的状态,可能会产生不正确的结果并/或破坏正在存取的资源。
虽然白盒测试是保证类和应用质量的关键步骤,但手工执行白盒测试的困难常常使开发队伍取舍两难,要么干脆跳过这一步骤,要么大大简化。有效地执行白盒测试需要开发人员确切知道什么测试用例对于全面执行被测类是必要的。这一切对于手工测试来说都是相当困难的。当前的研究结果表明一个典型的软件企业仅测试了其开发的30%的源代码。一个原因是编写测试很少走到的执行路径或边界条件的测试用例很困难。要达到有效的白盒测试所要求的覆盖性指标需要相当数量的路径被执行过。例如,1万行的源程序可能有几千万条路径;手工编写能够测试所有路径的输入数据几乎是不可能的。
Jtest的测试生成系统专利技术(patent #5,784,553 & #5,761,408)为开发人员提供了一种省时有效的白盒测试方法。Jtest通过自动生成和执行能够全面测试类代码的测试用例,使白盒测试完全自动化。Jtest使用一个符号化的虚拟机执行类,并搜寻未捕获的运行时异常。对于检测到的每个未捕获的运行时异常,Jtest报告一个错误,并提供导致错误的栈轨迹和调用序列。Jtest的先进技术保证它能够自动测试类的所有代码分支,从而彻底检查被测类的结构。
换句话说,Jtest自动生成高质量的测试用例集合,发现尽可能多的结构性错误,而且:
- 不需要用户写一点测试脚本语言或测试用例。
- 不需要用户写测试驱动程序。
- 不修改源代码。
- 不要求完整的应用。
Jtest报告下列未捕获的运行时异常:
- 行为错误的方法:这些方法对于某些特定输入不会产生异常。必须修改这些代码。
- 非预期参数:这一问题出现在当某方法遇到非预期的输入(不知任何处理)而产生一个异常。这些问题的修正可以通过检查输入并产生一个IllegalArgumentException (IAE)(假如该输入是非法的)。改正这类问题可以使代码更清晰更易维护。
- 行为正确的方法:这时,方法的正确输出是产生一个异常。在这种情形下,建议开发人员修改代码,将这类异常的产生置于方法的throw子句中。这会得到更清晰的代码并易于维护。
- 仅为开发人员使用的方法:在这种情况下,这些方法“不被假设”成处理Jtest生成的输入,开发人员是这些方法的唯一使用者,并且不传递这些输入参数。最好的办法是修改这些代码,让它产生一个IAE。这将带来额外的好处,使代码更易阅读。
总之,通过执行自动白盒测试,并提示上述类型的问题,Jtest能够为开发人员节省大量的时间并防止了错误。由于能够自动执行白盒测试的各个步骤,Jtest对开发人员来说是非常实用的,为了保证质量可以经常执行这一综合性测试。更进一步,使用测试生成系统技术产生的测试输入,Jtest使得白盒测试比手工测试更精确更有效。
白盒(构造)测试验证对一个类的非预期输入不会导致程序的崩溃。为执行白盒测试,您需要设计和执行根据类的内部结构编写的测试输入,检查是否存在会导致类运行失败的任何可能的对类的使用,以及是否存在某些编程缺陷可能会导致代码更容易出错。白盒测试能否成功的关键取决于测试输入的能力,是否能够尽可能全面地覆盖类的方法,并找出引起未捕捉到的运行时异常的输入。
尽可能早的防止和检测构造问题对Java软件开发来说更为关键。在大多数语言(如C和C++)中,一个非法程序操作常常导致程序的突然中断。Java相对来说提供了一种非常简单的机制来捕获运行时出现的异常并让程序继续运行,这种机制的设计可以简化对系统和其它服务调用的处理。另一方面,一个非法操作引起运行时异常确实指出了程序中的一个错误。捕捉它们并让程序继续运行通常比C++中的突然中断更有问题。带有问题的程序将继续运行是乎好象没有问题出现过,但极有可能进入一种矛盾状态,并产生不准确的结果或破坏它所存取的资源。
虽然白盒测试是保证类和应用质量的一个关键步骤,但手工执行的难度通常会使开发人员望而却步或草草了事。有效地执行白盒测试需要我们能够确定要完全检查被测类那些测试用例是必需的,这对于手工测试来说是太难了。目前的研究表明,典型的公司只测试了其开发的30%的代码,而其余的70%从来没有被测过。一个原因是编写能够测试很少执行的路径或极端的条件的测试用例很困难。例如,一个典型的1万行代码大约有1亿条可能的路径;手工编写能够执行所有路径的测试输入是不可行的或者说几乎是不可能的。
Jtest使用独特的技术完全自动化白盒测试过程。Jtest分析每个被测类的内部结构,自动设计和执行能够完全测试类的结构的测试用例,然后确定每个测试输入是否会产生一个未捕获的运行时异常。对于检测到的每个异常报告一个错误信息并提供一个导致错误的栈轨迹和调用序列。
假如我们有下面一个类代码,并要测试其构造是否强壮。 packageexamples.eval;
publicclassSimple
{
public static int map (int index) {
switch (index) {
case 0:
case10:
return -1;
case 2:
case 20:
default:
return -2;
}
}
public static boolean startsWith (String str, String match) {
for (int i = 0; i < match.length (); ++i)
if (str.charAt (i) != match.charAt (i))
return false;
return true;
}
public static int add (int i1, int i2) {
return i1 + i2;
}
}
Jtest分析该类,然后生成大量的根据非预期输入设计的测试用例,并执行。Jtest自动生成的测试用例发现了下列没有捕获的运行时异常。
下图显示了Jtest为测试该类的构造自动生成的一些测试用例,通过一组广泛的输入变化完全执行该类的方法。
Jtest能够对任何Java类或部件进行白盒测试,包括那些引用外部资源(如外部文件、数据库、EJB和CORBA)的类。如果您正在测试一些引用外部资源的类,Jtest将自动生成必要的桩函数,或者您可以选择直接调用实际的外部方法或您自己的桩函数。对使用CORBA的类,Jtest提供ORB和其它引用对象的桩函数。对使用EJB的类,Jtest调用Bean初始化程序并提供一个模拟的容器,然后执行白盒测试,保证这些类能够恰当地执行。
您可以根据自己的需要剪裁Jtest的错误报告。如果您对代码中一些正当的异常情况使用了特定的@exception注释标记,Jtest将屏蔽它们。如果您使用了特定的@pre注释标记为有效的方法输入说明允许的范围,Jtest将屏蔽掉落在该范围之外的输入所发现的错误。您还可以使用快捷菜单或禁用配置面板屏蔽异常情况。
黑盒测试
黑盒测试检查一个类的运行是否符合其定义,也称功能测试;即检查一个类对每个输入是否产生正确的输出。通常输入数据不是通过检查代码的结构(像在白盒测试中所做的)设计出来的,而是根据类的定义(描述代码做什么)。黑盒测试对于保证类的强壮性是重要的,它等于保证类是否按照假设的那样去做,是否实现了定义的所有部分。
黑盒测试通常需要用户严格确定需要测试什么,建立测试计划,建立一组输入和相应的正确输出。然后常常使用一个工具运行输入数据,并帮助用户验证是否产生正确的输出。
使用Jtest进行黑盒测试比使用任何其它工具更简单(也更有效)。Jtest的专利技术通过分析类的字节码自动生成一组核心输入;这些输入经过精心设计能够达到尽可能高的代码覆盖。由于自动生成的输入的设计目标是覆盖类的方法,而非验证其定义,因此用户可能需要增加自己的测试用例。(这一点是可以理解的,自动技术是针对语法的,机器并不能理解人的真正意图。)
用户定义的输入可以直接添加到树形表示的测试用例中,其中节点代表方法的每个参数或者能够存贮在全局或局部存储库中的常量和方法;在这里,输入可以方便地加入到任何方法的参数中。当测试进行时,Jtest自动执行所有的输入并以简单的树形表示显示相应的输出。在以后的功能测试或回归测试测试中出现错误时,Jtest会自动通告用户。
由于Jtest自动生成了一组很棒的核心输入,大大减少了开发人员和测试人员需要建立的测试用例的数量,并且能够比手工设计覆盖程序中更多的代码--多快好省。
黑盒(功能性)测试检查一个类的行为是否符合其功能说明。为了执行黑盒测试,您需要建立一组输入/输出关系,它们能够测试是否准确实现了类的功能说明。对说明文档中的每一项至少需要有一个测试用例,最好这些测试用例还能测试每一项说明的各种边界条件。测试用例准备好后,您执行它们并验证是否产生了准确的结果。
如果您的类代码中含有按合同设计(DbC)格式的说明信息,Jtest能够完全自动化黑盒测试过程。如果没有,Jtest也能比手工做黑盒测试更容易更有效。
DbC是一种形式化方法,使用注解在代码中加入说明信息。基本上,使用一种描述软件合同的形式语言能够明确表达代码说明。这些合同说明了这样的需求:
- 在调用一个方法之前必须满足的条件(前置条件)。
- 在调用一个方法之后必须满足的条件(后置条件)。
- 在执行中的说明点必须满足的断言。
Jtest读取类代码中用DbC语言定义的说明信息,然后自动基于这些信息开发测试用例。Jtest设计按照下面的规则设计黑盒测试用例:
- 如果有后置条件,Jtest建立验证是否满足这些条件的测试用例。
- 如果有断言,Jtest建立试图使断言失败的测试用例。
- 如果有不变条件(即应用于类的所有方法),Jtest建立试图使不变条件失败的测试用例。
- 如果有前置条件,Jtest试图找到能够走通前置条件中所有路径的输入。
- 如果被测方法调用的其它方法中包含已说明的前置条件,Jtest确定被测方法是否能将不允许的值传递给其它方法。
如果发现非法的说明信息,Jtest报告如下:
关于Jtest如何自动建立和执行验证类功能性测试用例以及DbC说明如何帮助Jtest的黑盒测试的详细描述,参见“利用按合同设计方法自动化Java软件和部件测试”。
如果您没有使用DbC,Jtest也能帮助您建立黑盒测试用例。您可以使用Jtest自动生成一组测试用例作为黑盒测试用例集合的基础,然后通过加入自己的测试用例扩展它们。
可以有多种加入测试用例的方法。例如:
- 直接在表示每个方法参数的树节点中键入方法的输入。
- 设置全局或局部的常量和方法,然后加入到任何方法参数。
- JUnit格式的测试类作为测试用例。
如果一个类引用外部资源,您可以进入自己的桩函数或让Jtest调用实际的外部方法。
当测试运行时,Jtest使用任何可以得到的桩函数,自动执行输入,并以树形表示显示对应这些输入的输出结果。然后您可以观察输出结果并验证。当说明和回归测试错误出现时,Jtest能够自动通报。
回归测试
执行准确的回归测试是保证软件质量和可靠性的另一必要步骤。回归测试即使用前面测试所用的同一组输入和测试参数测试修改过的代码,它是保证一个类在修改后不会引入新的错误或者检查是否成功地排除了错误的唯一途径。每次一个类被修改或用于一个新环境,都应该进行回归测试以保证类的完整性。
Jtest的回归测试使得您能够在类一级执行回归测试,这意味着您能够更早地运行测试用例以监测代码的完整性。Jtest完全自动执行回归测试有关的所有步骤。假使用户没有说明正确的输出,Jtest能够记忆前面的输出结果,并在每次测试时进行比较,当任何输出发生变化时报告一个错误。当然,如果用户指定正确的输出结果,在回归测试时Jtest使用这些值作为参考结果。为了使自动的回归测试尽可能快速准确,Jtest在测试一个或一组类时自动保存所有的测试输入和设置,然后将测试加入到Jtest的菜单选项中。其结果是,用户执行回归测试所需要的只是在测试菜单中选择一个合适的测试,然后按下开始按钮。您还可以将批处理模式的Jtest集成到夜间建立中,保证回归错误能够及时发现和纠正。
编程标准执行
什么是编程标准
执行编程标准是一种已经被证明能够增强软件可靠性和缩短开发周期的软件开发实践。编程标准是一些“规则”,它们通过减少出错的机会防止错误。编程标准应该在代码刚编写完后立即执行。如果能够坚持执行编程标准,它们能够有效地阻止编程错误进入代码中。
我们用一个例子来说明。下面代码中一个简单的空格错误破坏了代码的功能性: public class PT_TLS {
static int method (int i) {
switch (i) {
case 4:
case3:
i++;
break;
case 25:
wronglabel:
break;
default:
}
return i;
}
public static void main (String args[]) {
int i = method (3);
System.out.println (i);
}
}
开发人员打算写case 3而不是case3,这个简单的失误使case3变成了一个标号。当i等于3时,程序不会进入case3,而总是进入default。该代码 [1] [2] 下一页
|