用 JUnit进行单元测试是一个功能强大的方法,它可以确保您的代码基础的完整性,但是一些不变量比其他(方法调用序列是其中一种)更难测试。在诊断Java 代码这一部分,Eric Allen描述了怎样在您的单元测试中使用记录器(一种特殊的侦听器),来确保一个方法调用序列按恰当的顺序发生。请点击文章顶部和底部的 讨论,与作者和其他读者在论坛上分享您关于本文的看法。
随着时间的推移,当系统开发人员,维护人员甚至是系统详细说明改变时,JUnit 框架提供一个很好的方法来改善系统的坚固性。通过测试,您可以检查到代码的某些不变量是受支持的。
测试通常分为两类: 单元和 接受测试:
单元测试确保组成组件完成其应完成的功能。
接受测试确保系统的最高级功能出现在用户面前时,与它设计时的功能一致。
JUnit 可帮助进行单元测试。
理想情况下,为系统开发的单元测试会完全覆盖组成部分的预期不变量的设置,并能确保新的开发人员所作的任何更改都不会破坏现有代码。
实际上,一些不变量将会被测试忽略。部分原因是一些不变量在没达到全面的系统测试水平时,陷入到系统的许多孤立组件的交互作用中。
在本文中,我将讨论一个那种类型的不变量以及如何使用一个复杂的单元测试来检查此不变量。我要讨论的不变量类型是一组相关方法序列调用的恰当顺序。
与 JUnit 握手
在继续之前,熟悉 JUnit 和学会怎样轻松使用它来为您的代码写单元测试非常重要。在 参考资料一节,我已经包括了一个链接,它能链接到下载和开始使用 JUnit 所需要的所有信息。(如果您熟悉 JUnit,请直接跳到 第 1 个示例。)
单元测试为开发人员提供下列功能:
从接口透视图设计类
除去发行包中的类混乱
自动确认捕捉变化的错误
单元测试过程通常按照以下步骤进行:
决定您的组件该做什么。
正式地(或非正式地,取决于复杂性)设计您的组件。
写出单元测试来检查组件的活动。(在这一步,测试将不编译;代码还没写。写测试的目的是用来帮助确定组件的功能目的。)
按设计写出组件代码;如果有必要,则进行单元重组。
当测试(从第 3 步开始)通过后,停止编码过程。
集体讨论其它的代码中断的可能性;写出测试进行确认,然后修改代码。
每次探测到一个缺陷就要写一个新的测试。
每次改动代码后都要重新开始 全部测试。
JUnit 是由 Erich Gamma 和 Kent Beck 创建的一个简单构架,可用来编写可重复的测试,它使得构造一个可增加改动的测试套件变得相对简单,该测试套件可帮助开发人员评估开发的进展以及探测非故意的影响。JUnit 是 xUnit 架构的一个实例。
有了 JUnit,每个测试实例继承了 TestCase 类。其中名字以 “test” 开始的每个无参数的公共方法每次执行一次。测试方法调用测试下的组件,并对该组件的行为做出断言。在不能做出断言的时候,JUnit 还会报告失败的位置。
由于以下的原因,JUnit 尤其有用:
它是一个完整的、开放源代码的产品;您不必自己写或购买一个框架。
因为它是公开源代码的,所以它的许多用户都是很有经验的。
它允许您从产品代码中分离出测试代码。
在构建过程中很容易整合。
现在,您已经了解了 JUnit,让我们看一个示例。
Greeters 和 Senders
考虑下面的示例,它将给外部客户端发送不同的消息:
清单 1. 同客户端通信的三个类
public class Greeter { public void sayHello() {...} public void sayGoodbye() {...}}public class Sender { public void sendFirstMessage() {...} public void sendSecondMessage() {...}}public class Coordinator extends Thread { Sender s; Greeter g; public Coordinator(Sender _s, Greeter _g) { this.s = _s; this.g = _g; } public void run() { g.sayHello(); s.sendFirstMessage(); s.sendSecondMessage(); g.sayGoodbye(); }}
第一个类, Greeter ,负责建立和中断与外部客户端的连接。第二个类, Sender ,负责给客户端发送不同的消息。第三个类, Coordinator ,管理另外两个类的实例,确保它们共同合作同客户端进行通信。
勿庸置疑,按适当的顺序调用这些方法是至关重要的。但是将来的扩展和代码的单元重组可能会不经意间改变方法调用的顺序。例如,另一个开发人员可能将 Greeter 和 Sender 方法的调用移到单独的线程中,使用信号来控制调用它们的顺序。
不管发生什么改变,我们要怎样将测试放在我们的套件,才能保证在任何情况下都能按正确的顺序调用方法呢?与许多单元测试不同,我们不能仅仅调用这些方法并检查结果,因为我们想要检查的,并不是使用其中任何一个方法得出的结果。
记录您的下一伟大的步骤…
解决方法是使用一个特殊类型的侦听器,我们称之为 Recorder (记录器)。记录器保存它们所注册的每个对象上的每个方法调用。
记录器按方法被调用的顺序线性地保存这些记录,这非常象一个盒式磁带。通过将相同的 Recorder 安装到每一个对象中,可以检查这些对象中方法的调用顺序。请考虑下面的代码:
清单 2. 在每个对象中安装相同的 Recorder
public class Recorder { private StringBuffer tape; public Recorder() { this.tape = new StringBuffer(); } public String playBack() { return tape.toString(); } public void record(String s) { tape.append(s); }}public class Greeter { private Recorder r; public Greeter(Recorder _r) { this.r = _r; } public void sayHello() { r.record("sayHello();"); ... } public void sayGoodbye() { r.record("sayGoodbye();"); ... }}public class Sender { private Recorder r; public Sender(Recorder _r) { this.r = _r; } public void sendFirstMessage() { r.record("sendFirstMessage();"); ... } public void sendSecondMessage() { r.record("sendSecondMessage();"); ... }}
用 String 还是 ToString
注意 Sender 和 Greeter 中的每一个方法必须向 Recorder 通报新方法的调用。在这种方法下,记录器就像其他侦听器一样:任何改变发生时都必须通知它们。
另外,注意被传送到 Recorder 的消息是一个简单的 String 。使用 String 消息有利也有弊。一方面,一个较复杂的对象在每一种方法调用时都能被保存下来,提供更详细的信息。另一方面,这么复杂的对象将使得测试工作更加困难。
例如,使用清单 2 中的记录器,只要把下面简单的测试加到我们的套件中,就能决定调用的顺序:
清单 3. 一个 JUnit 测试
public void testOrderOfInvocation() throws InterruptedException { Recorder r = new Recorder(); Greeter g = new Greeter(r); Sender s = new Sender(r); Coordinator c = new Coordinator(s, g); c.start(); c.join(); assertEquals("sayHello();sendFirstMessage();sendSecondMessage(); sayGoodbye();",r.playBack());}
由于我们已经将消息保存为简单的 String 型变量 s,检查 playBack 的内容的测试就很简单:只要写出正确的 String ,然后与之对照检查就可以了。
另一方面,如果我们已经使用了一个较复杂类型的对象,我们不得不为这些对象中的每一个都构造一个同样的实例,并重述所有记录过的实例,检查它们之中每一个的等同性。另外,这还需要我们为记录过的对象的每个类写一个 equals 方法。
像这样的话,一次测试要做很多工作。我不知道您怎么样,但是我宁愿把时间花在写更多较简单的测试上(设计代码使得它们更容易),而不愿为我的测试写基础结构的代码。
这两种方法间的一个折衷是制作另一个 Recorder ,它可以存储非常复杂的数据,但有一个简单的 toString 方法可用来测试,就如上面提到的那个。于是,较复杂的数据可用于其他的测试,检查调用序列的详细属性。
准备好测试
用 Recorder 进行测试的思想可应用到许多类型的测试中:
除检查调用的简单顺序之外,记录器可在分布环境中使用,确保通信中不同的不变量在相互通信过程中保持不变。
记录器也可用来和 GUI 一起确保响应各种预期的用户操作。
简而言之,记录器提供了一种测试组件集合体的方法,它比大多数单元测试覆盖的范围大,但还是比整个系统小。我希望您能和我一样,觉得它有用。
没有什么可留恋,只有抑制不住的梦想,