交易管理
在TestContext框架中,事务是由TransactionalTestExecutionListener管理的,该监听器是默认配置好的,即使你没有在测试类上显式声明@TestExecutionListeners。然而,要启用对事务的支持,你必须在使用@ContextConfiguration语义加载的ApplicationContext中配置一个PlatformTransactionManagerbean(后续会提供更多细节)。此外,你还必须在类级别或方法级别为你的测试声明Spring的@Transactional注解。
测试管理的事务
测试管理的事务(test-managed transactions)是通过使用TransactionalTestExecutionListener以声明性方式管理的,或者通过使用TestTransaction以编程方式管理的(稍后会有描述)。不应将此类事务与Spring管理的事务(由Spring在为测试加载的ApplicationContext中直接管理的事务)或应用管理的事务(在测试调用的应用代码中以编程方式管理的事务)混淆。Spring管理和应用管理的事务通常会参与测试管理的事务中。然而,如果Spring管理或应用管理的事务配置的传播类型不是REQUIRED或SUPPORTS,则应谨慎使用(详情请参见事务传播中的讨论)。
抢占式超时和测试管理的事务
在使用测试框架的任何形式的抢占式超时功能与Spring的测试管理事务结合时,必须谨慎。
具体来说,Spring的测试支持会在当前测试方法被调用之前,将事务状态绑定到当前线程(通过java.lang.ThreadLocal变量)。如果测试框架为了支持抢占式超时而在新线程中调用当前的测试方法,那么在当前测试方法中执行的任何操作将不会包含在测试管理的事务中。因此,这些操作的结果将不会与测试管理的事务一起回滚。相反,这些操作将会被提交到持久存储中——例如关系型数据库中——即使Spring已经正确地回滚了测试管理的事务。
可能出现这种情况的情况包括但不限于以下几种:
- JUnit 4的
@Test(timeout = …)支持以及TimeOut规则 - JUnit Jupiter的
assertTimeoutPreemptively(…)方法(位于org.junit.jupiter.api.Assertions类中) - 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进行程序化的事务管理。
请注意,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"))
}
}
如事务回滚和提交行为中所解释的,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 方法不得接受任何参数。
然而,在使用 SpringExtension 与 JUnit Jupiter 进行测试时,@BeforeTransaction 和 @AfterTransaction 方法可以可选地接受参数,这些参数将由任何已注册的 JUnit ParameterResolver 扩展(如 SpringExtension)来解析。这意味着可以向 @BeforeTransaction 和 @AfterTransaction 方法提供特定于 JUnit 的参数,比如 TestInfo 或来自测试的 ApplicationContext 的 bean,如下例所示。
- Java
- Kotlin
@BeforeTransaction
void verifyInitialDatabaseState(@Autowired DataSource dataSource) {
// 使用 DataSource 在开始事务之前验证初始状态
}
}
@BeforeTransaction
fun verifyInitialDatabaseState(@Autowired dataSource: DataSource) {
// 使用 DataSource 在开始事务之前验证初始状态
}
对于事务性测试方法,任何前置方法(例如用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。
所有与交易相关的注释的演示
- 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持久化上下文状态的应用代码时,确保在运行该代码的测试方法中刷新底层的单元工作(unit of work)。如果未刷新底层单元工作,就可能会产生误报:您的测试通过了,但在实际的生产环境中同样的代码却会抛出异常。请注意,这适用于任何维护内存中单元工作的ORM框架。在以下基于Hibernate的示例测试用例中,一个方法演示了误报情况,而另一个方法则正确地展示了刷新会话后的结果:
- Java
- Kotlin
// ...
@Autowired
SessionFactory sessionFactory;
@Transactional
@Test // 不应抛出异常!
public void falsePositive() {
/updateEntityInHibernateSession();
// 误报:当Hibernate会话最终被刷新时(即在生产代码中),会抛出异常
}
@Transactional
@Test(expected = ...)
public void updateWithSessionFlush() {
AndUpdateEntityInHibernateSession();
// 测试中需要手动刷新会话以避免误报
.sessionFactory.getCurrentSession().flush();
}
// ...
// ...
@Autowired
lateinit var sessionFactory: SessionFactory
@Transactional
@Test // 不应抛出异常!
fun falsePositive() {
/updateEntityInHibernateSession()
// 误报:当Hibernate会话最终被刷新时(即在生产代码中),会抛出异常
}
@Transactional
@Test(expected = ...)
fun updateWithSessionFlush() {
:updateEntityInHibernateSession()
// 测试中需要手动刷新会话以避免误报
-sessionFactory.getCurrentSession().flush()
}
// ...
以下是JPA的对应示例方法:
- Java
- Kotlin
// ...
@PersistenceContext
EntityManager entityManager;
@Transactional
@Test // 不应抛出异常!
public void falsePositive() {
:updateEntityInJpaPersistenceContext();
// 误报:当JPA EntityManager最终被刷新时(即在生产代码中),会抛出异常
}
@Transactional
@Test(expected = ...)
public void updateWithEntityManagerFlush() {
/updateEntityInJpaPersistenceContext();
// 测试中需要手动刷新EntityManager以避免误报
entityManager.flush();
}
// ...
// ...
@PersistenceContext
lateinit var entityManager: EntityManager
@Transactional
@Test // 不应抛出异常!
fun falsePositive() {
/updateEntityInJpaPersistenceContext()
// 误报:当JPA EntityManager最终被刷新时(即在生产代码中),会抛出异常
}
@Transactional
@Test(expected = ...)
fun updateWithEntityManagerFlush() {
:updateEntityInJpaPersistenceContext()
// 测试中需要手动刷新EntityManager以避免误报
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(...) 会触发@PrePersist回调,但不会触发@PostPersist回调
repo.save(new Person("Jane"));
// 需要手动刷新才能触发@PostPersist回调
entityManager.flush();
// 依赖@PostPersist回调的测试代码
// 将会被调用...
}
// ...
// ...
@Autowired
lateinit var repo: JpaPersonRepository
@PersistenceContext
lateinit var entityManager: EntityManager
@Transactional
@Test
fun savePerson() {
// EntityManager#persist(...) 会触发@PrePersist回调,但不会触发@PostPersist回调
repo.save(Person("Jane"))
// 需要手动刷新才能触发@PostPersist回调
entityManager.flush()
// 依赖@PostPersist回调的测试代码
// 将会被调用...
}
// ...
有关使用所有JPA生命周期回调的示例,请参考Spring Framework测试套件中的JpaEntityListenerTests。