Java单元测试实践-28.spring-test数据库操作自动回滚处理

Java单元测试实践-00.目录(9万多字文档+700多测试示例)
https://blog.csdn.net/a82514921/article/details/107969340

1. spring-test数据库操作自动回滚处理

在进行单元测试时,可以使用spring-test提供的支持,使执行单元测试时的数据库操作使用事务,在测试方法结束时可以回滚数据库操作。

通过上述方法,可以使执行单元测试时不对数据库数据产生影响,也存在一些例外场景需要具体分析。

1.1. spring-test提供的事务相关类与注解

1.1.1. AbstractTransactionalJUnit4SpringContextTests类

参考 https://docs.spring.io/spring/docs/4.3.26.RELEASE/spring-framework-reference/htmlsingle/#testcontext-support-classes-junit4 。

AbstractTransactionalJUnit4SpringContextTests是AbstractJUnit4SpringContextTests的抽象事务扩展,它为JDBC访问添加了一些便利功能。该类要求在ApplicationContext中定义javax.sql.DataSource bean和PlatformTransactionManager bean。扩展AbstractTransactionalJUnit4SpringContextTests时,可以访问受保护的jdbcTemplate实例变量,该变量可用于执行SQL语句以查询数据库,可用于在执行与数据库相关的应用程序代码之前和之后确认数据库状态,Spring确保此类查询在与应用程序代码相同的事务范围内运行。

在Spring应用中,通常会定义org.springframework.jdbc.datasource.DataSourceTransactionManager类,作为事务配置中的transaction-manager参数对应的对象,DataSourceTransactionManager类是PlatformTransactionManager接口的实现类。

1.1.2. TransactionalTestExecutionListener类

参考 https://docs.spring.io/spring/docs/4.3.26.RELEASE/spring-framework-reference/htmlsingle/#testcontext-tel-config ,Spring默认提供的TestExecutionListener实现中,包含TransactionalTestExecutionListener,其提供具有默认回滚语义的事务测试执行。

参考 https://docs.spring.io/spring/docs/4.3.26.RELEASE/spring-framework-reference/htmlsingle/#testcontext-tx 。

在TestContext框架中,事务由默认配置的TransactionalTestExecutionListener管理,即使未在测试类显示地声明@TestExecutionListeners注解时也生效。为了启用事务的支持,需要在通过@ContextConfiguration加载的ApplicationContext中配置一个PlatformTransactionManager Bean;除此之外,需要在测试类级别或测试方法级别声明Spring的@Transactional注解。

测试管理的事务可以通过声明式的TransactionalTestExecutionListener或编程式的TestTransaction进行管理。测试管理的事务与Spring管理的事务,及应用管理的事务不同,后两者通常会参与测试管理的事务。当Spring管理的事务或应用管理的事务使用REQUIRED或SUPPORTS之外的传播类型时,需要特别注意。

将测试方法声明@Transactional注解会使得测试使用事务执行,默认情况下会在测试执行完毕后自动回滚。若在测试类使用了@Transactional注解,则该测试类每个测试方法在执行时都会使用事务。未声明@Transactional注解的测试方法(在测试类或测试方法级别)执行时不会使用事务。

事务的提交与回滚行为可以通过@Commit与@Rollback注解进行配置。

1.1.3. @Rollback注解

参考 https://docs.spring.io/spring/docs/4.3.26.RELEASE/spring-framework-reference/htmlsingle/#__rollback 。

@Rollback注解指定使用了事务的测试方法在完成时是否应当回滚。当@Rollback注解value值为true时,事务会回滚,否则会提交。@Rollback注解value值默认为true。

@Rollback注解可在测试类级别或测试方法级别指定,若在测试类级别指定,则该测试类的所有测试方法会使用相同的@Rollback注解配置。

1.1.4. @Commit注解

参考 https://docs.spring.io/spring/docs/4.3.26.RELEASE/spring-framework-reference/htmlsingle/#__commit 。

