3.4、No stupid subclasses - TestCase again
我们应用命令模式来表示一个测试。命令执行依赖一个这样的方法:execute(),在TestCase称为run(),通过它使命令得到调用,这使得我们能用这个相同的接口实现不同的命令。
我们需要一个普遍的接口来运行我们的测试。然而所有的测试用例可能是在一个类中用不同的方法实现的,这样可以避免为每一种测试方法创建一个类,从而导致类的数量急剧增长。某个复杂测试用例类也许实现许多不同的测试方法,每个测试方法定义了一个简单测试用例。每个简单测试用例方法有象这样的名字:testMoneyequals或testMoneyAdd,测试用例并不需要遵守那个简单的命令模式接口,同一个Command类的不同实例可以调用不同的测试方法。因此,下一个问题就是,在测试客户(测试的调用者)的眼里,要让所有的测试用例看起来是一样的。
回顾一下,这个问题被设计模式解决了,我们想到了Adapter模式。Adapter模式的意图就是,将一个已经存在的接口转变为客户所需要的接口。这符合我们的需要,Adapter有几种不同的方式做到这一点。一个方式就是类适配(class adapter),就是用子类来适配接口,具体说就是,用一个子类来继承已有的类,用已有类中的方法来构造客户所需要的新的方法。例如,要将testMoneyequals适配为runTest,我们继承MoneyTest类,覆写runTest方法,这个方法调用testMoneyEquals方法。
public class TestMoneyEquals extends MoneyTest { public TestMoneyEquals() { super("testMoneyEquals"); } protected void runTest () { testMoneyEquals(); } } |
使用子类适配的方式要求为每个测试用例实现一个子类,这增加了测试者的负担。JUnit框架的一个目标就是,在增加一个用例时尽量保持简单。另外,为每个测试方法创建一个子类也会导致类膨胀,如果有许多类,这些类中就那么一个方法,这是不值得的,为它们取有意义的名字都很困难。
Java提供了匿名内隐类机制,解决了命名问题。我们用匿名内隐类来达到Adapter目的,且不用命名:
TestCase test= new MoneyTest("testMoneyEquals ") { protected void runTest() { testMoneyEquals(); } }; |
这比通常的子类继承方便多了,它仍然在编译时进行类型检查,代价是增加了开发人员的负担。Smalltalk Best Practice Patterns描述了这个问题的另外一个解决方案,不同的实例在相同的pluggable behavior下行为表现不同。其思想就是,使用一个类,这个类可以参数化,即根据不同的参数值执行不同的逻辑,因此避免了子类继承。
最简单的可插入行为(pluggable behavior)形式是可插入选择子(Pluggable Selector)。在SmallTalk中,Pluggable Selector是一个变量,它指向一个方法,是一个方法指针。这个思想不局限于SmallTalk,也适用于Java。在Java中没有方法选择子的概念,然而,Java的反射(reflection)API能根据方法名这个字符串来调用方法,我们能利用Java的反射特性实现Pluggable Selector。通常我们很少使用Java反射,在这里,我们要涉及一个底层结构框架,它实现了反射。
JUnit提供给测试客户两种选择:或者使用Pluggable Selector,或者使用匿名内隐类。默认地,我们使用Pluggable Selector方式,即runTest方法。在这种方式中,测试用例的名字必须与测试方法的名字一致。如下所示,我们用反射特性调用方法。首先,我们查看方法对象,一旦有了这个方法对象,我们就可以传给它参数,并调用它。由于我们的测试方法不带参数,因此,我们传进一个空的参数数组:
protected void runTest() throws Throwable { Method runMethod= null; try { runMethod= getClass().getMethod(fName, new Class[0]); } catch (NoSuchMethodException e) { assert("Method \""+fName+"\" not found", false); } try { runMethod.invoke(this, new Class[0]); } // catch InvocationTargetException and IllegalAccessException } |
JDK1.1反射API只让我们查找public方法,因此你必须把测试方法定义为public,否则你会得到NoSuchMethodException例外。
这是该阶段的设计,Adapter模式和Pluggable Selector模式。
图4:TestCase应用了Adapter模式(匿名内隐类)和Pluggable Selector模式
由于TestCase中只有一个runTest方法,那么是不是说一个TestCase中只能放一个测试方法呢?为此引入Pluggable Selector模式。在TestCase中放置多个名为testXxx()的方法,在new一个TestCase时,用selector指定哪个testXxx方法与模板方法runTest对接。
3.5、不用担心是一个测试用例还是许多测试用例-TestSuite
一个系统通常要运行许多测试。现在,JUnit能运行一个测试,并用TestResult报告结果,下一步就是扩展JUnit,让它能运行许多不同的测试。如果测试的调用者并不在意它是运行一个测试还是许多测试,即它用同样的方式运行一个测试和运行许多测试,那么这个问题就解决了。Composite模式可以解决这个问题,它的意图就是,将许多对象组成树状的具有部分/整体层次的结构,Composite让客户用同样的接口处理单个的对象和整体组合对象。部分/整体的层次结构在此很有意义,一个组合测试可能是有许多小的组合测试构成的,小的组合测试可能是有单个的简单测试构成的。
Composite模式有以下参与者:
- Component:是一个公共的统一的接口,用于与测试交互,无论这个测试是简单测试还是组合测试。
- Composite:用于维护测试集合的接口,这个测试集合就是组合测试。
- Leaf:表示简单测试用例,遵从Component接口。