1. 程式人生 > >基於mock物件和JUnit框架簡化Spring Web元件單元測試

基於mock物件和JUnit框架簡化Spring Web元件單元測試

對於Java元件開發者來說,他們都盼望擁有一組能夠對元件開發提供全面測試功能的好用的單元測試。一直以來,與測試獨立的Java物件相比,測試傳統型J2EE Web元件是一項更為困難的任務,因為Web元件必須執行在某種伺服器平臺上並且它們還要與基於HTTP的Web互動細節相聯絡。

易測性(在框架中測試每個元件而不管其具體種類)是Spring框架所提倡的關鍵原則之一。從這一角度看,Spring是對核心J2EE模型的一個重大改進—在以前情況下,在容器外進行元件測試是很難實現的,而且即使是容器內測試也往往要求複雜的安裝過程。

本文正是想集中探討Spring的易測性特徵—它能使得對Web元件進行單元測試就象測試普通Java物件(POJO)一樣容易。

一、Spring Mock類簡介

Mock物件是一個術語,原來主要流行於eXtreme程式設計師和JUnit小組中。在單元測試上下文中,一個mock物件是指這樣的一個物件——它能夠用一些“虛構的佔位符”功能來“模擬”實現一些物件介面。在測試過程中,這些虛構的佔位符物件可用簡單方式來模仿對於一個元件的期望的行為和結果,從而讓你專注於元件本身的徹底測試而不用擔心其它依賴性問題。

Spring從J2EE的Web端為每個關鍵介面提供了一個mock實現:

MockHttpServletRequest—幾乎每個單元測試中都要使用這個類,它是J2EE Web應用程式最常用的介面HttpServletRequest的mock實現。

MockHttpServletResponse—此物件用於HttpServletResponse介面的mock實現。

MockHttpSession—這是另外一個經常使用的mock物件(後文將討論此類在會話繫結處理中的應用)。

DelegatingServletInputStream—這個物件用於ServletInputStream介面的mock實現。

DelegatingServletOutputStream—這個物件將代理ServletOutputStream實現。在需要攔截和分析寫向一個輸出流的內容時,你可以使用它。

總之,在實現你自己的測試控制器時,上面這些物件是最為有用的。然而,Spring也提供了下列相應於其它不太常用的元件的mock實現(如果你是一個底層API開發者,那麼你可能會找到其各自的相應用法):

MockExpressionEvaluator—這個mock物件主要應用於你想開發並測試你自己的基於JSTL的標籤庫時。

MockFilterConfig—這是FilterConfig介面的一個mock實現。

MockPageContext—這是JSP PageContext介面的一個mock實現。你會發現這個物件的使用有利於測試預編譯的JSP。

MockRequestDispatcher—RequestDispatcher介面的一個mock實現,你主要在其它mock物件內使用它。

MockServletConfig—這是ServletConfig介面的一個mock實現。在單元測試某種Web元件(例如Struts框架所提供的Web元件)時,要求你設定由MockServletContext所實現的ServletConfig和ServletContext介面。

那麼,我們該如何使用這些mock物件呢?我們知道,HttpServletRequest是一個持有描述HTTP引數的固定值的元件,而正是這些引數驅動Web元件的功能。MockHttpServletRequest,作為HttpServletRequest介面的一個實現,允許你設定這些不可改變的引數。在典型的Web元件測試情形下,你可以例項化這個物件並按如下方式設定其中的任何引數:
//指定表單方法和表單行為

MockHttpServletRequest request = new MockHttpServletRequest("GET", "/main.app"); request.addParameter("choice", expanded); request.addParameter("contextMenu", "left");

同樣地,你可以例項化並全面地控制和分析HttpResponse和HttpSession物件。接下來,讓我們簡要觀察Spring所提供的特定的JUnit框架擴充套件。

二、JUnit框架擴充套件

Spring提供了下列一些特定的JUnit框架擴充套件:

AbstractDependencyInjectionSpringContextTests—這是一個針對所有測試的超類,其具體使用依賴於Spring上下文。

AbstractSpringContextTests—這是一個針對所有的JUnit測試情形的超類。它使用一個Spring上下文。並且,一般在測試中不是直接使用它,而是使用AbstractDependencyInjectionSpringContextTests或者AbstractTransactionalSpringContextTests這樣的派生類。

AbstractTransactionalSpringContextTests—這是一個針對所有測試的超類,我們一般把它應用在事務相關的測試中。注意,一旦完成每個測試它就會正常地回滾事務;而且你需要過載onSetUpInTransaction和onTearDownInTransaction方法以便手工開始並提交事務。

AbstractTransactionalDataSourceSpringContextTests—這是AbstractTransactionalSpringContextTests的一個子類,它使用了Spring的基於JDBC的jdbcTemplate工具類。
所有上面這些擴充套件將極大程度地簡化在測試時對於相關操作的依賴性注入和事務管理。

三、普通Web測試情形

在此,我們將回顧測試Web元件的普通情形以及怎樣在其中使用Spring的mock物件和JUnit框架擴充套件。

(一)確定一個正確的檢視

基於輸入引數生成正確的檢視可能是在操作一個Web應用程式時最普通的功能。在Spring MVC的上下文中,這意味著Spring MVC將基於引數的狀態返回某種ModelAndView物件。你可以通過簡單地利用如下的Mock物件以一個常規JUnit測試方式來測試這項功能:

