事务管理
在 TestContext 框架中,事务由 TransactionalTestExecutionListener
管理,即使你没有在测试类上显式声明 @TestExecutionListeners
,它也是默认配置的。然而,为了启用事务支持,你必须在使用 @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
规则 -
JUnit Jupiter 的
org.junit.jupiter.api.Assertions
类中的assertTimeoutPreemptively(…)
方法 -
TestNG 的
@Test(timeOut = …)
支持
启用和禁用事务
为测试方法添加 @Transactional
注解会导致测试在一个事务中运行,默认情况下,该事务会在测试完成后自动回滚。如果一个测试类被注解为 @Transactional
,则该类层次结构中的每个测试方法都会在一个事务中运行。未在类或方法级别上注解 @Transactional
的测试方法不会在事务中运行。请注意,@Transactional
不支持测试生命周期方法——例如,使用 JUnit Jupiter 的 @BeforeAll
、@BeforeEach
等注解的方法。此外,被注解为 @Transactional
但其 propagation
属性设置为 NOT_SUPPORTED
或 NEVER
的测试不会在事务中运行。
表 1. @Transactional
属性支持
属性 | 是否支持测试管理的事务 |
---|---|
value 和 transactionManager | 是 |
propagation | 仅支持 Propagation.NOT_SUPPORTED 和 Propagation.NEVER |
isolation | 否 |
timeout | 否 |
readOnly | 否 |
rollbackFor 和 rollbackForClassName | 否:请使用 TestTransaction.flagForRollback() 代替 |
noRollbackFor 和 noRollbackForClassName | 否:请使用 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 时,@PostPersist
、@PreUpdate
和 @PostUpdate
回调不会被调用,除非在实体被保存或更新后调用 entityManager.flush()
。同样,如果一个实体已经附加到当前工作单元(与当前持久化上下文相关联),尝试重新加载该实体将不会导致 @PostLoad
回调,除非在尝试重新加载实体之前调用 entityManager.clear()
。
以下示例展示了如何刷新 EntityManager
以确保在实体持久化时调用 @PostPersist
回调。一个具有 @PostPersist
回调方法的实体监听器已为示例中使用的 Person
实体注册。
- 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 生命周期回调的工作示例。