トランザクション管理

TestContext フレームワークでは、テストクラスで @TestExecutionListeners を明示的に宣言しなくても、トランザクションはデフォルトで設定される TransactionalTestExecutionListener によって管理されます。ただし、トランザクションのサポートを有効にするには、@ContextConfiguration セマンティクスでロードされる ApplicationContext で PlatformTransactionManager Bean を構成する必要があります(詳細については後で説明します)。さらに、テストのクラスレベルまたはメソッドレベルで Spring の @Transactional アノテーションを宣言する必要があります。

テスト管理されたトランザクション

テスト管理トランザクションは、TransactionalTestExecutionListener を使用して宣言的に管理されるか、TestTransaction (後述) を使用してプログラム的に管理されるトランザクションです。このようなトランザクションを、Spring 管理トランザクション (テスト用にロードされた ApplicationContext 内の Spring によって直接管理されるトランザクション) またはアプリケーション管理トランザクション (テストによって呼び出されるアプリケーションコード内でプログラムによって管理されるトランザクション) と混同しないでください。Spring 管理トランザクションとアプリケーション管理トランザクションは通常、テスト管理トランザクションに参加します。ただし、Spring 管理またはアプリケーション管理のトランザクションが REQUIRED または SUPPORTS 以外の伝播型で構成されている場合は注意が必要です (詳細については、トランザクションの伝播に関する説明を参照してください)。

プリエンプティブタイムアウトとテスト管理されたトランザクション

Spring のテスト管理トランザクションと組み合わせて、テストフレームワークからプリエンプティブタイムアウトを使用する場合は注意が必要です。

特に、Spring のテストサポートは、現在のテストメソッドが呼び出される前に、トランザクション状態を(java.lang.ThreadLocal 変数を介して)現在のスレッドにバインドします。テストフレームワークがプリエンプティブタイムアウトをサポートするために新しいスレッドで現在のテストメソッドを呼び出した場合、現在のテストメソッド内で実行されたアクションはテスト管理トランザクション内で呼び出されません。そのようなアクションの結果は、テスト管理されたトランザクションではロールバックされません。それどころか、テスト管理のトランザクションが Spring によって適切にロールバックされたとしても、そのようなアクションは永続ストア(たとえば、リレーショナルデータベース)にコミットされます。

これが発生する可能性のある状況には、以下が含まれますが、これらに限定されません。

  • JUnit 4 の @Test(timeout = …​) サポートと TimeOut ルール

  • org.junit.jupiter.api.Assertions クラスの JUnit Jupiter の assertTimeoutPreemptively(…​) メソッド

  • TestNG の @Test(timeOut = …​) サポート

トランザクションの有効化と無効化

テストメソッドに @Transactional アノテーションを付けると、トランザクション内でテストが実行され、デフォルトでは、テストの補完後に自動的にロールバックされます。テストクラスに @Transactional アノテーションが付けられている場合、そのクラス階層内の各テストメソッドはトランザクション内で実行されます。(クラスまたはメソッドレベルで) @Transactional アノテーションが付けられていないテストメソッドは、トランザクション内で実行されません。@Transactional は、テストライフサイクルメソッド(たとえば、JUnit Jupiter の @BeforeAll@BeforeEach などでアノテーションが付けられたメソッド)ではサポートされていないことに注意してください。さらに、@Transactional でアノテーションが付けられているが、propagation 属性が NOT_SUPPORTED または NEVER に設定されているテストはトランザクション内で実行されません。

表 1: @Transactional 属性のサポート
属性 テスト管理されたトランザクションでサポート

value and transactionManager

はい

propagation

Propagation.NOT_SUPPORTED と Propagation.NEVER のみがサポートされています

isolation

いいえ

timeout

いいえ

readOnly

いいえ

rollbackFor and rollbackForClassName

いいえ: 代わりに TestTransaction.flagForRollback() を使用してください

noRollbackFor and noRollbackForClassName

いいえ: 代わりに TestTransaction.flagForCommit() を使用してください

