Spring 3.1 M2:使用 @Configuration 類和 Profiles 進行測試

工程 | Sam Brannen | 2011 年 6 月 21 日 | ...

正如 Jürgen Höller 在宣佈 Spring 3.1 M2 釋出的帖子中提到的那樣,Spring TestContext Framework(*) 已經過全面改進,為 @Configuration 類和環境 Profile 提供了第一等的測試支援。

在本文中,我將首先向您介紹一些示例,演示這些新的測試特性。然後,我將介紹 TestContext 框架中使這些新特性成為可能的一些新的擴充套件點。

      請注意:這是我公司部落格 www.swiftmind.com 的交叉釋出文章。

背景

在 Spring 2.5 中,我們引入了 Spring TestContext Framework,它提供了註解驅動的整合測試支援,可與 JUnit 或 TestNG 一起使用。本部落格中的示例將側重於基於 JUnit 的測試,但此處使用的所有特性也適用於 TestNG。

TestContext 框架的核心是允許您使用 @ContextConfiguration 註解測試類,以指定用於為測試載入 ApplicationContext 的配置檔案。預設情況下,ApplicationContext 使用 GenericXmlContextLoader 載入,後者從 XML Spring 配置檔案載入上下文。然後,您可以透過使用 @Autowired@Resource@Inject 註解測試類中的欄位來訪問 ApplicationContext 中的 bean。

Spring 3.0 透過 @Configuration 類引入了基於 Java 的配置支援,但 TestContext 框架直到現在才提供適當的 ContextLoader 來支援測試中的 @Configuration 類。Spring 3.1 M2 為此引入了一個新的 AnnotationConfigContextLoader,並且 @ContextConfiguration 註解已更新,透過新的 classes 屬性支援宣告 @Configuration 類。

現在讓我們看一些示例。

使用基於 XML 的配置進行整合測試

Spring 參考手冊的測試章節提供了許多如何使用 XML 配置檔案配置整合測試的示例,但我們在此包含一個示例作為快速介紹。

如果您已經熟悉 Spring TestContext Framework,請隨意跳到下一節。


<?xml version="1.0" encoding="UTF-8"?>
<beans ...>

    <!-- this bean will be injected into the OrderServiceTest class -->
    <bean id="orderService" class="com.example.OrderServiceImpl">
        <!-- set properties, etc. -->
    </bean>
    
    <!-- other beans -->

</beans>

package com.example;

@RunWith(SpringJUnit4ClassRunner.class)
// ApplicationContext will be loaded from "classpath:/com/example/OrderServiceTest-context.xml"
@ContextConfiguration
public class OrderServiceTest {

    @Autowired
    private OrderService orderService;

    @Test
    public void testOrderService() {
        // test the orderService
    }
}

在前面的示例中,我們配置 JUnit 使用 SpringJUnit4ClassRunner 來執行我們的測試。我們透過使用 JUnit 的 @RunWith 註解來實現這一點。我們還使用 Spring 的 @ContextConfiguration 註解來註解我們的測試類,而沒有指定任何屬性。在這種情況下,將使用預設的 GenericXmlContextLoader,並且遵循約定優於配置的原則,Spring 將從 classpath:/com/example/OrderServiceTest-context.xml 載入我們的 ApplicationContext。在 testOrderService() 方法中,我們可以直接測試使用 @Autowired 注入到測試例項中的 OrderService。請注意,orderServiceOrderServiceTest-context.xml 中被定義為一個 bean。

使用 @Configuration 類進行整合測試

Spring 3.1 M2 對使用 @Configuration 類進行整合測試的支援類似於上面的基於 XML 的示例。因此,讓我們重新調整該示例,使用 @Configuration 類和新的 AnnotationConfigContextLoader


package com.example;

@RunWith(SpringJUnit4ClassRunner.class)
// ApplicationContext will be loaded from the static inner ContextConfiguration class
@ContextConfiguration(loader=AnnotationConfigContextLoader.class)
public class OrderServiceTest {

