写下确保方法调用按正确顺序发生的单元测试的记录器
Eric E. Allen (eallen@cs.rice.edu)
博士研究生,Java 编程语言小组,Rice 大学
用 JUnit 进行单元测试是一个功能强大的方法,它可以确保您的代码基础的完整性,但是一些不变量比其他(方法调用序列是其中一种)更难测试。在诊断 Java 代码这一部分,Eric Allen 描述了怎样在您的单元测试中使用记录器(一种非凡的侦听器),来确保一个方法调用序列按恰当的顺序发生。请点击文章顶部和底部的讨论,与作者和其他读者在论坛上分享您关于本文的看法。
随着时间的推移,当系统开发人员,维护人员甚至是系统具体说明改变时,JUnit 框架提供一个很好的方法来改善系统的坚固性。通过测试,您可以检查到代码的某些不变量是受支持的。
测试通常分为两类:单元和接受测试:
单元测试确保组成组件完成其应完成的功能。
接受测试确保系统的最高级功能出现在用户面前时,与它设计时的功能一致。
JUnit 可帮助进行单元测试。
代码快速跟踪
清单 1. 同客户端通信的三个类
Greeter 类建立和中断同外部客户端的连接。Sender 类发送各种消息。 Coordinator 类治理另外两个类的实例,确保它们一起工作。
清单 2. 在每个对象中安装相同的记录器
通过将相同的、非凡的 Listener(称为 记录器)安装到每一个对象中,可以决定这些对象中方法的调用顺序。
清单 3. 一个 JUnit 测试
将消息存储为简单的 String,用这种简单的测试检查 playBack 的内容已能够满足我们的需要,而不必建立一个测试来检查记录的实例的每个可能反复。
理想情况下,为系统开发的单元测试会完全覆盖组成部分的预期不变量的设置,并能确保新的开发人员所作的任何更改都不会破坏现有代码。
实际上,一些不变量将会被测试忽略。部分原因是一些不变量在没达到全面的系统测试水平时,陷入到系统的许多孤立组件的交互作用中。
在本文中,我将讨论一个那种类型的不变量以及如何使用一个复杂的单元测试来检查此不变量。我要讨论的不变量类型是一组相关方法序列调用的恰当顺序。
与 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 一起确保响应各种预期的用户操作。
简而言之,记录器提供了一种测试组件集合体的方法,它比大多数单元测试覆盖的范围大,但还是比整个系统小。我希望您能和我一样,觉得它有用。
参考资料
请点击文章顶部或底部的讨论,参与关于本文的讨论论坛。
JUnit 主页 提供了大量关于 JUnit 和相关主题的信息。
Malcolm Davis 所著的“使用 Ant 和 JUnit 进行递增开发”(developerWorks,2000 年 11 月)讨论了怎样将这些工具集成到您的开发环境中去。
假如喜欢 JUnit,您可能想要检查全部面向多种语言的 xUnit 测试工具。
xUnit 工具套件是与极限编程 (XP) 一起使用的,极限编程是一种新的功能强大的方法,可快速开发整洁坚固的软件。
关于 XP 的快速初级读本,请查阅由 Roy Miller 和 Chris Collins (developerWorks,2001 年 3 月)合著的 “XP 精华”。
使用 VAJ 开发?Allison Pearce Wilson 提供了大量的关于 VAJ 和 XP 的资料。
" The UML Profile for Framework Architectures"(以 PDF 幻灯片格式)重点介绍了 JUnit 的具体案例分析。
关于作者
Eric Allen 毕业于 Cornell 大学,获得计算机科学和数学的学士学位。他目前是 Cycorp,Inc. Java 软件开发的负责人,并是 Rice 大学的编程语言小组的一个兼职研究生。他的研究涉及在源程序和字节码层次上的 Java 语言的语义模型和静态分析工具的开发。目前,他正在为 NextGen 编程语言(类属运行类型的 Java 语言扩展)实现一个源程序到字节码的编译器。可通过 eallen@cyc.com 联系 Eric。