メソッドレベルのライフサイクルメソッド — たとえば、JUnit Jupiter の @BeforeEach または @AfterEach でアノテーションが付けられたメソッド — テスト管理トランザクション内で実行されます。一方、スイートレベルおよびクラスレベルのライフサイクルメソッド — たとえば、JUnit Jupiter の @BeforeAll または @AfterAll でアノテーションが付けられたメソッドと TestNG の @BeforeSuite@AfterSuite@BeforeClass または @AfterClass でアノテーションが付けられたメソッド — テスト管理トランザクション内で実行されません

トランザクション内でスイートレベルまたはクラスレベルのライフサイクルメソッドでコードを実行する必要がある場合は、対応する PlatformTransactionManager をテストクラスに挿入し、それを TransactionTemplate と共に使用してプログラムによるトランザクション管理を行うことができます。

AbstractTransactionalJUnit4SpringContextTests および AbstractTransactionalTestNGSpringContextTests は、クラスレベルでのトランザクションサポート用に事前構成されていることに注意してください。

次の例は、Hibernate ベースの UserRepository の統合テストを記述する一般的なシナリオを示しています。

  • Java

  • Kotlin

@SpringJUnitConfig(TestConfig.class)
@Transactional
class HibernateUserRepositoryTests {

	@Autowired
	HibernateUserRepository repository;

	@Autowired
	SessionFactory sessionFactory;

	JdbcTemplate jdbcTemplate;

	@Autowired
	void setDataSource(DataSource dataSource) {
		this.jdbcTemplate = new JdbcTemplate(dataSource);
	}

	@Test
	void createUser() {
		// track initial state in test database:
		final int count = countRowsInTable("user");

		User user = new User(...);
		repository.save(user);

		// Manual flush is required to avoid false positive in test
		sessionFactory.getCurrentSession().flush();
		assertNumUsers(count + 1);
	}

	private int countRowsInTable(String tableName) {
		return JdbcTestUtils.countRowsInTable(this.jdbcTemplate, tableName);
	}

	private void assertNumUsers(int expected) {
		assertEquals("Number of rows in the [user] table.", expected, countRowsInTable("user"));
	}
}
@SpringJUnitConfig(TestConfig::class)
@Transactional
class HibernateUserRepositoryTests {

	@Autowired
	lateinit var repository: HibernateUserRepository

	@Autowired
	lateinit var sessionFactory: SessionFactory

	lateinit var jdbcTemplate: JdbcTemplate

	@Autowired
	fun setDataSource(dataSource: DataSource) {
		this.jdbcTemplate = JdbcTemplate(dataSource)
	}

	@Test
	fun createUser() {
		// track initial state in test database:
		val count = countRowsInTable("user")

		val user = User()
		repository.save(user)

		// Manual flush is required to avoid false positive in test
		sessionFactory.getCurrentSession().flush()
		assertNumUsers(count + 1)
	}

	private fun countRowsInTable(tableName: String): Int {
		return JdbcTestUtils.countRowsInTable(jdbcTemplate, tableName)
	}

	private fun assertNumUsers(expected: Int) {
		assertEquals("Number of rows in the [user] table.", expected, countRowsInTable("user"))
	}
}

トランザクションのロールバックとコミットの動作で説明したように、データベースに加えられた変更は TransactionalTestExecutionListener によって自動的にロールバックされるため、createUser() メソッドの実行後にデータベースをクリーンアップする必要はありません。

トランザクションのロールバックとコミットの動作

デフォルトでは、テストトランザクションはテストの補完後に自動的にロールバックされます。ただし、トランザクションのコミットとロールバックの動作は、@Commit および @Rollback アノテーションを介して宣言的に構成できます。詳細については、アノテーションサポートセクションの対応するエントリを参照してください。

プログラムによるトランザクション管理

TestTransaction の静的メソッドを使用して、プログラムでテスト管理されたトランザクションと対話できます。例: テストメソッド内、メソッドの前、メソッドの後に TestTransaction を使用して、現在のテスト管理トランザクションを開始または終了したり、ロールバックまたはコミットのために現在のテスト管理トランザクションを構成したりできます。TransactionalTestExecutionListener が有効になると、TestTransaction のサポートが自動的に使用可能になります。