    @Configuration
    static class ContextConfiguration {

        // this bean will be injected into the OrderServiceTest class
        @Bean
        public OrderService orderService() {
            OrderService orderService = new OrderServiceImpl();
            // set properties, etc.
            return orderService;
        }
    }

    @Autowired
    private OrderService orderService;

    @Test
    public void testOrderService() {
        // test the orderService
    }
}

這個示例與基於 XML 的示例之間有一些顯著的區別

  1. 沒有 XML 檔案。
  2. Bean 定義已使用靜態內部類 ContextConfiguration 中的 @Configuration@Bean 從 XML 轉換為 Java。
  3. AnnotationConfigContextLoader 已透過 @ContextConfigurationloader 屬性指定。

除此之外,測試的配置和實現保持不變。

那麼,Spring 是如何知道使用靜態內部類 ContextConfiguration 來載入 ApplicationContext 的呢?答案是約定優於配置。預設情況下,如果沒有顯式宣告類,AnnotationConfigContextLoader 將查詢測試類中名為 ContextConfiguration 的靜態內部類。根據 @Configuration 類的要求,此靜態內部類必須是非 final 和非 private 的。

注意:從 Spring 3.1 M2 開始,預設配置類必須精確命名為 ContextConfiguration。然而,從 Spring 3.1 RC1 開始,命名限制已經解除。換句話說,從 RC1 開始,您可以選擇任意命名您的預設配置類,但其他要求仍然適用。

在下面的示例中,我們將看到如何宣告顯式配置類。


package com.example;

@Configuration
public class OrderServiceConfig {

    // this bean will be injected into the OrderServiceTest class
    @Bean
    public OrderService orderService() {
        OrderService orderService = new OrderServiceImpl();
        // set properties, etc.
        return orderService;
    }
}

package com.example;

@RunWith(SpringJUnit4ClassRunner.class)
// ApplicationContext will be loaded from the OrderServiceConfig class
@ContextConfiguration(classes=OrderServiceConfig.class, loader=AnnotationConfigContextLoader.class)
public class OrderServiceTest {

    @Autowired
    private OrderService orderService;

    @Test
    public void testOrderService() {
        // test the orderService
    }
}

我們現在已將靜態內部類 ContextConfiguration 提取到一個名為 OrderServiceConfig 的頂級類中。要指示 AnnotationConfigContextLoader 使用此配置類而不是依賴預設配置,我們只需透過 @ContextConfiguration 的新 classes 屬性宣告 OrderServiceConfig.class。與 @ContextConfiguration 用於資源位置的 locations 屬性一樣,我們可以透過向 classes 屬性提供一個 Class[] 陣列來宣告多個配置類 — 例如:@ContextConfiguration(classes={Config1.class, Config2.class}, ... )

關於使用 @Configuration 類進行整合測試的介紹到此結束。現在讓我們看看 Spring 對環境 profile 的測試支援。

使用環境 Profile 進行整合測試

正如 Chris Beams 在 Spring 3.1 M1 的釋出公告及其後續部落格 Introducing @Profile 中討論的那樣,Spring 3.1 在框架中引入了對環境和 Profile(又稱 bean 定義 Profile)概念的一流支援。從 Spring 3.1 M2 開始,還可以配置整合測試,以針對各種測試場景啟用特定的 bean 定義 Profile。這可以透過使用新的 @ActiveProfiles 註解來註解測試類,並提供在為測試載入 ApplicationContext 時應啟用的 Profile 列表來實現。

注意:@ActiveProfiles 可以與新的 SmartContextLoader SPI 的任何實現一起使用(參見後面的討論),但 @ActiveProfiles 支援更簡單的 ContextLoader SPI 的實現。

讓我們來看一些使用 XML 配置和 @Configuration 類的示例。


