Spring Framework 3.2 RC1:全新測試功能

工程 | Sam Brannen | 2012 年 11 月 07 日 | ...

如同 Juergen Hoeller 在他發布 Spring Framework 3.2 RC1 發布 的文章中提到的,Spring 團隊在測試支援方面引入了一些令人興奮的新功能。最重要的是,我們為測試網路應用程式新增了第一級的支援。[1]

      請注意:這是一篇來自我的 交叉貼文,源自我公司 Swiftmind 的部落格。

在這篇文章中,我們將首先看看 Spring Framework 中一些通用的新測試功能,然後我們將詳細介紹使用 WebApplicationContext 以及請求會話範圍 bean 進行測試的支援。最後,我們將看看對 ApplicationContextInitializers 的支援,並簡要討論應用程式上下文階層測試的路線圖。

Rossen Stoyanchev 稍後將發布一篇詳細的文章,介紹新的 Spring MVC Test 框架,該框架為測試 Spring MVC 應用程式提供第一級的支援。因此,請務必持續關注,因為它建立在本文稍後討論的基本網路測試支援之上。



通用新功能與更新


建置與相依性

spring-test 模組現在針對並支援 JUnit 4.10 和 TestNG 6.5.2 進行建置,並且 spring-test 現在相依於 junit:junit-dep Maven 構件,而不是 junit:junit,這表示您可以完全控制您對 Hamcrest 函式庫的相依性(例如,hamcrest-corehamcrest-all 等)。

泛型工廠方法

泛型工廠方法是使用 Java 泛型實作 工廠方法設計模式 的方法。以下是一些泛型工廠方法的範例簽章


public static <T> T mock(Class<T> clazz) { ... }

public static <T> T proxy(T obj) { ... }

在 Spring 配置中使用泛型工廠方法絕非專用於測試,但諸如 EasyMock.createMock(MyService.class)Mockito.mock(MyService.class) 之類的泛型工廠方法通常用於在測試應用程式上下文中為 Spring bean 建立動態 mock 物件。例如,在 Spring Framework 3.2 之前,以下配置可能無法將 OrderRepository 自動注入到 OrderService 中。原因是,根據 bean 在應用程式上下文中初始化的順序,Spring 可能會將 orderRepository bean 的類型推斷為 java.lang.Object 而不是 com.example.repository.OrderRepository


<beans>

  <!-- OrderService is autowired with OrderRepository -->
  <context:component-scan base-package="com.example.service"/>

  <bean id="orderRepository" class="org.easymock.EasyMock"
      factory-method="createMock"
      c:_="com.example.repository.OrderRepository" />

</beans>

在 Spring 3.2 中,現在可以正確推斷工廠方法的泛型回傳類型,並且 mock 物件的依類型自動注入應該可以如預期般運作。因此,諸如 MockitoFactoryBeanEasyMockFactoryBeanSpringockito 之類的自訂解決方案可能不再必要。

Mock 物件

我們引入了 MockEnvironment,它補充了現有的 MockPropertySource,以完成對 Spring 3.1 中引入的環境和屬性來源抽象的 mock 支援。

關於網路元件的單元測試支援,我們為現有的 Servlet API mock 物件新增了功能,例如 MockServletContextMockHttpSessionMockFilterChainMockRequestDispatcher,並且我們引入了與 REST Web Services 相關的新 mock 物件:用戶端方面的 MockClientHttpRequestMockClientHttpResponse,以及伺服器端方面的 MockHttpInputMessageMockHttpOutputMessage

JDBC 測試支援

在 Spring 3.2 中,我們棄用了 SimpleJdbcTestUtils,改用改進後的 JdbcTestUtils 類別,除了 SimpleJdbcTestUtils 之前提供的所有功能外,它還提供了新的 countRowsInTableWhere()dropTables() 實用方法。這些變更有助於避免與使用已棄用的 SimpleJdbcTemplate 相關的編譯器警告,並提供一種方便的方法來計算表格中使用 WHERE 子句的列數,以及刪除表格清單。同樣地,AbstractTransactionalJUnit4SpringContextTestsAbstractTransactionalTestNGSpringContextTests 也已回溯配備了 jdbcTemplate 實例變數,以及委派給 JdbcTestUtils 中對應項目的 countRowsInTableWhere()dropTables() 方法。

交易管理器配置