次の例は、TestTransaction の機能の一部を示しています。詳細については、TestTransaction (Javadoc) の javadoc を参照してください。

  • Java

  • Kotlin

@ContextConfiguration(classes = TestConfig.class)
public class ProgrammaticTransactionManagementTests extends
		AbstractTransactionalJUnit4SpringContextTests {

	@Test
	public void transactionalTest() {
		// assert initial state in test database:
		assertNumUsers(2);

		deleteFromTables("user");

		// changes to the database will be committed!
		TestTransaction.flagForCommit();
		TestTransaction.end();
		assertFalse(TestTransaction.isActive());
		assertNumUsers(0);

		TestTransaction.start();
		// perform other actions against the database that will
		// be automatically rolled back after the test completes...
	}

	protected void assertNumUsers(int expected) {
		assertEquals("Number of rows in the [user] table.", expected, countRowsInTable("user"));
	}
}
@ContextConfiguration(classes = [TestConfig::class])
class ProgrammaticTransactionManagementTests : AbstractTransactionalJUnit4SpringContextTests() {

	@Test
	fun transactionalTest() {
		// assert initial state in test database:
		assertNumUsers(2)

		deleteFromTables("user")

		// changes to the database will be committed!
		TestTransaction.flagForCommit()
		TestTransaction.end()
		assertFalse(TestTransaction.isActive())
		assertNumUsers(0)

		TestTransaction.start()
		// perform other actions against the database that will
		// be automatically rolled back after the test completes...
	}

	protected fun assertNumUsers(expected: Int) {
		assertEquals("Number of rows in the [user] table.", expected, countRowsInTable("user"))
	}
}

トランザクション外でのコードの実行

場合によっては、トランザクションテストメソッドの前後で、トランザクションコンテキストの外で特定のコードを実行する必要がある場合があります。たとえば、テストを実行する前にデータベースの初期状態を確認したり、テストの実行後に予想されるトランザクションのコミット動作を確認したりするためです (テストはトランザクションをコミットするように構成されていました)。TransactionalTestExecutionListener は、まさにそのようなシナリオのために @BeforeTransaction および @AfterTransaction アノテーションをサポートします。テストクラス内の任意の void メソッド、またはテストインターフェース内の任意の void デフォルトメソッドにこれらのアノテーションのいずれかを付けることができ、TransactionalTestExecutionListener はトランザクション前メソッドまたはトランザクション後メソッドが適切なタイミングで実行されることを保証します。

一般に、@BeforeTransaction メソッドと @AfterTransaction メソッドは引数を受け入れてはなりません。

ただし、Spring Framework 6.1 では、JUnit Jupiter で SpringExtension を使用するテストの場合、@BeforeTransaction および @AfterTransaction メソッドは、SpringExtension などの登録された JUnit ParameterResolver 拡張機能によって解決される引数をオプションで受け入れることができます。これは、次の例に示すように、TestInfo やテストの ApplicationContext の Bean などの JUnit 固有の引数を @BeforeTransaction および @AfterTransaction メソッドに提供できることを意味します。

  • Java

  • Kotlin

@BeforeTransaction
void verifyInitialDatabaseState(@Autowired DataSource dataSource) {
	// Use the DataSource to verify the initial state before a transaction is started
}
@BeforeTransaction
fun verifyInitialDatabaseState(@Autowired dataSource: DataSource) {
	// Use the DataSource to verify the initial state before a transaction is started
}

前メソッド (JUnit Jupiter の @BeforeEach でアノテーションが付けられたメソッドなど) と後メソッド (JUnit Jupiter の @AfterEach でアノテーションが付けられたメソッドなど) は、トランザクションテストメソッドのテスト管理トランザクション内で実行されます。

同様に、@BeforeTransaction または @AfterTransaction のアノテーションが付けられたメソッドは、トランザクションテストメソッドに対してのみ実行されます。