<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:jdbc="http://www.springframework.org/schema/jdbc"
	xmlns:jee="http://www.springframework.org/schema/jee"
	xsi:schemaLocation="...">

	<bean id="transferService" class="com.bank.service.internal.DefaultTransferService">
		<constructor-arg ref="accountRepository"/>
		<constructor-arg ref="feePolicy"/>
	</bean>

	<bean id="accountRepository" class="com.bank.repository.internal.JdbcAccountRepository">
		<constructor-arg ref="dataSource"/>
	</bean>

	<bean id="feePolicy" class="com.bank.service.internal.ZeroFeePolicy"/>

	<beans profile="dev">
		<jdbc:embedded-database id="dataSource">
			<jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
			<jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
		</jdbc:embedded-database>
	</beans>

	<beans profile="production">
		<jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
	</beans>

</beans>

package com.bank.service;

@RunWith(SpringJUnit4ClassRunner.class)
// ApplicationContext will be loaded from "classpath:/app-config.xml"
@ContextConfiguration("/app-config.xml")
@ActiveProfiles("dev")
public class TransferServiceTest {

    @Autowired
    private TransferService transferService;

    @Test
    public void testTransferService() {
        // test the transferService
    }
}

執行 TransferServiceTest 時,其 ApplicationContext 將從類路徑根目錄下的 app-config.xml 配置檔案載入。如果您檢查 app-config.xml,您會注意到 accountRepository bean 依賴於一個 dataSource bean;然而,dataSource 沒有被定義為一個頂級 bean。相反,dataSource 定義了兩次:一次在 production Profile 中,一次在 dev Profile 中。

透過使用 @ActiveProfiles("dev") 註解 TransferServiceTest,我們指示 Spring TestContext Framework 載入 ApplicationContext,並將其活動 Profile 設定為 {"dev"}。結果,將建立一個嵌入式資料庫,並且 accountRepository bean 將被連線到開發 DataSource。這很可能就是我們在整合測試中想要的!

以下程式碼清單演示瞭如何實現相同的配置和整合測試,但使用 @Configuration 類而不是 XML。


@Configuration
@Profile("dev")
public class StandaloneDataConfig {

	@Bean
	public DataSource dataSource() {
		return new EmbeddedDatabaseBuilder()
			.setType(EmbeddedDatabaseType.HSQL)
			.addScript("classpath:com/bank/config/sql/schema.sql")
			.addScript("classpath:com/bank/config/sql/test-data.sql")
			.build();
	}
}

@Configuration
@Profile("production")
public class JndiDataConfig {

	@Bean
	public DataSource dataSource() throws Exception {
		Context ctx = new InitialContext();
		return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
	}
}

@Configuration
public class TransferServiceConfig {

	@Autowired DataSource dataSource;

	@Bean
	public TransferService transferService() {
		return new DefaultTransferService(accountRepository(), feePolicy());
	}

	@Bean
	public AccountRepository accountRepository() {
		return new JdbcAccountRepository(dataSource);
	}

	@Bean
	public FeePolicy feePolicy() {
		return new ZeroFeePolicy();
	}
}

package com.bank.service;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader=AnnotationConfigContextLoader.class,
    classes={TransferServiceConfig.class, StandaloneDataConfig.class, JndiDataConfig.class})
@ActiveProfiles("dev")
public class TransferServiceTest {

    @Autowired
    private TransferService transferService;

    @Test
    public void testTransferService() {
        // test the transferService
    }
}

在此變體中,我們將 XML 配置拆分為三個獨立的 @Configuration

  • TransferServiceConfig:使用 @Autowired 透過依賴注入獲取 dataSource
  • StandaloneDataConfig:定義一個適用於開發者測試的嵌入式資料庫的 dataSource
  • JndiDataConfig:定義一個在生產環境中從 JNDI 獲取的 dataSource

與基於 XML 的配置示例一樣,我們仍然使用 @ActiveProfiles("dev") 註解 TransferServiceTest,但這次我們透過 @ContextConfiguration 註解指定了 AnnotationConfigContextLoader 和所有三個配置類。測試類本身的主體完全保持不變。

有關如何簡化上述 @Configuration 類的詳細資訊,請參閱Spring 3.1 M1: Introducing @Profile 部落格文章。

