JUnit源码分析(二)——观察者模式

    我们知道JUnit支持不同的使用方式:swt、swing的UI方式,甚至控制台方式,那么对于这些不同的UI我们如何提供统一的接口供它们获取测试过程的信息(比如出现的异常信息,测试成功,测试失败的代码行数等等)?我们试想一下这个场景,当一个error或者exception产生的时候,测试能够马上通知这些UI客户端:发生错误了,发生了什么错误,错误是什么等等。显而易见,这是一个订阅-发布机制应用的场景,应当使用观察者模式。那么什么是观察者模式呢?

观察者模式(Observer)

Observer是对象行为型模式之一

1.意图:定义对象间的一种一对多的依赖关系,当一个对象的状态发现改变时,所有依赖于它的对象都得到通知并被自动更新

2.适用场景:
1)当一个抽象模型有两个方面,其中一个方面依赖于另一个方面,通过观察者模式将这两者封装在不同的独立对象当中,以使它们可以独立的变化和复用
2)当一个对象改变时,需要同时改变其他对象,并且不知道其他对象的具体数目
3)当一个对象需要引用其他对象,但是你又不想让这个对象与其他对象产生紧耦合的时候

3.UML图:
                 
   Subject及其子类维护一个观察者列表,当需要通知所有的Observer对象时调用Nitify方法遍历Observer集合,并调用它们的update方法更新。而具体的观察者实现Observer接口(或者抽象类),提供具体的更新行为。其实看这张图,与Bridge有几分相似,当然两者的意图和适用场景不同。

4.效果:
1)目标和观察者的抽象耦合,目标仅仅与抽象层次的简单接口Observer松耦合,而没有与具体的观察者紧耦合
2)支持广播通信
3)缺点是可能导致意外的更新,因为一个观察者并不知道其他观察者,它的更新行为也许将导致一连串不可预测的更新的行为

5.对于观察者实现需要注意的几个问题:
1)谁来触发更新?最好是由Subject通知观察者更新,而不是客户,因为客户可能忘记调用Notify
2)可以通过显式传参来指定感兴趣的更新
3)在发出通知前,确保Subject对象状态的一致性,也就是Notify操作应该在最后被调用
4)当Subject和Observer的依赖关系比较复杂的时候,可以通过一个更新管理器来管理它们之间的关系,这是与中介者模式的结合应用。

    讨论完观察者模式,那我们来看JUnit是怎么实现这个模式的。在junit.framework包中我们看到了一个Observer接口——TestListener,看看它的代码:

package junit.framework;

/**
 * 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, Throwable t);  
   /**
     * A test ended.
     */
     public void endTest(Test test); 
   /**
     * A test started.
     */
    public void startTest(Test test);
}

    接口清晰易懂,就是一系列将测试过程的信息传递给观察者的操作。具体的子类将接受这些信息,并按照它们的方式显示给用户。
     比如,我们看看swing的UI中的TestRunner,它将这些信息显示在一个swing写的UI界面上:

    public void startTest(Test test) {
        showInfo("Running: "+test);
    }

    public void addError(Test test, Throwable t) {
        fNumberOfErrors.setText(Integer.toString(fTestResult.errorCount()));
        appendFailure("Error", test, t);
    }
    public void addFailure(Test test, Throwable t) {
        fNumberOfFailures.setText(Integer.toString(fTestResult.failureCount()));
        appendFailure("Failure", test, t);
    }
    public void endTest(Test test) {
        setLabelValue(fNumberOfRuns, fTestResult.runCount());
        fProgressIndicator.step(fTestResult.wasSuccessful());
    }

可以看到,它将错误信息,异常信息保存在List或者Vector集合内,然后显示在界面上:

private void showErrorTrace() {
        int index= fFailureList.getSelectedIndex();
        if (index == -1)
            return;
    
        Throwable t= (Throwable) fExceptions.elementAt(index);
        if (fTraceFrame == null) {
            fTraceFrame= new TraceFrame();
            fTraceFrame.setLocation(100, 100);
           }
        fTraceFrame.showTrace(t);
        fTraceFrame.setVisible(true);
    }
    private void showInfo(String message) {
        fStatusLine.setFont(PLAIN_FONT);
        fStatusLine.setForeground(Color.black);
        fStatusLine.setText(message);
    }
    private void showStatus(String status) {
        fStatusLine.setFont(BOLD_FONT);
        fStatusLine.setForeground(Color.red);
        fStatusLine.setText(status);
    }

而Junit中的目标对象(Subject)就是TestResult对象,它有添加观察者的方法:

/**
     * Registers a TestListener
     */
    public synchronized void addListener(TestListener listener) {
        fListeners.addElement(listener);
    }

而通知观察者又是怎么做的呢?请看这几个方法,都是循环遍历观察者列表,并调用相应的更新方法:

/**
     * 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 = fListeners.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 = fListeners.elements(); e.hasMoreElements();) {
            ((TestListener) e.nextElement()).addFailure(test, t);
        }
    }

    /**
     * Registers a TestListener
     */
    public synchronized void addListener(TestListener listener) {
        fListeners.addElement(listener);
    }

    /**
     * Informs the result that a test was completed.
     */
    public synchronized void endTest(Test test) {
        for (Enumeration e = fListeners.elements(); e.hasMoreElements();) {
            ((TestListener) e.nextElement()).endTest(test);
        }
    }

使用这个模式后带来的好处:
1)上面提到的Subject与Observer的抽象耦合,使JUnit可以支持不同的使用方式
2)支持了广播通信,目标对象不关心有多少对象对自己注册,它只是通知注册的观察者

最后,我实现了一个简单的ConsoleRunner,在控制台执行JUnit,比如我们写了一个简单测试:

package junit.samples;

import junit.framework.*;

/**
 * Some simple tests.
 * 
 */
public class SimpleTest extends TestCase {
    protected int fValue1;

    protected int fValue2;

    public SimpleTest(String name) {
        super(name);
    }

    public void setUp() {
        fValue1 = 2;
        fValue2 = 3;
    }

    public void testAdd() {
        double result = fValue1 + fValue2;
        assert(result == 5);
    }

    public void testEquals() {
        assertEquals(12, 12);
        assertEquals(12L, 12L);
        assertEquals(new Long(12), new Long(12));

        assertEquals("Size", 12, 12);
        assertEquals("Capacity", 12.0, 11.99, 0.01);
    }
}

使用ConsoleRunner调用这个测试,代码很简单,不多做解释了:

package net.rubyeye.junit.framework;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;

import junit.framework.Test;
import junit.framework.TestListener;
import junit.framework.TestResult;
import junit.samples.SimpleTest;
//实现观察者接口
public class ConsoleRunner implements TestListener {

    private TestResult fTestResult;

    private Vector fExceptions;

    private Vector fFailedTests;

    private List fFailureList;

    public ConsoleRunner() {
        fExceptions = new Vector();
        fFailedTests = new Vector();
        fFailureList = new ArrayList();
    }

    public void endTest(Test test) {
        System.out.println("测试结束:");
        String message = test.toString();
        if (fTestResult.wasSuccessful())
            System.out.println(message + " 测试成功!");
        else if (fTestResult.errorCount() == 1)
            System.out.println(message + " had an error");
        else
            System.out.println(message + " had a failure");

        for (int i = 0; i < fFailureList.size(); i++) {
            System.out.println(fFailureList.get(i));
        }
        for (int i = 0; i < fFailedTests.size(); i++) {
            System.out.println(fFailureList.get(i));
        }
        for (int i = 0; i < fExceptions.size(); i++) {
            System.out.println(fFailureList.get(i));
        }

        System.out.println("------------------------");
    }

    public void startTest(Test test) {
        System.out.println("开始测试:" + test);
    }

    public static TestResult createTestResult() {
        return new TestResult();
    }

    private String truncateString(String s, int length) {
        if (s.length() > length)
            s = s.substring(0, length) + "";
        return s;
    }

    public void addError(Test test, Throwable t) {
        System.out.println(fTestResult.errorCount());
        appendFailure("Error", test, t);
    }

    public void addFailure(Test test, Throwable t) {
        System.out.println(fTestResult.failureCount());
        appendFailure("Failure", test, t);
    }

    private void appendFailure(String kind, Test test, Throwable t) {
        kind += ": " + test;
        String msg = t.getMessage();
        if (msg != null) {
            kind += ":" + truncateString(msg, 100);
        }
        fFailureList.add(kind);
        fExceptions.addElement(t);
        fFailedTests.addElement(test);
    }

    public void go(String args[]) {
        Method[] methods = SimpleTest.class.getDeclaredMethods();

        for (int i = 0; i < methods.length; i++) {
            //取所有以test开头的方法
            if (methods[i].getName().startsWith("test")) {
                Test test = new SimpleTest(methods[i].getName());
                fTestResult = createTestResult();
                fTestResult.addListener(ConsoleRunner.this);
                //执行测试
                test.run(fTestResult);
            }
        }

    }

    public static void main(String args[]) {
        new ConsoleRunner().go(args);
    }

}

文章转自庄周梦蝶  ,原文发布时间5.17

时间: 2024-10-01 04:09:14

JUnit源码分析(二)——观察者模式的相关文章

JUnit源码分析(四)——从Decorator模式说起

其实我这系列小文,名为源码分析,其实是自己读<设计模式>的读书笔记.Decorator模式在java的IO库中得到应用,java的IO库看起来复杂,其实理解了Decorator模式再回头看可以很好理解并使用.     Decorator模式,也就是装饰器模式,是对象结构型模式之一. 1.意图:动态地给一个对象添加一些额外的职责.给对象添加功能,我们首先想到的是继承,但是如果每增一个功能都需要继承,类的继承体系将无可避免地变的庞大和难以理解.面向对象设计的原则:优先使用组合,而非继承,继承的层次

