3.4 不愚蠢的子类-再谈TestCase
我们已经应用了命令模式来描述测试。命令模式依赖一个像execute()(在TestCase中叫run())的简单方法来实现。这个简单的接口允许我们通过同一个接口来调用不同的方法实现。
我们需要一个接口来执行测试。无论如何,在同一个测试类中,所有的测试用例均以不同的方法来实现。这样可避免不断增长的类。一个给定的测试用例类可以实现很多不同的方法,每一个方法定义一个简单的测试用例。每一个测试用例通过类似于testMoneyEquals或者testMoneyAdd之类的名称来描述。这些测试用例不需要遵从一致的命令接口。类的不同实例,需要调用不同的方法。因此,我们下一个问题是让所有的测试用例都以同一种面目展示给测试调用者。
回顾可解决该问题的设计模式,适配器模式进入我们的脑中。适配器有以下的定义:“把一个类的接口转换为客户端期望的另一种接口形式”。这听起来很搭配。适配器有多种形式来做到这一点。其中一种方法为使用一个适配器类,它通过子类的方式来适配期望的接口。例如,把testMoneyEquals适配成runTest,我们可实现一个MoneyTest的子类,并且重载runTest去调用testMoneyEquals。
public class TestMoneyEquals extends MoneyTest {
public TestMoneyEquals() { super("testMoneyEquals"); }
protected void runTest () { testMoneyEquals(); }
}
子类的使用需要我们为每一个测试用例都实现一个子类。这样会增添测试者的负担。它违背了JUnit的目标即尽可能简单地去添加一个测试用例。另外,为每一个测试方法都创建一个子类会造成类的泛滥,许多类只有一个简单的方法并不值得这种资源的浪费,并且很难去给这些类提供有意义的命名。
Java提供了匿名内部类来解决类命名的问题。通过匿名内部类,我们可创建一个不需要类名的适配器:
TestCase test= new MoneyTest("testMoneyEquals ") {
protected void runTest() { testMoneyEquals(); }
};
一种简单的可拔插形式称为插件式选择器。插件式选择器来源于Smalktalk,但不局限于Smalktalk,它也适合于Java。在Java语言中没有方法选择器的概念。但是,Java的反射API可允许我们通过方法名去调用这个方法。在Java中我们可利用这个特性去实现插件式选择器。另外,我们通常不在普通的应用代码使用反射中。在我们的项目中要处理一个基础设施框架,反射正好用的上。
JUnit允许客户端选择使用插件式选择器或者实现一个匿名适配类。这样,我们把插件式选择器作为runTest方法的默认实现。在这个项目中,测试用例的名称等同于测试方法的名称。我们使用反射来调用方法以实现以上。首先,我们需要查询Method对象。一旦我们获取了方法对象,即可传参数来调用它。如果测试方法没有参数,我们可传递一个空参数数组:
protected void runTest() throws Throwable {
Method runMethod= null;
try {
runMethod= getClass().getMethod(fName, new Class[0]);
} catch (NoSuchMethodException e) {
assertTrue("Method \""+fName+"\" not found", false);
}
try {
runMethod.invoke(this, new Class[0]);
}
// catch InvocationTargetException and IllegalAccessException
}
JDK1.1 反射API只允许我们查询public方法。因此你必须定义测试方法为public,否则你将得到一个NoSuchMethodException异常。以下是设计快照,加入了适配器和插件式选择器。
图4 TestCase提供匿名内部类实现的适配器或者插件式选择器
3.5 不用关心一个或者多个--测试套件
我们需要运行多个测试。到目前为止,JUnit可以运行一个简单的测试用例并且通过TestResult来报告结果。我们下一个挑战是让它能运行多个不同的测试。当测试的调用者不必关心是运行一个还是多个测试用例时,这个问题就很容易解决了。一个流行的模式很适合这种情况,它就是组合模式。它的含义为:“将对象以树形结构组织起来,以达到整体与部分的层次结构。使得客户端对单个对象和组合对象的使用具有一致性。”整体和部分的层次结构,是我们感兴趣的关键点。我们需要支持测试套件的套件的套件。
组合模式引进以下的参与者:
元件:声明在我们测试中相互引用的接口。
组合物:实现元件这个接口,并包含测试的集合。
叶子:表现为组合中的一个测试用例,它符合组合物的接口形式。
这个模式告诉我们,需要引用一个抽象类同时为单个和组合对象定义公共的接口。这个类的主要形式为一个接口。在Java中使用组合模式,我们更倾向于定义一个接口,而不是一个抽象类。使用接口可避免JUnit在测试中使用特殊的基类。必须要求的是,所有的测试用例都要符合接口的形式。因此我们引用一个接口:
public interface Test {
public abstract void run(TestResult result);
}
测试用例在组合模式中符合叶子形式,需要实现以上的接口。
接着,我们引进组合物这个参与者。我们命名这个类为TestSuite。一个TestSuite把子测试用例放在一个集合中:
public class TestSuite implements Test {
private Vector fTests= new Vector();
}
run()方法委托给它的子测试用例来实现:
public void run(TestResult result) {
for (Enumeration e= fTests.elements(); e.hasMoreElements(); ) {
Test test= (Test)e.nextElement();
test.run(result);
}
}
图5 使用了组合模式的测试套件
最后,客户端必须能添加测试用例到套件中,通过addTest方法可以做到:
public void addTest(Test test) {
fTests.addElement(test);
}
注意,以上代码只依赖于Test这个接口。一旦TestCase和TestSuite符合Test接口,我们可以递归组合测试套件的套件。所有开发者可创建他们自己的测试套件。我们可以把那些套件组合为一个TestSuite,再运行它们。
以下是创建一个TestSuite的例子:
public static Test suite() {
TestSuite suite= new TestSuite();
suite.addTest(new MoneyTest("testMoneyEquals"));
suite.addTest(new MoneyTest("testSimpleAdd"));
}
这样工作地很好,但它要求我们手动添加所有的测试到一个套件中。早期的JUnit采用者告诉我们,这样很愚蠢。无论何时,你写了一个新的测试用例,你必须记得把它添加到静态的suite()方法中,否则它将不能运行。我们为TestSuite增加了一个更方便的构造函数,它以测试用例类作为参数。目的是为了提取测试方法并且创建一个套件来包含它们。这些测试方法必须遵循一个简单的约定,它们的方法名称要以“test”开头,并且没有参数。这个便利的构造函数使用以上的约定,通过使用反射寻找测试方法来构造这些测试对象。使用这个构造函数的简单代码如下:
public static Test suite() {
return new TestSuite(MoneyTest.class);
}
如果你只想运行这些测试用例中的一部分,原来的方式依然是有用的。
3.6 总结
我们已经在JUnit旅程的结尾部分了。以下的图例展示了使用模式的JUnit设计。
图6 JUnit模式总结
注意,TestCase作为框架的抽象中心,只是使用了4种模式。
这里是浏览所有模式的另一种方式。在这个故事墙中,你能按顺序地看到每个模式所带来的影响的抽象表现。因此,命令模式创建了TestCase类,模板方法模式创建了run方法,等等。(故事墙的表示法是图6去除了所有文字的表示法)。
图7 JUnit模式故事墙