交易管理

在 TestContext framework 中,交易由 TransactionalTestExecutionListener 管理,即使您未在測試類別上明確宣告 @TestExecutionListeners,預設也會組態此監聽器。但是,若要啟用交易支援,您必須在透過 @ContextConfiguration 語意載入的 ApplicationContext 中組態 PlatformTransactionManager bean (稍後提供更多詳細資訊)。此外,您必須在測試的類別或方法層級宣告 Spring 的 @Transactional 註解。

測試管理交易

測試管理交易是使用 TransactionalTestExecutionListener 以宣告方式管理,或使用 TestTransaction (稍後說明) 以程式設計方式管理的交易。您不應將此類交易與 Spring 管理交易 (由 Spring 直接在為測試載入的 ApplicationContext 中管理的交易) 或應用程式管理交易 (在測試叫用的應用程式碼中以程式設計方式管理的交易) 混淆。Spring 管理和應用程式管理交易通常會參與測試管理交易。但是,如果 Spring 管理或應用程式管理交易組態為任何傳播類型而非 REQUIREDSUPPORTS,則應謹慎使用 (請參閱 交易傳播 的討論以取得詳細資訊)。

先佔逾時和測試管理交易

將測試框架的任何形式的先佔逾時與 Spring 的測試管理交易結合使用時,必須格外小心。

具體而言,Spring 的測試支援在叫用目前測試方法之前,將交易狀態繫結至目前執行緒 (透過 java.lang.ThreadLocal 變數)。如果測試框架為了支援先佔逾時而在新執行緒中叫用目前的測試方法,則在測試管理交易中不會叫用目前測試方法中執行的任何動作。因此,任何此類動作的結果都不會隨測試管理交易回滾。相反地,即使測試管理交易已由 Spring 正確回滾,此類動作仍會提交至永久儲存區,例如,關聯式資料庫。

可能發生這種情況的情況包括但不限於下列各項。

  • JUnit 4 的 @Test(timeout = …​) 支援和 TimeOut 規則

  • JUnit Jupiter 的 org.junit.jupiter.api.Assertions 類別中的 assertTimeoutPreemptively(…​) 方法

  • TestNG 的 @Test(timeOut = …​) 支援

啟用和停用交易

使用 @Transactional 註解測試方法會導致測試在交易內執行,預設情況下,測試完成後會自動回滾。如果測試類別使用 @Transactional 註解,則該類別階層中的每個測試方法都會在交易內執行。未在類別或方法層級使用 @Transactional 註解的測試方法不會在交易內執行。請注意,測試生命週期方法 (例如,使用 JUnit Jupiter 的 @BeforeAll@BeforeEach 等註解的方法) 不支援 @Transactional。此外,使用 @Transactional 註解但將 propagation 屬性設定為 NOT_SUPPORTEDNEVER 的測試不會在交易內執行。

表 1. @Transactional 屬性支援
屬性 測試管理交易支援

valuetransactionManager

propagation

僅支援 Propagation.NOT_SUPPORTEDPropagation.NEVER

isolation

timeout

readOnly

rollbackForrollbackForClassName

否:請改用 TestTransaction.flagForRollback()

noRollbackFornoRollbackForClassName

否:請改用 TestTransaction.flagForCommit()

方法層級生命週期方法 (例如,使用 JUnit Jupiter 的 @BeforeEach@AfterEach 註解的方法) 會在測試管理交易內執行。另一方面,套件層級和類別層級生命週期方法 (例如,使用 JUnit Jupiter 的 @BeforeAll@AfterAll 註解的方法,以及使用 TestNG 的 @BeforeSuite@AfterSuite@BeforeClass@AfterClass 註解的方法) 不會在測試管理交易內執行。

如果您需要在套件層級或類別層級生命週期方法中在交易內執行程式碼,您可能希望將對應的 PlatformTransactionManager 注入到您的測試類別中,然後將其與 TransactionTemplate 搭配使用以進行程式化交易管理。

