事务管理

在 TestContext 框架中,事务由 TransactionalTestExecutionListener 管理,即使您未在测试类上显式声明 @TestExecutionListeners,默认情况下也会配置该监听器。 但是,要启用对事务的支持,您必须在通过 @ContextConfiguration 语义加载的 ApplicationContext 中配置 PlatformTransactionManager Bean(稍后将提供更多详细信息)。 此外,您必须在测试的类或方法级别声明 Infra @Transactional 注解。

测试管理的事务

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

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

当结合使用测试框架的任何形式的抢占式超时与 Infra 测试管理的事务时,必须谨慎。

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

可能发生这种情况的情况包括但不限于以下情况。

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

请注意,AbstractTransactionalJUnit4InfraContextTestsAbstractTransactionalTestNGInfraContextTests 已预先配置为在类级别提供事务支持。

以下示例演示了为基于 Hibernate 的 UserRepository 编写集成测试的常见场景:

  • Java

@JUnitConfig(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() {
    // 跟踪测试数据库中的初始状态:
    final int count = countRowsInTable("user");

    User user = new User(...);
    repository.save(user);

    // 需要手动刷新以避免测试中的误报
    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"));
  }
}

事务回滚和提交行为 中所述,createUser() 方法运行后无需清理数据库,因为对数据库所做的任何更改都会由 TransactionalTestExecutionListener 自动回滚。

事务回滚和提交行为

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

编程式事务管理

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

以下示例演示了 TestTransaction 的一些功能。 有关更多详细信息,请参阅 TestTransaction 的 javadoc。

  • Java

