使用環境設定檔進行情境配置

Spring 框架對環境和設定檔(又稱「Bean 定義設定檔」)的概念提供一流的支援,並且可以配置整合測試,以便針對各種測試情境啟用特定的 Bean 定義設定檔。這是透過使用 @ActiveProfiles 註解標註測試類別,並提供在載入測試的 ApplicationContext 時應啟用的設定檔列表來實現。

您可以將 @ActiveProfilesSmartContextLoader SPI 的任何實作搭配使用,但舊版的 ContextLoader SPI 的實作不支援 @ActiveProfiles

考慮使用 XML 配置和 @Configuration 類別的兩個範例

<!-- app-config.xml -->
<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 profile="default">
		<jdbc:embedded-database id="dataSource">
			<jdbc:script
				location="classpath:com/bank/config/sql/schema.sql"/>
		</jdbc:embedded-database>
	</beans>

</beans>
  • Java

  • Kotlin

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

	@Autowired
	TransferService transferService;

	@Test
	void testTransferService() {
		// test the transferService
	}
}
@ExtendWith(SpringExtension::class)
// ApplicationContext will be loaded from "classpath:/app-config.xml"
@ContextConfiguration("/app-config.xml")
@ActiveProfiles("dev")
class TransferServiceTest {

	@Autowired
	lateinit var transferService: TransferService

	@Test
	fun testTransferService() {
		// test the transferService
	}
}

TransferServiceTest 執行時,其 ApplicationContext 是從類路徑根目錄下的 app-config.xml 配置檔案載入的。如果您檢查 app-config.xml,您可以看到 accountRepository Bean 依賴於 dataSource Bean。但是,dataSource 未定義為頂層 Bean。相反地,dataSource 定義了三次:在 production 設定檔、dev 設定檔和 default 設定檔中。

透過使用 @ActiveProfiles("dev") 註解 TransferServiceTest,我們指示 Spring TestContext Framework 載入 ApplicationContext,並將活動設定檔設定為 {"dev"}。因此,會建立一個嵌入式資料庫並填入測試資料,並且 accountRepository Bean 會與開發 DataSource 的參考建立連線。這很可能就是我們在整合測試中想要的。

有時將 Bean 指派給 default 設定檔很有用。預設設定檔中的 Bean 僅在未明確啟用其他設定檔時才會包含在內。您可以使用它來定義在應用程式預設狀態下使用的「後備」Bean。例如,您可以明確地為 devproduction 設定檔提供資料來源,但在這兩者都未啟用時,將記憶體內資料來源定義為預設值。

以下程式碼清單示範如何使用 @Configuration 類別而不是 XML 來實作相同的配置和整合測試

  • Java

  • Kotlin

@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("dev")
class StandaloneDataConfig {

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

  • Kotlin

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

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

	@Bean(destroyMethod = "")
	fun dataSource(): DataSource {
		val ctx = InitialContext()
		return ctx.lookup("java:comp/env/jdbc/datasource") as DataSource
	}
}
  • Java

  • Kotlin

@Configuration
@Profile("default")
public class DefaultDataConfig {

	@Bean
	public DataSource dataSource() {
		return new EmbeddedDatabaseBuilder()
			.setType(EmbeddedDatabaseType.HSQL)
			.addScript("classpath:com/bank/config/sql/schema.sql")
			.build();
	}
}
@Configuration
@Profile("default")
class DefaultDataConfig {

	@Bean
	fun dataSource(): DataSource {
		return EmbeddedDatabaseBuilder()
				.setType(EmbeddedDatabaseType.HSQL)
				.addScript("classpath:com/bank/config/sql/schema.sql")
				.build()
	}
}
  • Java

  • Kotlin

@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();
	}
}
@Configuration
class TransferServiceConfig {

	@Autowired
	lateinit var dataSource: DataSource

	@Bean
	fun transferService(): TransferService {
		return DefaultTransferService(accountRepository(), feePolicy())
	}

	@Bean
	fun accountRepository(): AccountRepository {
		return JdbcAccountRepository(dataSource)
	}

	@Bean
	fun feePolicy(): FeePolicy {
		return ZeroFeePolicy()
	}
}
  • Java

  • Kotlin

@SpringJUnitConfig({
		TransferServiceConfig.class,
		StandaloneDataConfig.class,
		JndiDataConfig.class,
		DefaultDataConfig.class})
@ActiveProfiles("dev")
class TransferServiceTest {

	@Autowired
	TransferService transferService;

