对于最新稳定版本,请使用 Spring Framework 7.0.6spring-doc.cadn.net.cn

事务管理

在 TestContext 框架中,事务由 TransactionalTestExecutionListener 管理,该监听器默认已配置,即使您未在测试类上显式声明 @TestExecutionListeners。然而,要启用对事务的支持,您必须在通过 PlatformTransactionManager 语义加载的 ApplicationContext 中配置一个 @ContextConfiguration bean(稍后会提供更详细的说明)。此外,您必须在测试类或测试方法级别上声明 Spring 的 @Transactional 注解。spring-doc.cadn.net.cn

测试管理的事务

测试管理的事务(test-managed transactions)是指通过声明式方式使用 TransactionalTestExecutionListener,或以编程方式使用 TestTransaction(稍后描述)来管理的事务。你不应将这类事务与 Spring 管理的事务(即在为测试加载的 ApplicationContext 中由 Spring 直接管理的事务)或应用程序管理的事务(即在测试调用的应用程序代码中以编程方式管理的事务)相混淆。Spring 管理的事务和应用程序管理的事务通常会参与到测试管理的事务中。然而,如果 Spring 管理或应用程序管理的事务配置了除 REQUIREDSUPPORTS 之外的任何传播行为类型,则应格外谨慎(详情请参见关于事务传播的讨论)。spring-doc.cadn.net.cn

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

在使用测试框架中的任何形式的抢占式超时机制时,若同时结合 Spring 管理的测试事务,必须格外谨慎。spring-doc.cadn.net.cn

具体来说,Spring 的测试支持会在当前测试方法被调用之前,通过一个 java.lang.ThreadLocal 变量将事务状态绑定到当前线程。如果测试框架为了支持抢占式超时而在一个新线程中调用当前测试方法,那么在该测试方法内执行的任何操作都不会在测试管理的事务中执行。因此,此类操作的结果不会随测试管理的事务一起回滚。相反,即使 Spring 正确地回滚了测试管理的事务,这些操作仍会被提交到持久化存储(例如关系型数据库)中。spring-doc.cadn.net.cn

可能发生这种情况的情形包括但不限于以下几种。spring-doc.cadn.net.cn

启用和禁用事务

使用 @Transactional 注解测试方法会导致该测试在事务中运行,默认情况下,该事务会在测试完成后自动回滚。 如果测试类使用了 @Transactional 注解,则该类层次结构中的每个测试方法都会在事务中运行。 未在类或方法级别使用 @Transactional 注解的测试方法不会在事务中运行。 请注意,@Transactional 不支持用于测试生命周期方法——例如,使用 JUnit Jupiter 的 @BeforeAll@BeforeEach 等注解的方法。 此外,即使测试方法使用了 @Transactional 注解,但如果其 propagation 属性被设置为 NOT_SUPPORTEDNEVER,则该测试也不会在事务中运行。spring-doc.cadn.net.cn

表1. @Transactional 属性支持
属性 支持测试管理的事务

valuetransactionManagerspring-doc.cadn.net.cn

是的spring-doc.cadn.net.cn

propagationspring-doc.cadn.net.cn

仅支持 Propagation.NOT_SUPPORTEDPropagation.NEVERspring-doc.cadn.net.cn

isolationspring-doc.cadn.net.cn

nospring-doc.cadn.net.cn

timeoutspring-doc.cadn.net.cn

nospring-doc.cadn.net.cn

readOnlyspring-doc.cadn.net.cn

nospring-doc.cadn.net.cn

rollbackForrollbackForClassNamespring-doc.cadn.net.cn

否:请改用 TestTransaction.flagForRollback()spring-doc.cadn.net.cn

noRollbackFornoRollbackForClassNamespring-doc.cadn.net.cn

否:请改用 TestTransaction.flagForCommit()spring-doc.cadn.net.cn

方法级别的生命周期方法——例如,使用 JUnit Jupiter 的 @BeforeEach@AfterEach 注解的方法——会在测试管理的事务中运行。另一方面,套件级别和类级别的生命周期方法——例如,使用 JUnit Jupiter 的 @BeforeAll@AfterAll 注解的方法,以及使用 TestNG 的 @BeforeSuite@AfterSuite@BeforeClass@AfterClass 注解的方法——不会在测试管理的事务中运行。spring-doc.cadn.net.cn

