Junit设计模式分析


Junit 中的设计模式 JUnit 设计模式分析 刘兵 (技术顾问 bliu76@yeah.net) 该文章发表与<程序员>6 期 摘要 JUnit 是一个优秀的 Java 单元测试框架,由两位世界级软件大师 Erich Gamma 和 Kent Beck 共同开发完 成。本文将向读者介绍在开发 JUnit 的过程中是怎样应用设计模式的。 关键词:单元测试 JUnit 设计模式 1 JUnit 概述 1.1 JUnit 概述 JUnit 是一个开源的 java 测试框架,它是 XUnit 测试体系架构的一种实现。在 JUnit 单元 测试框架的设计时,设定了三个总体目标,第一个是简化测试的编写,这种简化包括测试框 架的学习和实际测试单元的编写;第二个是使测试单元保持持久性;第三个则是可以利用既 有的测试来编写相关的测试。所以这些目的也是为什么使用模式的根本原因。 1.2 JUnit 开发者 JUnit 最初由 Erich Gamma 和 Kent Beck 所开发。Erich Gamma 博士是瑞士苏伊士国际 面向对象技术软件中心的技术主管,也是巨著《设计模式》的四作者之一。Kent Beck 先生 是 XP(Extreme Programming)的创始人,他倡导软件开发的模式定义,CRC 卡片在软件开 发过程中的使用,基于 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)的方式来架构系统。其设计思想是通过从零开始 来应用设计模式,然后一个接一个,直至你获得最终合适的系统架构。 Junit 源码解析 1/1 Junit 中的设计模式 3 JUnit 设计模式 3.1 Command(命令)模式 3.1.1 问题 JUnit是一个测试 framework,测试人员只需开发测试用例。然后把这些测试用例(TestCase) 组成请求(可能是一个或者多个),发 送 到 JUnit,然后由 JUnit 执行,最后报告详细测试结果。 其中包括执行的时间,错误方法,错误位置等。这样测试用例的开发人员就不需知道 JUnit 内部的细节,只要符合它定义的请求格式即可。从 JUnit 的角度考虑,它并不需要知道请求 TestCase 的具体操作信息,仅把它当作一种命令来执行,然后把执行测试结果发给测试人员。 这样就使 JUnit 框架和 TestCase 的开发人员独立开来,使得请求的一方不必知道接收请求一 方的详细信息,更不必知道是怎样被接收,以及怎样被执行的,实现系统的松耦合。 3.1.2 模式的选择 Command(命令)模式(请参见 Gamma, E., et al. Design Patterns: Elements of Reusable Object-Oriented Software, Addison-Wesley, Reading, MA, 1995)则能够比较好地满足需求。摘 引其意图(intent),"将一个请求封装成一个对象,从而使你可用不同的请求对客户进行参 数化;对请求进行排队或记录请求日志..."Command 模式告诉我们可以为一个操作生成一个 对象并给出它的一个"execute(执行)"方法。 3.1.3 实现 为了实现 Command 模式,首先定义了一个接口 Test,其中 Run 便是 Command 的 Execute 方法。然后又使用 Default Adapter 模式为这个接口提供缺省实现的抽象类 TestCase,这样开 发人员就可以从这个缺省实现进行继承,而不必从 Test 接口直接实现。 Test run() TestCase run() 我们首先来分析 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); Junit 源码解析 2/2 Junit 中的设计模式 } 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.1.4 效果 下面来考虑经过使用 Command 模式后给系统的架构带来了那些效果: Command 模式将实现请求的一方(TestCase 开发)和调用一方(JUnit )进行解藕 Command 模式使新的 TestCase 很容易加入,无需改变已有的类,只需继承 TestCase 类 即可,这样方便了测试人员 Command 模式可以将多个 TestCase 进行组合成一个复合命令,你将看到 TestSuit 就是 它的复合命令,当然它使用了 Composite 模式 Command 模式容易把请求的 TestCase 组合成请求队列,这样使接收请求的一方(Junit Fromwork),容易决定是否执行请求,一旦发现测试用例失败或者错误可以立刻停止进 行报告 Command 模式可以在需要的情况下,方便实现对请求的 Undo 和 Redo,以及记录 Log, 这部分目前在 JUnit 中还没有实现,将来是很容易加入的 3.2 Composite(组合) 3.2.1 问题 为了获得对系统测试的信心,需要运行多个测试用例。通过使用 Command 模式,JUnit 能够方便的运行一个单独的测试用例之后产生测试结果。可是在实际的测试过程中,需要把 多个测试用例进行组合成为一个复合的测试用例,当作一个请求发送给 JUnit.这样 JUnit 就 会面临一个问题,必须考虑测试请求的类型,是一个单一的 TestCase 还是一个复合的 TestCase,甚至要区分到底有多少个 TestCase。这样 Junit 框架就要完成像下面这样的代码: if(isSingleTestCase(objectRequest)){ //如果是单个的 TestCase,执行 run,获得测试结果 (TestCase)objectRequest.run() }else if(isCompositeTestCase(objectRequest)){ //如果是一个复合 TestCase,就要执行不同的操作,然后进行复杂的算法进行分 //解,之后再运行每一个 TestCase,最后获得测试结果,同时又要考虑 //如果中间测试出现错误怎么办????、 Junit 源码解析 3/3 Junit 中的设计模式 ………………………… ………………………… } 这会使 JUnit 必须考虑区分请求(TestCase)的类型(是单个 testCase 还是复合 testCase), 而实际上大多数情况下,测试人员认为这两者是一样的。对于这两者的区别使用,又会使测 试用例的编写变得更加复杂,难以维护和扩展。于是要考虑,怎样设计 JUnit 才可以实现不 需要区分单个 TestCase 还是复合 TestCase,把它们统一成相同的请求? 3.2.2 模式的选择 当 JUnit 不必区分其运行的是一个或多个测试用例时,能够轻松地解决这个问题的模式就 是 Composite(组合)模式。摘引其意图,"将对象组合成树形结构以表示'部分-整体'的层次 结构。Composite 使得用户对单个对象和组合对象的使用具有一致性。"在这里'部分-整体' 的层次结构是解决问题的关键,可以把单个的 TestCase 看作部分,而把复合的 TestCase 看 作整体(称为 TestSuit)。这样使用该模式便可以恰到好处得解决了这个难题。 首先看 Composite 模式的结构: Composite 模式引入以下的参与者: Component:这是一个抽象角色,它给参加组合的对象规定一个接口。这个角色, 给出共有的接口和默认行为。其实就我们的 Test 接口,它定义出 run 方法。 Composite:实现共有接口并维护一个测试用例的集合。就是复合测试用例 TestSuit Leaf:代表参加组合的对象,它没有下级子对象,仅定义出参加组合的原始对象的 行为,其实就是单一的测试用例 TestCase,它仅实现 Test 接口的方法。 其实 componsite 模式根据所实现的接口类型区分为两种形式,分别称为安全式和透明式。 JUnit 中使用了安全式的结构,这样在 TestCase 中没有管理子对象的方法。 3.2.3 实现 composite 模式告诉我们要引入一个 Component 抽象类,为 Leaf 对象和 composite 对象 定义公共的接口。这个类的基本意图就是定义一个接口。在 Java 中使用 Composite 模式时, 优先考虑使用接口,而非抽象类,因此引入一个 Test 接口。当然我们的 leaf 就是 TestCase 了。其源代码如下: //composite 模式中的 Component 角色 Junit 源码解析 4/4 public interface Test { Junit 中的设计模式 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 类型)中保存了其子测试用例,提供 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 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。测试人员可创建一个组合了这些测试用例 Junit 源码解析 5/5 Junit 中的设计模式 的 TestSuit 来运行它们所有的 TestCase。 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.2.4 效果 我们来考虑经过使用 Composite 模式后给系统的架构带来了那些效果: 简化了 JUnit 的代码 JUnit可以统一处理组合结构 TestSuite 和单个对象 TestCase。使 JUnit 开发变得简单容易,因为不需要区分部分和整体的区别,不需要写一些充斥着 if else 的选择语句。 定义了 TestCase 对象和 TestSuite 的类层次结构 基本对象 TestCase 可以被组合成更复 杂的组合对象 TestSuite,而这些组合对象又可以被组合,如上个例子,这样不断地递归 下去。在程序的代码中,任何使用基本对象的地方都可方便的使用组合对象,大大简化 系统维护和开发。 使得更容易增加新的类型的 TestCase,如下面介绍的 Decorate 模式来扩展 TestCase 的功 能 3.3 Template Method(模板方法) 3.3.1 问题 在实际的测试中,为了测试业务逻辑,必须构造一些参数或者一些资源,然后才可进行测 试,最后必须释放这些系统资源。如测试数据库应用时,必须创建数据库连接 Connection, 然后执行操作,最后必须释放数据库的连接等。如下代码: public void testUpdate(){ // Load the Oracle JDBC driver and Connect to the database DriverManager.registerDriver(new oracle.jdbc.OracleDriver()); String url = "jdbc:oracle:thin:@localhost:1521:ORA91"; Junit 源码解析 6/6 Junit 中的设计模式 Connection conn = DriverManager.getConnection (url, "hr", "hr"); // Select first_name and last_name column from the employees table ResultSet rset = stmt.executeQuery ("select FIRST_NAME, LAST_NAME from EMPLOYEES"); …………… // Disconnect conn.close (); } 其实这种情况很多,如测试 EJB 时,必须进行 JNDI 的 LookUp,获得 Home 接口等。可是 如果在一个 TestCase 中有几个测试方法,例如测试对数据库的 Insert,Update,Delete,Select 等操作,这些操作必须在每个方法中都首先获得数据库连接 connection,然后测试业务逻辑, 最后再释放连接。这样就增加了测试人员的工作,反复的书写这些代码,与 JUnit 当初的设 计目标不一致?怎样解决这个问题? 3.3.2 模式的选择 接下来要解决的问题是给开发者一个便捷的“地方”,用于放置他们的初始化代码,测试 代码,和释放资源的代码,类似对象的构造函数,业务方法,析构函数一样。并且必须保证 每次运行测试代码之前,都运行初始化代码,最后运行释放资源代码,并且每一个测试的结 果都不会影响到其它的测试结果。这样就达到了代码的复用,提供了测试人员的效率。 Template Method(模板方法)可以比较好得解决这个的问题。摘引其意图,“定义一个 操作中算法的骨架,并将一些步骤延迟到子类中。Template Method 使得子类能够不改变一 个算法的结构即可重新定义该算法的某些特定步骤。”这完全恰当。这样可以使测试者能够 分别来考虑如何编写初始化和释放代码,以及如何编写测试代码。不管怎样,这种执行的次 序对于所有测试都将保持相同,而不管初始化代码如何编写,或测试代码如何编写。 Template Method(模板方法)静态结构如下图所示: 这里设计到两个角色,有如下责任 AbstractClass 定义多个抽象操作,以便让子类实现。并且实现一个具体的模板方法, 它给出了一个顶级逻辑骨架,而逻辑的组成步骤在相应的抽象操作中,推迟到子类里实 现。模板方法也有可能调用一些具体的方法。 ConcreteClass 实现父类的抽象操作方法,它们是模板方法的组成步骤。 每一个 AbstractClass 可能有多个 ConcreteClass 与之对应,而每一个 ConcreteClass 分别实现抽 象操作,从而使得顶级逻辑的实现各不相同。 3.3.3 实现 Junit 源码解析 7/7 于是我们首先把 TestCase 分成几个方法,哪些是抽象操作以便让开发人员去实现,哪个是 Junit 中的设计模式 具体的模板方法,现在我们来看 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(); runTest(); tearDown(); } } setUp 方法让测试人员实现,去初始化测试信息,如数据库的连接, EJB Home 接口的 JNDI 等,而 tearDown 方法则是实现测试完成后的资源释放等清除操作。runTest 方法则是开发 人员实现的测试业务逻辑。最后 TestCase 的方法 runBare 则是模板方法,它实现了测试的 逻辑骨架,而测试逻辑的组成步骤 setUp, runTest, teardown,推迟到具体的子类实现,如一 个具体的测试类 public class TestHelloWorldTestClientJUnit1 extends TestCase { public void setUp() throws Exception { initialize();//初始化 JNDI 信息 create(); //获得 EJB 的Home 接口,和远程接口 } public void testGetMessage() throws RemoteException { assertNotNull(ERROR_NULL_REMOTE, helloWorld); this.assertEquals("Hello World",helloWorld.getMessage(""));//测试业务逻辑 } public void tearDown() throws Exception { helloWorldHome = null; //释放 EJB 的Home 接口 helloWorld = null; //释放 EJB 的远程接口 } } 子类实现了 setUp,tearDown 方法,和一个测试方法 testGetMessage,为什么名称不是 runTest,这就是在下面介绍 Adapter 模式的原因,它把 testGetMessage 方法适配成 runTest。 下面是类的关系图: Junit 源码解析 8/8 Junit 中的设计模式 TestHelloWorldTestClientJUnit1 setUp() tearDown() testGetMessage() TestCase runBare() runTest() setUp() tearDown() 3.3.4 效果 我们来考虑经过使用 Template Method 模式后给系统的架构带来了那些效果: 在各个测试用例中的公共的行为(初始化信息和释放资源等)被提取出来,可以避免代 码的重复,简化了测试人员的工作。 在 TestCase 中实现一个算法的不变部分,并且将可变的行为留给子类来实现。增强了 系统的灵活性。使 JUnit 框架仅负责算法的轮廓和骨架,而测试的开发人员则负责给出 这个算法的各个逻辑步骤。 3.4 Adapter(适配器) 3.4.1 问题 我们已经应用了 Command 模式来表现一个测试用例。Command 依赖于一个单独的像 execute()这样的方法(在 TestCase 中称为 run())来对其进行调用。这样允许我们能够通过 相同的接口来调用一个 command 的不同实现。 如果实现一个测试用例,就必须实现继承 Testcase,然后实现 run 方法,实际是 testRun, 测试人员就把所有的测试用例都继承相同的类,这样的结果就会造成产生出大量的子类,使 系统的测试维护相当困难,并且 setUp 和 tearDown 仅为这个 testRun 服务,其他的测试也必 须完成相应的代码,从而增加了开发人员的工作量,怎样解决这个问题? 为了避免类的急剧扩散,试想一个给定的测试用例类可以实现许多不同的方法,每一个方 法都有一个描述性的名称,如 testGetMessage 或 testSetMessage。这样测试用例并不符合简 单的 command 接口。因此又会带来另外一个问题就是,使所有测试方法从测试调用者的角 度(JUnit 框架)上看都是相同的。怎样解决这个接口不匹配问题? 3.4.2 模式的选择 思考设计模式的适用性,Adapter(适配器)模式便映入脑海。Adapter 具有以下意图“将 一个类的接口转换成客户希望的另外一个接口”。这听起来非常适合。把具有一定规则的描 述性方法如 testGetMessage,转化为 JUnit 框架所期望的 Command(TestCase 的 run)从而 方便框架执行测试。Adapter 模式又分为类适配器和对象适配器。类适配器是静态的实现在 这里不适合使用,于是使用了对象适配器。 对象的适配器模式的结构如下图所示: Junit 源码解析 9/9 Junit 中的设计模式 这里涉及到三个角色,有如下责任 Target 系统所期望的目标接口 Adaptee 现有需要适配的接口 Adapter 适配器角色,把源接口转化成目标接口 3.4.3 实现 在实现对象的适配时,首先在 TestCase 中定义测试方法的命名规则必须是 public void testXXXXX()这样我们解析方法的名称,如果符合规则认为是测试方法,然后使用 Adapter 模式把这些方法,适配成 Command 的 runTest 方法。在实现时使用了 java 的反射技术,这 样便可很容易实现动态适配。代码如下 protected void runTest() throws Throwable { //使用名称获得对象的方法,如 testGetMessage,然后动态调用,适配成 runTest 方法 Method runMethod= getClass().getMethod(fName, null); runMethod.invoke(this, new Class[0]); } 在这里目标接口 Target 和适配器 Adapter 变成了同一个类,TestCase,而测试用例,作为 Adaptee,其结构图如下: TestHelloWorldTestClientJUnit1 testGetMessage() TestCase runTest() 3.4.4 效果 我们来考虑经过使用 Adapter 模式后给系统的架构带来了那些效果: 使用 Adapter 模式简化测试用例的开发,通过按照方法命名的规范来开发测试用例,不 需要进行大量的类继承,提高代码的复用,减轻测试人员的工作量 使用 Adapter 可以重新定义 Adaptee 的部分行为,如增强异常处理等 3.5 Observer(观察者) 3.5.1 问题 Junit 源码解析 10/10 如果测试总是能够正确运行,那么我们将没有必要编写它们。只有当测试失败时测试才 是有意义的,尤其是当我们没有预期到它们会失败的时候。更有甚者,测试能够以我们所预 期的方式失败。JUnit 区分了失败(failures)和错误(errors)。失败的可能性是可预期的, Junit 中的设计模式 并且以使用断言(assertion )来进行检查。而错误则是不可预期的问题,如 ArrayIndexOutOfBoundsException。因此我们必须进行报告测试的进行状况,或者打印到控 制台,或者是文件,或者 GUI 界面,甚至同时需要输出到多种介质。如 JUnit 提供了三种方 式如 Text,AWT,Swing 这三种运行方式,并且 JUnit 需要提供方便的扩展接口,这样就存在 对象间的依赖关系,当测试进行时的状态发生时(TestCase 的执行有错误或者失败等),所 有依赖这些状态的对象必须自动更新,但是 JUnit 又不希望为了维护一致性而使各个类紧密 耦合,因为这样会降低它们的重用性,怎样解却这个问题? 3.5.2 模式的选择 同样需要思考设计模式的适用性,Observer(观察者)模式便是第一个要考虑的。Observer 观察者模式是行为模式,又叫做发布-订阅(Publish-Subscribe) 模式,模型-视图 (Model/View)模式,源-监听器(Source/Listener)模式。具有以下意图“定义对象间的 一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并 被自动更新”。这听起来非常适合需求。在 JUnit 测试用例时,测试信息一旦发生改变,如发 生错误或者失败,结束测试等,各种输出就要有相应的更新,如文本输出就要在控制台打印 信息,GUI 则在图形中标记错误信息等。 Observer(观察者)模式的结构如下图所示: Observer(观察者)模式的角色 Subject 提供注册和删除观察者对象的方法,可以保存多个观察者 ConcreteSubject 当它的状态发生改变时,向它的各个观察者发出通知 Observer 定义那些目标发生改变时需要获得通知的对象一个更新接口 ConcreteObserver 实现更新接口 3.5.3 实现 首先定义 Observer 观察者的就是 TestListener,它是一个接口,定义了几个方法,说明它监 听的几个方法。如测试开始,发生失败,发生错误,测试结束等监听事件的时间点。由具体 的类来实现。 // A Listener for test progress public interface TestListener { // An error occurred. Junit 源码解析 11/11 public void addError(Test test, Throwable t); Junit 中的设计模式 // 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("."); } 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 和 ConcreteSubject 是同一个类,我们看它的实现 public class TestResult extends Object { / / 使用 Vector 来保存,事件的监听者 protected Vector 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) { Junit 源码解析 12/12 for (Enumeration e= cloneListeners().elements(); e.hasMoreElements(); ) { Junit 中的设计模式 ((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) { for (Enumeration e= cloneListeners().elements(); e.hasMoreElements(); ) { ((TestListener)e.nextElement()).addError(test, t); } } //以下省略了 addFailure 和endTest 代码 } 我们来查看它们的类图关系。 ResultPrinter addError() addFailure() endTest() getWriter() startTest() TestRes ult fListeners : java.util.Vector addError() addFailure() addListener() cloneListeners() endTest() removeListener() startTest() TestListener addError() addFailure() endTest() startTest() 3.5.4 效果 我们来考虑经过使用 Observer 模式后给系统的架构带来了那些效果: Subject 和 Observer 之间地抽象耦合 一个 TestResult 所知道的仅仅是它有一系列的观 察者,每个观察者都实现 TestListener 接口,TestResult 不必知道任何观察者属于哪一个 具体的实现类,这样使 TestResult 和观察者之间的耦合是抽象的和最小的。 支持广播通信 被观察者 TestResult 会向所有的登记过的观察者如 ResultPrinter 发出通 知。这样不像通常的请求,通知的发送不需指定它的接收者,目标对象并不关心到底有 多少对象对自己感兴趣,它唯一的职责就是通知它的观察者。 3.6 Decorate(装饰) 3.6.1 问题 经过以上的分析知道 TestCase 是一个及其重要的类,它定义了测试步骤和测试的处理。 Junit 源码解析 13/13 Junit 中的设计模式 但是作为一个框架,应该提供很方便的方式进行扩展,进行二次开发。允许不同的开发人员 开发适合自己的 TestCase,如希望 Testcase 可以多次反复执行, TestCase 可以处理多线程, TestCase 可以测试 Socket 等扩展功能。当然使用继承机制是增加功能的一种有效途径,例 如 RepeatedTest 继承 TestCase 实现多次测试用例,测试人员然后继承 RepeatedTest 来实现。 但是这种方法不够灵活,是静态的,因为每增一种功能就必须继承,使子类数目呈爆炸式的 增长,开发人员不能动态的控制对功能增加的方式和时机。JUnit 必须采用一种合理,动态 的方式进行扩展。 3.6.2 模式的选择 同样需要思考设计模式的适用性,Decorator(装饰)模式是首先要考虑的。Decorator(装 饰)模式又名包装(Wrapper)模式。其意图是“动态地给一个对象添加一些额外的职责。就 增加功能来说,Decorator 模式相比生成子类更为灵活”。这完全符合我们的需求,可以动态 的为 TestCase 增加职责,或者可以动态地撤销,动态的任意组合。 Decorator(装饰)模式的结构如下图所示: Decorator(装饰)模式的角色如下: Component 给出抽象接口,以规范对象 ConcreteComponent 定义一个将要接收附加责任的类 Decorator 持有一个构件对象的实例,并且定义一个与抽象构件 Component 一致的接口 ConcreteDecorator 负责给构件对象附加职责 3.6.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) { Junit 源码解析 14/14 fTest= test; Junit 中的设计模式 } public void run(TestResult result) { fTest.run(result); } } 虽然 Decoretor 类不是一个抽象类,在实际应用中也不一定是抽象类,但是由于它的功能是 一个抽象角色,因此称它为抽象装饰。下面是一个具体的装饰类 RepeatedTest 它可以多次执 行一个 TestCase,这增强了 TestCase 的职责 public class RepeatedTest extends TestDecorator { private int fTimesRepeat; public RepeatedTest(Test test, int repeat) { super(test); fTimesRepeat= repeat; } //为ConcreteComponent 增加功能,可以执行多次 public void run(TestResult result) { for (int i= 0; i < fTimesRepeat; i++) { if (result.shouldStop()) break; super.run(result); //委托父类完成 } } } 然后可看出这几个类之间的关系如下图: RepeatedTest TestSetup TestCase TestDecorator Test 于是我们就可以动态的为 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 装饰类再次增加功能。 Junit 源码解析 15/15 Junit 中的设计模式 3.6.4 效果 我们来考虑经过使用 Decorator 模式后给系统的架构带来了那些效果: 实现了比静态继承更加灵活的方式,动态的增加功能。 如果希望给 TestCase 增加功 能如多次测试,则不需要直接继承,而只需使用装饰类 RepeatedTest 如下即可 suite.addTest (new RepeatedTest(new Testmath("testAdd"),12)); 这样便方便的为一个 TestCase 增加功能。 避免在层次结构中的高层的类有太多的特征 Decorator 模式提供了一种“即用即付”的 方式来增加职责,它不使用类的多层次继承来实现功能的累积,而是从简单的 TestCase 组合出复杂的功能,如下增加了两种功能,而不用两层继承来实现。 suite.addTest(new TestSetup(new RepeatedTest(new Testmath("testAdd"),12))); 这样开发人员不必为不需要的功能付出代价。 3.7 总结 最后,让我们作一个简单的总结,由于在 JUnit 中使用了大量的模式,增强框架的灵活性, 方便性,易扩展性。本人对 Junit 进行了代码简化(仅 13 个类),保留了它的精华(架构和 模式),可供大家学习。 Junit 源码解析 16/16
还剩15页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 15 金币 [ 分享pdf获得金币 ] 4 人已下载

下载pdf

pdf贡献者

openkk

贡献于2011-09-03

下载需要 15 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf