事务管理

在 TestContext 框架中,事务由 TransactionalTestExecutionListener 管理,该监听器默认配置,即使您没有在测试类上显式声明 @TestExecutionListeners。但是,要启用对事务的支持,您必须在 ApplicationContext 中配置一个 PlatformTransactionManager bean,该 bean 使用 @ContextConfiguration 语义加载(稍后将提供更多详细信息)。此外,您必须在测试的类级别或方法级别声明 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.NOT_SUPPORTEDPropagation.NEVER

隔离

超时

只读

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将确保您的 before-transaction 方法或 after-transaction 方法在适当的时间运行。

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

但是,从 Spring Framework 6.1 开始,对于使用SpringExtension(与 JUnit Jupiter 结合使用)的测试,@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回调将不会被调用。类似地,如果实体已附加到当前工作单元(与当前持久化上下文相关联),则尝试重新加载实体不会导致@PostLoad回调,除非在尝试重新加载实体之前调用entityManager.clear()

以下示例显示了如何刷新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...
}

// ...

查看 Spring 框架测试套件中的 JpaEntityListenerTests,了解使用所有 JPA 生命周期回调的示例。