在 TestContext 框架中,事务是由 TransactionalTestExecutionListener 管理的,即使你没有在你的测试类上明确声明 @TestExecutionListeners,它也是默认配置的。然而,要启用对事务的支持,你必须在 ApplicationContext 中配置一个PlatformTransactionManager Bean,该 bean 以 @ContextConfiguration 语义加载(后面将提供进一步细节)。此外,你必须在测试的类或方法层声明 Spring 的 @Transactional 注解。
测试管理的事务
测试管理的事务是通过使用 TransactionalTestExecutionListener 来声明性地管理的事务,或通过使用 TestTransaction(后面描述)来程序性地管理的事务。你不应该把这种事务与 Spring 管理的事务(那些直接由 Spring 在测试加载的 ApplicationContext 中管理的事务)或应用程序管理的事务(那些在被测试调用的应用程序代码中以编程方式管理的事务)混淆。Spring 管理的和应用管理的事务通常参与测试管理的事务。然而,如果 Spring 管理的或应用管理的事务被配置为除 REQUIRED 或 SUPPORTS 之外的任何传播类型,你应该谨慎行事(详见关于事务传播的讨论)。
:::tips 抢占式超时和测试管理的事务
当从测试框架中使用任何形式的抢占式超时与 Spring 的测试管理事务相结合时,必须谨慎行事。
具体来说,Spring 的测试支持在当前测试方法被调用之前将事务状态绑定到当前线程(通过 java.lang.ThreadLocal 变量)。如果测试框架在一个新的线程中调用当前的测试方法,以支持抢占式超时,在当前测试方法中执行的任何操作都不会在测试管理的事务中被调用。因此,任何此类操作的结果都不会随测试管理的事务回滚。相反,这些操作将被提交到持久性存储—例如,关系数据库—即使测试管理的事务被 Spring 正确地回滚了。
可能发生这种情况的情况包括但不限于以下情况。
- JUnit 4 的
@Test(timeout = ...)
支持和 TimeOut 规则 - org.junit.jupiter.api.Assertions 类中 JUnit Jupiter 的 assertTimeoutPreemptively(…)方法
- TestNG 的 @Test(timeOut = …) 支持 :::
启用和停用事务
用 @Transactional
来注解一个测试方法会导致测试在一个事务中运行,默认情况下,在测试完成后会自动回滚。如果一个测试类被@Transactional
注解,该类层次中的每个测试方法都会在一个事务中运行。没有用 @Transactional
注解的测试方法(在类或方法级别)不会在事务中运行。注意,@Transactional
不支持测试生命周期方法—例如,用 JUnit Jupiter 的 @BeforeAll、@BeforeEach 等注解的方法。此外,被 @Transactional
注解但传播属性被设置为 NOT_SUPPORTED 或 NEVER 的测试也不会在事务中运行。
Attribute | 是否支持测试管理事务 |
---|---|
value and transactionManager | yes |
propagation | 只支持 Propagation.NOT_SUPPORTED 和 Propagation.NEVER |
isolation | no |
timeout | no |
readOnly | no |
rollbackFor 和 rollbackForClassName | no: 使用 TestTransaction.flagForRollback() 替代 |
noRollbackFor 和 noRollbackForClassName | no: 使用 TestTransaction.flagForCommit() 替代 |
:::tips 方法级的生命周期方法—例如,用 JUnit Jupite r的 @BeforeEach 或 @AfterEach 注解的方法—是在测试管理的事务中运行。另一方面,套装级和类级生命周期方法—例如,用 JUnit Jupiter 的 @BeforeAll 或 @AfterAll 注解的方法和用 TestNG 的 @BeforeSuite、@AfterSuite、@BeforeClass 或 @AfterClass 注解的方法—不能在测试管理事务中运行。
如果你需要在事务中运行套装级或类级生命周期方法的代码,你可能希望在你的测试类中注入一个相应的 PlatformTransactionManager,然后用 TransactionTemplate 进行程序化事务管理。 :::
请注意,AbstractTransactionalJUnit4SpringContextTests 和 AbstractTransactionalTestNGSpringContextTests 是在类的层面上预设了对事务性支持。
下面的例子演示了为基于 Hibernate 的 UserRepository 编写集成测试的一个常见场景。
@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() {
// 追踪测试数据库中的初始状态。
// 查看在数据库中这个 user 表的数据行数
final int count = countRowsInTable("user");
User user = new User(...);
repository.save(user);
// 需要手动 flush 以避免测试中出现假阳性。
// 这个可能是 hibernate 的数据库 sessionFactory 对象
sessionFactory.getCurrentSession().flush();
// 断言表中的数据是否增加了 1 个
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"));
}
}
正如在事务回滚和提交行为中所解释的,在 createUser()
方法运行后,没有必要清理数据库,因为对数据库所做的任何改变都会被TransactionalTestExecutionListener 自动回滚。
事务回滚和提交行为
默认情况下,测试事务将在测试完成后自动回滚;然而,事务提交和回滚行为可以通过 @Commit
和 @Rollback
注解来声明性地配置。更多细节见注解支持部分的相应条目。
编程式事务管理
你可以通过使用 TestTransaction 的静态方法与测试管理的事务进行程序化的交互。例如,你可以在测试方法、before 方法和 after 方法中使用TestTransaction 来开始或结束当前测试管理的事务,或者配置当前测试管理的事务来回滚或提交。只要启用TransactionalTestExecutionListener,对 TestTransaction 的支持就自动可用。
下面的例子演示了 TestTransaction 的一些功能。更多细节请参见 TestTransaction 的 javadoc。
@ContextConfiguration(classes = TestConfig.class)
public class ProgrammaticTransactionManagementTests extends
// 父类提供了前面手动写的哪些 JdbcTemplate 还有查询表中行数的工具类、开启事物注解等的功能
AbstractTransactionalJUnit4SpringContextTests {
@Test
public void transactionalTest() {
// 断言测试数据库的初始状态,只有 2 条数据
assertNumUsers(2);
// 从指定表中删除所有行的便捷方法。
deleteFromTables("user");
// 对数据库的修改将被提交!
TestTransaction.flagForCommit();
TestTransaction.end();
// 确定测试管理的事务当前是否处于活动状态。
assertFalse(TestTransaction.isActive());
assertNumUsers(0);
// 开启一个事物
TestTransaction.start();
// 对数据库进行其他操作,这些操作将
// 在测试完成后自动回滚......
}
protected void assertNumUsers(int expected) {
assertEquals("Number of rows in the [user] table.", expected, countRowsInTable("user"));
}
}
在事务之外运行代码
偶尔,你可能需要在事务性测试方法之前或之后运行某些代码,但在事务性上下文之外—例如,在运行测试之前验证初始数据库状态,或在测试运行后验证预期的事务性提交行为(如果测试被配置为提交事务)。TransactionalTestExecutionListener 支持 @BeforeTransaction 和@AfterTransaction 注解,正是为了这种情况。你可以用这些注解之一来注解测试类中的任何无效方法或测试接口中的任何无效默认方法,TransactionalTestExecutionListener 确保你的事务前方法或事务后方法在适当的时间运行。
:::tips 任何之前的方法(比如用 JUnit Jupiter 的 @BeforeEach 注解的方法)和任何之后的方法(比如用 JUnit Jupiter 的 @AfterEach 注解的方法)都在事务中运行。此外,对于没有被配置为在事务中运行的测试方法,用 @BeforeTransaction 或 @AfterTransaction 注解的方法不会被运行。 :::
配置一个事务管理器
TransactionalTestExecutionListener 期望在测试的 Spring ApplicationContext 中定义一个 PlatformTransactionManager Bean。如果在测试的ApplicationContext 中存在多个 PlatformTransactionManager 实例,你可以通过使用 @Transactional("myTxMgr")
或@Transactional(transactionManager = "myTxMgr")
声明一个限定符,或者 TransactionManagementConfigurer 可以通过 @Configuration 类实现。请查阅 TestContextTransactionUtils.retrieveTransactionManager()
的 javadoc,以了解用于在测试的 ApplicationContext 中查找事务管理器的算法的细节。
所有事务相关注解的演示
下面这个基于 JUnit Jupiter 的例子显示了一个虚构的集成测试场景,突出了所有与事务相关的注解。这个例子不是为了演示最佳实践,而是为了演示如何使用这些注解。更多信息和配置实例请参见注解支持部分。用于 @Sql 的事务管理包含一个额外的例子,该例子将 @Sql 用于具有默认事务回滚语义的声明性 SQL 脚本执行。下面的例子显示了相关的注解:
@SpringJUnitConfig
@Transactional(transactionManager = "txMgr")
@Commit
class FictitiousTransactionalTest {
@BeforeTransaction
void verifyInitialDatabaseState() {
// 在事物开始前验证初始状态的逻辑
}
@BeforeEach
void setUpTestDataWithinTransaction() {
// 在事物中设置测试数据
}
@Test
// 覆盖类级的 @Commit 设置。
@Rollback
void modifyDatabaseWithinTransaction() {
// 使用测试数据和修改数据库状态的逻辑
}
@AfterEach
void tearDownWithinTransaction() {
// 在事物中运行拆分逻辑
}
@AfterTransaction
void verifyFinalDatabaseState() {
// 验证事物回滚后最终状态的逻辑
}
}
<br />**测试 ORM 代码时避免误报**
当你测试操纵 Hibernate 会话或 JPA 持久化上下文状态的应用程序代码时,确保在运行该代码的测试方法中刷新基础工作单元。不冲洗底层工作单元会产生假阳性结果。你的测试通过了,但同样的代码在实际的生产环境中却抛出了异常。注意,这适用于任何维护内存工作单元的 ORM 框架。在下面这个基于 Hibernate 的测试案例中,一个方法显示了一个假阳性,而另一个方法正确地暴露了刷新会话的结果。
// ...
@Autowired
SessionFactory sessionFactory;
@Transactional
@Test // no expected exception!
public void falsePositive() {
updateEntityInHibernateSession();
// 假阳性:一旦 Hibernate 会话最终被刷新,就会抛出一个异常。
// 会话最终被刷新(即,在生产代码中),就会抛出一个异常。
}
@Transactional
@Test(expected = ...)
public void updateWithSessionFlush() {
updateEntityInHibernateSession();
// 需要手动冲洗以避免测试中出现假阳性。
sessionFactory.getCurrentSession().flush();
}
// ...
下面的例子显示了 JPA 的匹配方法。
// ...
@PersistenceContext
EntityManager entityManager;
@Transactional
@Test // no expected exception!
public void falsePositive() {
updateEntityInJpaPersistenceContext();
// 假阳性:一旦 JPA 被刷新,就会抛出一个异常。
// EntityManager 最终被刷新(即,在生产代码中),就会抛出一个异常。
}
@Transactional
@Test(expected = ...)
public void updateWithEntityManagerFlush() {
updateEntityInJpaPersistenceContext();
// Manual flush is required to avoid false positive in test
entityManager.flush();
}
// ...