JUnit设计模式分析
http://www.uml.org.cn/sjms/200442724.htm
IT先锋资深顾问 grid liu
这篇文章由grid liu发表在<程序员〉上。grid liu在IT先锋中担任资深顾问,负责J2EE技术的顾问咨询和培训工作。
摘要
JUnit是一个优秀的Java单元测试框架,由两位世界级软件大师Erich Gamma 和 Kent Beck共同开发完成。本文将向读者介绍在开发JUnit的过程中是怎样应用设计模式的。
关键词:单元测试 JUnit 设计模式
1 JUnit概述
1.1 JUnit概述
JUnit是一个开源的java测试框架,它是Xuint测试体系架构的一种实现。在JUnit单元测试框架的设计时,设定了三个总体目标,第一个是简化测试的编写,这种简化包括测试框架的学习和实际测试单元的编写;第二个是使测试单元保持持久性;第三个则是可以利用既有的测试来编写相关的测试。所以这些目的也为什么使用模式的根本原因。
1.2 JUnit开发者
JUnit最初由Erich Gamma 和 Kent Beck所开发。Erich Gamma博士是瑞士苏伊士国际面向对象技术软件中心的技术主管,也是巨著《设计模式》的四作者之一。Kent Beck先生是XP(Extreme Programming)的创始人,他倡导软件开发的模式定义,CRC卡片在软件开发过程中的使用,HotDraw软件的体系结构,基于xUnit的测试框架,重新评估了在软件开发过程中测试优先的编程模式。是《The Smalltalk Best Practice Patterns》、《Extreme Programming Explained》和《Planning Extreme Programming(与Martin Fowler合著)》的作者。
由于JUnit是两位世界级大师的作品,所以值得大家细细品味,现在就把JUnit中使用的设计模式总结出来与大家分享。我按照问题的提出,模式的选择,具体实现,使用效果这种过程展示如何将模式应用于JUnit。
2 JUnit体系架构
JUnit的设计使用以Patterns Generate Architectures(请参见Patterns Generate Architectures, Kent Beck and Ralph Johnson, ECOOP 94)的方式来架构系统。其设计思想是通过从零开始来应用设计模式,然后一个接一个,直至你获得最终合适的系统架构。
3 JUnit设计模式
3.1 JUnit框架组成
l 对测试目标进行测试的方法与过程集合,可将其称为测试用例。(TestCase)
l 测试用例的集合,可容纳多个测试用例(TestCase),将其称作测试包。(TestSuite)
l 测试结果的描述与记录。(TestResult)
l 测试过程中的事件监听者 (TestListener)
l 每一个测试方法所发生的与预期不一致状况的描述,称其测试失败元素。(TestFailure)
l JUnit Framework中的出错异常。(AssertionFailedError)
3.2 Command(命令)模式
3.2.1 问题
首先要明白,JUnit是一个测试framework,测试人员只需开发测试用例。然后把这些测试用例组成请求(有时可能是一个或者多个),发送到JUnit FrameWork,然后由JUnit执行测试,最后报告详细测试结果。其中包括执行的时间,错误方法,错误位置等。这样测试用例的开发人员就不需知道JUnit内部的细节,只要符合它定义的请求格式即可。从JUnit的角度考虑,它并不需要知道请求TestCase的操作信息,仅把它当作一种命令来执行,然后把执行测试结果发给测试人员。这样就使JUnit 框架和TestCase的开发人员独立开来,使得请求的一方不必知道接收请求一方的详细信息,更不必知道是怎样被接收,以及怎样被执行的,实现系统的松耦合。
3.2.2 模式的选择
Command(命令)模式(请参见Gamma, E., et al. Design Patterns: Elements of Reusable Object-Oriented Software, Addison-Wesley, Reading, MA, 1995)则能够比较好地满足需求。摘引其意图(intent),将一个请求封装成一个对象,从而使你可用不同的请求对客户进行参数化;对请求进行排队或记录请求日志...Command告诉我们可以为一个操作生成一个对象并给出它的一个execute(执行)方法。
3.2.3 实现
为了实现Command模式,首先定义了一个接口Test,其中Run便是Command的Execute方法。然后又使用Default Adapter模式为这个接口提供了缺省实现TestCase抽象类,这样我们开发人员就可以从这个缺省实现进行集成,而不必从Test接口进行实现。
我们首先来分析Test接口。它存在一个是countTestCases方法,它来统计这次测试有多少个TestCase,另外一个方法就是我们的Command模式的Excecute方法,这里命名为run。还有一个参数TestResult,它来统计测试结果
public interface Test {
/**
* Counts the number of test cases that will be run by this test.
*/
public abstract int countTestCases();
/**
* Runs a test and collects its result in a TestResult instance.
*/
public abstract void run(TestResult result);
}
TestCase是该接口的抽象实现,它增加了一个测试名称,因为每一个TestCase在创建时都要有一个名称,因此若一个测试失败了,你便可识别出是哪个测试失败。
public abstract class TestCase extends Assert implements Test {
/**
* the name of the test case
*/
private String fName;
public void run(TestResult result) {
result.run(this);
}
}
这样我们的开发人员,编写测试用例时,只需继承TestCase,来完成run方法即可,然后JUnit获得测试用例,执行它的run方法,把测试结果记录在TestResult之中,目前可以暂且这样理解。
3.2.4 效果
下面来考虑经过使用Command模式后给系统的架构带来了那些效果:
l Command模式将实现请求的一方(TestCase开发)和调用一方(JUnit Fromwork)进行解藕
l Command模式使新的TestCase很容易的加入,无需改变已有的类,只需继承TestCase类即可,这样方便了测试人员
l Command模式可以将多个TestCase进行组合成一个复合命令,实际你将看到TestSuit就是它的复合命令,当然它使用了Composite模式
l Command模式容易把请求的TestCase组合成请求队列,这样使接收请求的一方(Junit Fromwork),容易的决定是否执行请求,或者一旦发现测试用例失败或者错误可以立刻停止进行报告
l Command模式可以在需要的情况下,方便的实现对请求的Undo和Redo,以及记录Log,这部分目前在JUnit中还没有实现,将来是很容易加入的
3.3 Composite(组合)
3.3.1 问题
为了获得对系统状态的信心,需要运行多个测试用例。通过我们使用Command模式,JUnit能够方便的运行一个单独的测试案例之后产生测试结果。可是在实际的测试过程中,需要把多个测试用例进行组合成为一个复合的测试用例,当作一个请求发送给JUnit.这样JUnit就为面临一个问题,必须考虑测试请求的类型,是一个单一的TestCase还是一个复合的TestCase,甚至于要区分到底有多少个TestCase。这样Junit框架就要完成像下面这样的代码:
if(isSingleTestCase(objectRequest)){
//如果是单个的TestCase,执行run,获得测试结果
objectRequest.run()
}else if(isCompositeTestCase(objectRequest)){
//如果是一个复合TestCase,就要执行不同的操作,然后进行复杂的算法进行分
//解,之后再运行每一个TestCase,最后获得测试结果,同时又要考虑
//如果中间测试错误怎样????、
…………………………
…………………………
}
这样JUnit必须考虑区分请求(TestCase)的类型(是单个testCase还是复合testCase),而实际上大多数情况下,测试人员认为这两者是一样的。对于这两者的区别使用,又会使程序变得更加复杂,难以维护和扩展。于是要考虑,怎样设计JUnit才可以实现不需要区分单个TestCase还是复合TestCase,把它们统一成相同的请求?
3.3.2 模式的选择
当测试调用者不必关心其运行的是一个或多个测试案例的请求时,能够轻松地解决这个问题模式就是Composite(组合)模式。摘引其意图,将对象组合成树形结构以表示部分-整体的层次结构。Composite使得用户对单个对象和组合对象的使用具有一致性。在这里部分-整体的层次结构是解决问题的关键,可以把单个的TestCase看作部分,而把复合的TestCase(TestSuit)看作整体。这样我们使用该模式便可以恰到好处得解决这个难题。
Composite模式结构
Composite模式引入以下的参与者:
n Component:这是一个抽象角色,它给参加组合的对象规定一个接口。这个角色,给出共有的接口和默认得行为。其实就我们的Test接口,它定义出run方法。
n Composite:实现共有接口并维护一个测试的集合。就是我们的复合TestCase,TestSuit
n Leaf:代表参加组合的对象,它没有下级子对象,仅定义出参加组合的原始对象的行为,其实就是单一的测试用例TestCase,它仅实现Test接口的方法。
其实componsite模式根据所实现的接口区分为两种形式,分别称为安全式和透明式。JUnit中使用了安全式的结构,这样在TestCase中没有管理子对象的方法。
3.3.3 实现
composite模式告诉我们要引入一个Component抽象类,为Leaf对象和composite对象定义公共的接口。这个类的基本意图就是定义一个接口。在Java中使用Composite模式时,优先考虑使用接口,而非抽象类,因此引入一个Test接口。当然我们的leaf就是TestCase了。其源代码如下:
//composite模式中的Component角色
public interface Test {
public abstract void run(TestResult result);
}
//composite模式中的Leaf角色
public abstract class TestCase extends Assert implements Test {
public void run(TestResult result) {
result.run(this);
}
}
下面,列出Composite源码。将其取名为TestSuit类。TestSuit有一个属性fTests (Vector类型)中保存了其子测试用例(child test),提供addTest方法来实现增加子对象TestCase ,并且还提供testCount 和tests 等方法来操作子对象。最后通过run()方法实现对其子对象进行委托(delegate),最后还提供addTestSuite方法实现递归,构造成树形。
public class TestSuite implements Test {
private Vector fTests= new Vector(10);
public void addTest(Test test) {
fTests.addElement(test);
}
public int testCount() {
return fTests.size();
}
public Enumeration tests() {
return fTests.elements();
}
public void run(TestResult result) {
for (Enumeration e= tests(); e.hasMoreElements(); ) {
Test test= (Test)e.nextElement();
runTest(test, result);
}
}
public void addTestSuite(Class testClass) {
addTest(new TestSuite(testClass));
}
}
分析了Composite模式的实现后我们列出它的组成,如下图:
注意所有上面的代码是对Test接口进行实现的。由于TestCase和TestSuit两者都符合Test接口,我们可以通过addTestSuite递归地将TestSuite再组合成TestSuite,这样将构成树形结构。所有开发者都能够创建他们自己的TestSuit。测试人员可创建一个组合了这些套件的TestSuit来运行它们所有的TestCase。
public class AllTests extends TestCase {
public AllTests(String s) {
super(s);
}
public static Test suite() {
TestSuite suite1 = new TestSuite(我的测试TestSuit1);
TestSuite suite2 = new TestSuite(我的测试TestSuit2);
suite1.addTestSuite(untitled6.Testmath.class);
suite2.addTestSuite(untitled6.Testmulti.class);
suite1.addTest(suite2);
return suite1;
}
}
其结构如下图
3.3.4 效果
我们来考虑经过使用Composite模式后给系统的架构带来了那些效果:
l 简化了JUnit的代码 JUnit可以统一处理组合结构TestSuite和单个对象TestCase。使JUnit开发变得简单容易,因为不需要区分部分和整体的区别,不需要写一些充斥着if else的选择语句。
l 定义了TestCase对象和TestSuite的类层次结构 基本对象TestCase可以被组合成更复杂的组合对象TestSuite,而这些组合对象又可以被组合,如我们上个例子,这样不断地递归下去。客户代码中,任何使用基本对象的地方都方便的使用组合对象,大大简化系统维护和开发。
l 使得更容易增加新的类型的TestCase,如很下面介绍的Decorate模式来扩展TestCase的功能
l 使设计变得更加一般化。
3.4 Template Method(模板方法)
3.4.1 问题
在实际的测试中,为了测试业务逻辑,必须构造一些参数或者一些资源,然后才可进行测试,最后必须释放这些系统资源。如测试数据库应用时,必须创建数据库连接Connection,然后执行数据库的操作,最后实现释放数据库的连接。如下代码:
public void testUpdate(){
// Load the Oracle JDBC driver
DriverManager.registerDriver(new oracle.jdbc.OracleDriver());
String url = jdbc:oracle:thin:@localhost:1521:ORA91;
// Connect to the database
Connection conn = DriverManager.getConnection (url, hr, hr);
PreparedStatement pstmt =
conn.prepareStatement (insert into PersonTab values (?));
// Bind the Person object
pstmt.setObject (1, person, OracleTypes.JAVA_STRUCT);
// Execute the insertion
pstmt.executeUpdate ()
// Disconnect
conn.close ();
}
其实这种情况很多,如测试EJB时,必须进行JNDI的LookUp,获得Home接口,其他的情况初始化参数等。可是如果我们在一个TestCase中有几个测试方法,例如测试对数据库的Insert,Update,Delete,Select等操作,这些操作必须在每个方法中都首先获得数据库连接connection,然后测试业务逻辑,最后再释放连接。这样就增加了开发人员的工作,反复的书写这些代码,与JUnit当初的设计目标不一致?怎样解决这个问题?
3.4.2 模式的选择
接下来要解决的问题是给开发者一个便捷的“地方”,用于放置他们的初始化代码,测试代码,和释放资源的代码,类似对象的构造函数,业务方法,析构函数一样。并且必须保证每次运行测试代码之前,都运行初始化代码,最后运行释放资源代码,并且每一个测试的结果都不会影响到其它的测试结果。这样就达到了代码的复用,提供了开发人员的效率。
Template Method(模板方法)比较好地涉及到我们的问题。摘引其意图,“定义一个操作中算法的骨架,并将一些步骤延迟到子类中。Template Method使得子类能够不改变一个算法的结构便可重新定义该算法的某些特定步骤。”这完全恰当。这样可以使测试者能够分别来考虑如何编写初始化和释放代码,以及如何编写测试代码。不管怎样,这种执行的次序对于所有测试都将保持相同,而不管初始化代码如何编写,或测试代码如何编写。
Template Method(模板方法)静态结构如下图所示:
这里设计到两个角色,有如下责任
l AbstractClass 定义多个抽象操作,以便让子类实现。并且实现一个具体的模板方法,它给出了一个顶级逻辑骨架,而逻辑的组成步骤在相应的抽象操作中,推迟到子类里实现。模板方法也有可能调用一些具体的方法。
l ConcreteClass 实现父类的抽象操作方法,它们是模板方法的组成步骤。 每一个AbstractClass可能有多个ConcreteClass与之对应,而每一个ConcreteClass分别实现抽象操作,从而使得顶级逻辑的实现各不相同。
3.4.3 实现
于是我们首先把 TestCase分成几个方法,哪些是抽象操作以便让开发人员去实现,哪个是具体的模板方法,现在我们来看TestCase源码
public abstract class TestCase extends Assert implements Test {
//抽象操作,以便让子类实现
protected void setUp() throws Exception {
}
//抽象操作,以便让子类实现
protected void runTest() throws Throwable {
}
//抽象操作,以便让子类实现
protected void tearDown() throws Exception {
}
//具体的模板方法,定义出逻辑骨架
public void runBare() throws Throwable {
setUp();
try {
runTest();
}
finally {
tearDown();
}
}
}
setUp方法定义成protected让开发人员实现,去初始化测试信息,如数据库的连接, EJB Home接口的JNDI查找等信息,而tearDown方法则是实现测试完成后的资源释放等清除操作。RunTest方法则是开发人员实现的测试业务逻辑。最后TestCase的方法runBare则是模板方法,它实现了测试的逻辑骨架,而测试逻辑的组成步骤setUp, runTest, teardown,推迟到具体的子类实现,如一个具体的测试类
public class TestHelloWorldTestClientJUnit1 extends TestCase {
public void setUp() throws Exception {
super.setUp();
initialize();
create();
}
public void testGetMessage() throws RemoteException {
String strMsg = Hello World;
assertNotNull(ERROR_NULL_REMOTE, helloWorld);
this.assertEquals(strMsg,helloWorld.getMessage());
}
public void tearDown() throws Exception {
helloWorldHome = null;
helloWorld = null;
super.tearDown();
}
}
研究看它们的类图:
3.4.4 效果
我们来考虑经过使用Template Method模式后给系统的架构带来了那些效果:
l 在各个测试用例中的公共的行为(初始化信息和释放资源等)被提取出来,可以避免代码的重复,简化了开发人员的工作。
l 在TestCase中实现一个算法的不变部分,并且将可变的行为留给子类来实现。增强了系统的灵活性。使JUnit框架仅负责算法的轮廓和骨架,而测试的开发人员则负责给出这个算法的各个逻辑步骤。
3.5 Adapter(适配器)
3.5.1 问题
我们已经应用Command模式来表现一个测试。Command依赖于一个单独的像execute()这样的方法(在TestCase中称为run())来对其进行调用。这个简单接口允许我们能够通过相同的接口来调用一个command的不同实现。
如果实现一个测试用例,就必须实现继承Testcase,然后实现run方法,实际是(testRun),然而这样我们就把所有的测试用例都实现相同类的不同方法,这样的结果就会造成产生大量的子类,使系统的测试维护相当困难,并且setUp和tearDown仅为这个testRun服务,其他的测试也必须完成相应的代码,从而增加了开发人员的工作量,怎样解决这个问题?
为了避免类的急剧扩散,试想一个给定的测试用例类(testcase class)可以实现许多不同的方法,每一个方法都有一个描述性的名称,如testMoneyEquals或testMoneyAdd。这样测试案例并不符合简单的command接口。因此又带来另外一个问题就是,使所有测试方法从测试调用者的角度(JUnit框架)上看都是相同的。怎样解决这个问题?
3.5.2 模式的选择
思考设计模式的适用性,Adapter(适配器)模式便映入脑海。Adapter具有以下意图“将一个类的接口转换成客户希望的另外一个接口”。这听起来非常适合。把具有一定规则的描述性方法如testMoneyEquals,转化为JUnit框架所期望的Command(TestCase的run)从而方便框架执行测试。Adapter模式又分为类适配器和对象适配器。类适配器是静态的实现在这里不适合使用,于是使用了对象适配器。
对象的适配器模式的结构如下图所示:
这里涉及到三个角色,有如下责任
l Target 系统所期望的目标接口
l Adaptee 现有需要适配的接口
l Adapter 适配器角色,把源接口转化成目标接口
3.5.3 实现
在实现对象的适配时,首先在TestCase中定义测试方法的命名规则必须是public void testXXXXX()这样我们解析方法的名称,如果符合规则认为是测试方法,然后使用Adapter模式把这些方法,适配成Command的runTest方法。在实现时使用了java的反射技术,这样便可很容易实现动态适配。代码如下
protected void runTest() throws Throwable {
Method runMethod= null;
try {
//使用名称获得对象的方法,如testMoneyEquals,然后动态调用,适配成runTest方法
runMethod= getClass().getMethod(fName, null);
runMethod.invoke(this, new Class[0]);
} catch (Exception e) {
fail(+e.getMessage());
e.fillInStackTrace();
throw e;
}
}
在这里目标接口Target和适配器Adapter变成了同一个类,TestCase,而我们的测试用例,作为 Adaptee,其结构图如下:
3.5.4 效果
我们来考虑经过使用Adapter模式后给系统的架构带来了那些效果:
l 使用Adapter模式简化测试用例的开发,通过按照方法命名的规范来开发测试,不需要进行大量的类继承,提高代码的复用,减轻测试人员的工作量
l 使用Adapter可以重新定义Adaptee的部分行为,如增强异常处理等
3.6 Observer(观察者)
3.6.1 问题
如果测试总是能够正确运行,那么我们将没有必要编写它们。只有当测试失败时测试才是有意义的,尤其是当我们没有预期到它们会失败的时候。更有甚者,测试能够以我们所预期的方式失败,例如通过计算一个不正确的结果;或者它们能够以更加吸引人的方式失败,例如通过编写一个数组越界。JUnit区分了失败(failures)和错误(errors)。失败的可能性是可预期的,并且以使用断言(assertion)来进行检查。而错误则是不可预期的问题,如ArrayIndexOutOfBoundsException。因此我们必须进行报告测试的进行状况,或者打印到控制台,或者是文件,或者GUI界面,甚至同时需要输出到多种介质。如JUnit提供了三种方式如Text,AWT,Swing这三种运行方式,并且JUnit需要提供方便的扩展接口,这样就存在对象间的依赖关系,当测试进行时的状态发生时(TestCase的执行有错误或者失败等),所有依赖这些状态的对象必须自动更新,但是JUnit又不希望为了维护一致性而使各个类紧密耦合,因为这样会降低它们的重用性,怎样解却这个问题?
3.6.2 模式的选择
同样需要思考设计模式的适用性,Observer(观察者)模式便是第一个要考虑的。Observer观察者模式是行为模式,又叫做发布-订阅(Publish-Subscribe)模式,模型-视图(Model/View)模式,源-监听器(Source/Listener)模式。具有以下意图“定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新”。这听起来非常适合需求。在JUnit测试用例时,测试信息一旦发生改变,如发生错误或者失败,结束测试等,各种输出就要有相应的更新,如文本输出就要在控制台打印信息,GUI则在图形中标记错误的信息等。
Observer(观察者)模式的结构如下图所示:
Observer(观察者)模式的角色
l Subject 提供注册和删除观察者对象的方法,可以保存多个观察者
l ConcreteSubject 当它的状态发生改变时,向它的各个观察者发出通知
l Observer 定义那些目标发生改变时需要获得通知的对象一个更新接口
l ConcreteObserver 实现更新接口
3.6.3 实现
首先定义Observer观察者的就是TestListener,它是一个接口,定义了几个方法,说明它监听的几个方法。如测试开始,发生失败,发生错误,测试结束等监听事件的时间点。由具体的类来实现。
/**
* A Listener for test progress
*/
public interface TestListener {
/**
* An error occurred.
*/
public void addError(Test test, Throwable t);
/**
* A failure occurred.
*/
public void addFailure(Test test, AssertionFailedError t);
/**
* A test started.
*/
public void startTest(Test test);
/**
* A test ended.
*/
public void endTest(Test test);
}
在JUnit里有三种方式来实现TestListener,如TextUI,AWTUi,SwingUI并且很容易使开发人员进行扩展,只需实现TestListener即可。下面看在TextUi方式是如何实现的,它由一个类ResultPrinter实现。
public class ResultPrinter implements TestListener {
PrintStream fWriter; * A test ended.
public PrintStream getWriter() {
return fWriter;
}
public void startTest(Test test) {
getWriter().print(.);
if (fColumn++ >= 40) {
getWriter().println();
fColumn= 0;
}
}
public void addError(Test test, Throwable t) {
getWriter().print(E);
}
public void addFailure(Test test, AssertionFailedError t) {
getWriter().print(F);
}
public void endTest(Test test) {
}
}
在JUnit中使用TestResult来收集测试的结果,它使用Collecting Parameter(收集参数)设计模式(The Smalltalk Best Practice Patterns中有介绍),它实际是ConcreteSubject,在JUnit中没有定义Subject,我们看它的实现
public class TestResult extends Object {
//使用Vector来保存,事件的监听者
protected Vector fListeners;
public TestResult() {
fListeners= new Vector();
}
/**
* Registers a TestListener
*/
public synchronized void addListener(TestListener listener) {
fListeners.addElement(listener);
}
/**
* Unregisters a TestListener
*/
public synchronized void removeListener(TestListener listener) {
fListeners.removeElement(listener);
}
/**
* Informs the result that a test will be started.
*/
public void startTest(Test test) {
final int count= test.countTestCases();
synchronized(this) {
fRunTests+= count;
}
for (Enumeration e= cloneListeners().elements(); e.hasMoreElements(); ) {
((TestListener)e.nextElement()).startTest(test);
}
}
/**
* Adds an error to the list of errors. The passed in exception
* caused the error.
*/
public synchronized void addError(Test test, Throwable t) {
fErrors.addElement(new TestFailure(test, t));
for (Enumeration e= cloneListeners().elements(); e.hasMoreElements(); ) {
((TestListener)e.nextElement()).addError(test, t);
}
}
/**
* Adds a failure to the list of failures. The passed in exception
* caused the failure.
*/
public synchronized void addFailure(Test test, AssertionFailedError t) {
fFailures.addElement(new TestFailure(test, t));
for (Enumeration e= cloneListeners().elements(); e.hasMoreElements(); ) {
((TestListener)e.nextElement()).addFailure(test, t);
}
}
/**
* Informs the result that a test was completed.
*/
public void endTest(Test test) {
for (Enumeration e= cloneListeners().elements(); e.hasMoreElements(); ) {
((TestListener)e.nextElement()).endTest(test);
}
}
}
我们来查看类图
3.6.4 效果
我们来考虑经过使用Observer模式后给系统的架构带来了那些效果:
l Subject和Observer之间地抽象耦合 一个TestResult所知道的仅仅是它有一系列的观察者,每个观察者都实现TestListener接口,TestResult不知道任何观察者属于哪一个具体的实现类,这样使TestResult和观察者之间的耦合是抽象的和最小的。
l 支持广播通信 被观察者TestResult会向所有的登记过的观察者如ResultPrinter发出通知。这样不像通常的请求,通知的发送不需指定它的接收者,目标对象并不关心到底有多少对象对自己感兴趣,它唯一的职责就是通知它的观察者。
3.7 Decorate(装饰)
3.7.1 问题
经过以上的分析知道TestCase是一个及其重要的类,它定义了测试步骤和测试的处理。但是作为一个框架,应该提供很方便的方式进行扩展,二次开发。容许不同的开发人员开发适合自己的TestCase,如希望Testcase可以多次反复执行, TestCase进行处理多线程, TestCase可以测试Socket等扩展功能。当然使用继承机制是增加功能的一种有效途径,例如RepeatedTest继承TestCase实现多次测试用例,开发人员然后继承RepeatedTest来实现。但是这种方法不够灵活,是静态的,因为每增一种功能就必须继承,使子类数目呈爆炸式的增长,开发人员不能动态的控制对功能增加的方式和时机。JUnit必须采用一种合理,动态的方式进行扩展。
3.7.2 模式的选择
同样需要思考设计模式的适用性,Decorator(装饰)模式是首先要考虑的。Decorator(装饰)模式又名包装(Wrapper)模式。其意图是“动态地给一个对象添加一些额外的职责。就增加功能来说,Decorator模式相比生成子类更为灵活”。这完全符合我们的需求,可以动态的为TestCase增加职责,或者可以动态地撤销,动态的任意组合。
Decorator(装饰)模式的结构如下图所示:
Decorator(装饰)模式的角色如下:
l Component 给出抽象接口,以规范对象
l ConcreteComponent 定义一个将要接收附加责任的类
l Decorator 持有一个构件对象的实例,并且定义一个与抽象构件一致的接口
l ConcreteDecorator 负责给构件对象附加职责
3.7.3 实现
明白了Decorator模式的结构后,其实Test接口便是Component抽象构件角色。TestCase便是ConcreteComponent具体构件角色。必须增加Decorator角色,于是开发TestDecorator类,它首先要实现接口Test,然后有一个私有的属性Test fTest,接口的实现run都委托给fTest 的run,该方法将有ConcreteComponent具体的装饰类来实现,以增强功能。代码如下:
public class TestDecorator extends Assert implements Test {
//装饰类的构件,将给它增加功能
protected Test fTest;
public TestDecorator(Test test) {
fTest= test;
}
public void run(TestResult result) {
fTest.run(result);
}
public Test getTest() {
return fTest;
}
}
虽然Decoretor类不是一个抽象类,在实际应用中也不一定是抽象类,但是由于它的功能是一个抽象角色,因此称它为抽象装饰。下面是一个具体的装饰类RepeatedTest它可以多次执行一个TestCase,这增强了TestCase的职责
public class RepeatedTest extends TestDecorator {
private int fTimesRepeat;
public RepeatedTest(Test test, int repeat) {
super(test);
fTimesRepeat= repeat;
}
public void run(TestResult result) {
for (int i= 0; i < fTimesRepeat; i++) {
if (result.shouldStop())
break;
super.run(result);
}
}
}
然后可看出这几个类之间的关系如下图:
于是我们就可以动态的为TestCase增加功能,如下:
public static Test suite() {
TestSuite suite = new TestSuite();
suite.addTest(new TestSetup(new RepeatedTest(new Testmath(testAdd),12)));
return suite;
}
于是就可以动态的实现功能的增加,首先使用一个具体的TestCase,然后通过RepeatedTest给这个TestCase增加功能,可以进行多次测试,然后又通过TestSetup装饰类再次增加功能。
3.7.4 效果
我们来考虑经过使用Decorator模式后给系统的架构带来了那些效果:
l 实现了比静态继承更加灵活的方式,动态的增加功能。 如果希望给TestCase增加功能如多次测试,则不需要直接继承,而只需使用装饰类RepeatedTest如下即可
suite.addTest (new RepeatedTest(new Testmath(testAdd),12)
这样便方便的为一个TestCase增加功能。
l 避免在层次结构中的高层的类有太多的特征 Decorator模式提供了一种“即用即付”的方式来增加职责,它不使用类的多层次继承来实现功能的累积,而是从简单的TestCase组合出复杂的功能,如下增加了两种功能,而不用两层继承来实现。
suite.addTest(new TestSetup(new RepeatedTest(new Testmath(testAdd),12)));
这样开发人员不必为不需要的功能付出代价。
3.8 总结
最后,让我们作一个简单的总结,由于在JUnit中使用了大量的模式,增强框架的灵活性,方便性,易扩展性。
4 参考资料
n JUnit A Cooks Tour, www.JUnit.org
n [GOF95] Erich Gamma etc., Design Patterns: Elements of Reusable Object-Oriented Software, Addison-Wesley, 1995. 中译本:《设计模式:可复用面向对象软件的基础》,李英军等译,机械工业出版社,2000 年9月。
n Java与模式 阎宏 电子工业出版社 2002 年10月
n 单元测试 《程序员》 2002年7期