事务管理

在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的assertTimeoutPreemptively(…​)方法在org.junit.jupiter.api.Assertions类中

  • 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中的静态方法来以编程方式与测试管理的事务进行交互。例如,您可以在测试方法、before方法和after方法中使用TestTransaction来启动或结束当前的测试管理事务,或者配置当前的测试管理事务以进行回滚或提交。只要启用了TransactionalTestExecutionListener,就会自动支持TestTransaction

以下示例演示了TestTransaction的一些特性。有关更多详细信息,请参阅TestTransaction的javadoc。

  • Java

  • Kotlin

@ContextConfiguration(classes = TestConfig.class)
public class ProgrammaticTransactionManagementTests extends
		AbstractTransactionalJUnit4SpringContextTests {

	@Test
	public void transactionalTest() {
		// 在测试数据库中断言初始状态:
		assertNumUsers(2);

		deleteFromTables("user");

		// 对数据库的更改将被提交!
		TestTransaction.flagForCommit();
		TestTransaction.end();
		assertFalse(TestTransaction.isActive());
		assertNumUsers(0);

		TestTransaction.start();
		// 执行针对数据库的其他操作,这些操作将在测试完成后自动回滚...
	}

	protected void assertNumUsers(int expected) {
		assertEquals("在[user]表中的行数。", expected, countRowsInTable("user"));
	}
}
@ContextConfiguration(classes = [TestConfig::class])
class ProgrammaticTransactionManagementTests : AbstractTransactionalJUnit4SpringContextTests() {

	@Test
	fun transactionalTest() {
		// 在测试数据库中断言初始状态:
		assertNumUsers(2)

		deleteFromTables("user")

		// 对数据库的更改将被提交!
		TestTransaction.flagForCommit()
		TestTransaction.end()
		assertFalse(TestTransaction.isActive())
		assertNumUsers(0)

		TestTransaction.start()
		// 执行针对数据库的其他操作,这些操作将在测试完成后自动回滚...
	}

	protected fun assertNumUsers(expected: Int) {
		assertEquals("在[user]表中的行数。", expected, countRowsInTable("user"))
	}
}

在事务之外运行代码

偶尔,您可能需要在事务性测试方法之前或之后运行某些代码,但在事务上下文之外,例如,在运行测试之前验证初始数据库状态或在测试运行后验证预期的事务提交行为(如果测试配置为提交事务)。TransactionalTestExecutionListener支持@BeforeTransaction@AfterTransaction注解,用于处理这种情况。您可以在测试类中的任何void方法或测试接口中的任何void默认方法上注释这些注解,TransactionalTestExecutionListener会确保您的before-transaction方法或after-transaction方法在适当的时间运行。

一般来说,@BeforeTransaction@AfterTransaction方法不应接受任何参数。

然而,从Spring Framework 6.1开始,对于使用JUnit Jupiter的SpringExtension的测试,@BeforeTransaction@AfterTransaction方法可以选择接受参数,这些参数将由任何注册的JUnitParameterResolver扩展(如SpringExtension)解析。这意味着可以向@BeforeTransaction@AfterTransaction方法提供JUnit特定的参数,如TestInfo或测试的ApplicationContext中的bean,如下例所示。

  • Java

  • Kotlin

@BeforeTransaction
void verifyInitialDatabaseState(@Autowired DataSource dataSource) {
	// 使用DataSource在事务开始之前验证初始状态
}
@BeforeTransaction
fun verifyInitialDatabaseState(@Autowired dataSource: DataSource) {
	// 使用DataSource在事务开始之前验证初始状态
}

任何before方法(例如使用JUnit Jupiter的@BeforeEach注释的方法)和任何after方法(例如使用JUnit Jupiter的@AfterEach注释的方法)都在事务性测试方法的测试管理事务中运行。

同样,使用@BeforeTransaction@AfterTransaction注释的方法仅对事务性测试方法运行。

配置事务管理器

TransactionalTestExecutionListener期望在测试的Spring ApplicationContext中定义一个PlatformTransactionManager bean。如果测试的ApplicationContext中有多个PlatformTransactionManager实例,您可以通过使用@Transactional("myTxMgr")@Transactional(transactionManager = "myTxMgr")进行限定,或者可以通过@Configuration类实现TransactionManagementConfigurer。请参阅javadoc中的TestContextTransactionUtils.retrieveTransactionManager(),了解用于查找测试的ApplicationContext中事务管理器的算法的详细信息。

所有与事务相关的注解演示