@Commit注解指定使用了事务的测试方法在完成时应当提交。@Commit可以作为@Rollback(false)的替代,代码会更明确。与@Rollback注解类似,@Commit注解也可以声明在测试类级别或测试方法级别。

1.2. 使数据库操作使用事务自动回滚的配置

使单元测试的数据库操作使用事务,且自动回滚的配置如下。

1.2.1. 配置TransactionalTestExecutionListener

为了使单元测试使用事务,需要配置TransactionalTestExecutionListener类,可以通过继承AbstractTransactionalJUnit4SpringContextTests类,或直接使用TransactionalTestExecutionListener类的方式实现:

  • 继承AbstractTransactionalJUnit4SpringContextTests类

单元测试类可直接或间接继承自AbstractTransactionalJUnit4SpringContextTests类,在AbstractTransactionalJUnit4SpringContextTests类中通过@TestExecutionListeners注解指定了TransactionalTestExecutionListener类,在类级别指定了@Transactional注解。

  • 直接使用TransactionalTestExecutionListener类

在单元测试类或超类中可以通过@TestExecutionListeners注解直接指定TransactionalTestExecutionListener类,并在测试类(或超类)或测试方法级别声明@Transactional注解。

示例如下:

@TestExecutionListeners({TransactionalTestExecutionListener.class})
@Transactional

@Transactional注解可以在测试类级别或测试方法级别指定,可参考示例TestDbRollbackBase、TestDBRB_CYTM类。

1.2.2. 配置@Rollback注解

完成对TransactionalTestExecutionListener的配置后,测试执行时会使用事务。

在测试类或测试方法级别若不指定@Rollback注解(也不指定@Commit注解),当使用事务时,默认会自动回滚。可参考示例TestDBRB_CY1类;

在测试类或测试方法级别指定@Rollback注解时,效果同上,当使用事务时,会自动回滚。可参考示例TestDBRB_CY2、TestDBRB_M类。

1.3. 使数据库操作使用事务最终提交的配置

使单元测试的数据库操作使用事务,且最终提交的配置如下。

1.3.1. 配置TransactionalTestExecutionListener

同上,略。

1.3.2. 配置@Commit注解

为了使测试执行时使用事务,且最终提交,可在测试类或测试方法级别使用@Commit注解(或@Rollback(false)注解)。可参考示例TestDBRB_CN类。

在同一个测试类中的不同的测试方法,可以使用不同的注解,使得部分方法自动回滚,部分方法最终提交。可参考示例TestDBRB_M方法。

1.4. 单元测试使用事务时的相关日志

为了使spring-test在日志中打印事务相关处理的日志,需要在日志配置中,允许打印org.springframework.test.context.transaction.TransactionContext类的日志,级别为INFO。

完成上述配置后,当单元测试执行时使用事务时,日志中会出现以下事务相关内容:

  • 开始事务时的日志

在开始事务时的日志示例如下,可以看到“Began transaction (…) for test context”内容:

TransactionContext.startTransaction(TransactionContext.java:106)  -

 Began transaction (1) for test context [DefaultTestContext@5328a9c1 testClass = TestDBRB_CYTM,

 testInstance = adrninistrator.test.testdatabase.rollback.TestDBRB_CYTM@5b78fdb1,
 testMethod = testWithTx@TestDBRB_CYTM,
 testException = [null],
 mergedContextConfiguration = [MergedContextConfiguration@44d70181 testClass = TestDBRB_CYTM,
 locations = '{classpath:applicationContext.xml}',
 classes = '{}',
 contextInitializerClasses = '[]',
 activeProfiles = '{}',
 propertySourceLocations = '{}',
 propertySourceProperties = '{}',
 contextCustomizers = set[[empty]],
 contextLoader = 'org.springframework.test.context.support.DelegatingSmartContextLoader',
 parent = [null]]]; transaction manager [org.springframework.jdbc.datasource.DataSourceTransactionManager@35c09b94]; rollback [true]
  • 提交事务时的日志

在提交事务时的日志示例如下,可以看到“Committed transaction for test”内容:

