1、介绍
在较早的文章(Test Infected: Programmers Love Writing Tests)中,我们描述了如何用一个简单的框架编写可重复的测试;本文则说明这个框架是如何构造的。
仔细地学习JUnit框架,从中可以看出我们是如何设计这个框架的。我们看到不同层次的JUnit教程,但在本文中,我们希望更清楚地说明问题。弄清JUnit的设计思路是非常有价值的。
我们先讨论一下Junit的目标,这些目标会在JUnit的每个细小之处得到体现。围绕着JUnit的目标,我们给出Junit框架的设计和实现。我们会用模式和程序实现例子来描述这个设计。我们还会看到,在开发这个框架时,当然还有其它的可选途径。
2、目标
什么是JUnit的目标?
首先,我们回到开发的前提假设。我们假设如果一个程序不能自动测试,那么它就不会工作。但有更多的假设认为,如果开发人员保证程序能工作,那么它就会永远正常工作,与与这个假设相比,我们的假设实在是太保守了。
从这个观点出发,开发人员编写了代码,并进行了调试,还不能说他的工作完成了,他必须编写测试脚本,证明程序工作正常。然而,每个人都很忙,没有时间去进行测试工作。他们会说,我编写程序代码的时间都很紧,那有时间去写测试代码呢?
因此,首要的目标就是,构建一个测试框架,在这个框架里,开发人员能编写测试代码。框架要使用熟悉的工具,无需花很多精力就可以掌握。它还要消除不必要的代码,除了必须的测试代码外,消除重复劳动。
如果仅仅这些是测试要作的,那么你在调试器中写一个表达式就可以实现。但是,测试不仅仅这些。虽然你的程序工作很好,但这不够,因为你不能保证集成后的即使一分钟内你的程序是否还会正常,你更不能保证5年内它还是否正常,那时你已经离开很久了。
因此,测试的第二个目标就是创建测试,并能保留这些测试,将来它们也是有价值的,其它的人可以执行这些测试,并验证测试结果。有可能的话,还要把不同人的测试收集在一起,一起执行,且不用担心它们之间互相干扰。
最后,还要能用已有的测试创建新的测试。每次创建新的测试设置或测试钳(test fixture)是很花费代价的,框架能复用测试设置,执行不同的测试。
3、JUnit设计
最早,JUnit的设计思路源于"用模式生成架构(Patterns Generate Architectures)"一文。它的思想就是,从0开始设计一个系统,一个一个地应用模式,直到最后构造出这个系统的架构,这样就完成一个系统的设计。我们马上提出要解决的架构问题,用模式来解决这个问题,并说明如何在JUnit中应用这些模式的。
3.1、从测试用例TestCase开始
首先我们创建一个对象来表示基础概念:测试用例(TestCase)。测试用例常常就存在于开发人员的
头脑中,他们用不同的方式实现测试用例:
- 打印语句
- 调试表达式
- 测试脚本
如何我们想很容易地操纵测试,那么就必须把测试作为对象。开发人员脑海中的测试是模糊的,测试作为对象,就使得测试更具体了,测试就可以长久保留以便将来有用,这是测试框架的目标之一。同时,对象开发人员习惯于对象,因此把测试作为对象就能达到让编写测试代码更具吸引力的目的。
在这里,命令模式(command)满足我们的需要。该模式把请求封装成对象,即为请求操作生成一个对象,这个对象中有一个“执行(execute)”方法。命令模式中,请求者不是直接调用命令执行者,而是通过一个命令对象去调用执行者,具体说,先为命令请求生成一个命令对象,然后动态地在这个命令对象中设置命令执行者,最后用命令对象的execute方法调用命令执行者。这是TestCase类定义代码:〔此处译者有添加〕
public abstract class TestCase implements Test {
...
}
因为我们希望通过继承复用这个类,我门把它定义成“public abstract”。现在我们先不管它实现Test接口,在此时的设计里,你只要把TestCase看成是一个单个的类就行了。
每个TestCase有一个名字属性,当测试出现故障时,可以用它来识别是哪个测试用例。
public abstract class TestCase implements Test { private final String fName; public TestCase(String name) { fName= name; } public abstract void run(); … } |
为了说明JUnit的演化进程,我们用图来表示各个设计阶段的架构。我们用简单的符号,灰色路标符号表明所使用的模式。当这个类在模式中的角色很明显时,就在路标中只指明模式名称;如果这个类在模式中的角色不清晰,则在路标中还注明该类对应的参与模式。这个路标符号避免了混乱,见图1所示。
图1 TestCase类应用了命令模式