以下基于JUnit Jupiter的示例展示了一个虚构的集成测试场景,突出显示了所有与事务相关的注解。该示例并不旨在展示最佳实践,而是展示这些注解如何使用。有关更多信息和配置示例,请参阅注解支持部分。使用@Sql进行声明性SQL脚本执行的事务管理包含了一个额外的示例,使用@Sql进行具有默认事务回滚语义的声明性SQL脚本执行。以下示例展示了相关的注解:

  • Java

  • Kotlin

@SpringJUnitConfig
@Transactional(transactionManager = "txMgr")
@Commit
class FictitiousTransactionalTest {

	@BeforeTransaction
	void verifyInitialDatabaseState() {
		// 在事务开始之前验证初始状态的逻辑
	}

	@BeforeEach
	void setUpTestDataWithinTransaction() {
		// 在事务内设置测试数据
	}

	@Test
	// 覆盖类级别的@Commit设置
	@Rollback
	void modifyDatabaseWithinTransaction() {
		// 使用测试数据并修改数据库状态的逻辑
	}

	@AfterEach
	void tearDownWithinTransaction() {
		// 在事务内运行“拆除”逻辑
	}

	@AfterTransaction
	void verifyFinalDatabaseState() {
		// 在事务回滚后验证最终状态的逻辑
	}

}
@SpringJUnitConfig
@Transactional(transactionManager = "txMgr")
@Commit
class FictitiousTransactionalTest {

	@BeforeTransaction
	fun verifyInitialDatabaseState() {
		// 在事务开始之前验证初始状态的逻辑
	}

	@BeforeEach
	fun setUpTestDataWithinTransaction() {
		// 在事务内设置测试数据
	}

	@Test
	// 覆盖类级别的@Commit设置
	@Rollback
	fun modifyDatabaseWithinTransaction() {
		// 使用测试数据并修改数据库状态的逻辑
	}

	@AfterEach
	fun tearDownWithinTransaction() {
		// 在事务内运行“拆除”逻辑
	}

	@AfterTransaction
	fun verifyFinalDatabaseState() {
		// 在事务回滚后验证最终状态的逻辑
	}

}
测试ORM代码时避免误报

当测试应用程序代码操作Hibernate会话或JPA持久性上下文的状态时,请确保在运行该代码的测试方法中刷新底层工作单元。未刷新底层工作单元可能导致误报:您的测试通过了,但相同的代码在生产环境中抛出异常。请注意,这适用于任何维护内存中工作单元的ORM框架。在以下基于Hibernate的示例测试用例中,一个方法展示了一个误报,另一个方法正确地展示了刷新会话的结果:

  • Java

  • Kotlin

// ...

@Autowired
SessionFactory sessionFactory;

@Transactional
@Test // 没有预期异常!
public void falsePositive() {
	updateEntityInHibernateSession();
	// 误报:一旦Hibernate会话最终刷新(即在生产代码中),将抛出异常
}

@Transactional
@Test(expected = ...)
public void updateWithSessionFlush() {
	updateEntityInHibernateSession();
	// 需要手动刷新以避免测试中的误报
	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.flush();
}

// ...
// ...

@PersistenceContext
lateinit var entityManager:EntityManager

@Transactional
@Test // 没有预期异常!
fun falsePositive() {
	updateEntityInJpaPersistenceContext()
	// 误报:一旦JPA EntityManager最终刷新(即在生产代码中),将抛出异常
}

@Transactional
@Test(expected = ...)
void updateWithEntityManagerFlush() {
	updateEntityInJpaPersistenceContext()
	// 需要手动刷新以避免测试中的误报
	entityManager.flush()
}

// ...
Testing ORM entity lifecycle callbacks

Similar to the note about avoiding false positives when testing ORM code, if your application makes use of entity lifecycle callbacks (also known as entity listeners), make sure to flush the underlying unit of work within test methods that run that code. Failing to flush or clear the underlying unit of work can result in certain lifecycle callbacks not being invoked.

For example, when using JPA, @PostPersist, @PreUpdate, and @PostUpdate callbacks will not be called unless entityManager.flush() is invoked after an entity has been saved or updated. Similarly, if an entity is already attached to the current unit of work (associated with the current persistence context), an attempt to reload the entity will not result in a @PostLoad callback unless entityManager.clear() is invoked before the attempt to reload the entity.

The following example shows how to flush the EntityManager to ensure that @PostPersist callbacks are invoked when an entity is persisted. An entity listener with a @PostPersist callback method has been registered for the Person entity used in the example.

  • 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...
}

// ...

See JpaEntityListenerTests in the Spring Framework test suite for working examples using all JPA lifecycle callbacks.