TransactionContext.endTransaction(TransactionContext.java:140)  -

 Committed transaction for test: [DefaultTestContext@70325d20 testClass = TestDBRB_M,

 testInstance = adrninistrator.test.testdatabase.rollback.TestDBRB_M@7c2327fa,
 testMethod = testNoRollback@TestDBRB_M,
 testException = [null],
 mergedContextConfiguration = [MergedContextConfiguration@23aae55 testClass = TestDBRB_M,
 locations = '{classpath:applicationContext.xml}',
 classes = '{}',
 contextInitializerClasses = '[]',
 activeProfiles = '{}',
 propertySourceLocations = '{}',
 propertySourceProperties = '{}',
 contextCustomizers = set[[empty]],
 contextLoader = 'org.springframework.test.context.support.DelegatingSmartContextLoader',
 parent = [null]]]
  • 回滚事务时的日志

在回滚事务时的日志示例如下,可以看到“Rolled back transaction for test”内容:

TransactionContext.endTransaction(TransactionContext.java:140)  -

 Rolled back transaction for test: [DefaultTestContext@70325d20 testClass = TestDBRB_M,

 testInstance = adrninistrator.test.testdatabase.rollback.TestDBRB_M@6ce90bc5,
 testMethod = testRollback1@TestDBRB_M,
 testException = [null],
 mergedContextConfiguration = [MergedContextConfiguration@23aae55 testClass = TestDBRB_M,
 locations = '{classpath:applicationContext.xml}',
 classes = '{}',
 contextInitializerClasses = '[]',
 activeProfiles = '{}',
 propertySourceLocations = '{}',
 propertySourceProperties = '{}',
 contextCustomizers = set[[empty]],
 contextLoader = 'org.springframework.test.context.support.DelegatingSmartContextLoader',
 parent = [null]]]

1.5. 测试代码与被测试代码均使用事务时的冲突

以下测试代码与被测试代码中使用事务时,@Transactional注解的事务传播类型propagation均使用默认的REQUIRED。

若在被测试代码中使用了事务,可能会影响被测试代码中的事务,在使用时需要注意。

以下所述的被测试代码,在事务中先后执行了数据库操作1与数据库操作2。

  • 仅在被测试代码中使用事务时,被测试代码在执行时,若数据库操作1正常,操作2出现异常,则操作1会回滚,在测试代码中读取对应操作1对应记录已回滚。可参考示例TestDatabaseTxMockMem类。

  • 测试代码与被测试代码中均使用事务,测试方法指定事务结束后自动回滚,被测试代码在执行时,若数据库操作1正常,操作2出现异常,此时并不会立即回滚(需要等待测试方法执行完毕后统一回滚),在测试代码中读取对应操作1对应记录未回滚。可参考示例TestDBRB_CYTx1类。

  • 测试代码与被测试代码中均使用事务,测试方法通过@Commit注解指定事务结束后最终提交,被测试代码在执行时,若数据库操作1正常,操作2出现异常,此时并不会回滚;当测试方法执行完毕时,由于尝试进行提交,会出现以下异常。可参考示例TestDBRB_CYTx2类。

org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only

事务注解的事务传播类型使用其他值的情况略。

1.6. 未出现异常时回滚,出现异常时不回滚的优化处理

使用TransactionalTestExecutionListener作为测试执行监听器时,测试方法执行时若出现异常(被测试代码抛出异常、断言检查不通过等),也会进行回滚。可参考示例TestDBRB_CYError类。

从实际使用经验来看,在进行单元测试时,若使用自动回滚的方式使单元测试对数据库的修改回滚,对于执行失败的测试方法,数据不回滚更便于通过数据库中保留的记录分析问题出现的原因。

为了使执行单元测试时使用事务,在未出现异常时进行回滚,出现异常时不进行回滚,可按照以下方式进行优化处理。

  • 创建继承自TransactionalTestExecutionListener类的自定义类,重载afterTestMethod()方法,在该方法中进行以下处理;
  • 调用TestContext.getTestException()方法判断测试方法执行是否出现异常;
  • 从ThreadLocal获取当前使用的TransactionContext对象,当出现异常时,将其flaggedForRollback变量设置为false,使其后续执行commit而不是rollback;
  • 调用父类的afterTestMethod()方法,执行原有的测试方法结束前处理。

上述自定义类需要定义在test模块的org.springframework.test.context.transaction包中,因为TransactionContext类不是public。

经过以上处理后,可以使单元测试时使用事务,在未出现异常时进行回滚,出现异常时不进行回滚。上述自定义类示例如下,可参考示例TransactionalTestErrorSkipExecutionListener类。

public class TransactionalTestErrorSkipExecutionListener extends TransactionalTestExecutionListener {

    private static final Logger logger = LoggerFactory.getLogger(TransactionalTestErrorSkipExecutionListener.class);

    @Override
    public void afterTestMethod(TestContext testContext) throws Exception {
        String testClassName = testContext.getTestClass().getSimpleName();
        String testMethodName = testContext.getTestMethod().getName();

        boolean commit = false;

        Throwable throwable = testContext.getTestException();
        if (throwable != null) {
            logger.info("出现异常,不回滚(提交)数据库操作 {}.{}() {} {}", testClassName, testMethodName, throwable.getClass().getSimpleName(),
                    throwable.getMessage());
            commit = true;
        } else {
            logger.info("未出现异常,回滚(不提交)数据库操作 {}.{}()", testClassName, testMethodName);
        }

        TransactionContext txContext = TransactionContextHolder.getCurrentTransactionContext();
        if (txContext != null && commit) {
            // 不回滚(提交)数据库操作时,修改TransactionContext的flaggedForRollback变量为false,使其执行commit而不是rollback
            txContext.setFlaggedForRollback(false);
        }

        super.afterTestMethod(testContext);
    }
}

以上修改TransactionContext的flaggedForRollback变量为false时,调用的“txContext.setFlaggedForRollback(false);”操作可以通过反射实现,如“Whitebox.setInternalState(txContext, “flaggedForRollback”, false);”。

使用上述自定义类TransactionalTestErrorSkipExecutionListener的示例如下,可参考示例TestDBRB_CYErrorSkip类。

@TestExecutionListeners({TransactionalTestErrorSkipExecutionListener.class})
@Transactional

1.7. spring-test事务处理相关代码分析

1.7.1. 是否回滚默认标志初始化

TransactionConfigurationAttributes类中的defaultRollback变量为默认的是否回滚标志,其初始化步骤如下所示:

执行SpringJUnit4ClassRunner类的构造函数,在其中执行createTestContextManager()方法获得TestContextManager对象;

在SpringJUnit4ClassRunner.createTestContextManager()方法中,调用TestContextManager类构造函数TestContextManager(Class testClass);

在TestContextManager类构造函数中,调用TestContextManager类构造函数TestContextManager(TestContextBootstrapper testContextBootstrapper);

在TestContextManager类构造函数中,调用testContextBootstrapper.getTestExecutionListeners()方法,对应AbstractTestContextBootstrapper类的getTestExecutionListeners()方法;

在AbstractTestContextBootstrapper.getTestExecutionListeners()方法中,获得测试类的@TestExecutionListeners注解指定的TestExecutionListener实现类列表,调用instantiateListeners()方法,参数为@TestExecutionListeners注解指定的TestExecutionListener实现类列表;

在AbstractTestContextBootstrapper.instantiateListeners()方法中,遍历传入的@TestExecutionListeners注解指定的TestExecutionListener实现类列表,依次调用BeanUtils.instantiateClass()方法对其进行实例化。当处理到TransactionalTestExecutionListener类时,会对该类进行初始化,调用其中的“TransactionConfigurationAttributes defaultTxConfigAttributes = new TransactionConfigurationAttributes();”方法;

调用TransactionConfigurationAttributes类无参构造函数,在其中调用TransactionConfigurationAttributes类构造函数TransactionConfigurationAttributes(String transactionManagerName, boolean defaultRollback),参数2为true;

