閱讀提要 StrutsTestCase是一個強(qiáng)有力的易于使用的針對Struts行為的測試框架。StrutsTestCase,并與傳統(tǒng)型JUnit測試相結(jié)合,將會帶給你一個相當(dāng)高的測試覆蓋率并提高你的產(chǎn)品的可靠性。
一、引言 StrutsTestCase是一個用于測試Struts行為的基于Junit的測試框架。如果你使用Struts,那么你會注意到它可以提供給你一種容易而有效的方式來測試你的應(yīng)用程序的Struts行為類。
典型的J2EE應(yīng)用程序都是分層構(gòu)建的,如圖1所示。
·DAO層封裝了數(shù)據(jù)庫存取。Hibernate映射和對象類,Hibernate查詢,實(shí)體EJBs,或一些其它的實(shí)體-關(guān)系持續(xù)性技術(shù)都可以在這一層找到。
·商業(yè)層包含更高級的商業(yè)服務(wù)。理想地,這個商業(yè)層將是相對獨(dú)立于數(shù)據(jù)庫實(shí)現(xiàn)。在這個層上經(jīng)常使用會話EJBs。
·描述層包含為用戶顯示應(yīng)用程序數(shù)據(jù)并解釋用戶請求。在一個Struts應(yīng)用程序中,這一層典型地使用JSP/JSTL頁面來顯示數(shù)據(jù)并且使用Struts行為來解釋用戶查詢。
·客戶層基本上是運(yùn)行于用戶機(jī)器上的web瀏覽器??蛻舳诉壿?例如,JavaScript)有時被放在這里,盡管很難對其進(jìn)行有效地測試。
DAO和商業(yè)層的測試或者可以通過使用經(jīng)典的JUnit測試或者使用各種JUnit擴(kuò)展來進(jìn)行,具體依賴于架構(gòu)的實(shí)現(xiàn)細(xì)節(jié)。DbUnit是一種用來進(jìn)行數(shù)據(jù)庫單元測試的良好選擇。
另一方面,測試Struts行為總是很困難的事情。即使在商業(yè)層嚴(yán)格地限制于商業(yè)層的構(gòu)建時,Struts行為也總要包含重要數(shù)據(jù)校驗(yàn),轉(zhuǎn)換和流程控制代碼。不對Struts行為進(jìn)行測試將會在代碼覆蓋率上留下一道很臟的鴻溝。StrutsTestCase會讓你填充這條鴻溝。
對行為層進(jìn)行單元測試還帶來其它一些益處:
·可以更好地規(guī)劃視圖和控制層,從而使之更為簡單清晰。
·更容易重構(gòu)行為類。
·避免冗余的未使用的行為類。
·測試實(shí)例有助于對行為層進(jìn)行歸檔-這在創(chuàng)建屏幕時是很有用的。
上面是基于測試開發(fā)的典型好處,并且它們可以應(yīng)用于在各種情況下使用的Struts行為層。
二、StrutsTestCase簡介 StrutsTestCase工程提供了一種靈活又方便的方法來從JUnit框架內(nèi)測試Struts行為。它能夠使你對你的Struts行為進(jìn)行白色盒子測試-通過在調(diào)用行為后建立請求參數(shù)并檢查結(jié)果Request或Session的狀態(tài)。
StrutsTestCase允許或者是一個模仿測試方式-這時框架模擬web服務(wù)器容器,或者是一個容器內(nèi)方式-這時使用Cactus框架來從服務(wù)器容器(例如Tomcat)內(nèi)部運(yùn)行測試。一般地,我比較喜歡模仿測試方式,因?yàn)樗鼮檩p量級的且運(yùn)行更快些,并因此允許較寬松的開發(fā)周期。
所有的StrutsTestCase單元測試類或者派生于MockStrutsTestCase以進(jìn)行模仿測試,或者派生于CactusStrutsTestCase以進(jìn)行容器內(nèi)測試。在此我們先討論模仿測試,因?yàn)樗筝^少的配置并且運(yùn)行較快些。
三、實(shí)戰(zhàn)StrutsTestCase 為了使用StrutsTestCase來測試這個行為,我們創(chuàng)建一個擴(kuò)展類MockStrutsTestCase的新類。這個類提供一系列方法來構(gòu)建一個模擬的HTTP請求,調(diào)用相應(yīng)的Struts行為以及一旦在行為完成時校驗(yàn)應(yīng)用程序狀態(tài)。
可以設(shè)想有一個在線的具有多條件查找功能的住所數(shù)據(jù)庫。這個查找函數(shù)是通過/search.do行為實(shí)現(xiàn)的。這個行為將基于指定的條件完成一次多條件查找,并把結(jié)果列表放置在一個稱為results的請求范圍屬性中。例如,下列URL應(yīng)該顯示一個在法國的所有的住所結(jié)果列表:
/search.do?country=FR
現(xiàn)在,假定我們想要使用一個測試驅(qū)動的方式來實(shí)現(xiàn)這個方法。我們創(chuàng)建該行為類并更新Struts配置文件。我們還編制測試實(shí)例來測試(空的)這個行為類。通過使用一種嚴(yán)格的測試驅(qū)動的開發(fā)方法,我們可以首先創(chuàng)建測試實(shí)例,然后實(shí)現(xiàn)代碼來匹配該測試實(shí)例。在實(shí)踐中,具體的順序可能因要測試的代碼而有所不同。
起始的測試情形看去如下樣子:
public void testSearchByCountry() { setRequestPathInfo("/search.do"); addRequestParameter("country", "FR"); actionPerform(); } |
在此,我們建立要調(diào)用的路徑(setRequestPathInfo())并且添加一請求參數(shù)(addRequestParameter())。然后,我們用actionPerform()來調(diào)用行為類。這將驗(yàn)證Struts配置并且調(diào)用相應(yīng)的行動類,但是將不測試該行為的實(shí)際所做。為此,我們需要驗(yàn)證行動的結(jié)果。
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實(shí)際上指定正確的tiles定義:
public void testSearchByCountry() { setRequestPathInfo("/search.do"); addRequestParameter("country", "FR"); actionPerform(); verifyNoActionErrors(); verifyTilesForward("success", "accommodation.list.def"); assertNotNull(request.getAttribute("results")); } |
在實(shí)踐中,我們可能想在該測試結(jié)果上實(shí)現(xiàn)特定的商業(yè)測試。例如,假定結(jié)果屬性是一個List-它包含一列約100個Hotel域?qū)ο?,并且我們想要保證所有在該列表中的賓館都在法國。為了實(shí)現(xiàn)這種類型的測試,代碼將非常相似于標(biāo)準(zhǔn)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); ... } } |
當(dāng)你測試更復(fù)雜的情形時,你可能想要測試系列化的行為。例如,假定用戶在法國查詢所有的賓館并且點(diǎn)擊一個入口來顯示相應(yīng)的查詢細(xì)節(jié)。假定我們有一個Struts行為來顯示一個給定賓館的細(xì)節(jié)信息,可以作如下調(diào)用:
/displayDetails.do?id=123456
通過使用StrutsTestCase,我們能夠容易地在相同的測試情形下模仿一系列的行為-一個用戶在法國查詢所有的賓館,然后點(diǎn)擊一個入口來顯示相應(yīng)的查詢細(xì)節(jié):
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錯誤處理 測試錯誤處理也是一件很重要的事情。假定,如果指定一個無效的國家代碼時,我們想要檢查應(yīng)用程序仍然運(yùn)行良好。為此,我們可以寫一個新的測試方法并且使用verifyActionErrors()檢查返回的Struts ErrorMessages:
public void testSearchByInvalidCountry() { setRequestPathInfo("/search.do"); addRequestParameter("country", "XX"); actionPerform(); verifyActionErrors( new String[] {"error.unknown,country"}); verifyForward("failure"); } |
有時你想直接在ActionForm對象中進(jìn)行數(shù)據(jù)校驗(yàn)。為此,你可以使用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")); } |
在此,我們可以確保在出現(xiàn)錯誤后無效的國家代碼被正確地存儲在ActionForm中。
五、定制測試環(huán)境 重載setUp()方法有時是很有用的-它讓你指定非缺省的配置選項(xiàng)。在這個例子中,我們使用一個不同的struts-config.xml文件并且不激活XML配置文件校驗(yàn):
public void setUp() { super.setUp(); setConfigFile("/WEB-INF/my-struts-config.xml"); setInitParameter("validating","false"); } |
六、第一級性能測試 測試一個行為或一系列的行為是一個十種優(yōu)秀的測試方式-它要求能夠存取響應(yīng)次數(shù)。從Struts行為中進(jìn)行測試允許你校驗(yàn)全局的服務(wù)器端性能(當(dāng)然,除去產(chǎn)生JSP頁面)。為了盡快隔離和移除性能問題以及把它們集成到構(gòu)建過程中以幫助避免性能回退,在單元-測試級上進(jìn)行一些第一級性能測試是個很不錯的注意。
下面是我用來進(jìn)行第一級Struts性能測試的基本原則:
·用盡可能多的組合來測試多條件搜索查詢(為了檢查這些索引已被正確定義了)。
·測試大容量的查詢(返回大量結(jié)果的查詢)來檢查響應(yīng)次數(shù)和結(jié)果頁面(如果使用的話)。
·測試單個的和重復(fù)的查詢(來檢查緩沖性能,如果使用緩沖策略的話)。
有一些開源庫可以用于幫助進(jìn)行性能測試,例如由Mike Clark維護(hù)的JUnitPerf。然而,把它們集成到StrutsTestCase中可能有些復(fù)雜。在很多情況下,一個簡單的定時器即可以實(shí)現(xiàn)這一功能。下面是一種簡單而有效的實(shí)現(xiàn)第一級性能測試的方法:
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) } |
七、結(jié)論 一般地,單元測試是進(jìn)行靈敏編程特別是基于測試開發(fā)的一個基本部分。StrutsTestCase為我們提供一種容易并且有效的方法來單元測試Struts行為;否則,如果使用JUnit來進(jìn)行單元測試則相當(dāng)困難。