環境プロファイルを使用したコンテキスト設定

Spring Framework には、環境とプロファイル(別名「Bean 定義プロファイル」)の概念に対するファーストクラスのサポートがあり、さまざまなテストシナリオで特定の Bean 定義プロファイルをアクティブにするように統合テストを構成できます。これは、テストクラスに @ActiveProfiles アノテーションを付けて、テスト用に ApplicationContext をロードするときにアクティブ化する必要があるプロファイルのリストを提供することで実現されます。

@ActiveProfiles は SmartContextLoader SPI のどの実装でも使用できますが、@ActiveProfiles は古い ContextLoader SPI の実装ではサポートされていません。

XML 構成と @Configuration クラスを使用した 2 つの例を検討してください。

<!-- 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 プロファイルで 3 回定義されます。

TransferServiceTest に @ActiveProfiles("dev") のアノテーションを付けることにより、Spring TestContext フレームワークに、{"dev"} に設定されたアクティブなプロファイルで ApplicationContext をロードするよう指示します。その結果、組み込みデータベースが作成され、テストデータが入力され、accountRepository Bean が開発 DataSource への参照で接続されます。これはおそらく統合テストで必要なものです。

Bean を default プロファイルに割り当てると便利な場合があります。デフォルトプロファイル内の Bean は、他のプロファイルが特にアクティブ化されていない場合にのみ含まれます。これを使用して、アプリケーションのデフォルト状態で使用される「フォールバック」Bean を定義できます。例: dev および production プロファイルのデータソースを明示的に提供できますが、どちらもアクティブでない場合は、メモリ内データソースをデフォルトとして定義できます。

次のコードリストは、XML の代わりに @Configuration クラスを使用して同じ構成および統合テストを実装する方法を示しています。

  • 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 構成を 4 つの独立した @Configuration クラスに分割しました。

  • TransferServiceConfig@Autowired を使用して、依存性注入により dataSource を取得します。

  • StandaloneDataConfig: 開発者テストに適した組み込みデータベースの dataSource を定義します。

  • JndiDataConfig: 本番環境で JNDI から取得される dataSource を定義します。

  • DefaultDataConfig: アクティブなプロファイルがない場合に、デフォルトの組み込みデータベースの dataSource を定義します。

XML ベースの構成例と同様に、TransferServiceTest に @ActiveProfiles("dev") のアノテーションを付けますが、今回は @ContextConfiguration アノテーションを使用して 4 つの構成クラスすべてを指定します。テストクラスの本体自体は完全に変更されていません。

多くの場合、特定のプロジェクト内の複数のテストクラスで単一のプロファイルセットが使用されます。@ActiveProfiles アノテーションの重複した宣言を避けるために、ベースクラスで @ActiveProfiles を 1 回宣言することができ、サブクラスはベースクラスから @ActiveProfiles 構成を自動的に継承します。次の例では、@ActiveProfiles の宣言(およびその他のアノテーション)が抽象スーパークラス AbstractIntegrationTest に移動されています。

Spring Framework 5.3 以降、テスト構成は、囲んでいるクラスから継承することもできます。詳細については、@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 を実装し、@ActiveProfiles の resolver 属性を使用してそれを登録できます。詳細については、対応する 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)
	}
}