Transaction Management

在 TestContext 框架中,事务由 TransactionalTestExecutionListener 管理,该监听器默认情况下已配置,即使你没有在你测试类上显式申报 @TestExecutionListeners。但是,要启用对事务的支持,你必须在使用 @ContextConfiguration 语义加载的 ApplicationContext 中配置一个 PlatformTransactionManager bean(稍后将提供更多详细信息)。此外,你必须为你的测试在类级或方法级上申报 Spring 的 @Transactional 注解。

Test-managed Transactions

测试管理的事务是通过使用 TransactionalTestExecutionListener 声明式管理的事务,或通过使用 TestTransaction 程序化管理的事务(稍后描述)。您不应将此类事务与 Spring 管理的事务(直接由 Spring 在为测试加载的 ApplicationContext 中管理的事务)或应用程序管理的事务(在测试调用的应用程序代码中通过编程管理的事务)混淆。Spring 管理的事务和应用程序管理的事务通常参与测试管理的事务。但是,如果 Spring 管理的事务或应用程序管理的事务配置的传播类型与 REQUIREDSUPPORTS 以外的任何其他传播类型,则应小心(请参阅有关 transaction propagation 的讨论以了解详情)。

Example 1. Preemptive timeouts and test-managed transactions

在将测试框架的任何形式的抢占超时与 Spring 的测试管理事务结合使用时,必须小心。 具体来说,Spring 的测试支持在调用当前测试方法 之前 将事务状态绑定到当前线程(通过 java.lang.ThreadLocal 变量)。如果测试框架在一个新的线程中调用当前测试方法以支持抢占超时,则在当前测试方法中执行的任何操作 不会 在测试管理的事务中被调用。因此,任何此类操作的结果都不会随测试管理的事务一起回滚。相反,这些操作将提交到持久存储 中——例如关系数据库 ——即使 Spring 正确回滚了测试管理的事务也是如此。 可能发生这种情况的场景包括但不限于以下内容。

  • JUnit 4 的 `@Test(timeout = …​)`支持和 `TimeOut`规则

  • JUnit Jupiter 在 `org.junit.jupiter.api.Assertions`类中的 `assertTimeoutPreemptively(…​)`方法

  • TestNG 的 `@Test(timeOut = …​)`支持

Enabling and Disabling Transactions

使用 @Transactional 对测试方法进行注释会导致测试在事务中运行,该事务在默认情况下会在测试完成后自动回滚。如果使用 @Transactional 对一个测试类进行注释,那么该类层次结构中的每个测试方法都在事务中运行。没有使用 @Transactional(在类级或方法级)对进行注释的测试方法不会在事务中运行。请注意, @Transactional 不支持测试生命周期方法——例如,使用 JUnit Jupiter 的 @BeforeAll@BeforeEach 等进行注释的方法。此外,用 @Transactional 进行注释但将 propagation 属性设置为 NOT_SUPPORTEDNEVER 的测试不会在事务中运行。

Table 1. @Transactional attribute support
Attribute Supported for test-managed transactions

value and transactionManager

yes

propagation

仅支持`Propagation.NOT_SUPPORTED`和`Propagation.NEVER`

isolation

no

timeout

no

readOnly

no

rollbackFor and rollbackForClassName

no: use TestTransaction.flagForRollback() instead

noRollbackFor and noRollbackForClassName

no: use TestTransaction.flagForCommit() instead

方法级生命周期方法——例如,使用 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"))
	}
}

Transaction Rollback and Commit Behavior 中所述,无需在运行 createUser() 方法后清理数据库,因为对数据库所做的任何更改都将自动由 TransactionalTestExecutionListener 回滚。

Transaction Rollback and Commit Behavior

默认情况下,测试事务将在测试完成后自动回滚;但是,可以通过 @Commit@Rollback 注释以声明方式配置事务提交和回滚行为。有关更多详细信息,请参阅 annotation support 部分中的相应条目。

Programmatic Transaction Management

你可以使用 TestTransaction 中的静态方法以编程方式与测试管理的事务进行交互。例如,你可以在测试方法、方法之前和方法之后使用 TestTransaction 来启动或结束当前测试管理的事务,或为回滚或提交配置当前测试管理的事务。只要启用了 TransactionalTestExecutionListener,就会自动提供对 TestTransaction 的支持。

以下示例演示了 TestTransaction 的一些功能。有关更多详细信息,请参阅https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/test/context/transaction/TestTransaction.html[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"))
	}
}

Running Code Outside of a Transaction

有时,您可能需要在事务性测试方法之前或之后运行某些代码,但位于事务性上下文之外——例如,为了在运行测试之前验证初始数据库状态或在测试运行之后验证预期的事务提交行为(如果将测试配置为提交事务)。TransactionalTestExecutionListener 支持 @BeforeTransaction@AfterTransaction 注释,用于支持上述场景。您可以使用这些注释之一在测试类中的任何 void 方法或者在测试接口中的任何 void 默认方法上添加注释,而 TransactionalTestExecutionListener 确保您的事务前方法或事务后方法在适当的时间运行。

一般来说,@BeforeTransaction@AfterTransaction 方法不能接受任何参数。 但是,从 Spring Framework 6.1 开始,对于使用SpringExtension和 JUnit Jupiter 的测试,@BeforeTransaction@AfterTransaction 方法可以选择接受参数,这些参数将由任何已注册的 JUnit ParameterResolver 扩展(例如 SpringExtension)解析。这意味着可以向`@BeforeTransaction` 和 @AfterTransaction 方法提供 JUnit 特定的参数,如`TestInfo` 或测试的 ApplicationContext 中的 bean,如下面的示例中所示。

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
}

任何前置方法(例如使用 JUnit Jupiter 的 @BeforeEach 注释的方法)以及任何后置方法(例如使用 JUnit Jupiter 的 @AfterEach`注释的方法)针对事务性测试方法运行时会运行在测试管理的事务中。 类似地,使用 `@BeforeTransaction@AfterTransaction 注释的方法只会针对事务性测试方法运行。

Configuring a Transaction Manager

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

Demonstration of All Transaction-related Annotations

以下基于 JUnit Jupiter 的示例显示了一个虚构的集成测试场景,该场景突出了所有与事务相关的注释。该示例的目的是演示这些注释如何使用,而不是演示最佳实践。有关详细信息和配置示例,请参阅annotation support 部分。Transaction management for @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
	}

}
Avoid false positives when testing ORM code

当您测试操纵 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()
}

// ...
Testing ORM entity lifecycle callbacks

类似于有关在测试 ORM 代码时避免 false positives 的注释,如果你的应用程序利用实体生命周期回调(也称为实体侦听器),请确保在运行该代码的测试方法内刷新底层的工作单元。未能_flush_ 或_clear_ 底层的工作单元可能会导致不调用某些生命周期回调。 例如,当使用 JPA 时,@PostPersist@PreUpdate@PostUpdate 回调不会被调用,除非在保存或更新实体后调用了 entityManager.flush()。类似地,如果某个实体已经附加到当前工作单元(与当前持久性上下文相关联),则尝试重新加载该实体不会导致 @PostLoad 回调,除非在尝试重新加载该实体之前调用了 entityManager.clear()。 以下示例展示了如何刷新 EntityManager 以确保在持久化实体时调用 @PostPersist 回调。一个有 @PostPersist 回调方法的实体侦听器已为示例中使用的 Person 实体注册。

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 生命周期回调的工作示例。