トランザクションマネージャーの構成

TransactionalTestExecutionListener は、テストのために PlatformTransactionManager Bean が Spring ApplicationContext で定義されることを期待しています。テストの ApplicationContext 内に PlatformTransactionManager のインスタンスが複数ある場合は、@Transactional("myTxMgr") または @Transactional(transactionManager = "myTxMgr") を使用して修飾子を宣言するか、@Configuration クラスによって TransactionManagementConfigurer を実装できます。テストの ApplicationContext でトランザクションマネージャーを検索するために使用されるアルゴリズムの詳細については、TestContextTransactionUtils.retrieveTransactionManager() の javadoc を参照してください。

すべてのトランザクション関連のアノテーションのデモンストレーション

次の JUnit Jupiter ベースの例は、すべてのトランザクション関連のアノテーションを強調表示する架空の統合テストシナリオを示しています。この例は、ベストプラクティスを示すことを目的としたものではなく、これらのアノテーションがどのように使用できるかを示すことを目的としています。詳細と設定例については、「アノテーションサポート」セクションを参照してください。@Sql のトランザクション管理には、デフォルトのトランザクションロールバックセマンティクスで宣言型 SQL スクリプトを実行するために @Sql を使用する追加の例が含まれています。次の例は、関連するアノテーションを示しています。

  • Java

  • Kotlin

@SpringJUnitConfig
@Transactional(transactionManager = "txMgr")
@Commit
class FictitiousTransactionalTest {

	@BeforeTransaction
	void verifyInitialDatabaseState() {
		// logic to verify the initial state before a transaction is started
	}

	@BeforeEach
	void setUpTestDataWithinTransaction() {
		// set up test data within the transaction
	}

	@Test
	// overrides the class-level @Commit setting
	@Rollback
	void modifyDatabaseWithinTransaction() {
		// logic which uses the test data and modifies database state
	}

	@AfterEach
	void tearDownWithinTransaction() {
		// run "tear down" logic within the transaction
	}

	@AfterTransaction
	void verifyFinalDatabaseState() {
		// logic to verify the final state after transaction has rolled back
	}

}
@SpringJUnitConfig
@Transactional(transactionManager = "txMgr")
@Commit
class FictitiousTransactionalTest {

	@BeforeTransaction
	fun verifyInitialDatabaseState() {
		// logic to verify the initial state before a transaction is started
	}

	@BeforeEach
	fun setUpTestDataWithinTransaction() {
		// set up test data within the transaction
	}

	@Test
	// overrides the class-level @Commit setting
	@Rollback
	fun modifyDatabaseWithinTransaction() {
		// logic which uses the test data and modifies database state
	}

	@AfterEach
	fun tearDownWithinTransaction() {
		// run "tear down" logic within the transaction
	}

	@AfterTransaction
	fun verifyFinalDatabaseState() {
		// logic to verify the final state after transaction has rolled back
	}

}
ORM コードのテスト時に誤検知を回避する

Hibernate セッションまたは JPA 永続コンテキストの状態を操作するアプリケーションコードをテストするときは、そのコードを実行するテストメソッド内の基になる作業単位を必ずフラッシュしてください。基礎となる作業単位のフラッシュに失敗すると、誤検知が発生する可能性があります。テストは成功しますが、本番環境で同じコードが例外をスローします。これは、メモリ内の作業単位を維持する ORM フレームワークに適用されることに注意してください。次の Hibernate ベースのテストケースでは、1 つのメソッドが偽陽性を示し、もう 1 つのメソッドがセッションをフラッシュした結果を正しく公開しています。

  • Java

  • Kotlin

// ...

@Autowired
SessionFactory sessionFactory;

@Transactional
@Test // no expected exception!
public void falsePositive() {
	updateEntityInHibernateSession();
	// False positive: an exception will be thrown once the Hibernate
	// Session is finally flushed (i.e., in production code)
}

@Transactional
@Test(expected = ...)
public void updateWithSessionFlush() {
	updateEntityInHibernateSession();
	// Manual flush is required to avoid false positive in test
	sessionFactory.getCurrentSession().flush();
}

