GUI 一般是基于模型-视图-控制器体系结构设计的。其中,视图是从模型中分离出来的。这种分离对自动化测试是一个挑战,因为我们很难检验模型中的状态改变是否在视图中得到了适当的反映,这样就产生了臭名昭著的 Liar View。诊断 Java 代码的这部分讨论的就是 Liar View 错误模式。
Liar, liar!
设想一下:您已经为一个分布式系统精心设计了一个极好的 GUI 程序,它包含了客户机请求的所有东西及其它一些东西。您已经让它运行通过了一个自动化测试套件的测试 ― 由于不变量的数量是个天文数字,因此,自动化测试是必须的。测试的结果是程序获得了一张“无错误的健康证明书”。
发布这个 GUI 的期限到了,但是,作为一个象您这样严格的程序员,只是为了发现错误的行为 ― 本该在自动化测试中就被捕捉到的行为,您启动了程序,对它做最后一次手工测试。但愿您能够避免这种情形。真的,您能够。
Liar View 错误模式
好的调试从好的测试开始。由于 GUI 程序中有大量的不变量需要检查,因此,自动化测试是必需的。但有时尽管已经通过了一套测试,在手工检查时,程序仍会出现本该由这些测试之一发现的错误行为。
快速跟踪代码
清单 1. JTable 及表模型 一个说明 Liar View 如何产生的简单 GUI 清单 2. 检查视图、模型及行的内容使断言说明真相
在分布式和多线程系统中,这种行为是常见的。在这些情况下,程序的“不确定性”本质经常就是原因所在。但在 GUI 中,却有另一种常见的原因 ― Liar View 错误模式。
症状
多数 GUI 程序测试,跟通常的程序测试一样,遵循下列步骤:
启动程序
检查程序状态的一些特征
尝试修改状态
检查状态是否已经按意愿被修改
但就象我已提到过的,有时对运行时程序行为的手工检查的结果会与测试得到的成功结果相矛盾:屏幕上可能显示一个队列,包含被测试(按设想进行)确认删除了的元素;而对象中可能包含报告显示已被更新了的陈旧数据。
类似这样的错误会使我们对自己的心智是否健全产生怀疑,或者更糟地陷入到康德的怀疑论哲学,怀疑起原因本身的有效性。
不要让这些发生在您身上。当正确对待原因时,它确实是有用的。尽管有反面的报告,但很少有程序员会在写代码时永久地丧失心智( 永久地是一个关键词)。
起因
找到这些错误的一个关键是要认识到,至少有一部分错误可以在测试套件中找到。
在测试 GUI 程序的过程中,错误最常发生的地方是在最后一步:检查状态是否已经按意愿被修改。原因是 GUI 一般是基于模型-视图-控制器(MVC)体系结构设计的。Swing 类库甚至把这种体系结构建到了 GUI 类自身的结构中。
在 MVC 体系结构中,程序的内部状态保存在模型中。视图响应改变模型状态的事件并相应更新屏幕图像。控制器把这两个组件连接在一起。
这种体系结构的优点是把视图从模型中分离出来,使得各自的实现可以独立修改。但是它对自动化测试方法却是一个挑战:我们很难检验模型中的状态改变是否在视图中得到了适当的反映。当这两者之间存在矛盾时,我们就会遇到一个 Liar View 错误模式的实例。
例如,考虑下面的简单 GUI。它在一列元素的内容被更新时显示其内容。 Controller 类的 main 方法被用作一个简单的测试。在实际的应用程序中,我把这个方法移到单独的测试类中,并将其挂到 JUnit 中(请参阅 参考资料)。
为了让我们能够使测试以慢动作方式进行,并在每个事件发生时对它进行手工检查,我添加了 pause() 方法和 PAUSE 字段。
清单 1. JTable 及表模型
1
import
java.awt.
*
;
2
import
java.awt.event.
*
;
3
import
java.util.Vector;
4
import
javax.swing.
*
;
5
public
class
Controller {
6
private
static
final
int
PAUSE
=
1
;
7
private
static
void
assert
(
boolean
assertion) {
8
if
(
!
assertion) {
9
throw
new
RuntimeException(
"
Assertion Failed
"
);
10
}
11
}
12
private
void
pause() {
13
try
{
14
synchronized
(
this
) {
15
wait(PAUSE);
16
}
17
}
18
catch
(InterruptedException e) {
19
}
20
}
21
public
static
void
main(String[] args) {
22
Controller controller
=
new
Controller();
23
JFrame frame
=
new
JFrame(
"
Test
"
);
24
Model model
=
new
Model();
25
JList view
=
new
JList(model);
26
view.setPreferredSize(
new
Dimension(
200
,
100
));
27
frame.getContentPane().add(view);
28
frame.pack();
29
frame.setVisible(
true
);
30
assert
(model.getSize()
==
0
);
31
32
controller.pause();
33
model.add(
"
test0
"
);
34
controller.pause();
35
model.add(
"
test1
"
);
36
controller.pause();
37
assert
(model.getSize()
==
2
);
38
controller.pause();
39
model.remove(
0
);
40
controller.pause();
41
assert
(model.getSize()
==
1
);
42
controller.pause();
43
System.exit(
0
);
44
}
45
}
46
class
Model
extends
AbstractListModel {
47
private
Vector elements
=
new
Vector();
48
public
synchronized
Object getElementAt(
int
index) {
49
return
elements.get(index);
50
}
51
52
public
synchronized
int
getSize() {
53
return
elements.size();
54
}
55
public
synchronized
void
add(Object o) {
56
int
index
=
this
.getSize();
57
this
.elements.add(o);
58
this
.fireIntervalAdded(
this
, index, index);
59
}
60
public
synchronized
void
remove(
int
index) {
61
this
.elements.remove(index);
62
}
63
}
64
您可能已经注意到这段代码有一个严重错误。如果我们运行这段测试代码,所有的断言都会是成功的,它表明已在列表中正确地添加或除去了项目。但如果我们通过某种方法,例如把 PAUSE 设成 1000,使运行速度降下来,那么我们就能以手工检查的方式运行测试。猜猜结果是什么?我们注意到,视图中不曾有项目被除去。
视图没被更新的原因是, Model 类中的 remove() 方法从未调用 fireIntervalRemoved() 来通知侦听器:模型的状态已经改变了。
但我们的测试方法中的所有断言都取得了成功。为什么呢?因为这些断言只是在模型中,而不是在视图中检查发生的更改。因为模型被适当地更新了,断言没能检测到遗漏的事件触发。