如果您熟悉 Spring TestContext Framework 中對交易整合測試的支援,那麼您可能知道用於測試的交易管理器必須按照慣例稱為 "transactionManager"。自 Spring 2.5 以來,這可以透過 @TransactionConfiguration 註解覆寫(例如,@TransactionConfiguration(transactionManager="txMgr"));但是,如果應用程式上下文中存在單個 PlatformTransactionManger,則不再需要使用此註解。換句話說,只要上下文中只定義了一個交易管理器,就不再需要限定該交易管理器的名稱是什麼:如果只有一個,TestContext 框架將直接使用它。

Spring 3.1 引入了 TransactionManagementConfigurer 介面,用於在使用 @Configuration 類別與 @EnableTransactionManagement 結合使用時(即,與使用 XML 配置 <tx:annotation-driven /> 相反),以程式設計方式指定與 @Transactional 方法一起使用的交易管理器。因此,從 Spring 3.2 開始,如果您的元件之一(即,通常是 @Configuration 類別)實作了 TransactionManagementConfigurer,則 TestContext 框架將使用該元件指定的交易管理器。



Spring TestContext Framework


這篇文章的其餘部分明確討論了 Spring TestContext Framework 中的新功能。如果您已經熟悉 TestContext 框架,請隨時跳到下一節。否則,您可能需要先熟悉以下段落中連結提供的資訊。

在 Spring 2.5 中,我們引入了 Spring TestContext Framework,它提供了基於註解驅動的整合測試支援,可用於 JUnit 或 TestNG。這篇文章中的範例將重點放在基於 JUnit 的測試上,但是這裡使用的所有功能也適用於 TestNG。

在 Spring 3.1 中,我們修訂了 Spring TestContext Framework,新增了對 使用 @Configuration 類別和環境配置檔進行測試 的支援。



載入 WebApplicationContext


  • 問題:您如何告訴 TestContext 框架載入 WebApplicationContext
  • 答案:只需使用 @WebAppConfiguration 註解您的測試類別即可。

實際上就是這麼簡單。測試類別上 @WebAppConfiguration 的存在指示 TestContext 框架 (TCF) 應該為您的整合測試載入 WebApplicationContext (WAC)。在幕後,TCF 確保建立 MockServletContext 並提供給您測試的 WAC。預設情況下,您的 MockServletContext 的基本資源路徑將設定為 "src/main/webapp"。這被解釋為相對於 JVM 根目錄的路徑(即,通常是專案的路徑)。如果您熟悉 Maven 專案中網路應用程式的目錄結構,您就會知道 "src/main/webapp" 是 WAR 根目錄的預設位置。如果您需要覆寫此預設值,只需為 @WebAppConfiguration 註解提供替代路徑(例如,@WebAppConfiguration("src/test/webapp"))。如果您希望從類別路徑而不是檔案系統參考基本資源路徑,只需使用 Spring 的 classpath: 前綴。

請注意,Spring 對 WebApplicationContexts 的測試支援與其對標準 ApplicationContexts 的支援相當。當使用 WebApplicationContext 進行測試時,您可以透過 @ContextConfiguration 宣告 XML 配置檔案或 @Configuration 類別。您當然也可以自由使用任何其他測試註解,例如 @TestExecutionListeners@TransactionConfiguration@ActiveProfiles 等。

讓我們看一些範例...

慣例


@RunWith(SpringJUnit4ClassRunner.class)

// defaults to "file:src/main/webapp"
@WebAppConfiguration

// detects "WacTests-context.xml" in same package
// or static nested @Configuration class
@ContextConfiguration

public class WacTests {
	//...
}

上面的範例示範了 TestContext 框架對慣例優於配置的支援。如果您使用 @WebAppConfiguration 註解測試類別,而未指定資源基本路徑,則資源路徑將有效地預設為 "file:src/main/webapp"。同樣地,如果您宣告 @ContextConfiguration 而未指定資源 locations、註解的 classes 或上下文 initializers,Spring 將嘗試使用慣例偵測配置的存在(即,與 WacTests 類別或靜態巢狀 @Configuration 類別位於同一套件中的 "WacTests-context.xml")。

預設資源語意


@RunWith(SpringJUnit4ClassRunner.class)

// file system resource
@WebAppConfiguration("webapp")

// classpath resource
@ContextConfiguration("/spring/test-servlet-config.xml")

public class WacTests {
	//...
}

此範例示範了如何使用 @WebAppConfiguration 明確宣告資源基本路徑,以及使用 @ContextConfiguration 宣告 XML 資源位置。這裡需要注意的重要事項是這兩個註解的路徑具有不同的語意。預設情況下,@WebAppConfiguration 資源路徑是基於檔案系統的;而 @ContextConfiguration 資源位置是基於類別路徑的。

