閱讀提要 StrutsTestCase是一個強有力的便於使用的針對Struts行為的測試架構。StrutsTestCase,並與傳統型JUnit測試相結合,將會帶給你一個相當高的測試覆蓋率並提高你的產品的可靠性。
一、引言
StrutsTestCase是一個用於測試Struts行為的基於Junit的測試架構。如果你使用Struts,那麼你會注意到它可以提供給你一種容易而有效方式來測試你的應用程式的Struts行為類。
典型的J2EE應用程式都是分層構建的,如圖1所示。
·DAO層封裝了資料庫存取。Hibernate映射和對象類,Hibernate查詢,實體EJBs,或一些其它的實體-關係持久性技術都可以在這一層找到。
·商業層包含更進階的商務服務。理想地,這個商業層將是相對獨立於資料庫實現。在這個層上經常使用會話EJBs。
·描述層包含為使用者顯示應用程式資料並解釋使用者請求。在一個Struts應用程式中,這一層典型地使用JSP/JSTL頁面來顯示資料並且使用Struts行為來解釋使用者查詢。
·客戶層基本上是運行於使用者機器上的web瀏覽器。用戶端邏輯(例如,JavaScript)有時被放在這裡,儘管很難對其進行有效地測試。
圖1.典型的J2EE架構
DAO和商業層的測試或者可以通過使用經典的JUnit測試或者使用各種JUnit擴充來進行,具體依賴於架構的實現細節。DbUnit是一種用來進行資料庫單元測試的良好選擇。
另一方面,測試Struts行為總是很困難的事情。即使在商業層嚴格地限制於商業層的構建時,Struts行為也總要包含重要資料校正,轉換和流程式控制制代碼。不對Struts行為進行測試將會在程式碼涵蓋範圍上留下一道很髒的鴻溝。StrutsTestCase會讓你填充這條鴻溝。
對行為層進行單元測試還帶來其它一些益處:
·可以更好地規劃視圖和控制層,從而使之更為簡單清晰。
·更容易重構行為類。
·避免冗餘的未使用的行為類。
·測試執行個體有助於對行為層進行歸檔-這在建立螢幕時是很有用的。
上面是基於測試開發的典型好處,並且它們可以應用於在各種情況下使用的Struts行為層。
二、StrutsTestCase簡介
StrutsTestCase工程提供了一種靈活又方便的方法來從JUnit架構內測試Struts行為。它能夠使你對你的Struts行為進行白色盒子測試-通過在調用行為後建立請求參數並檢查結果Request或Session的狀態。
StrutsTestCase允許或者是一個模仿測試方式-這時架構類比web伺服器容器,或者是一個容器內方式-這時使用Cactus架構來從伺服器容器(例如Tomcat)內部運行測試。一般地,我比較喜歡模仿測試方式,因為它更為輕量級的且運行更快些,並因此允許較寬鬆的開發週期。
所有的StrutsTestCase單元測試類或者派生於MockStrutsTestCase以進行模仿測試,或者派生於CactusStrutsTestCase以進行容器內測試。在此我們先討論模仿測試,因為它要求較少的配置並且運行較快些。
三、實戰StrutsTestCase
為了使用StrutsTestCase來測試這個行為,我們建立一個擴充類MockStrutsTestCase的新類。這個類提供一系列方法來構建一個類比的HTTP請求,調用相應的Struts行為以及一旦在行為完成時校正應用程式狀態。
可以設想有一個線上的具有多條件尋找功能的住所資料庫。這個尋找函數是通過/search.do行為實現的。這個行為將基於指定的條件完成一次多條件尋找,並把結果清單放置在一個稱為results的請求範圍屬性中。例如,下列URL應該顯示一個在法國的所有的住所結果清單:
/search.do?country=FR
現在,假定我們想要使用一個測試驅動的方式來實現這個方法。我們建立該行為類並更新Struts設定檔。我們還編製測試執行個體來測試(空的)這個行為類。通過使用一種嚴格的測試驅動的開發方法,我們可以首先建立測試執行個體,然後實現代碼來匹配該測試執行個體。在實踐中,具體的順序可能因要測試的代碼而有所不同。
起始的測試情形看去如下樣子:
public void testSearchByCountry() {
setRequestPathInfo("/search.do");
addRequestParameter("country", "FR");
actionPerform();
}
在此,我們建立要調用的路徑(setRequestPathInfo())並且添加一請求參數(addRequestParameter())。然後,我們用actionPerform()來調用行為類。這將驗證Struts配置並且調用相應的行動類,但是將不測試該行為的實際所做。為此,我們需要驗證行動的結果。
public void testSearchByCountry() {
setRequestPathInfo("/search.do");
addRequestParameter("country", "FR");
actionPerform();
verifyNoActionErrors();
verifyForward("success");
assertNotNull(request.getAttribute("results"));
}
在此,我們檢查三件事情:
·不存在ActionError訊息(verifyNoActionErrors())。
·返回"success"forward。
·results屬性被放置到請求範圍中。
如果我們正在使用tiles,我們也可以通過使用verifyTilesForward()來保證"success"forward實際上指定正確的tiles定義:
public void testSearchByCountry() {
setRequestPathInfo("/search.do");
addRequestParameter("country", "FR");
actionPerform();
verifyNoActionErrors();
verifyTilesForward("success", "accommodation.list.def");
assertNotNull(request.getAttribute("results"));
}
在實踐中,我們可能想在該測試結果上實現特定的商業測試。例如,假定結果屬性是一個List-它包含一列約100個Hotel域對象,並且我們想要保證所有在該列表中的賓館都在法國。為了實現這種類型的測試,代碼將非常相似於標準JUnit測試:
public void testSearchByCountry() {
setRequestPathInfo("/search.do");
addRequestParameter("country", "FR");
actionPerform();
verifyNoActionErrors();
verifyForward("success");
assertNotNull(request.getAttribute("results"));
List results = (List) request.getAttribute("results");
assertEquals(results.size(), 100);
for (Iterator iter = results.iterator();
iter.hasNext();) {
Hotel hotel = (Hotel) iter.next();
assertEquals(hotel.getCountry, TestConstants.FRANCE);
...
}
}
當你測試更複雜的情形時,你可能想要測試系列化的行為。例如,假定使用者在法國查詢所有的賓館並且點擊一個入口來顯示相應的查詢細節。假定我們有一個Struts行為來顯示一個給定賓館的細節資訊,可以作如下調用:
/displayDetails.do?id=123456
通過使用StrutsTestCase,我們能夠容易地在相同的測試情形下模仿一系列的行為-一個使用者在法國查詢所有的賓館,然後點擊一個入口來顯示相應的查詢細節:
public void testSearchAndDisplay() {
setRequestPathInfo("/search.do");
addRequestParameter("country", "FR");
actionPerform();
verifyNoActionErrors();
verifyForward("success");
assertNotNull(request.getAttribute("results"));
List results = (List) request.getAttribute("results");
assertEquals(results.size(),100);
Hotel hotel = (Hotel) results.get(0);
setRequestPathInfo("/displayDetails.do");
addRequestParameter("id", hotel.getId());
actionPerform();
verifyNoActionErrors();
verifyForward("success");
Hotel hotel = (Hotel)request.getAttribute("hotel");
assertNotNull(hotel);
...
}
四、測試Struts錯誤處理
測試錯誤處理也是一件很重要的事情。假定,如果指定一個無效的國家代碼時,我們想要檢查應用程式仍然運行良好。為此,我們可以寫一個新的測試方法並且使用verifyActionErrors()檢查返回的Struts ErrorMessages:
public void testSearchByInvalidCountry() {
setRequestPathInfo("/search.do");
addRequestParameter("country", "XX");
actionPerform();
verifyActionErrors( new String[] {"error.unknown,country"});
verifyForward("failure");
}
有時你想直接在ActionForm對象中進行資料校正。為此,你可以使用getActionForm(),如下所示:
public void testSearchByInvalidCountry() {
setRequestPathInfo("/search.do");
addRequestParameter("country", "XX");
actionPerform();
verifyActionErrors( new String[] {"error.unknown,country"});
verifyForward("failure");
SearchForm form = (SearchForm) getActionForm();
assertEquals("Scott", form.getCountry("XX"));
}
在此,我們可以確保在出現錯誤後無效的國家代碼被正確地儲存在ActionForm中。
五、定製測試環境
重載setUp()方法有時是很有用的-它讓你指定非預設的配置選項。在這個例子中,我們使用一個不同的struts-config.xml檔案並且不啟用XML設定檔校正:
public void setUp() {
super.setUp();
setConfigFile("/WEB-INF/my-struts-config.xml");
setInitParameter("validating","false");
}
六、第一級效能測試
測試一個行為或一系列的行為是一個十種優秀的測試方式-它要求能夠存取響應次數。從Struts行為中進行測試允許你校正全域的伺服器端效能(當然,除去產生JSP頁面)。為了儘快隔離和移除效能問題以及把它們整合到構建過程中以協助避免效能回退,在單元-測試級上進行一些第一級效能測試是個很不錯的注意。
下面是我用來進行第一級Struts效能測試的基本原則:
·用儘可能多的組合來測試多條件搜尋查詢(為了檢查這些索引已被正確定義了)。
·測試大容量的查詢(返回大量結果的查詢)來檢查響應次數和結果頁面(如果使用的話)。
·測試單個的和重複的查詢(來檢查緩衝效能,如果使用緩衝策略的話)。
有一些開源庫可以用於協助進行效能測試,例如由Mike Clark維護的JUnitPerf。然而,把它們整合到StrutsTestCase中可能有些複雜。在很多情況下,一個簡單的定時器即可以實現這一功能。下面是一種簡單而有效實現第一級效能測試的方法:
public void testSearchByCountry() {
setRequestPathInfo("/search.do");
addRequestParameter("country", "FR");
long t0 = System.currentTimeMillis();
actionPerform();
long t1 = System.currentTimeMillis() - t0;
log.debug("Country search request processed in " + t1 + " ms");
assertTrue("Country search too slow", t1 >= 100)
}
七、結論
一般地,單元測試是進行靈敏編程特別是基於測試開發的一個基本部分。StrutsTestCase為我們提供一種容易並且有效方法來單元測試Struts行為;否則,如果使用JUnit來進行單元測試則相當困難。