事务管理

在 TestContext 框架中,事务由 TransactionalTestExecutionListener 管理, 即使您没有在测试类上明确声明 @TestExecutionListeners,它也默认配置。 但是,要启用事务支持,您必须在通过 @ContextConfiguration 语义加载的 ApplicationContext 中配置一个 PlatformTransactionManager bean(稍后提供更多详细信息)。 此外,您必须在测试的类或方法级别声明 Spring 的 @Transactional 注解。

测试管理的事务

测试管理的事务是通过使用 TransactionalTestExecutionListener 声明式管理,或通过使用 TestTransaction 编程式管理(稍后描述)的事务。 您不应将此类事务与 Spring 管理的事务(那些直接由 Spring 在为测试加载的 ApplicationContext 中管理的事务)或应用程序管理的事务(那些在测试调用的应用程序代码中编程式管理的事务)混淆。 Spring 管理的事务和应用程序管理的事务通常参与测试管理的事务。 但是,如果 Spring 管理的事务或应用程序管理的事务配置了除 REQUIREDSUPPORTS 之外的任何传播类型,则应谨慎使用(有关详细信息,请参阅 事务传播 的讨论)。

Example 1. 抢占式超时和测试管理的事务

当将测试框架的任何形式的抢占式超时与 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 的测试不在事务中运行。

Table 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 一起用于编程式事务管理。

请注意,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 来启动或结束当前测试管理的事务,或将当前测试管理的事务配置为回滚或提交。 只要启用了 TransactionalTestExecutionListenerTestTransaction 的支持就会自动可用。

以下示例演示了 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 开始,对于使用 SpringExtension 和 JUnit Jupiter 的测试,@BeforeTransaction@AfterTransaction 方法可以选择接受参数,这些参数将由任何已注册的 JUnit ParameterResolver 扩展(例如 SpringExtension)解析。 这意味着 JUnit 特定的参数(例如 TestInfo)或来自测试 ApplicationContext 的 bean 可以提供给 @BeforeTransaction@AfterTransaction 方法,如下例所示。

Java
@BeforeTransaction
void verifyInitialDatabaseState(@Autowired DataSource dataSource) {
	// Use the DataSource to verify the initial state before a transaction is started
}
Kotlin
@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
// ...

@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();
}

// ...
Kotlin
// ...

@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
// ...

@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();
}

// ...
Kotlin
// ...

@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 回调。 已为示例中使用的 Person 实体注册了一个带有 @PostPersist 回调方法的实体监听器。

Java
// ...

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

// ...
Kotlin
// ...

@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 生命周期回调的实际示例。