// ...
// ...

@Autowired
lateinit var sessionFactory: SessionFactory

@Transactional
@Test // no expected exception!
fun falsePositive() {
	updateEntityInHibernateSession()
	// False positive: an exception will be thrown once the Hibernate
	// Session is finally flushed (i.e., in production code)
}

@Transactional
@Test(expected = ...)
fun updateWithSessionFlush() {
	updateEntityInHibernateSession()
	// Manual flush is required to avoid false positive in test
	sessionFactory.getCurrentSession().flush()
}

// ...

次の例は、JPA の一致方法を示しています。

  • Java

  • Kotlin

// ...

@PersistenceContext
EntityManager entityManager;

@Transactional
@Test // no expected exception!
public void falsePositive() {
	updateEntityInJpaPersistenceContext();
	// False positive: an exception will be thrown once the JPA
	// EntityManager is finally flushed (i.e., in production code)
}

@Transactional
@Test(expected = ...)
public void updateWithEntityManagerFlush() {
	updateEntityInJpaPersistenceContext();
	// Manual flush is required to avoid false positive in test
	entityManager.flush();
}

// ...
// ...

@PersistenceContext
lateinit var entityManager:EntityManager

@Transactional
@Test // no expected exception!
fun falsePositive() {
	updateEntityInJpaPersistenceContext()
	// False positive: an exception will be thrown once the JPA
	// EntityManager is finally flushed (i.e., in production code)
}

@Transactional
@Test(expected = ...)
void updateWithEntityManagerFlush() {
	updateEntityInJpaPersistenceContext()
	// Manual flush is required to avoid false positive in test
	entityManager.flush()
}

// ...
ORM エンティティライフサイクルコールバックのテスト

ORM コードをテストする際の検知の回避に関する注意事項と同様に、アプリケーションでエンティティライフサイクルコールバック(エンティティリスナーとも呼ばれます)を使用する場合は、そのコードを実行するテストメソッド内の基になる作業単位を必ずフラッシュしてください。基になる作業単位のフラッシュまたはクリアに失敗すると、特定のライフサイクルコールバックが呼び出されない可能性があります。

例: JPA を使用する場合、エンティティが保存または更新された後に entityManager.flush() が呼び出されない限り、@PostPersist@PreUpdate@PostUpdate コールバックは呼び出されません。同様に、エンティティが(現在の永続コンテキストに関連付けられている)現在の作業単位にすでに接続されている場合、エンティティをリロードしようとする前に entityManager.clear() が呼び出されない限り、エンティティをリロードしようとしても @PostLoad コールバックは発生しません。

次の例は、EntityManager をフラッシュして、エンティティが永続化されたときに @PostPersist コールバックが呼び出されるようにする方法を示しています。この例で使用されている Person エンティティには、@PostPersist コールバックメソッドを持つエンティティリスナーが登録されています。

  • Java

  • Kotlin

// ...

@Autowired
JpaPersonRepository repo;

@PersistenceContext
EntityManager entityManager;

@Transactional
@Test
void savePerson() {
	// EntityManager#persist(...) results in @PrePersist but not @PostPersist
	repo.save(new Person("Jane"));

	// Manual flush is required for @PostPersist callback to be invoked
	entityManager.flush();

	// Test code that relies on the @PostPersist callback
	// having been invoked...
}

// ...
// ...

@Autowired
lateinit var repo: JpaPersonRepository

@PersistenceContext
lateinit var entityManager: EntityManager

@Transactional
@Test
fun savePerson() {
	// EntityManager#persist(...) results in @PrePersist but not @PostPersist
	repo.save(Person("Jane"))

	// Manual flush is required for @PostPersist callback to be invoked
	entityManager.flush()

	// Test code that relies on the @PostPersist callback
	// having been invoked...
}

// ...

すべての JPA ライフサイクルコールバックを使用した実例については、Spring Framework テストスイートの JpaEntityListenerTests [GitHub] (英語) を参照してください。