@ContextConfiguration(classes = TestConfig.class)
public class ProgrammaticTransactionManagementTests extends
    AbstractTransactionalJUnit4InfraContextTests {

  @Test
  public void transactionalTest() {
    // 断言测试数据库中的初始状态:
    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 注解,正是为了这种场景。 您可以使用这些注解之一注解测试类中的任何 void 方法或测试接口中的任何 void 默认方法,并且 TransactionalTestExecutionListener 确保您的事务前方法或事务后方法在适当的时间运行。

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

但是,从 TODAY Framework 6.1 开始,对于在 JUnit Jupiter 中使用 InfraExtension 的测试,@BeforeTransaction@AfterTransaction 方法可以选择接受参数,这些参数将由任何注册的 JUnit ParameterResolver 扩展(例如 InfraExtension)解析。 这意味着可以将 JUnit 特定的参数(如 TestInfo)或来自测试 ApplicationContext 的 Bean 提供给 @BeforeTransaction@AfterTransaction 方法,如下例所示。

  • Java

@BeforeTransaction
void verifyInitialDatabaseState(@Autowired DataSource dataSource) {
	// 使用 DataSource 在启动事务之前验证初始状态
}

任何 before 方法(例如使用 JUnit Jupiter 的 @BeforeEach 注解的方法)和任何 after 方法(例如使用 JUnit Jupiter 的 @AfterEach 注解的方法)都在事务性测试方法的测试管理事务中运行。

同样,使用 @BeforeTransaction@AfterTransaction 注解的方法仅针对事务性测试方法运行。

配置事务管理器

TransactionalTestExecutionListener 期望在测试的 Infra ApplicationContext 中定义一个 PlatformTransactionManager Bean。 如果在测试的 ApplicationContext 中有多个 PlatformTransactionManager 实例,您可以使用 @Transactional("myTxMgr")@Transactional(transactionManager = "myTxMgr") 声明限定符,或者可以由 @Configuration 类实现 TransactionManagementConfigurer。 有关用于在测试的 ApplicationContext 中查找事务管理器的算法的详细信息,请参阅 TestContextTransactionUtils.retrieveTransactionManager() 的 javadoc

所有事务相关注解的演示

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

  • Java

@JUnitConfig
@Transactional(transactionManager = "txMgr")
@Commit
class FictitiousTransactionalTest {

  @BeforeTransaction
  void verifyInitialDatabaseState() {
    // 在启动事务之前验证初始状态的逻辑
  }

  @BeforeEach
  void setUpTestDataWithinTransaction() {
    // 在事务中设置测试数据
  }

  @Test
  // 覆盖类级 @Commit 设置
  @Rollback
  void modifyDatabaseWithinTransaction() {
    // 使用测试数据并修改数据库状态的逻辑
  }

  @AfterEach
  void tearDownWithinTransaction() {
    // 在事务中运行 "拆解" 逻辑
  }

  @AfterTransaction
  void verifyFinalDatabaseState() {
    // 在事务回滚后验证最终状态的逻辑
  }

}
测试 ORM 代码时避免误报

当您测试操作 Hibernate 会话或 JPA 持久性上下文状态的应用程序代码时,请务必在运行该代码的测试方法中刷新底层工作单元。 未能刷新底层工作单元可能会产生误报:您的测试通过了,但相同的代码在实时生产环境中抛出异常。 请注意,这适用于任何维护内存中工作单元的 ORM 框架。 在以下基于 Hibernate 的示例测试用例中,一个方法演示了误报,另一个方法正确公开了刷新会话的结果:

  • Java

// ...

@Autowired
SessionFactory sessionFactory;

@Transactional
@Test // 没有预期的异常!
public void falsePositive() {
  updateEntityInHibernateSession();
  // 误报:一旦 Hibernate Session 最终被刷新(即在生产代码中),就会抛出异常
}

@Transactional
@Test(expected = ...)
public void updateWithSessionFlush() {
  updateEntityInHibernateSession();
  // 需要手动刷新以避免测试中的误报
  sessionFactory.getCurrentSession().flush();
}

// ...

以下示例显示了 JPA 的匹配方法:

  • Java

// ...

@PersistenceContext
EntityManager entityManager;

@Transactional
@Test // 没有预期的异常!
public void falsePositive() {
  updateEntityInJpaPersistenceContext();
  // 误报:一旦 JPA EntityManager 最终被刷新(即在生产代码中),就会抛出异常
}

@Transactional
@Test(expected = ...)
public void updateWithEntityManagerFlush() {
  updateEntityInJpaPersistenceContext();
  // 需要手动刷新以避免测试中的误报
  entityManager.flush();
}

// ...
测试 ORM 实体生命周期回调

类似于关于在测试 ORM 代码时避免 误报 的说明,如果您的应用程序使用了实体生命周期回调(也称为实体监听器),请务必在运行该代码的测试方法中刷新底层工作单元。 未能 刷新 (flush)清除 (clear) 底层工作单元可能会导致某些生命周期回调未被调用。

例如,当使用 JPA 时,除非在保存或更新实体后调用 entityManager.flush(),否则不会调用 @PostPersist@PreUpdate@PostUpdate 回调。 同样,如果实体已附加到当前工作单元(与当前持久性上下文关联),则除非在尝试重新加载实体之前调用 entityManager.clear(),否则重新加载实体的尝试将不会导致 @PostLoad 回调。

以下示例显示了如何刷新 EntityManager 以确保在持久化实体时调用 @PostPersist 回调。 已为示例中使用的 Person 实体注册了具有 @PostPersist 回调方法的实体监听器。

  • Java

// ...

@Autowired
JpaPersonRepository repo;

@PersistenceContext
EntityManager entityManager;

@Transactional
@Test
void savePerson() {
  // EntityManager#persist(...) 导致 @PrePersist 但不是 @PostPersist
  repo.save(new Person("Jane"));

  // 需要手动刷新才能调用 @PostPersist 回调
  entityManager.flush();

  // 依赖于 @PostPersist 回调的测试代码
  // 已经被调用...
}

// ...

请参阅 TODAY Framework 测试套件中的 JpaEntityListenerTests, 了解使用所有 JPA 生命周期回调的工作示例。