public void final testGettingToDetails throws Exception{ MyController myController = new MyController(); myController.setDetailsView( detailsViewName ); MockHttpServletRequest request = new MockHttpServletRequest(); MockHttpServletResponse response = new MockHttpServletResponse(); request.setMethod("POST"); request.addParameter("viewDetails", "true"); ModelAndView modelAndView = myController.handleRequest(request, response); assertEquals("Incorrect view name", detailsViewName,modelAndView.getViewName());

既然控制器很可能會利用一些服務物件來決定結果檢視,那麼你還可以定製控制器中所用的這些mock服務物件。關於利用定製物件的更多資料,請參考mockobjects.com。

(二)會話相關的操作

對於任何J2EE Web應用程式來說,另一個必須實現的操作是HttpSession繫結處理。例如,Spring MVC可能需要決定是否一個物件處於會話中及其具體狀態以便產生正確的結果。你可以利用MockHttpSession物件和JUnit框架測試這種情形。請參考如下的程式碼片斷:

public void testInvokesCorrectMethodWithSession() throws Exception { TestController cont = new TestController(); MockHttpServletRequest request = new MockHttpServletRequest(

"GET", "/invoiceView.app"); request.setSession(new MockHttpSession(null)); HttpServletResponse response = new MockHttpServletResponse(); ModelAndView mv = cont.handleRequest(request, response); assertTrue("Invoked loggedIn method", cont.wasInvoked("loggedIn")); assertTrue("view name is ",mv.getViewName().equals("loggedIn")); assertTrue("Only one method invoked", cont.getInvokedMethods() == 1); //測試控制器但是不使用會話 request = new MockHttpServletRequest("GET", "/invoiceView.app"); response = new MockHttpServletResponse(); try { cont.handleRequest(request, response); fail("Should have rejected request without session"); } catch (ServletException ex) { //在此加入期盼的異常處理 } }

(三)轉發和重定向

一個Spring MVC元件執行的操作能夠導致轉發或重定向到另一個URL。如果你的目標是分析轉發或重定向的結果,那麼你可以測試這一情形—通過分析MockHttpResponse物件並進而確定有哪些內容包含在它的重定向或轉發值中,如下所示:

String responseString = ((MockHttpServletResponse)httpResponse).getForwardedUrl();

assertEquals( "Did not forward to the expected URL", responseString, expectedString);

四、生成正確的二進位制輸出

如何確定你有多少次必須實現“View as PDF”這一功能?下面的JUnit程式碼片斷使用mock輸出流物件實現這一功能的正確測試:

public void testPDFGeneration() throws Exception{

MockHttpServletRequest request = new MockHttpServletRequest();

MockHttpServletResponse response = new MockHttpServletResponse();

viewInvoiceAsPDFController.handleRequest( request, response ); 

byte[] responsePDFValues = response.getContentAsByteArray();

byte[] expectedPDFValues = loadBytesFromTestFile();

assertTrue( "Did not generate expected PDF content.", 

Arrays.equals(responsePDFValues,expectedPDFValues ));

}

注意,在此你的控制器ViewInvoiceAsPDFController不是返回ModelAndView物件,而是產生了二進位制輸出—你可以使用一個二進位制的陣列形式來捕獲此控制器並對此進行正確性評價。

五、事務性單元測試

到目前為止,你已看到了相對簡單的JUnit測試—它僅發生在用mock物件支援的一個控制器的上下文中。但是,如果測試一個Web元件只有在一個事務性上下文(例如,通過依賴性注入與Hibernate整合到一起)中才有意義的情況又會怎麼樣呢?不必擔心,Spring MVC為JUnit框架提供了一個體面的擴充套件集合—它能準確地提供依賴性注入和事務安全測試(也就是,任何更新在測試完成後都將被回滾)。

測試步驟:

讓我們看一種假想的情形—你要實現一個元件(例如MyTransactionalController)測試,該元件執行在一個事務性的上下文中(也即,其方法呼叫的結果發生在一個事務內並且它應該在測試執行完後被回滾):

1.建立一個定製的JUnit類(MyTransactionalControllerTest),它擴充套件了Spring的JUnit擴充套件類

AbstractTransactionalSpringContextTests:

import org.springframework.test.AbstractDependencyInjectionSpringContextTests;

public class MyTransactualControllerTest extends

AbstractTransactionalSpringContextTests {

public class.

2.為了實現從Spring內建的單元測試中發現Spring管理的bean,你需要過載getConfigLocations()方法並且返回上下文檔案位置的String陣列,請看如下:

protected abstract String[] getConfigLocations(){

return new String[] {"classpath:/test/spring-context.xml"};

}

3.擁有該類的一個測試屬性及其相關聯的getter和setter。由於AbstractTransactionalSpringContextTests利用了auto-wiring(這是Spring框架的一個特性—能夠根據類屬性的名字識別類依賴性並且用Spring bean填入相匹配的名字或ID)技術而且在測試時它將自動地解決類的依賴性問題,所以在Spring上下文檔案中該類屬性具有與Spring管理的bean一樣的名字並且在測試時每個屬性都有一個適當命名的setter:

public MyTransactualController myTransactualController;

/**

* @返回myTransactualController。

*/

public MyTransactualController getMyTransactualController() {

return this.myTransactualController;

}

/**

*@引數myTransactualController。

*/

public void setMyTransactualController(

MyTransactualController myTransactualController) {

this.myTransactualController = myTransactualController;

}

4.就象你通常操作“普通的”JUnit測試一樣實現測試方法:

public void testCorrectBehavior() throws Exception{ //執行該事務性方法 myTransactualController.submitPayment( new Payment( 100 ) ); assertTrue( myTransactualController.isValid() ); }

注意,你是在呼叫可能會更新資料庫的方法submitPayment。Spring的JUnit擴充套件(AbstractTransactionalSpringContextTests)將在這個測試方法結束後實現自動回滾。

5.如果你需要執行任何安裝或清除任務,則可以過載AbstractTransactionalSpringContextTests的onSetUpBeforeTransaction()或onSetUpInTransaction()方法。AbstractTransactionalSpringContextTests將過載從TestCase繼承來的setUp()和tearDown()方法並且使其成為final型別。

六、小結

至此,你已經學習瞭如何使用Spring單元測試框架和Web元件mock物件。通過使用這兩個工具,你將會極大地提高你的Web元件的開發效率。