事務管理

在 TestContext 框架中,事務由 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(在類或方法級別)的測試方法不在事務中執行。請注意,@Transactional 不支援測試生命週期方法——例如,註解了 JUnit Jupiter 的 @BeforeAll@BeforeEach 等方法。此外,註解了 @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 使用它來進行程式設計式事務管理。

以下示例演示了為基於 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 中的靜態方法來程式設計式地與測試管理的事務進行互動。例如,你可以在測試方法、前置方法和後置方法中使用 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 會確保你的事務前方法或事務後方法在適當的時間執行。

一般來說,@BeforeTransaction@AfterTransaction 方法不能接受任何引數。

然而,自 Spring Framework 6.1 起,對於使用 JUnit Jupiter 結合 SpringExtension 的測試,@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
}

任何前置方法(例如,註解了 JUnit Jupiter 的 @BeforeEach 的方法)和任何後置方法(例如,註解了 JUnit Jupiter 的 @AfterEach 的方法)都在事務性測試方法的測試管理的事務中執行。

類似地,註解了 @BeforeTransaction@AfterTransaction 的方法僅對事務性測試方法執行。

配置事務管理器

TransactionalTestExecutionListener 要求在測試的 Spring ApplicationContext 中定義一個 PlatformTransactionManager bean。如果在測試的 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 的示例測試用例中,一個方法演示了假陽性,另一個方法正確地展示了重新整理會話的結果

  • 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