JUnit源码分析(二)

    在上面我们已经提到了junit.extentions包中的内容TestSetup.来看看整个包的结构吧. 先简要的介绍下包中各个类的功能.ActiveTestSuite对TestSuite进行了改进,使得每个test运行在一个单独的线程里面,并且只到所有的线程都结束了才会结束整个测试.ExceptionTestCase是对TestCase进行的改进,可以方便的判断测试类是否抛出了期望的异常.而剩下的三个类,大概你看的出来是使用了装饰模式来设计的.其中TestDecorator为具体装饰类

JUnit源码分析(三)

三.微观--执行流程与代码风格 来过一遍JUnit的执行流程吧,这样你就能对JUnit有个清晰的认识,虽然作为一个使用者这完全是不必要的.从<JUnit in Action>直接拿来一张JUnit流程图. 哦,也许你看晕了,我来当下导游好了.上面已经提到了TestRunner是BaseTestRunner的子类,在三个不同的ui包中各有一个TestRunner.这里我们仅以junit.textui包中的为例. TestRunner作为入口程序是怎么被启动的呢?习惯了使用容器的我们现在也许很少考

JUnit源码分析 (三)——Template Method模式

在JUnit执行测试时,我们经常需要初始化一些环境供测试代码使用,比如数据库连接.mock对象等等,这些初始化代码应当在每一个测试之前执行并在测试方法运行后清理.在JUnit里面就是相应的setUp和tearDown方法.如果没有这两个方法,那么我们要在每个测试方法的代码内写上一大堆重复的初始化和清理代码,这是多么愚蠢的做法.那么JUnit是怎么让setUp和tearDown在测试执行前后被调用的呢?     如果你查看下TestCase方法,你会发现TestCase和TestSuite的run

关于Junit源码的一些探索

Junit介绍 Junit 是由 Erich Gamma 和 Kent Beck 编写的一个开源的单元测试框架,主要用于白盒测试.Junit 的设计精简,易学易用,但是功能却非常强大. 在Junit3中,使用时只需继承TestCase类,并且 1. 测试方法名以test开头,返回void,方法中没有参数 2. 使用方法setUp()做初始化工作,使用方法tearDown()做清理工作 3. 使用Assert类的方法来判断测试用例结果 Junit源码分析 Junit的流程图: Junit的完整生命

Hhadoop-2.7.0中HDFS写文件源码分析(二):客户端实现之DFSPacket

一.简介       HDFS在数据传输过程中,针对数据块Block,不是整个block进行传输的,而是将block切分成一个个的数据包进行传输.而DFSPacket就是HDFS数据传输过程中对数据包的抽象. 二.实现       HDFS客户端在往DataNodes节点写数据时,会以数据包packet的形式写入,且每个数据包包含一个包头,n个连续的校验和数据块checksum chunks和n个连续的实际数据块 actual data chunks,每个校验和数据块对应一个实际数据块,被用来做

TOMCAT源码分析——生命周期管理(二)

前言 我在<TOMCAT源码分析--生命周期管理(一)>一文中介绍了TOMCAT生命周期类接口设计.JMX.容器以及基于容器的事件与监听等内容,本文将接着介绍Tomcat7.0中容器生命周期的管理. 容器生命周期 每个容器都会有自身的生命周期,其中也涉及状态的迁移,以及伴随的事件生成,本节详细介绍Tomcat中的容器生命周期实现.所有容器的转态转换(如新疆.初始化.启动.停止等)都是由外到内,由上到下进行,即先执行父容器的状态转换及相关操作,然后再执行子容器的转态转换,这个过程是层层迭代执行的

HBase源码分析之MemStore的flush发起时机、判断条件等详情(二)

        在<HBase源码分析之MemStore的flush发起时机.判断条件等详情>一文中,我们详细介绍了MemStore flush的发起时机.判断条件等详情,主要是两类操作,一是会引起MemStore数据大小变化的Put.Delete.Append.Increment等操作,二是会引起HRegion变化的诸如Regin的分裂.合并以及做快照时的复制拷贝等,同样会触发MemStore的flush流程.同时,在<HBase源码分析之compact请求发起时机.判断条件等详情(一

Hhadoop-2.7.0中HDFS写文件源码分析(二):客户端实现(1)

一.综述       HDFS写文件是整个Hadoop中最为复杂的流程之一,它涉及到HDFS中NameNode.DataNode.DFSClient等众多角色的分工与合作.       首先上一段代码,客户端是如何写文件的: Configuration conf = new Configuration(); FileSystem fs = FileSystem.get(conf); Path file = new Path("demo.txt"); FSDataOutputStream