閱讀667 返回首頁    go 微軟 go windows


JUnit源碼分析(二)——觀察者模式


    我們知道JUnit支持不同的使用方式:swt、swing的UI方式,甚至控製台方式,那麼對於這些不同的UI我們如何提供統一的接口供它們獲取測試過程的信息(比如出現的異常信息,測試成功,測試失敗的代碼行數等等)?我們試想一下這個場景,當一個error或者exception產生的時候,測試能夠馬上通知這些UI客戶端:發生錯誤了,發生了什麼錯誤,錯誤是什麼等等。顯而易見,這是一個訂閱-發布機製應用的場景,應當使用觀察者模式。那麼什麼是觀察者模式呢?

觀察者模式(Observer)

Observer是對象行為型模式之一

1.意圖:定義對象間的一種一對多的依賴關係,當一個對象的狀態發現改變時,所有依賴於它的對象都得到通知並被自動更新

2.適用場景:
1)當一個抽象模型有兩個方麵,其中一個方麵依賴於另一個方麵,通過觀察者模式將這兩者封裝在不同的獨立對象當中,以使它們可以獨立的變化和複用
2)當一個對象改變時,需要同時改變其他對象,並且不知道其他對象的具體數目
3)當一個對象需要引用其他對象,但是你又不想讓這個對象與其他對象產生緊耦合的時候

3.UML圖:
                  observer.jpg
   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(
100100);
           }
        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(
1212);
        assertEquals(
12L12L);
        assertEquals(
new Long(12), new Long(12));

        assertEquals(
"Size"1212);
        assertEquals(
"Capacity"12.011.990.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) + "dot.gif";
        
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

最後更新:2017-05-17 13:34:57

  上一篇:go  rails應用遍曆Controllers目錄並取出所有的Controller和action
  下一篇:go  JUnit源碼分析(一)——Command模式和Composite模式