	@Test
	void testTransferService() {
		// test the transferService
	}
}
@SpringJUnitConfig(
		TransferServiceConfig::class,
		StandaloneDataConfig::class,
		JndiDataConfig::class,
		DefaultDataConfig::class)
@ActiveProfiles("dev")
class TransferServiceTest {

	@Autowired
	lateinit var transferService: TransferService

	@Test
	fun testTransferService() {
		// test the transferService
	}
}

在這個變體中,我們已將 XML 配置拆分為四個獨立的 @Configuration 類別

  • TransferServiceConfig:透過使用 @Autowired 的依賴注入來取得 dataSource

  • StandaloneDataConfig:為適合開發人員測試的嵌入式資料庫定義 dataSource

  • JndiDataConfig:定義從生產環境中的 JNDI 檢索的 dataSource

  • DefaultDataConfig:為預設嵌入式資料庫定義 dataSource,以防沒有設定檔處於活動狀態。

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

通常情況下,一組設定檔會在給定專案中的多個測試類別中使用。因此,為了避免重複宣告 @ActiveProfiles 註解,您可以在基底類別上宣告一次 @ActiveProfiles,而子類別會自動從基底類別繼承 @ActiveProfiles 配置。在以下範例中,@ActiveProfiles(以及其他註解)的宣告已移至抽象父類別 AbstractIntegrationTest

測試配置也可以從封閉類別繼承。有關詳細資訊,請參閱 @Nested 測試類別配置
  • Java

  • Kotlin

@SpringJUnitConfig({
		TransferServiceConfig.class,
		StandaloneDataConfig.class,
		JndiDataConfig.class,
		DefaultDataConfig.class})
@ActiveProfiles("dev")
abstract class AbstractIntegrationTest {
}
@SpringJUnitConfig(
		TransferServiceConfig::class,
		StandaloneDataConfig::class,
		JndiDataConfig::class,
		DefaultDataConfig::class)
@ActiveProfiles("dev")
abstract class AbstractIntegrationTest {
}
  • Java

  • Kotlin

// "dev" profile inherited from superclass
class TransferServiceTest extends AbstractIntegrationTest {

	@Autowired
	TransferService transferService;

	@Test
	void testTransferService() {
		// test the transferService
	}
}
// "dev" profile inherited from superclass
class TransferServiceTest : AbstractIntegrationTest() {

	@Autowired
	lateinit var transferService: TransferService

	@Test
	fun testTransferService() {
		// test the transferService
	}
}

@ActiveProfiles 也支援 inheritProfiles 屬性,可用於停用活動設定檔的繼承,如下列範例所示

  • Java

  • Kotlin

// "dev" profile overridden with "production"
@ActiveProfiles(profiles = "production", inheritProfiles = false)
class ProductionTransferServiceTest extends AbstractIntegrationTest {
	// test body
}
// "dev" profile overridden with "production"
@ActiveProfiles("production", inheritProfiles = false)
class ProductionTransferServiceTest : AbstractIntegrationTest() {
	// test body
}

此外,有時需要以程式設計方式而不是宣告方式解析測試的活動設定檔 — 例如,基於

  • 目前的作業系統。

  • 測試是否在持續整合建置伺服器上執行。

  • 特定環境變數的存在。

  • 自訂類別層級註解的存在。

  • 其他考量。

若要以程式設計方式解析活動 Bean 定義設定檔,您可以實作自訂 ActiveProfilesResolver,並使用 @ActiveProfilesresolver 屬性註冊它。如需更多資訊,請參閱相應的 javadoc。以下範例示範如何實作和註冊自訂 OperatingSystemActiveProfilesResolver

  • Java

  • Kotlin

// "dev" profile overridden programmatically via a custom resolver
@ActiveProfiles(
		resolver = OperatingSystemActiveProfilesResolver.class,
		inheritProfiles = false)
class TransferServiceTest extends AbstractIntegrationTest {
	// test body
}
// "dev" profile overridden programmatically via a custom resolver
@ActiveProfiles(
		resolver = OperatingSystemActiveProfilesResolver::class,
		inheritProfiles = false)
class TransferServiceTest : AbstractIntegrationTest() {
	// test body
}
  • Java

  • Kotlin

public class OperatingSystemActiveProfilesResolver implements ActiveProfilesResolver {

	@Override
	public String[] resolve(Class<?> testClass) {
		String profile = ...;
		// determine the value of profile based on the operating system
		return new String[] {profile};
	}
}
class OperatingSystemActiveProfilesResolver : ActiveProfilesResolver {

	override fun resolve(testClass: Class<*>): Array<String> {
		val profile: String = ...
		// determine the value of profile based on the operating system
		return arrayOf(profile)
	}
}