明確資源語意


@RunWith(SpringJUnit4ClassRunner.class)

// classpath resource
@WebAppConfiguration("classpath:test-web-resources")

// file system resource
@ContextConfiguration(
    "file:src/main/webapp/WEB-INF/servlet-config.xml")

public class WacTests {
	//...
}

在第三個範例中,我們看到我們可以透過指定 Spring 資源前綴來覆寫這兩個註解的預設資源語意。請將此範例中的註解與上一個範例進行比較。



使用網路 Mock 物件


為了提供全面的網路測試支援,Spring 3.2 引入了一個新的 ServletTestExecutionListener,預設情況下已啟用。當針對 WebApplicationContext 進行測試時,此 TestExecutionListener 在每個測試方法之前,透過 Spring Web 的 RequestContextHolder 設定預設的執行緒區域狀態,並根據透過 @WebAppConfiguration 配置的基本資源路徑建立 MockHttpServletRequestMockHttpServletResponseServletWebRequestServletTestExecutionListener 還確保 MockHttpServletResponseServletWebRequest 可以注入到測試實例中,並且一旦測試完成,它會清理執行緒區域狀態。

一旦您為測試載入了 WebApplicationContext,您可能會發現需要與網路 mock 物件互動,例如,設定您的測試夾具或在調用您的網路元件後執行斷言。以下範例示範了哪些 mock 物件可以自動注入到您的測試實例中。請注意,WebApplicationContextMockServletContext 都會在整個測試套件中快取;而其他 mock 物件則由 ServletTestExecutionListener 針對每個測試方法進行管理。

注入 Mock 物件


@WebAppConfiguration
@ContextConfiguration
public class WacTests {
	
	@Autowired WebApplicationContext wac; // cached
	
	@Autowired MockServletContext servletContext; // cached
	
	@Autowired MockHttpSession session;
	
	@Autowired MockHttpServletRequest request;
	
	@Autowired MockHttpServletResponse response;
	
	@Autowired ServletWebRequest webRequest;
	
	//...
}


請求和會話範圍 Bean


請求和會話範圍 bean 已被 Spring 支援多年,但測試它們一直有點棘手。從 Spring 3.2 開始,現在可以輕鬆地測試您的請求範圍和會話範圍 bean,只需遵循以下步驟

  1. 透過使用 @WebAppConfiguration 註解您的測試類別,確保為您的測試載入 WebApplicationContext
  2. 將 mock 請求或會話注入到您的測試實例中,並根據需要準備您的測試夾具。
  3. 調用您從配置的 WebApplicationContext 檢索的網路元件(即,透過依賴注入)。
  4. 針對 mock 物件執行斷言。

以下程式碼片段顯示了登入用例的 XML 配置。請注意,userService bean 相依於請求範圍的 loginAction bean。此外,LoginAction 是使用 SpEL 表達式 實例化的,該表達式從當前的 HTTP 請求中檢索使用者名稱和密碼。在我們的測試中,我們希望透過 TestContext 框架管理的 mock 物件配置這些請求參數。

請求範圍 Bean 配置


<beans>

  <bean id="userService"
      class="com.example.SimpleUserService"
      c:loginAction-ref="loginAction" />

  <bean id="loginAction" class="com.example.LoginAction"
      c:username="#{request.getParameter('user')}"
      c:password="#{request.getParameter('pswd')}"
      scope="request">
    <aop:scoped-proxy />
  </bean>
	
</beans>

RequestScopedBeanTests 中,我們將 UserService(即,被測試對象)和 MockHttpServletRequest 都注入到我們的測試實例中。在我們的 requestScope() 測試方法中,我們透過在提供的 MockHttpServletRequest 中設定請求參數來設定我們的測試夾具。當在我們的 userService 上調用 loginUser() 方法時,我們可以確保使用者服務可以存取當前 MockHttpServletRequest 的請求範圍 loginAction(即,我們剛剛在其中設定參數的那個)。然後,我們可以根據使用者名稱和密碼的已知輸入,針對結果執行斷言。

請求範圍 Bean 測試


@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WebAppConfiguration
public class RequestScopedBeanTests {
	
	@Autowired UserService userService;
	@Autowired MockHttpServletRequest request;
	
	@Test
	public void requestScope() {
		
		request.setParameter("user", "enigma");
		request.setParameter("pswd", "$pr!ng");
		
		LoginResults results = userService.loginUser();
		
		// assert results
	}
}