在TransactionConfigurationAttributes类构造函数TransactionConfigurationAttributes(String transactionManagerName, boolean defaultRollback)中,将defaultRollback变量值设为true。

1.7.2. 当前测试方法是否需要使用事务判断

TransactionalTestExecutionListener类在测试方法开始执行前,即beforeTestMethod()方法中,会判断当前方法是否需要使用事务执行,步骤如下所示:

在TransactionalTestExecutionListener.beforeTestMethod()方法中,调用TestContext对象的getTestMethod()方法获取当前执行的测试方法,调用getTestClass()方法获取当前执行的测试类;

调用attributeSource.getTransactionAttribute()方法,判断当前执行的测试方法与测试类是否包含使用事务的属性;

若当前执行的测试方法或测试类包含使用事务的属性,则调用getTransactionManager()方法获取PlatformTransactionManager对象;

若PlatformTransactionManager对象获取成功,则创建新的TransactionContext对象,调用的构造函数为TransactionContext(TestContext testContext, PlatformTransactionManager transactionManager, TransactionDefinition transactionDefinition, boolean defaultRollback),参数4使用TransactionalTestExecutionListener.isRollback()方法的返回值;

在TransactionContext类的上述构造函数中,将defaultRollback、flaggedForRollback变量设置为参数4的值;

在TransactionalTestExecutionListener.beforeTestMethod()方法中,调用TransactionContext类的startTransaction()方法开启事务,并将创建的TransactionContext对象设置到ThreadLocal中;

在TransactionContext.startTransaction()方法中,会打印日志“Began transaction (…) for test context”。

1.7.3. 当前测试方法的事务是否回滚标志初始化

参考前文,创建TransactionContext对象时会调用TransactionalTestExecutionListener类的isRollback()方法,设置当前方法的事务是否回滚的标志初始值,步骤如下所示:

在TransactionalTestExecutionListener.isRollback()方法中,调用isDefaultRollback()方法;

在TransactionalTestExecutionListener.isDefaultRollback()方法中,优先返回当前测试类的@Rollback注解的value值;若未找到上述注解则返回TransactionConfigurationAttributes对象的isDefaultRollback()方法对应的值,参考前文可知其值为true;

在TransactionalTestExecutionListener.isRollback()方法中,再获取当前测试方法的@Rollback注解的value值,若获取到则返回;若未找到上述注解则返回前一步获取的值(即当前测试类的@Rollback注解的value值或true)。

根据以上步骤可知,当前方法的事务是否回滚的标志,优先使用当前测试方法的@Rollback注解的value值,若找不到上述注解则使用当前测试类的@Rollback注解的value值,若找不到上述注解则使用TransactionConfigurationAttributes.isDefaultRollback()方法的返回值,即true。

1.7.4. 当前测试方法的事务是否回滚标志修改

调用TransactionContext.setFlaggedForRollback()方法,可将TransactionContext类的flaggedForRollback变量值设置为指定值,即能够修改当前测试方法的事务是否回滚的标志。

1.7.5. 结束事务时回滚还是提交的判断

TransactionalTestExecutionListener类在测试方法执行完毕前,即afterTestMethod()方法中,会判断当前方法是否需要结束事务,以及是回滚还是提交,步骤如下所示:

在TransactionalTestExecutionListener.afterTestMethod()方法中,从ThreadLocal中获取TransactionContext对象,若非空则说明当前方法执行时使用了事务,调用TransactionContext对象的endTransaction()方法结束事务;

在TransactionContext.endTransaction()方法中,判断若flaggedForRollback为true,则调用PlatformTransactionManager对象的rollback()方法,对当前事务进行回滚处理;若flaggedForRollback为false,则调用PlatformTransactionManager对象的commit()方法,对当前事务进行提交处理;

当进行回滚处理时,会打印日志“Rolled back transaction for test:”;当进行提交处理时,会打印日志“Committed transaction for test:”。

你可能感兴趣的:(Java,单元测试,java,单元测试,数据库,spring-test)