利用 Eclipse 進行單元測試在
您的傳統程式碼是不是要求使用匹配的類測試套件才能針對其原始碼庫執行?針對此類目的,jMock 堪稱是一個優秀的測試框架。但是,並不是所有情況都能夠適用,尤其是必須以 jMock 不期望的方式構造物件時。為避免生成自定義模擬物件套件才能支援應用程式中的單元測試的麻煩,可以調整 RMock,與 jMock 無縫地結合使用,從而解決這一問題。
模擬物件將模仿出於指導程式碼執行的惟一目的而編寫的類的行為,以便它在測試時符合程式碼執行要求。最終,模擬物件數目可以隨著應用程式類數目的增長而增長。使用 jMock、RMock 甚至 EasyMock 等框架有助於消除對物理的獨立存在的模擬物件集的需求。
EasyMock 框架的一個主要缺點是不能模擬具體類 —— 而只能模擬介面。在本文中,我將向您展示怎樣使用 jMock 框架來模擬具體類和
|
注:有關 JUnit、jMock 和 RMock 的最新二進位制檔案,請參閱 參考資料。
首先啟動 Eclipse 整合開發環境 (IDE)。接下來,建立一個基本 Java™ 專案,稍後將把 JUnit、jMock 和 RMock Java Archive (JAR) 庫匯入到該專案中。將 Java 專案命名為 TestingExample
當 JAR 檔案位於 Java 類路徑(即,已在 Eclipse 內配置的 Java 執行時環境(Java Runtime Environment,JRE))中時,請使用 Add JARs 按鈕。Add Variable 按鈕適用於檔案系統(本地或遠端)中的資源(包括 JAR)所駐留的具體目錄,並且通常可以引用此按鈕。在必須引用 Eclipse 中預設的那些特定資源或為特定的 Eclipse 工作區環境配置的那些特定資源時,請使用 Add Library
對於本示例,請單擊 Add External JARs 並瀏覽到已下載的 jMock 和 RMock JAR。將其新增到專案中。當顯示圖 2 中所示的屬性視窗時,請單擊 OK。
|
對於 TestExample 專案,您將使用來自四個類的原始碼:
- ServiceClass.java
- Collaborator.java
- ICollaborator.java
- ServiceClassTest.java
待測試的類將是 ServiceClass
,該類包含了一個方法:runService()
。服務方法將獲取實現簡單介面 ICollaborator
的 Collaborator
物件。具體的 Collaborator
類中實現了一個方法:executeJob()
。Collaborator
是必須正確模擬的類。
第四個類是測試類:ServiceClassTest
(實現的性質已經被儘可能地簡化)。清單 1 將顯示第四個類的程式碼。
在 ServiceClass
類中,if...else
程式碼塊是一個簡單的邏輯分支,根據測試期望說明選取一條路經 —— 而不是另一條路經 —— 之後測試將失敗(或通過)的原因。下面顯示了 Collaborator
類的原始碼。
Collaborator
類也十分簡單,它配有無引數的建構函式以及從 executeJob()
方法返回的簡單 String
。下面的程式碼顯示了 ICollaborator
類的程式碼。
介面 ICollaborator
有一個必須在 Collaborator
類中實現的方法。
以上程式碼就緒後,讓我們繼續檢驗怎樣在各種場景中成功地執行 ServiceClass
類的測試。
|
測試 ServiceClass
類中的服務方法十分簡單。假定測試要求為證明 runService()
方法並未執行 —— 換言之,返回的布林結果是 false。在這種情況下,傳遞給 runService()
方法的 ICollaborator
物件被模擬 為期望呼叫 executeJob()
方法,並返回除了 “success” 以外的字串。通過這種方法,確保把布林字串 false 返回給測試。
下面所示的是包含測試邏輯的 ServiceClassTest
類程式碼。
|
如果將在各種測試用例中執行公共操作,則在測試中包括 setUp()
tearDown()
方法也很不錯,但不作嚴格要求,除非要執行整合測試。 方法是一種很好的想法。包括
另請注意,使用 jMock 和 RMock,框架將在測試執行結束時或測試執行期間在所有模擬物件中檢查所有期望。並不實際需要為每個模擬期望包括 verify()
方法。當作為 JUnit 測試執行時,測試將通過,如下所示:
ServiceTestClass
類將擴充套件 jMock CGLIB 的 org.jmock.cglib.MockObjectTestCase
類。mockCollaborator
是一個十分簡單的 org.jmock.JMock
類。通常,用 jMock 生成模擬物件有兩種方法:
- 要模擬介面,則使用
new Mock(Class.class)
方法 - 要模擬具體類,則使用
mock(Class.class, "identifier")
方法
必須注意的是怎樣將模擬代理 傳遞給 ServiceClass
類中的 runService()
方法。使用 jMock,您可以從已建立的模擬物件(其中期望已經被設定)中提取代理實現。這一點在本文稍後的場景中至關重要,尤其是在涉及 RMock 的場景中。
|
假定 ServiceClass
類中的 runService()
方法僅接受 Collaborator
類的具體實現。jMock 能夠確保先前的測試通過而無需 更改期望嗎?是的,只要您能夠構造簡單預設樣式的 Collaborator
類。
更改 ServiceClass
類中的 runService()
方法使其反映以下程式碼。
ServiceClass
類的 if...else
邏輯分支保持不變(為了清晰起見)。同時,無引數建構函式仍然適用。注,並不總是需要有創造性邏輯,例如 while...do
子句或 for
迴圈來正確地測試類的方法。只要有針對類使用的物件的方法執行,簡單的模擬期望就足以測試那些執行。
您還必須更改 ServiceClassTest
類以匹配場景,如下所示:
這裡有幾點需要注意。第一,runService()
方法簽名已經不同於以往。它現在不接受 ICollaborator
介面,而接受具體類實現(Collaborator
類)。就測試框架而言,此更改非常重大(注,雖然在本質上反對多型,但是我們將使用傳遞具體類的示例(僅供舉例之用)。在實際的面向物件的場景中絕對不能這樣做)。
第二,模擬 Collaborator
類的方式已經更改。使用 jMock CGLIB 庫可以模擬具體類實現。提供給 jMock CGLIB 的 mock()
方法的附加 String
引數被用作建立的模擬物件的識別符號。使用 jMock(當然,還有 RMock)時,在單一測試用例內每個模擬物件設定都要求有惟一識別符號。這對於在公共的 setUp()
方法中或在實際測試方法內定義的模擬物件來說是正確的。
第三,測試方法的原始期望並未更改。仍然要求有 false 證明才能使測試通過。這是十分重要的,因為通過展示使用的測試框架足夠靈活、可以適應各種輸入帶來的更改、同時仍然允許獲得不變的測試結果,使它們在無法調節輸入生成同樣的結果時展示了其實際限制。
現在,重新執行作為 JUnit 測試的測試。測試將通過,如下所示:
在下一個場景中,情況會變得略微複雜一些。您將使用 RMock 框架來相對緩解一下這種困難的情形。
|
首先像以前一樣嘗試使用 jMock 來模擬 Collaborator
物件 —— 只是這一次,Collaborator
沒有預設的無引數建構函式。注,保留布林 false 結果的測試期望。
同時假定 Collaborator
物件要求使用字串和原始的 int
作為傳遞給建構函式的引數。清單 6 顯示了對 Collaborator
物件所做的更改。
Collaborator
類建構函式仍然十分簡單。用傳入引數設定類欄位。這裡不必使用任何其他邏輯,並且其 executeJob()
函式保持不變。
重新執行測試,並且示例的所有其他元件保持不變。結果是災難性的測試失敗,如下所示:
以上測試是作為簡單的 JUnit 測試執行的,沒有程式碼覆蓋。您可以用大多數程式碼覆蓋工具(例如,Cobertura 或 EclEmma)來執行本文中列出的任何一個測試。但是,用 Eclipse 內的程式碼覆蓋工具執行 RMock 測試時會帶來一些問題(參見 表 1)。以下程式碼顯示了實際堆疊跟蹤的程式碼片段。
失敗原因是 jMock 無法通過沒有無引數建構函式的類定義建立可行的模擬物件。例項化 Collaborator
物件的惟一方法是提供兩個簡單引數。您現在必須找到一種方法把引數提供給模擬物件例項化過程以達到同樣的效果,這就是使用 RMock 的原因。
要更正測試,必須執行一些修改。這些更改可能顯得十分重要,但是實際上,它們是一種相對簡單的解決方法,利用兩種框架的強大功能來實現目的。
必需的第一項更改是使測試類成為 RMock TestCase
,而不是成為 jMock CGLIB TestCase
。目的是在測試本身內啟用屬於 RMock 的那些模擬物件的較容易的配置並且 —— 更重要的是 —— 在最初設定期間啟用這些配置。經驗證明,如果測試類擴充套件的整個 TestCase
物件屬於 RMock,則通過兩個框架構造和使用模擬物件將更容易。此外,乍看之下,快速確定模擬物件的流程將更容易一些(在這裡,流程 用於描述使用模擬物件作為引數甚或作為其他模擬物件的返回型別的情況)。
必需的第二項更改是(至少)構造一個儲存傳遞給 Collaborator
類的建構函式的引數實際值的物件陣列。為了清晰起見,還可以包括建構函式接受的類型別的型別陣列並可以傳遞該陣列,以及剛剛描述為引數的物件陣列以例項化模擬 Collaborator
物件。
第三項更改涉及用正確語法構造對 RMock 模擬物件的一個或多個期望。而第四項也是最後一項必需的更改是使 RMock 模擬物件脫離記錄狀態轉入就緒狀態。
清單 9 顯示了對 ServiceClassTest
類的最終修改。它還顯示了 RMock 及其相關功能的引入。
首先,需要注意測試的期望仍未改變。RMockTestCase
類的匯入預示著引入 RMock 框架功能。接下來,測試類現在將擴充套件 RMockTestCase
,而不是 MockObjectTestCase
。稍後,我將向您展示在 TestClass
物件仍為 RMockTestCase
型別的物件的測試用例中重新引入 MockObjectTestCase
。
|
在 setUp()
方法內,用 Collaborator
類的構造方法所需的實際 值例項化物件陣列。該陣列被立刻傳遞給 RMock 的 intercept()
方法來幫助例項化模擬物件。方法的簽名類似於 jMock CGLIB mock()
Collaborator
型別的類強制轉換十分有必要,因為 intercept()
方法將返回型別 Object。 方法的簽名,因為這兩種方法將接納惟一模擬物件識別符號作為引數。模擬物件到
在測試方法本身 testRunServiceAndReturnFalse()
之內,您可以看到更多更改。模擬 Collaborator
物件的 executeJob()
方法將被呼叫。在此階段,模擬物件處於記錄狀態 —— 即簡單地定義物件將一直期望的方法呼叫,因此,模擬將相應地記錄期望。下一行是對模擬物件的通知,用於確保當它遇到 executeJob()
方法時,它應當返回字串 failure。因此,使用 RMock,您只需通過呼叫方法而不呼叫模擬物件(並傳遞它可能需要的任何引數)來描述期望,然後修改該期望以相應地調整任何返回型別。
最後,呼叫 RMock 方法 startVerification()
把模擬 Collaborator
物件轉為就緒狀態。模擬物件現已準備好在 ServiceClass
類中作為實際物件使用。該方法非常重要並且必須呼叫它才能避免測試初始化失敗。
再次重新執行 ServiceClassTest
以達到最終的肯定結果:在模擬物件例項化期間提供的引數造成了所有差別。圖 6 顯示 JUnit 測試已經通過。
assertFalse(result)
程式碼行表示與場景 1 相同的測試期望,而 RMock 像 jMock 以前那樣維持測試成功。在許多方面,這都十分重要,但是這裡更重要的特點可能是實踐了修正失敗測試的靈活 原則而不更改測試期望。惟一的差別是使用了另一個框架。
在下一個場景中,您將在一種特殊情況下使用 jMock 和 RMock。沒有一個框架能夠僅憑自身就實現正確結果,除非在測試內形成某種聯合。
|
如 前所述,我希望檢驗兩個框架必須協同工作才能實現某個結果的情況。否則,構建良好的測試每次都將失敗。在某些情況下,使用 jMock 還是 RMock 並不重要,例如,當需要模擬的介面或類存在於已經簽名的 JAR 中時。此類情況十分少見,但是當測試針對安全專有的產品(通常是這樣或那樣的一類現有軟體)中的應用程式程式設計介面 (API) 編寫程式碼時可能會出現此情況。
清單 10 顯示了兩個框架完成測試用例的示例。
在 setUp()
方法內,根據為擴充套件 jMock-CGLIB MockObjectTestCase
物件而建立的私有內部類例項化了新 "testcase"
。使用任何 jMock 功能時,這個小解決方法對於確保整個測試類為 RMock TestCase
物件十分有必要。例如,您將設定類似 testCase.once()
而不是類似 once()
的 jMock 期望,因為 TestClass
物件將擴充套件 RMockTestCase
。
構建基於 ClassB
類的模擬物件並向其提供期望。然後您將使用它幫助例項化 RMock Collaborator
模擬物件。待測試的類是 MyNewClass
類(在這裡顯示為私有內部類)。同時,其 executeJob()
方法將接收 Collaborator
executeSomeImportantFunction()
方法。 物件並執行
清單 11 和 12 分別顯示了 ClassA
和 ClassB
的程式碼。ClassA
是沒有實現的簡單類,而 ClassB
顯示了闡明要點所需的最少細節。
此類只是我使用的一個虛構類,用於強化一個要點:要模擬建構函式接收物件引數的類,有必要使用 RMock。
ClassB
類的 wierdMethod
將返回 failed。這是十分重要的,因為該類必須簡短地返回另一個字串才能使測試通過。
清單 13 顯示了測試示例的最重要部分:Collaborator
類。
注,首要的是,使用 jMock 框架模擬了 ClassB
類。使用 RMock,沒有一種實際方法從模擬物件中提取和使用代理,以便在測試 setUp()
方法中的其他位置使用該代理。使用 RMock,僅當呼叫 startVerification()
方法之後,才顯示代理物件。本例中的優點是使用 jMock,因為在需要返回自我模擬物件的情況下,可以 獲得設定其他模擬物件所需的資訊。
反過來,需要注意的第二點是您不能使用 jMock 框架來模擬 Collaborator
類。原因是該類沒有無引數建構函式。此外,在建構函式內有某種邏輯,這種邏輯將確定是否先獲得例項。事實上,出於本次討論的目的,ClassB
wierdMethod()
方法必須返回 passed 才能使 Collaborator
物件被例項化。但是,另請注意,在預設情況下,方法總是返回 failed
。測試成功明顯需要模擬 ClassB
。 中的
此外,不同於先前的示例,此場景中的類陣列作為附加引數被包含到了 intercept()
方法中。對此不作嚴格要求,但是用它作為金鑰可以快速識別在例項化 RMock 測試物件時使用的物件類。
執行新測試用例。這一次,您將看到成功的結果。圖 7 將顯示令人愉快的結果。
Collaborator
模擬物件已被正確設定,並且 mockClassB
物件將按預期執行。
|
正 如您已經在場景中看到的,jMock 和 RMock 都是用於測試 Java 程式碼的強大工具。但是,用於開發和測試的任何其他工具總是有限制。實際上,其他測試工具都是可用的,但是這些測試工具的執行情況都不如 RMock 和 jMock(在 Java 技術中)。個人經驗告訴我 Microsoft® .NET 框架也附帶了一些功能強大的工具(例如 TypeMock),但是那超出了本文(實際上還有平臺)的範圍。
表 1 顯示了兩個框架之間的一些不同之處以及隨著時間的推移遇到的可能問題,尤其是在 Eclipse 環境中。
測試模擬樣式 | jMock | RMock |
---|---|---|
可以模擬介面 | 是:新的 Mock() 方法 |
是:mock() 方法 |
可以模擬具體類 | 是:帶有 CGLIB 的 mock() 方法 |
是:mock() 或 intercept() 方法 |
可以模擬任何具體類 | 否:無引數建構函式必須存在 | 是 |
可以隨時獲得代理 | 是 | 否:僅當 startVerification() 處於就緒狀態後 |
使用其他 Eclipse 外掛的問題 | 無已知問題 | 是:與 Eclipse 的 CoverClipse 外掛存在記憶體衝突 |
|
我 鼓勵您使用這些框架,利用它們的力量來生成單元測試的結果。許多 Java 開發人員不習慣於頻繁編寫測試。而且如果需要編寫測試,通常都是十分簡單、覆蓋方法的主要功能目標的測試。要測試程式碼的某些 “難以達到的” 部分,jMock 和 RMock 都是優秀的選擇。
使用 jMock 和 RMock 將極大地減少程式碼中的 bug,提高使用經過證明的方法測試程式設計邏輯的技巧。此外,閱讀文件並用這些框架和其他框架的改進版本進行測試(並減少構造不好的程式碼)將對提高開發人員技巧有著額外的幫助。