請注意,AbstractTransactionalJUnit4SpringContextTestsAbstractTransactionalTestNGSpringContextTests 已預先組態為在類別層級提供交易支援。

下列範例示範了為基於 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"))
	}
}

交易回滾和提交行為 中所述,執行 createUser() 方法後,無需清理資料庫,因為對資料庫所做的任何變更都會由 TransactionalTestExecutionListener 自動回滾。

交易回滾和提交行為

預設情況下,測試交易會在測試完成後自動回滾;但是,可以透過 @Commit@Rollback 註解以宣告方式組態交易提交和回滾行為。如需更多詳細資訊,請參閱 註解支援 區段中的對應項目。

程式化交易管理

您可以使用 TestTransaction 中的靜態方法以程式設計方式與測試管理交易互動。例如,您可以在測試方法、before 方法和 after 方法中使用 TestTransaction 來啟動或結束目前的測試管理交易,或組態目前的測試管理交易以進行回滾或提交。只要啟用 TransactionalTestExecutionListener,就會自動提供對 TestTransaction 的支援。

下列範例示範了 TestTransaction 的一些功能。如需更多詳細資訊,請參閱 TestTransaction 的 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 會確保您的 before-transaction 方法或 after-transaction 方法在適當的時間執行。

一般而言,@BeforeTransaction@AfterTransaction 方法不得接受任何引數。

但是,從 Spring Framework 6.1 開始,對於使用 SpringExtension 和 JUnit Jupiter 的測試,@BeforeTransaction@AfterTransaction 方法可以選擇性地接受引數,這些引數將由任何已註冊的 JUnit ParameterResolver 擴充功能 (例如 SpringExtension) 解析。這表示可以將 JUnit 特定引數 (例如 TestInfo) 或來自測試 ApplicationContext 的 Bean 提供給 @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
}

任何 before 方法 (例如使用 JUnit Jupiter 的 @BeforeEach 註解的方法) 和任何 after 方法 (例如使用 JUnit Jupiter 的 @AfterEach 註解的方法) 都會在交易測試方法的測試管理交易內執行。

同樣地,使用 @BeforeTransaction@AfterTransaction 註解的方法僅針對交易測試方法執行。

組態交易管理員

TransactionalTestExecutionListener 預期在測試的 Spring ApplicationContext 中定義 PlatformTransactionManager bean。如果測試的 ApplicationContext 中有多個 PlatformTransactionManager 實例,您可以使用 @Transactional("myTxMgr")@Transactional(transactionManager = "myTxMgr") 宣告限定詞,或者 TransactionManagementConfigurer 可以由 @Configuration 類別實作。請參閱 TestContextTransactionUtils.retrieveTransactionManager() 的 javadoc,以取得在測試的 ApplicationContext 中查閱交易管理員所用演算法的詳細資訊。

所有交易相關註解的示範

下列基於 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 Session 或 JPA 持續性 Context 狀態的應用程式碼時,請務必在執行該程式碼的測試方法中清除基礎工作單元。未能清除基礎工作單元可能會產生誤報:您的測試通過,但相同的程式碼在實際生產環境中擲回例外狀況。請注意,這適用於任何維護記憶體中工作單元的 ORM 框架。在下列基於 Hibernate 的範例測試案例中,一個方法示範了誤報,而另一個方法正確地公開了清除 Session 的結果

  • 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 回呼。同樣地,如果實體已附加至目前的工作單元 (與目前的持續性 Context 相關聯),則重新載入實體的嘗試不會導致 @PostLoad 回呼,除非在嘗試重新載入實體之前叫用 entityManager.clear()

下列範例示範如何清除 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...
}

// ...

請參閱 Spring Framework 測試套件中的 JpaEntityListenerTests,以取得使用所有 JPA 生命週期回呼的工作範例。