Spring 3.1 M2:使用 @Configuration 類別和剖面進行測試

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

如同 Jürgen Höller 在他發布 Spring 3.1 M2 發行 文章中提到的,Spring TestContext Framework(*) 已全面改版,為 @Configuration 類別和環境剖面提供一流的測試支援。

在這篇文章中,我將首先帶您瀏覽一些範例,示範這些新的測試功能。然後,我將介紹 TestContext Framework 中一些新的擴充點,這些擴充點使這些新功能成為可能。

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

背景

在 Spring 2.5 中,我們推出了Spring TestContext Framework,它提供註解驅動的整合測試支援,可以與 JUnit 或 TestNG 一起使用。本部落格中的範例將重點放在基於 JUnit 的測試上,但此處使用的所有功能也適用於 TestNG。

在其核心,TestContext Framework 允許您使用 @ContextConfiguration 註解測試類別,以指定要使用哪些組態檔來載入測試的 ApplicationContext。預設情況下,ApplicationContext 是使用 GenericXmlContextLoader 載入的,它從 XML Spring 組態檔載入 Context。然後,您可以通過使用 @Autowired@Resource@Inject 註解測試類別中的欄位,從 ApplicationContext 存取 Bean。

Spring 3.0 透過 @Configuration 類別引入了對基於 Java 的組態的支援,但 TestContext Framework 直到現在才提供適當的 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. 已透過 @ContextConfigurationloader 屬性指定了 AnnotationConfigContextLoader

否則,測試的組態和實作保持不變。

那麼,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 屬性一樣,我們可以透過將 Class[] 陣列提供給 classes 屬性來宣告多個組態類別 — 例如:@ContextConfiguration(classes={Config1.class, Config2.class}, ... )

這結束了使用 @Configuration 類別進行整合測試的介紹。現在讓我們看看 Spring 對環境剖面的測試支援。

使用環境剖面進行整合測試

正如 Chris Beams 在他的 Spring 3.1 M1 發行 公告和他的後續部落格 Spring 3.1 M1:介紹 @Profile 中討論的那樣,Spring 3.1 在框架中為環境和剖面(又名Bean 定義剖面)的概念引入了一流的支援。截至 Spring 3.1 M2,整合測試也可以配置為為各種測試情境啟動特定的 Bean 定義剖面。這是通過使用新的 @ActiveProfiles 註解註解測試類別,並提供應在為測試載入 ApplicationContext 時啟動的剖面清單來實現的。

注意:@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 將從 classpath 根目錄下的 app-config.xml 組態檔載入。如果您檢查 app-config.xml,您會注意到 accountRepository Bean 依賴於 dataSource Bean;但是,dataSource 未定義為頂層 Bean。相反,dataSource 定義了兩次:一次在 production 剖面中,一次在 dev 剖面中。

透過使用 @ActiveProfiles("dev") 註解 TransferServiceTest,我們指示 Spring TestContext Framework 載入 ApplicationContext,並將活動剖面設定為 {"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:介紹 @Profile 部落格文章。

ApplicationContext 快取

自 Spring 2.5 以來,Spring TestContext Framework根據從給定測試的所有合併 Context 資源位置產生的金鑰,快取整合測試的 ApplicationContext。由於 ContextLoader SPI 僅支援位置,因此此金鑰產生演算法足以唯一識別用於載入 ApplicationContext 的組態。但是,隨著對組態類別和剖面的新增支援,舊的演算法已不再足夠。

因此,Context 快取金鑰產生演算法已在 Spring 3.1 M2 中更新,以包含以下所有內容

  • 位置(來自 @ContextConfiguration
  • 類別(來自 @ContextConfiguration
  • contextLoader (來自 @ContextConfiguration
  • activeProfiles (來自 @ActiveProfiles

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

SmartContextLoader 取代 ContextLoader SPI

正如本文前面暗示的那樣,Spring 3.1 M2 引入了新的 SmartContextLoader SPI,它取代了現有的 ContextLoader SPI。如果您計劃開發或已經開發了自己的自訂 ContextLoader,您可能需要仔細查看新的 SmartContextLoader 介面。與舊的 ContextLoader 介面相比,SmartContextLoader 可以處理資源位置和組態類別。此外,SmartContextLoader 可以在它載入的 Context 中設定活動 Bean 定義剖面。

ContextLoader 將繼續受到支援,並且該 SPI 的任何現有實作都應繼續按原樣工作;但是,如果您想在自訂載入器中支援組態類別或環境剖面,則需要實作 SmartContextLoader

DelegatingSmartContextLoader

如果您一直密切關注到目前為止呈現的範例,您可能已經注意到,當使用組態類別時,我們總是必須為 @ContextConfigurationloader 屬性明確宣告 AnnotationConfigContextLoader.class。但是,當我們指定 XML 組態檔(或依賴慣例優於配置)時,預設情況下會使用 GenericXmlContextLoader

如果 Spring 可以注意到我們是否正在使用組態類別或 XML 資源位置,然後自動選擇正確的 ContextLoader 來載入我們的應用程式 Context,這不是很好嗎?

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

因此,對於 Spring 3.1 RC1,我們計劃引入 DelegatingSmartContextLoader,它將委派給候選 SmartContextLoader 的清單(即 GenericXmlContextLoaderAnnotationConfigContextLoader),以確定哪個 Context 載入器適合給定測試類別的組態。然後,將使用獲勝的候選者來實際載入 Context。

一旦完成這項工作,DelegatingSmartContextLoader 將取代 GenericXmlContextLoader 作為預設載入器。歡迎隨時關注 JIRA 中此開發的進度:SPR-8387

總結

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


(*) 參考手冊尚未更新以反映對 @Configuration 類別和環境剖面的測試支援,但這些功能肯定會在 Spring 3.1 RC1 或 GA 中得到充分記錄。同時,每個新類別和註解的 JavaDoc 可以作為一個好的起點。

取得 Spring 電子報

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

訂閱

搶先一步

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

瞭解更多

取得支援

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

瞭解更多

即將舉辦的活動

查看 Spring 社群中所有即將舉辦的活動。

檢視全部