以下程式碼片段與我們上面看到的請求範圍 bean 的程式碼片段類似;但是,這次 userService bean 相依於會話範圍的 userPreferences bean。請注意,UserPreferences bean 是使用 SpEL 表達式實例化的,該表達式從當前的 HTTP 會話中檢索 theme。在我們的測試中,我們需要在 TestContext 框架管理的 mock 會話中配置主題。

會話範圍 Bean 配置


<beans>

  <bean id="userService"
      class="com.example.SimpleUserService"
      c:userPreferences-ref="userPreferences" />

  <bean id="userPreferences"
      class="com.example.UserPreferences"
      c:theme="#{session.getAttribute('theme')}"
      scope="session">
    <aop:scoped-proxy />
  </bean>

</beans>

SessionScopedBeanTests 中,我們將 UserServiceMockHttpSession 注入到我們的測試實例中。在我們的 sessionScope() 測試方法中,我們透過在提供的 MockHttpSession 中設定預期的 "theme" 屬性來設定我們的測試夾具。當在我們的 userService 上調用 processUserPreferences() 方法時,我們可以確保使用者服務可以存取當前 MockHttpSession 的會話範圍 userPreferences,並且我們可以根據配置的主題針對結果執行斷言。

會話範圍 Bean 測試


@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WebAppConfiguration
public class SessionScopedBeanTests {

  @Autowired UserService userService;
  @Autowired MockHttpSession session;

  @Test
  public void sessionScope() throws Exception {

    session.setAttribute("theme", "blue");

    Results results = userService.processUserPreferences();

    // assert results
  }
}


應用程式上下文初始化器


Spring 3.1 引入了 ApplicationContextInitializer 介面,允許以程式設計方式初始化 ConfigurableApplicationContext,例如,針對 Spring Environment 抽象註冊屬性來源或啟動 bean 定義配置檔。初始化器可以在 web.xml 中配置,方法是透過 ContextLoaderListenercontext-paramDispatcherServletinit-param 指定 contextInitializerClasses

若要在整合測試中使用上下文初始化器,只需透過 Spring 3.2 中引入的新 initializers 屬性在 @ContextConfiguration 中宣告初始化器類別。跨測試類別階層的初始化器繼承可以透過 inheritInitializers 屬性控制,預設值為 true。由於 ApplicationContextInitializer 提供了完全以程式設計方式初始化應用程式上下文的方法,因此初始化器可以選擇性地配置整個上下文。換句話說,如果已宣告初始化器,則在透過 @ContextConfiguration 配置的整合測試中,不再絕對需要 XML 資源位置或註解的類別。最後但並非最不重要的一點是,上下文初始化器根據 Spring 的 Ordered 介面或 @Order 註解進行排序

以下程式碼範例示範了在整合測試中使用上下文初始化器的各種方式。第一個範例顯示了如何結合 XML 資源位置配置單個初始化器。下一個範例宣告了多個上下文初始化器。第三個範例示範了在類別階層中使用初始化器,其中在 ExtendedTest 中宣告的上下文初始化器清單將與在 BaseTest 中宣告的清單合併。回想一下,初始化器的調用順序受到 Spring 的 Ordered 介面實作或 @Order 註解的存在的影響。第四個範例與第三個範例相同,只是 @ContextConfiguration 中的 inheritInitializers 屬性已設定為 false。結果是,父類別中宣告的任何上下文初始化器都將被忽略(即,覆寫)。最後一個範例示範了可以僅從上下文初始化器載入 ApplicationContext,而無需宣告 XML 資源位置或註解的類別。

單個初始化器


@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
    locations = "/app-config.xml",
    initializers = CustomInitializer.class)
public class ApplicationContextInitializerTests {}

多個初始化器


@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
  locations = "/app-config.xml",
  initializers = {
    PropertySourceInitializer.class,
    ProfileInitializer.class
  })
public class ApplicationContextInitializerTests {}

合併的初始化器


@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
    classes = BaseConfig.class,
    initializers = BaseInitializer.class)
public class BaseTest {}


@ContextConfiguration(
    classes = ExtendedConfig.class,
    initializers = ExtendedInitializer.class)
public class ExtendedTest extends BaseTest {}

覆寫的初始化器


@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
    classes = BaseConfig.class,
    initializers = BaseInitializer.class)
public class BaseTest {}


@ContextConfiguration(
    classes = ExtendedConfig.class,
    initializers = ExtendedInitializer.class,
    inheritInitializers = false)
public class ExtendedTest extends BaseTest {}

沒有資源的初始化器


// does not declare 'locations' or 'classes'
@ContextConfiguration(
    initializers = EntireAppInitializer.class)
