事務管理

在 TestContext 框架中,事務由 TransactionalTestExecutionListener 管理,即使您沒有在測試類上顯式宣告 @TestExecutionListeners,它也預設配置。但是,要啟用事務支援,您必須在透過 @ContextConfiguration 語義載入的 ApplicationContext 中配置一個 PlatformTransactionManager bean(稍後將提供更多詳細資訊)。此外,您必須在測試的類或方法級別宣告 Spring 的 @Transactional 註解。

測試管理的事務

測試管理的事務是透過使用 TransactionalTestExecutionListener 宣告性地管理或透過使用 TestTransaction 程式設計性地管理(稍後描述)的事務。您不應將此類事務與 Spring 管理的事務(在為測試載入的 ApplicationContext 中直接由 Spring 管理的事務)或應用程式管理的事務(在由測試呼叫的應用程式程式碼中程式設計性管理的事務)混淆。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.NOT_SUPPORTEDPropagation.NEVER

隔離

no

超時

no

只讀

no

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 確保您的事務前方法或事務後方法在適當的時間執行。

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

但是,對於使用 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
}

任何 before 方法(例如用 JUnit Jupiter 的 @BeforeEach 註解的方法)和任何 after 方法(例如用 JUnit Jupiter 的 @AfterEach 註解的方法)都在事務性測試方法的測試管理的事務中執行。

同樣,用 @BeforeTransaction@AfterTransaction 註解的方法僅針對事務性測試方法執行。

配置事務管理器

TransactionalTestExecutionListener 期望在測試的 Spring ApplicationContext 中定義一個 PlatformTransactionManager bean。如果測試的 ApplicationContext 中有多個 PlatformTransactionManager 例項,您可以使用 @Transactional("myTxMgr")@Transactional(transactionManager = "myTxMgr") 宣告一個限定符,或者 TransactionManagementConfigurer 可以由 @Configuration 類實現。有關在測試的 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 框架測試套件中的JpaEntityListenerTests

© . This site is unofficial and not affiliated with VMware.