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期