如果你需要在套件级别或类级别的生命周期方法中运行事务内的代码,你可能希望将相应的 PlatformTransactionManager 注入到你的测试类中,然后结合 TransactionTemplate 进行编程式事务管理。spring-doc.cadn.net.cn

以下示例演示了为基于 Hibernate 的 UserRepository 编写集成测试的常见场景:spring-doc.cadn.net.cn

@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自动回滚。spring-doc.cadn.net.cn

事务回滚与提交行为

默认情况下,测试事务在测试完成后会自动回滚;但是,可以通过 @Commit@Rollback 注解以声明方式配置事务的提交和回滚行为。更多详细信息,请参见注解支持部分中的相应条目。spring-doc.cadn.net.cn

编程式事务管理

您可以使用 TestTransaction 中的静态方法以编程方式与测试管理的事务进行交互。例如,您可以在测试方法、前置方法(before methods)和后置方法(after methods)中使用 TestTransaction 来启动或结束当前测试管理的事务,或者将当前测试管理的事务配置为回滚或提交。TestTransaction 的支持在启用 TransactionalTestExecutionListener 时会自动可用。spring-doc.cadn.net.cn

以下示例展示了 TestTransaction 的一些功能。有关更多详细信息,请参阅 TestTransaction 的 Javadoc。spring-doc.cadn.net.cn

@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 将确保你的事务前方法或事务后方法在适当的时机执行。spring-doc.cadn.net.cn

任何前置方法(例如使用 JUnit Jupiter 的 @BeforeEach 注解标注的方法) 和任何后置方法(例如使用 JUnit Jupiter 的 @AfterEach 注解标注的方法) 都会在事务内执行。此外,对于未配置为在事务内运行的测试方法, 不会执行使用 @BeforeTransaction@AfterTransaction 注解标注的方法。

配置事务管理器

TransactionalTestExecutionListener 期望在测试的 Spring ApplicationContext 中定义一个 PlatformTransactionManager Bean。如果测试的 ApplicationContext 中存在多个 PlatformTransactionManager 实例,您可以通过使用 @Transactional("myTxMgr")@Transactional(transactionManager = "myTxMgr") 来声明限定符,或者由 @Configuration 类实现 TransactionManagementConfigurer。请参阅 TestContextTransactionUtils.retrieveTransactionManager() 的 Javadoc,了解用于在测试的 ApplicationContext 中查找事务管理器的算法详情。spring-doc.cadn.net.cn

所有事务相关注解的演示

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

@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)。如果不刷新底层的工作单元,可能会产生误报(false positives):你的测试通过了,但相同的代码在真实的生产环境中却会抛出异常。请注意,这一点适用于任何维护内存中工作单元的 ORM 框架。在下面基于 Hibernate 的示例测试用例中,一个方法演示了误报的情况,而另一个方法则正确地通过刷新会话暴露了实际结果:spring-doc.cadn.net.cn

// ...

@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 的方法匹配:spring-doc.cadn.net.cn

// ...

@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 代码时避免误报的说明类似,如果你的应用程序使用了实体生命周期回调(也称为实体监听器),请确保在运行该代码的测试方法中刷新底层的工作单元。未能flush(刷新)或clear(清空)底层工作单元可能导致某些生命周期回调未被调用。spring-doc.cadn.net.cn

例如,在使用 JPA 时,除非在保存或更新实体后调用了 @PostPersist,否则 @PreUpdate@PostUpdateentityManager.flush() 回调方法将不会被触发。同样地,如果一个实体已经附加到当前工作单元(与当前持久化上下文关联),那么在尝试重新加载该实体时,除非在重新加载之前调用了 @PostLoad,否则不会触发 entityManager.clear() 回调。spring-doc.cadn.net.cn

以下示例展示了如何刷新 EntityManager,以确保在持久化实体时调用 @PostPersist 回调。示例中使用的 @PostPersist 实体已注册了一个包含 Person 回调方法的实体监听器。spring-doc.cadn.net.cn

// ...

@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 Framework 测试套件中的 JpaEntityListenerTests ,其中包含了使用所有 JPA 生命周期回调的示例。spring-doc.cadn.net.cn