ApplicationContext 快取

自 Spring 2.5 以來,Spring TestContext Framework 根據為給定測試合併的所有上下文資源位置生成的 key 來快取整合測試的 ApplicationContexts。由於 ContextLoader SPI 只支援 locations,這個 key 生成演算法足以唯一標識用於載入 ApplicationContext 的配置。然而,隨著對配置類和 Profile 的支援增加,舊的演算法已不再適用。

因此,Spring 3.1 M2 中更新了上下文快取 key 生成演算法,以包含以下所有內容:

  • locations (來自 @ContextConfiguration)
  • classes (來自 @ContextConfiguration)
  • contextLoader (來自 @ContextConfiguration)
  • activeProfiles (來自 @ActiveProfiles)

作為開發者,這意味著您可以實現一個基礎測試類,該類宣告一組特定的資源位置或配置類。然後,如果您想針對該基礎配置執行測試,但使用不同的活動 Profile,您可以擴充套件該基礎測試類,並使用 @ActiveProfiles 註解每個具體的子類,為每個子類提供一組不同的 Profile 來啟用。因此,這些子類中的每一個都將定義一組唯一的配置屬性,這將導致載入和快取不同的 ApplicationContexts

SmartContextLoader 取代 ContextLoader SPI

正如本文前面提到的,Spring 3.1 M2 引入了一個新的 SmartContextLoader SPI,它取代了現有的 ContextLoader SPI。如果您計劃開發或已經開發了自己的自定義 ContextLoader,您可能需要仔細研究新的 SmartContextLoader 介面。與舊的 ContextLoader 介面相比,SmartContextLoader 可以處理資源位置和配置類。此外,SmartContextLoader 可以在其載入的上下文中設定活動的 bean 定義 Profile。

ContextLoader 將繼續得到支援,並且該 SPI 的任何現有實現都應繼續按原樣工作;但是,如果您想在自定義載入器中支援配置類或環境 Profile,您將需要實現 SmartContextLoader

DelegatingSmartContextLoader

如果您一直密切關注迄今為止展示的示例,您可能已經注意到,在使用配置類時,我們總是必須為 @ContextConfigurationloader 屬性顯式宣告 AnnotationConfigContextLoader.class。但當我們指定 XML 配置檔案(或依賴約定優於配置)時,預設使用 GenericXmlContextLoader

如果 Spring 能判斷我們使用的是配置類還是 XML 資源位置,然後自動選擇合適的 ContextLoader 來載入我們的應用上下文,那豈不是很好?

是的,我們也這麼認為!;)

因此,對於 Spring 3.1 RC1,我們計劃引入一個 DelegatingSmartContextLoader,它將委託給一組候選的 SmartContextLoaders(即 GenericXmlContextLoaderAnnotationConfigContextLoader),以確定哪個上下文載入器適用於給定測試類的配置。然後,將使用獲勝的候選載入器來實際載入上下文。

這項工作完成後,DelegatingSmartContextLoader 將取代 GenericXmlContextLoader 成為預設載入器。請隨時在 JIRA 中關注此開發的進展:SPR-8387

總結

Spring 3.1 為 @Configuration 類和環境 Profile 提供了第一等的測試支援,我們鼓勵您儘快試用這些功能。M2 是 3.1 釋出列車中的最後一個里程碑。因此,如果您發現任何錯誤或有任何改進建議,現在就是採取行動的時候了!


(*) 參考手冊尚未更新以反映對 @Configuration 類和環境 Profile 的測試支援,但這些功能肯定會在 Spring 3.1 RC1 或 GA 版本釋出時得到完善的文件。在此期間,新類和註解的 JavaDoc 可以作為一個很好的起點。

獲取 Spring 新聞通訊

透過 Spring 新聞通訊保持聯絡

訂閱

搶先一步

VMware 提供培訓和認證,助您加速進步。

瞭解更多

獲得支援

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

瞭解更多

近期活動

檢視 Spring 社群所有近期活動。

檢視全部