一、引言
測試驅動開發在減少開發努力的同時也改進了軟件的開發質量。單元測試,作為一整套測試策略的基 礎,必須是全面的,且要求易於建立和執行迅速。然而,對執行環境和被測試類外部代碼的依賴性使我們 實現這些目標變得更為復雜。例如,把應用程序發布到容器將顯著地延長代碼和測試的周期;而對其它類 的依賴性通常也會導致測試的建立更加復雜和測試運行速度更為緩慢。
集成兩個流行的測試框架(StrutsTestCase和EasyMock)來單元測試Struts應用程序將會更為容易地 建立測試並加快測試速度。然而,這兩個框架之間尚存在一些“隔閡”,從而很難把它們理想地集成到一 起。在本文中,我將通過分析兩種方案(一個面向對象的方案和一個面向方面的方案)來探討這個問題。 同時,我還將展示面向方面編程(AOP)是如何通過簡化一些看起來很困難的問題的解決方案而進一步補充 面向對象編程(OOP)的。
二、集成需要
一個典型的Struts應用程序既能夠展示也其所使用的執行環境也會體現出類之間的依賴性問題;這是 因為Struts行為(Action)是在一個servlet容器內執行的,並且典型情況下會調用其它的類來處理請求 。模擬對象測試方法有助於消除其中不必要的依賴性。借助於繼承自基本JUnit測試集的 MockStrutsTestCase類,StrutsTestCase測試框架提供了對servlet容器的一種模擬實現。這顯然方便了 容器外測試,因而也相應地加快了單元測試周期。另一方面,另一個測試框架—EasyMock—進一步便利了 對協作類的動態模擬(Mock)。這個框架中所提供的模擬能夠用更簡單的實現來代替真正的類,並且添加 了校驗邏輯以支持單元測試。
非常清楚,把這兩個框架結合在一起是非常有益的—Struts應用程序便可以在非常真實的隔離環境下 進行測試。理想情況下,你需要使用下列步驟來實現這樣的一個單元測試:
1.建立MockStrutsTestCase以便模擬servlet容器。
2.借助於EasyMock來模擬行為所依賴的類。
3.設置模擬的期望值。
4.把模擬注入到當前測試的行為中。
5.繼續進行測試和校驗。
注意,上面步驟4中所執行的依賴性注入使被測試的Struts行為遠離了其真實的協作者而與一個模擬的 行為進行交互。為了把通過EasyMock生成的模擬注入到行為中,你需要從測試類內部存取這些行為相應的 實例。遺憾的是,這裡出現了一種障礙,因為我們無法輕易地從MockStrutsTestCase中實現這樣的存取。
三、OOP方案
那麼,你該如何從MockStrutsTestCase中存取行為實例呢?首先,讓我們來分析一下 MockStrutsTestCase和Struts的控制器組件之間的關系。
圖1中展示的關鍵關系有可能潛在地導致一種解決上面問題的方案。
圖1:此處展示的關系能夠建立一種OOP方案
.MockStrutsTestCase中提供了一個public類型的getter方法用於檢索ActionServlet。
.ActionServlet有一個protected類型的getter方法用於實現RequestProcessor。
.RequestProcessor把行為實例存儲為一個protected類型的成員。
你是否可以子類化ActionServlet和RequestProcessor從而使MockStrutsTestCase能夠存取行為呢?相 應的結果調用鏈看上去應該如下所示:
注意,在你分析完把MockStrutsTestCase鏈接到Struts行為的調用序列圖之後,你就會發現此方法是 行不通的。
圖2展示了存在於MockStrutsTestCase和Struts組件之間的關鍵性交互。
圖2:存在於MockStrutsTestCase和Struts組件之間的交互
圖2展示的問題涉及到Struts行為創建的時序問題。到行為內部的模擬注入必須在調用 MockStrutsTestCase.actionPerform()之前發生。然而,此時這些行為還不可用,因為只有在調用 actionPerform()後,Requ
estProcessor才能夠創建這些行為實例。
既然你不能很容易地把行為實例傳播到MockStrutsTestCase中,那麼,為什麼不子類化 RequestProcessor並重載processActionCreate()方法呢?在這個重載方法中,你可以存取所有的行為實 例;這樣以來,創建、配置和設置對相應行為實例的一個模擬一下子變得非常直接。因為應該在執行完 actionPerform()之後調用MockControl.verify()方法,所以,你還需要重載processActionPerform()以 進行此校驗調用。
這種方案對於測試正規的Struts應用程序是不太適合的。因為即使所有的行為僅與單個模擬進行交互 ,測試一個行為也有可能要求多個測試方法—每個方法都具有不同的模擬期望。為此,我們建議的方案是 :創建不同的RequestProcessor子類,相應於每個子類設置不同的模擬期望。另外,還需要多個Struts配 置文件來指定不同的RequestProcessor子類。最終,管理大量的測試將成為一件令人頭疼的事情。
四、AOP方案
因此,我們非常希望,在執行某行為之前能夠通過某種方式實現在MockStrutsTestCase中使用該行為 的實例。如果你熟悉AOP,那麼,你會立即意識到它所提供的簡單方案即能直接滿足這一要求。注意:這 裡的關鍵是定義一個切點,由它負責捕獲行為執行連接點;然後通過一個before advice把模擬注入到相 應的行為中。
在此,我選擇使用AspectJ框架來實現這一方案。當然,其它的例如Spring AOP這樣的AOP實現也應該 能夠良好工作。不過,Spring AOP還需要一個額外的步驟—通過Spring框架中的DelegatingActionProxy 類把對Struts行為的管理委托給Spring。
圖3展示了基於AOP方案的單元測試示例靜態模型。
圖3:基於AOP方案的單元測試示例靜態模型
SimpleAction是一個Struts行為的子類,同時與ActionService進行協作。其中,SimpleActionTest派 生於MockStrutsTestCase,用來測試SimpleAction。
SimpleActionTest使用EasyMock創建和建立一個模擬ActionService。SimpleActionTest還實現 StrutsActionPreExecuteListener接口以便在即將運行 SimpleAction的execute方法時接 收通知。作為通知的一部分,SimpleActionTest接收SimpleAction實例以便注入ActionService模擬。由 方面類StrutsActionPreExecuteNotifier負責通知任何實現監聽器接口的測試類,並且使相應的行為實例 可用。
下面的步驟描述了實現StrutsActionPreExecuteNotifier的過程:
◆首先,由一個切點選擇相應的測試方法執行連接點。另一方面,這個測試方法駐留於負責監聽該行 為的預執行事件的測試類中。另外,這個切點還會暴露當前執行的測試類對象: pointcut mockStrutsTest(StrutsActionPreExecuteListener actionTest):
◆然後,由第二個切點負責捕獲上面的行為執行連接點。通過結合第一個切點,匹配范圍被限制到該 行為相應的測試方法的調用流程的內部。這種進一步縮小的范圍對行為執行(並非通過測試方法激活)起 到過濾作用。最終,方面根本不會影響到最後生成的代碼。該行為及其相應的測試類實例都是經由切點參 數加以暴露的: pointcut strutsActionExecute(Action action, StrutsActionPreExecuteListener actionTest):
◆最後,由一個與前一個切點相關聯的before advice負責通知測試類(它們擔任行為事件的監聽器) 並且傳遞相應於模擬注入的行為實例:
圖4展示了這些類之間的動態交互情形。
圖4:類之間的動態交互
注意,圖中從行為到方面的虛線描述了對行為執行連接點的捕獲情況。此時序圖與第一個時序圖比較 ,其重要區別正在於行為執行之前發生的三個步驟:
1.一個切點捕獲行為執行連接點(由從SimpleAction指向StrutsActionPreExecuteNotifier的虛線箭 頭指出)。
2.方面的before advice負責通知測試類並且把相應的行為實例傳遞給它。
3.測試類把模擬對象注入到即將要開始執行的行為實例中。
現在,你可以基於前面概括的五個步驟繼續編寫行為測試。下面的代碼展示了相應於 SimpleActionTest的部分代碼,步驟已在注釋中標出。
使用MockStrutsTestCase和EasyMock進行行為測試的部分代碼:
在行動及其依賴的服務之間存在四種可能的復合關系:
每個行為依賴於一個服務。
每個行為依賴於多個服務。
多個行為依賴於一個服務。
多個行為依賴於多個服務。
我在此展示的方案能夠比較靈活而且相對容易地支持上面所有這四種情形,因為模擬創建、期望值建 立以及模擬注入都能夠在單個的測試類內實現。
你能夠不借助於監聽器接口就可以在StrutsActionPreExecuteNotifier內部模擬注入嗎?這看起來似 乎使得測試類實現更簡單一些。然而,實踐證明,類似早些時候討論的OOP方案,編寫多個方面以創建不 同的模擬對象並建立相應的不同的模擬期望是非常必要的。另外,在單個測試類內本地化模擬的創建與安 裝(借助於監聽技術,這是可能的)將變得更為方便。
五、總結
對於我們在本文中所討論的集成問題,有人可能會創造出一套相當不錯的OOP方案。然而,構造這種方 案很可能需要對Struts和StrutsTestCase有深入的理解才行,並且要付出相當的努力。影響本文中所討論 的兩個測試框架(StrutsTestCase和EasyMock)緊密集成的主要障礙在於,在Struts行為實例執行之前很 難實現對它的訪問。在認識了導致這種障礙的基本原因之後,AOP方案自然地出現在我們面前。不必再強 求於基於傳統型OOP的那種更復雜的方案,AOP允許我們把我們的方案更為緊密地映射到問題空間。
其實,AOP的真正“魔術”在於它的連接點模型,它能夠使你“穿越”中間對象(例如ActionServlet 和RequestProcessor)進而直指問題的核心。借助於AOP技術中確定橫切關注點這種非常“節儉”的方法 ,開發者即能夠設計出非常直觀而且更為簡單的解決方案。AOP,這種強有力的編程方法正好彌補了傳統 型OOP編程中所存在的不足。如果被恰當用於解決適當類型的問題,那麼,AOP有助於改進代碼的模塊化, 最終會產生出更為清晰和更易於理解的代碼。最後,非常希望本文不僅有助於你的Struts應用程序的單元 測試,而且還吸引你進一步探討AOP編程所體現出來的其它重要優點。