public class InitializerWithoutConfigFilesOrClassesTest {}


上下文快取


一旦 TestContext 框架為測試載入了 ApplicationContext,該上下文將被快取並重複用於所有後續在同一測試套件中宣告相同唯一上下文配置的測試。這裡要記住的重要事項是,ApplicationContext 由其上下文快取鍵唯一識別(即,用於載入它的配置參數的組合)。

從 Spring 3.2 開始,ApplicationContextInitializer 類別也包含在上下文快取鍵中。此外,如果上下文是 WebApplicationContext,則其基本資源路徑(透過 @WebAppConfiguration 定義)也將包含在上下文快取鍵中。有關快取的更多詳細資訊,請查閱參考手冊的 上下文快取 節。



應用程式上下文階層


注意截至 Spring Framework 3.2 RC1,對上下文階層的支援尚未實作。

在由 Spring TestContext Framework 管理的整合測試中,目前僅支援平面、非階層式上下文。換句話說,沒有簡單的方法可以為測試建立具有父子關係的上下文。但是,生產部署中支援上下文階層。因此,如果能夠測試它們會很好。

考慮到這一點,Spring 團隊希望引入整合測試支援,以載入具有父上下文的測試應用程式上下文,理想情況下,應支援以下常見階層。

  • WebApplicationContext ← Dispatcher WebApplicationContext
  • EAR ← 根 WebApplicationContext ← Dispatcher WebApplicationContext

目前的提案包括引入新的 @ContextHierarchy 註解,其中包含巢狀 @ContextConfiguration 宣告,以及 @ContextConfiguration 中的新 name 屬性,可用於合併覆寫上下文階層中的命名配置。

為了闡明該提案,讓我們看一些範例...

AppCtxHierarchyTests 示範了在單個測試類別中宣告的父子上下文階層,其中上下文是標準上下文(即,非網路)。

具有上下文階層的單個測試


@RunWith(SpringJUnit4ClassRunner.class)

@ContextHierarchy({
	@ContextConfiguration("parent.xml"),
	@ContextConfiguration("child.xml")
})
public class AppCtxHierarchyTests {}

ControllerIntegrationTests 示範了在單個測試類別中宣告的父子上下文階層,其中上下文是 WebApplicationContexts,並模擬典型的 Spring MVC 部署。

根 WAC & Dispatcher WAC


@RunWith(SpringJUnit4ClassRunner.class)

@WebAppConfiguration

@ContextHierarchy({
    @ContextConfiguration(
		name = "root",
		classes = WebAppConfig.class),
    @ContextConfiguration(
		name = "dispatcher",
		locations = "/spring/dispatcher-config.xml")
})
public class ControllerIntegrationTests {}

以下程式碼清單示範了如何在測試類別階層中建立上下文階層,其中測試類別階層中的每個層級都負責配置其自己在上下文階層中的層級。在這些子類別中執行測試將導致載入(和快取)三個應用程式上下文和兩個不同的上下文階層。

類別與上下文階層


@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(
  "file:src/main/webapp/WEB-INF/applicationContext.xml")
public abstract class AbstractWebTests{}

@ContextHierarchy(@ContextConfiguration("/spring/soap-ws-config.xml"))
public class SoapWebServiceTests extends AbstractWebTests{}

@ContextHierarchy(@ContextConfiguration("/spring/rest-ws-config.xml"))
public class RestWebServiceTests extends AbstractWebTests{}

歡迎提供意見回饋

如果您有興趣了解有關上下文階層提案的更多資訊,或想參與討論,請隨時關注以下 JIRA 問題並向我們提供您的意見回饋。



總結


Spring Framework 3.2 引入了多項新的測試功能,重點是為測試網路應用程式提供第一級的支援。我們鼓勵您盡快試用這些功能並向我們提供意見回饋。此外,請繼續關注 Rossen Stoyanchev 關於新的 Spring MVC Test 框架的後續文章。如果您發現任何錯誤或對改進有任何建議,現在是 採取行動 的時候了!



[1] 參考手冊尚未更新以反映對網路應用程式的測試支援,但這些功能肯定會在 Spring 3.2 GA 中得到充分記錄。



取得 Spring 電子報

隨時掌握 Spring 電子報的最新資訊

訂閱

領先一步

VMware 提供培訓和認證,以加速您的進展。

瞭解更多

取得支援

Tanzu Spring 在一個簡單的訂閱中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位檔案。

瞭解更多

即將到來的活動

查看 Spring 社群中所有即將到來的活動。

檢視全部