青年IT男

个人从事金融行业,就职过易极付、思建科技、某网约车平台等重庆一流技术团队,目前就职于某银行负责统一支付系统建设。自身对金融行业有强烈的爱好。同时也实践大数据、数据存储、自动化集成和部署、分布式微服务、响应式编程、人工智能等领域。同时也热衷于技术分享创立公众号和博客站点对知识体系进行分享。微信公众号:青年IT男。

Spring 5 中文解析核心篇-集成测试之TestContext(下)

Spring 5 中文解析核心篇-集成测试之TestContext(下)

3.5.7 测试Request和Session作用域bean

Spring从早期开始就支持Request范围和Session范围的Bean,你可以按照以下步骤测试Request范围和Session范围的Bean:

  • 通过使用@WebAppConfiguration注解测试类,确保为测试加载WebApplicationContext
  • 将模拟RequestSession注入到测试实例中,并根据需要准备测试装置。
  • 调用从配置的WebApplicationContext中检索到的Web组件(具有依赖项注入)。
  • 对模拟执行断言。

下一个代码片段显示了登录用例的XML配置。注意,userService bean与请求范围的loginAction bean有依赖关系。另外,通过使用SpEL表达式实例化LoginAction,该表达式从当前HTTP请求中检索用户名和密码。在我们的测试中,我们想通过TestContext框架管理的模拟来配置这些请求参数。以下清单显示了此用例的配置:

Request作用域bean配置

<beans>

    <bean id="userService" class="com.example.SimpleUserService"
            c:loginAction-ref="loginAction"/>

    <bean id="loginAction" class="com.example.LoginAction"
            c:username="#{request.getParameter('user')}"
            c:password="#{request.getParameter('pswd')}"
            scope="request">
        <aop:scoped-proxy/>
    </bean>

</beans>

RequestScopedBeanTests中,我们将UserService(即被测对象)和MockHttpServletRequest都注入到我们的测试实例中。在requestScope()测试方法中,我们通过在提供的MockHttpServletRequest中设置请求参数来设置测试配置。当在我们的userService上调用loginUser()方法时,可以确保该用户服务可以访问当前MockHttpServletRequest(即我们刚刚设置参数的那个)在请求范围内的loginAction。然后,我们可以根据用户名和密码的已知输入对结果进行断言。以下清单显示了如何执行此操作:

@SpringJUnitWebConfig
class RequestScopedBeanTests {

    @Autowired UserService userService;
    @Autowired MockHttpServletRequest request;

    @Test
    void requestScope() {
        request.setParameter("user", "enigma");
        request.setParameter("pswd", "$pr!ng");

        LoginResults results = userService.loginUser();
        // assert results
    }
}

以下代码段类似于我们之前针对请求范围的Bean看到的代码段。但是,这一次,userService bean与会话范围的userPreferences bean有依赖关系。注意,通过使用SpEL表达式实例化UserPreferences bean,该SpEL表达式从当前HTTP会话中检索主题。在我们的测试中,我们需要在由TestContext框架管理的模拟会话中配置主题。以下示例显示了如何执行此操作:

Session作用域bean配置

<beans>

    <bean id="userService" class="com.example.SimpleUserService"
            c:userPreferences-ref="userPreferences" />

    <bean id="userPreferences" class="com.example.UserPreferences"
            c:theme="#{session.getAttribute('theme')}"
            scope="session">
        <aop:scoped-proxy/>
    </bean>

</beans>

SessionScopedBeanTests中,我们将UserServiceMockHttpSession注入我们的测试实例。在sessionScope()测试方法中,我们通过在提供的MockHttpSession中设置期望的主题属性来设置测试配置。当在我们的userService上调用processUserPreferences()方法时,我们可以确保用户服务可以访问当前MockHttpSession的会话范围的userPreferences,并且可以基于配置的主题对结果执行断言。以下示例显示了如何执行此操作:

@SpringJUnitWebConfig
class SessionScopedBeanTests {

    @Autowired UserService userService;
    @Autowired MockHttpSession session;

    @Test
    void sessionScope() throws Exception {
        session.setAttribute("theme", "blue");

        Results results = userService.processUserPreferences();
        // assert results
    }
}

参考代码:org.liyong.test.annotation.test.spring.SessionScopedBeanTests

3.5.8 事物管理

TestContext框架中,事务由TransactionalTestExecutionListener进行管理,默认情况下配置该事务,即使你没有在测试类上显示声明@TestExecutionListeners也不例外。但是,要启用对事务的支持,必须在ApplicationContext中配置使用@ContextConfiguration语义加载的PlatformTransactionManager bean(稍后将提供更多详细信息)。此外,你必须在测试的类或方法级别声明Spring的@Transactional注解。

测试事物管理

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

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

当在测试框架中使用任何形式的抢占式超时并结合使用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的测试不在事务内运行。

@Transactional支持属性表格:

属性 测试管理事务的支持
valuetransactionManager yes
propagation 仅仅支持Propagation.NOT_SUPPORTED
isolation no
timeout no
readOnly no
rollbackForrollbackForClassName no: 使用TestTransaction.flagForRollback()替换
noRollbackFornoRollbackForClassName no: 使用TestTransaction.flagForCommit()替换

方法级生命周期方法(例如,用JUnit Jupiter的@BeforeEach@AfterEach注解的方法)在测试管理的事务中运行。另一方面,套件级和类级生命周期方法(例如,以JUnit Jupiter的@BeforeAll@AfterAll注解的方法以及以TestNG的@BeforeSuite@AfterSuite@BeforeClass@AfterClass注解的方法)不在内部运行测试管理的事务。

如果你需要在事务内的套件级或类级生命周期方法中执行代码,则可能希望将相应的PlatformTransactionManager注入测试类,然后将其与TransactionTemplate一起用于程序化事务管理。

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

下面的示例演示了为基于HibernateUserRepository编写集成测试的常见方案:

@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"));
    }
}

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

参考代码:org.liyong.test.annotation.test.spring.TransactionMergeUserRepositoryTestsorg.liyong.test.annotation.test.spring.TransactionUserRepositoryTests

事物回滚和提交

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

参考代码:org.liyong.test.annotation.test.spring.AnnotationTransactionManagementTests

编程式事物管理

你可以使用TestTransaction中的静态方法以编程方式与测试管理的事务进行交互。例如,可以在测试方法中、方法之前和方法之后使用TestTransaction来启动或结束当前的测试托管事务,或配置当前的测试托管事务以进行回滚或提交。每当启用TransactionalTestExecutionListener时,都会自动提供对TestTransaction的支持。下面的示例演示了TestTransaction的某些功能。有关更多详细信息,请参见javadoc中的TestTransaction

@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"));
    }
}

参考代码:org.liyong.test.annotation.test.spring.ProgrammaticTransactionManagementTests

在事务外运行代码

有时,你可能需要在事务测试方法之前或之后但在事务上下文之外执行某些代码。例如,在运行测试之前验证初始数据库状态或在测试运行之后验证预期的事务提交行为(如果测试已配置为提交事务)。

对于此类情况,TransactionalTestExecutionListener支持@BeforeTransaction@AfterTransaction注解。你可以使用这些注解之一来注释测试类中的任何void方法或测试接口中的任何void默认方法,并且TransactionalTestExecutionListener可以确保你的before事务方法或after事务方法在适当的时间运行。

任何before方法(例如,以JUnit Jupiter的@BeforeEach注解的方法)和任何after方法(例如,以JUnit Jupiter的@AfterEach注解的方法)都在事务中运行。此外,对于未配置为在事务内运行的测试方法,不会运行带有@BeforeTransaction@AfterTransaction注解的方法。

配置事物管理器

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

演示所有与事务相关的注解

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

@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() {
        // execute "tear down" logic within the transaction
    }

    @AfterTransaction
    void verifyFinalDatabaseState() {
        // logic to verify the final state after transaction has rolled back
    }

}

测试ORM代码时避免误报

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

// ...

@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();
}

// ...

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

// ...

@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();
}

// ...
3.5.9 执行SQL脚本

在针对关系数据库编写集成测试时,执行SQL脚本来修改数据库模式或将测试数据插入表中通常是有益的。spring-jdbc模块支持在加载Spring ApplicationContext时通过执行SQL脚本来初始化嵌入式数据库或现有数据库。有关详细信息,请参见嵌入式数据库支持和使用嵌入式数据库测试数据访问逻辑

尽管在加载ApplicationContext时初始化一次数据库以进行测试非常有用,但是有时在集成测试过程中能够修改数据库至关重要。以下各节说明在集成测试期间如何以编程方式和声明方式执行SQL脚本。

编程式地执行SQL脚本

Spring提供了以下选项,用于在集成测试方法中以编程方式执行SQL脚本。

  • org.springframework.jdbc.datasource.init.ScriptUtils
  • org.springframework.jdbc.datasource.init.ResourceDatabasePopulator
  • org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests
  • org.springframework.test.context.testng.AbstractTransactionalTestNGSpringContextTests

ScriptUtils提供了一组处理SQL脚本的静态实用程序方法,主要用于框架内的内部使用。但是,如果你需要完全控制SQL脚本的解析和执行方式,那么ScriptUtils可能比后面描述的其他一些替代方案更适合你的需要。有关更多细节,请参阅javadoc以了解ScriptUtils中的各个方法。

ResourceDatabasePopulator提供了一个基于对象的API,用于使用在外部资源中定义的SQL脚本以编程方式填充、初始化或清理数据库。ResourceDatabasePopulator提供了一些选项,用于配置解析和运行脚本时使用的字符编码、语句分隔符、注释分隔符和错误处理标志。每个配置选项都有一个合理的默认值。有关默认值的详细信息,请参阅javadoc。要运行ResourceDatabasePopulator中配置的脚本,可以调用populate(Connection)方法对java.sql.Connection执行填充器,或者execute(DataSource)方法对javax.sql.DataSource执行填充器。以下示例为测试schema和测试数据指定SQL脚本,将语句分隔符设置为@@,然后针对数据源执行脚本:

@Test
void databaseTest() {
    ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
    populator.addScripts(
            new ClassPathResource("test-schema.sql"),
            new ClassPathResource("test-data.sql"));
    populator.setSeparator("@@");
    populator.execute(this.dataSource);
    // execute code that uses the test schema and data
}

请注意,ResourceDatabasePopulator内部委托给ScriptUtils来解析和运行SQL脚本。同样,AbstractTransactionalJUnit4SpringContextTestsAbstractTransactionalTestNGSpringContextTests中的executeSqlScript(..)方法在内部使用ResourceDatabasePopulator运行SQL脚本。有关各种executeSqlScript(..)方法的详细信息,请参阅javadoc。

参考代码:org.liyong.test.annotation.test.spring.TransactionUserRepositoryTests

使用@Sql声明式地执行SQL脚本

除了上述用于以编程方式运行SQL脚本的机制之外,你还可以在Spring TestContext 框架中声明性地配置SQL脚本。具体来说,你可以在测试类或测试方法上声明@Sql注解,以配置单独的SQL语句或应在集成测试方法之前或之后针对给定数据库运行的SQL脚本的资源路径。@Sql的支持由SqlScriptsTestExecutionListener提供,默认情况下启用。

方法级别的@Sql声明默认情况下覆盖类级别的声明。从Spring框架5.2开始,可以通过@SqlMergeMode为每个测试类或每个测试方法配置此行为。有关更多详细信息,请参见使用@SqlMergeMode合并和覆盖配置

路径资源语义

每个路径都被解释为Spring资源。文本路径(例如,schema.sql)被视为相对于定义测试类的包的类路径资源。以斜杠开头的路径被视为绝对类路径资源(例如,/org/example/schema.sql)。通过使用指定的资源协议加载引用URL的路径(例如,以classpath:file:http:为前缀的路径)。以下示例显示了如何在基于JUnit Jupiter的集成测试类中的类级别和方法级别使用@Sql

@SpringJUnitConfig
@Sql("/test-schema.sql")
class DatabaseTests {

    @Test
    void emptySchemaTest() {
        // execute code that uses the test schema without any test data
    }

    @Test
    @Sql({"/test-schema.sql", "/test-user-data.sql"})
    void userTest() {
        // execute code that uses the test schema and test data
    }
}

参考代码:org.liyong.test.annotation.test.spring.InferredDataSourceTransactionalSqlScriptsTests

默认脚本检测

如果未指定任何SQL脚本或语句,则根据声明@Sql的位置来尝试检测默认脚本。如果无法检测到默认值,则抛出IllegalStateException

  • 类级声明:如果带注解的测试类是com.example.MyTest,则相应的默认脚本是classpath:com/example/MyTest.sql
  • 方法级声明:如果带注解的测试方法名为testMethod()且在com.example.MyTest类中定义,则相应的默认脚本为classpath:com/example/MyTest.testMethod.sql

声明多个@Sql集合

如果需要为给定的测试类或测试方法配置多组SQL脚本,但使用不同的语法配置、不同的错误处理规则或每组不同的执行阶段,则可以声明@Sql的多个实例。使用Java 8,你可以将@Sql用作可重复注解。否则,你可以使用@SqlGroup注解作为显式容器来声明@Sql的多个实例。

下面的示例演示如何将@Sql用作Java 8的可重复注解:

@Test
@Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`"))
@Sql("/test-user-data.sql")
void userTest() {
    // execute code that uses the test schema and test data
}

在前面的示例中呈现的方案中,test-schema.sql脚本对单行注解使用了不同的语法。

除了@Sql声明在@SqlGroup中分组在一起之外,以下示例与上述示例相同。在Java 8及更高版本中,使用@SqlGroup是可选的,但是你可能需要使用@SqlGroup与其他JVM语言(例如Kotlin)兼容。

@Test
@SqlGroup({
    @Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`")),
    @Sql("/test-user-data.sql")
)}
void userTest() {
    // execute code that uses the test schema and test data
}

脚本执行阶段

默认情况下,SQL脚本在相应的测试方法之前执行。但是,如果需要在测试方法之后运行一组特定的脚本(例如,清理数据库状态),则可以使用@Sql中的executionPhase属性,如以下示例所示:

@Test
@Sql(
    scripts = "create-test-data.sql",
    config = @SqlConfig(transactionMode = ISOLATED)
)
@Sql(
    scripts = "delete-test-data.sql",
    config = @SqlConfig(transactionMode = ISOLATED),
    executionPhase = AFTER_TEST_METHOD
)
void userTest() {
    // execute code that needs the test data to be committed
    // to the database outside of the test's transaction
}

请注意,分别从Sql.TransactionModeSql.ExecutionPhase静态导入了ISOLATEDAFTER_TEST_METHOD

@SqlConfig脚本配置

你可以使用@SqlConfig注解配置脚本解析和错误处理。当在集成测试类上声明为类级别的注解时,@SqlConfig充当测试类层次结构中所有SQL脚本的全局配置。通过使用@Sql注解的config属性直接声明时,@SqlConfig用作封闭的@Sql注解中声明的SQL脚本的本地配置。@SqlConfig中的每个属性都有一个隐式默认值,该默认值记录在相应属性的javadoc中。不幸的是,由于Java语言规范中为注解属性定义了规则,因此无法将null值分配给注解属性。因此,为了支持对继承的全局配置的覆盖,@SqlConfig属性的显式默认值为“”(对于字符串)、{}(对于数组)或DEFAULT(对于枚举)。这种方法允许@SqlConfig的本地声明通过提供除“”{}DEFAULT之外的值来有选择地覆盖@SqlConfig的全局声明中的各个属性。只要本地@SqlConfig属性不提供除“”{}DEFAULT以外的显式值,就会继承全局@SqlConfig属性。因此,显式本地配置将覆盖全局配置。

@Sql@SqlConfig提供的配置选项与ScriptUtilsResourceDatabasePopulator支持的配置选项等效,但是,是<jdbc:initialize-database /> XML名称空间元素提供的配置选项的超集。有关详细信息,请参见@Sql@SqlConfig中各个属性的javadoc。

@Sql事物管理

默认情况下,SqlScriptsTestExecutionListener会为使用@Sql配置的脚本推断所需的事务语义。具体来说,SQL脚本不是在没有事务的情况下运行,而是在现有的Spring管理的事务中运行(例如,由TransactionalTestExecutionListener管理的事务,以@Transactional进行注解的测试),或者在隔离的事务中运行,具体取决于transactionMode的配置值@SqlConfig中的属性,以及测试的ApplicationContext中是否存在PlatformTransactionManager。但是,作为最低要求,测试的ApplicationContext中必须存在一个javax.sql.DataSource

如果SqlScriptsTestExecutionListener用来检测DataSourcePlatformTransactionManager并推断事务语义的算法不符合你的需求,则可以通过设置@SqlConfigdataSourcetransactionManager属性来指定显式名称。此外,你可以通过设置@SqlConfigtransactionMode属性来控制事务传播行为(例如,是否应在隔离的事务中运行脚本)。虽然彻底讨论事务管理的所有支持的选项@Sql超出了这个范围参考手册,@SqlConfigSqlScriptsTestExecutionListener javadoc提供详细信息,和下面的示例显示了一个典型的测试场景中,使用JUnit Jupiter和事务与@Sql测试:

@SpringJUnitConfig(TestDatabaseConfig.class)
@Transactional
class TransactionalSqlScriptsTests {

    final JdbcTemplate jdbcTemplate;

    @Autowired
    TransactionalSqlScriptsTests(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    @Test
    @Sql("/test-data.sql")
    void usersTest() {
        // verify state in test database:
        assertNumUsers(2);
        // execute code that uses the test data...
    }

    int countRowsInTable(String tableName) {
        return JdbcTestUtils.countRowsInTable(this.jdbcTemplate, tableName);
    }

    void assertNumUsers(int expected) {
        assertEquals(expected, countRowsInTable("user"),
            "Number of rows in the [user] table.");
    }
}

请注意,在运行usersTest()方法之后,无需清理数据库,因为对数据库所做的任何更改(在test方法中或在/test-data.sql脚本中)都将自动回滚。 TransactionalTestExecutionListener(有关详细信息,请参见事务管理)。

使用@SqlMergeMode合并和覆盖配置

从Spring框架5.2开始,可以将方法级@Sql声明与类级声明合并。例如,这允许你为每个测试类提供一次数据库schema的配置或一些常见的测试数据,然后为每种测试方法提供特定于用例的其他测试数据。若要启用@Sql合并,请使用@SqlMergeMode(MERGE)注解测试类或测试方法。若要禁用特定测试方法(或特定测试子类)的合并,可以通过@SqlMergeMode(OVERRIDE)切换回默认模式。有关示例和更多详细信息,请查阅@SqlMergeMode注解文档部分

3.5.10 并行测试执行

Spring框架5.0引入了对使用Spring TestContext 框架时在单个JVM中并行执行测试的基本支持。通常,这意味着大多数测试类或测试方法可以并行执行,而无需更改测试代码或配置。

有关如何设置并行测试执行的详细信息,请参见你的测试框架,构建工具或IDE的文档。

请记住,将并发引入测试套件可能会导致意外的副作用,奇怪的运行时行为以及间歇性或看似随机失败的测试。因此,Spring团队为何时不并行执行测试提供了以下一般指导。

如果测试符合以下条件,则不要并行执行测试:

  • 使用Spring框架的@DirtiesContext支持。
  • 使用Spring Boot的@MockBean@SpyBean支持。
  • 使用JUnit 4的@FixMethodOrder支持或旨在确保测试方法按特定顺序运行的任何测试框架功能。但是请注意,如果整个测试类是并行执行的,则此方法不适用。
  • 更改共享服务或系统(如数据库,消息代理,文件系统等)的状态。这适用于嵌入式和外部系统。

如果并行测试执行失败,并有异常指出当前测试的ApplicationContext不再处于活动状态,则这通常意味着ApplicationContext已从ContextCache中的另一个线程中删除。

这可能是由于使用@DirtiesContext或由于从ContextCache自动驱逐。如果@DirtiesContext是罪魁祸首,则需要找到一种避免使用@DirtiesContext的方法,或者从并行执行中排除此类测试。如果已超过ContextCache的最大大小,则可以增加缓存的最大大小。有关详细信息,请参见上下文缓存的讨论。

Spring TestContext 框架中的并行测试执行只有在底层的TestContext实现提供了副本构造函数的情况下才可能执行,如TestContext的javadoc中所述。Spring中使用的DefaultTestContext提供了这样的构造函数。但是,如果你使用提供自定义TestContext实现的第三方库,则需要验证它是否适合并行测试执行。

3.5.11 TestContext 框架支持类

本节描述了支持Spring TestContext 框架的各种类。

Spring JUnit 4 Runner

Spring TestContext框架通过自定义运行程序(在JUnit 4.12或更高版本上受支持)提供了与JUnit 4的完全集成。通过使用@RunWith(SpringJUnit4ClassRunner.class)或简写的@RunWith(SpringRunner.class)注释测试类,开发人员可以实现基于JUnit 4的标准单元测试和集成测试,同时获得TestContext框架的优势,例如对加载应用程序上下文、测试实例的依赖关系注入、事务性测试方法执行等。如果你想将Spring TestContext 框架与替代运行程序(例如JUnit 4的Parameterized运行程序)或第三方运行程序(例如MockitoJUnitRunner)一起使用,则可以选择使用Spring对JUnit规则的支持。

以下代码清单显示了配置测试类与自定义Spring Runner一起运行的最低要求:

@RunWith(SpringRunner.class)
@TestExecutionListeners({})
public class SimpleTest {

    @Test
    public void testMethod() {
        // execute test logic...
    }
}

在前面的示例中,为@TestExecutionListeners配置了一个空列表以禁用默认监听器,否则将需要通过@ContextConfiguration配置ApplicationContext

Spring JUnit4规则

org.springframework.test.context.junit4.rules包提供以下JUnit 4规则(在JUnit 4.12或更高版本上受支持):

  • SpringClassRule
  • SpringMethodRule

SpringClassRule是一个JUnit TestRule,它支持Spring TestContext 框架的类级功能,而SpringMethodRule是一个JUnit MethodRule,它支持Spring TestContext 框架的实例级和方法级功能。

SpringRunner相比,Spring的基于规则的JUnit支持具有独立于任何org.junit.runner.Runner实现的优点,因此可以与现有的替代运行器(例如JUnit 4的Parameterized)或第三方结合使用运行器(例如MockitoJUnitRunner)。

为了支持TestContext框架的全部功能,必须将SpringClassRuleSpringMethodRule结合使用。以下示例显示了在集成测试中声明这些规则的正确方法:

// Optionally specify a non-Spring Runner via @RunWith(...)
@ContextConfiguration
public class IntegrationTest {

    @ClassRule
    public static final SpringClassRule springClassRule = new SpringClassRule();

    @Rule
    public final SpringMethodRule springMethodRule = new SpringMethodRule();

    @Test
    public void testMethod() {
        // execute test logic...
    }
}

JUnit 4 Support支持类

org.springframework.test.context.junit4包为基于JUnit 4的测试用例提供了以下支持类(在JUnit 4.12或更高版本上受支持):

  • AbstractJUnit4SpringContextTests
  • AbstractTransactionalJUnit4SpringContextTests

AbstractJUnit4SpringContextTests是抽象的基础测试类,该类将Spring TestContext 框架与JUnit 4环境中的显式ApplicationContext测试支持集成在一起。扩展AbstractJUnit4SpringContextTests时,可以访问protected applicationContext实例变量,该变量可用于执行显式bean查找或测试整个上下文的状态。

AbstractTransactionalJUnit4SpringContextTestsAbstractJUnit4SpringContextTests的抽象事务扩展,为JDBC访问添加了一些便利功能。此类期望在ApplicationContext中定义一个javax.sql.DataSource bean和PlatformTransactionManager bean。扩展AbstractTransactionalJUnit4SpringContextTests时,可以访问protected jdbcTemplate实例变量,该实例变量可用于运行SQL语句来查询数据库。你可以在运行与数据库相关的应用程序代码之前和之后使用此类查询来确认数据库状态,并且Spring确保此类查询在与应用程序代码相同的事务范围内运行。与ORM工具一起使用时,请确保避免误报。如JDBC测试支持中所述,AbstractTransactionalJUnit4SpringContextTests还提供了方便的方法,这些方法通过使用上述的jdbcTemplate委托给JdbcTestUtils中的方法。此外,AbstractTransactionalJUnit4SpringContextTests提供了executeSqlScript(..)方法,用于针对已配置的DataSource运行SQL脚本。

这些类为扩展提供了便利。如果你不希望将测试类绑定到特定于Spring的类层次结构,则可以使用@RunWith(SpringRunner.class)或Spring的JUnit规则来配置自己的自定义测试类。

JUnit Jupiter的SpringExtension

Spring TestContext 框架提供了与JUnit 5中引入的JUnit Jupiter测试框架的完全集成。通过使用@ExtendWith(SpringExtension.class)注解测试类,你可以实现基于JUnit Jupiter的标准单元测试和集成测试,并同时获得TestContext框架的好处,例如支持加载应用程序上下文、测试实例的依赖注入、事务性测试方法执行等等。

此外,得益于JUnit Jupiter中丰富的扩展API,Spring在Spring支持JUnit 4和TestNG的功能集之外提供了以下功能:

  • 测试构造函数、测试方法和测试生命周期回调方法的依赖注入。有关更多详细信息,请参见使用SpringExtension进行依赖注入

  • 对基于SpEL表达式、环境变量、系统属性等的条件测试执行的强大支持。有关更多详细信息和示例,请参见Spring JUnit Jupiter测试注解中有关@EnabledIf@DisabledIf的文档。

  • 自定义组合注解,结合了Spring和JUnit Jupiter的注解。有关更多详细信息,请参见“元注解支持测试”中的@TransactionalDevTestConfig@TransactionalIntegrationTest示例。

以下代码清单显示如何配置测试类以将SpringExtension@ContextConfiguration结合使用:

// Instructs JUnit Jupiter to extend the test with Spring support.
@ExtendWith(SpringExtension.class)
// Instructs Spring to load an ApplicationContext from TestConfig.class
@ContextConfiguration(classes = TestConfig.class)
class SimpleTests {

    @Test
    void testMethod() {
        // execute test logic...
    }
}

由于你还可以在JUnit 5中将注解用作元注解,因此Spring提供了@SpringJUnitConfig@SpringJUnitWebConfig组成的注解,以简化测试ApplicationContext和JUnit Jupiter的配置。

以下示例使用@SpringJUnitConfig减少前一示例中使用的配置量:

// Instructs Spring to register the SpringExtension with JUnit
// Jupiter and load an ApplicationContext from TestConfig.class
@SpringJUnitConfig(TestConfig.class)
class SimpleTests {

    @Test
    void testMethod() {
        // execute test logic...
    }
}

同样,以下示例使用@SpringJUnitWebConfig创建用于JUnit Jupiter的WebApplicationContext

// Instructs Spring to register the SpringExtension with JUnit
// Jupiter and load a WebApplicationContext from TestWebConfig.class
@SpringJUnitWebConfig(TestWebConfig.class)
class SimpleWebTests {

    @Test
    void testMethod() {
        // execute test logic...
    }
}

有关更多详细信息,请参见Spring JUnit Jupiter测试注解中有关@SpringJUnitConfig和@SpringJUnitWebConfig的文档。

使用SpringExtension依赖注入

SpringExtension从JUnit Jupiter实现了ParameterResolver扩展API,它使Spring可以为测试构造函数、测试方法和测试生命周期回调方法提供依赖项注入。

具体来说,SpringExtension可以将来自测试ApplicationContext的依赖项注入到使用@BeforeAll@AfterAll@BeforeEach@AfterEach@Test@RepeatedTest@ParameterizedTest等标记的测试构造函数和方法中。

构造函数注入

如果JUnit Jupiter测试类的构造函数中的特定参数为ApplicationContext类型(或其子类型)、或者使用@Autowired@Qualifier或@Value进行注解或元注解,则Spring会为此特定值注入值参数与测试的ApplicationContext中的相应bean或值。

如果认为构造函数是可自动装配的,则还可以将Spring配置为自动连接测试类构造函数的所有参数。如果满足以下条件之一(按优先顺序),则认为构造函数是可自动构造的。

  • 构造函数带有@Autowired注解。
  • @TestConstructorautowireMode属性设置为ALL的情况下出现在测试类中或以元形式出现。
  • 默认的测试构造函数自动装配模式已更改为ALL。

有关使用@TestConstructor以及如何更改全局测试构造函数自动装配模式的详细信息,请参见@TestConstructor

如果测试类的构造函数被认为是可自动装配的,则Spring负责解析构造函数中所有参数的参数。因此,在JUnit Jupiter中注册的其他ParameterResolver不能解析此类构造函数的参数。

如果在测试方法之前或之后使用@DirtiesContext关闭测试的ApplicationContext,则不得与JUnit Jupiter的@TestInstance(PER_CLASS)支持一起使用针对测试类的构造函数注入。

原因是@TestInstance(PER_CLASS)指示JUnit Jupiter在测试方法调用之间缓存测试实例。因此,测试实例将保留对最初从随后已关闭的ApplicationContext注入的bean的引用。由于在这种情况下测试类的构造函数将仅被调用一次,因此依赖注入不会再次发生,并且后续测试将与关闭的ApplicationContext中的bean进行交互,这可能会导致错误。

要将@DirtiesContext@TestInstance(PER_CLASS)一起用于“测试方法之前”或“测试方法之后”模式,必须配置通过字段或setter注入提供的Spring依赖项,以便可以在测试之间重新注入它们方法调用。

在下面的示例中,Spring将从TestConfig.class加载的ApplicationContext中的OrderService bean注入OrderServiceIntegrationTests构造函数中。

@SpringJUnitConfig(TestConfig.class)
class OrderServiceIntegrationTests {

    private final OrderService orderService;

    @Autowired
    OrderServiceIntegrationTests(OrderService orderService) {
        this.orderService = orderService;
    }

    // tests that use the injected OrderService
}

请注意,此功能使测试依赖项是最终的,因此是不可变的。

如果spring.test.constructor.autowire.mode属性是all(请参阅@TestConstructor),则可以在上一个示例中省略构造函数上@Autowired的声明,从而得到以下结果。

@SpringJUnitConfig(TestConfig.class)
class OrderServiceIntegrationTests {

    private final OrderService orderService;

    OrderServiceIntegrationTests(OrderService orderService) {
        this.orderService = orderService;
    }

    // tests that use the injected OrderService
}

方法注入

如果JUnit Jupiter测试方法或测试生命周期回调方法中的参数属于ApplicationContext类型(或其子类型),或者使用@Autowired@Qualifier@Value进行注解或元注解,则Spring会为此注入值。特定参数以及来自测试的ApplicationContext的相应bean。

在下面的示例中,Spring从TestConfig.class加载的ApplicationContext中将OrderService注入到deleteOrder()测试方法中:

@SpringJUnitConfig(TestConfig.class)
class OrderServiceIntegrationTests {

    @Test
    void deleteOrder(@Autowired OrderService orderService) {
        // use orderService from the test's ApplicationContext
    }
}

由于JUnit Jupiter中ParameterResolver支持的强大功能,你不仅可以从Spring中,而且可以从JUnit Jupiter本身或其他第三方扩展中,将多个依赖项注入到单个方法中。

下面的示例演示如何让Spring和JUnit Jupiter同时将依赖项注入到placeOrderRepeatedly()测试方法中。

@SpringJUnitConfig(TestConfig.class)
class OrderServiceIntegrationTests {

    @RepeatedTest(10)
    void placeOrderRepeatedly(RepetitionInfo repetitionInfo,
            @Autowired OrderService orderService) {

        // use orderService from the test's ApplicationContext
        // and repetitionInfo from JUnit Jupiter
    }
}

注意,通过使用JUnit Jupiter中的@RepeatedTest,测试方法可以访问RepetitionInfo

TestNG支持类

org.springframework.test.context.testng包为基于TestNG的测试用例提供以下支持类:

  • AbstractTestNGSpringContextTests
  • AbstractTransactionalTestNGSpringContextTests

AbstractTestNGSpringContextTests是抽象的基础测试类,该类将Spring TestContext 框架与TestNG环境中的显式ApplicationContext测试支持集成在一起。扩展AbstractTestNGSpringContextTests时,可以访问protected applicationContext实例变量,该变量可用于执行显式bean查找或测试整个上下文的状态。

AbstractTransactionalTestNGSpringContextTestsAbstractTestNGSpringContextTests的抽象事务扩展,为JDBC访问添加了一些便利功能。此类期望在ApplicationContext中定义一个javax.sql.DataSource bean和PlatformTransactionManager bean。扩展AbstractTransactionalTestNGSpringContextTests时,可以访问protected jdbcTemplate实例变量,该实例变量可用于执行SQL语句来查询数据库。你可以在运行与数据库相关的应用程序代码之前和之后使用此类查询来确认数据库状态,并且Spring确保此类查询在与应用程序代码相同的事务范围内运行。与ORM工具一起使用时,请确保避免误报。如JDBC测试支持中所述,AbstractTransactionalTestNGSpringContextTests还提供了方便的方法,这些方法通过使用上述的jdbcTemplate委托给JdbcTestUtils中的方法。此外,AbstractTransactionalTestNGSpringContextTests提供了executeSqlScript(..)方法,用于针对已配置的DataSource运行SQL脚本。

这些类为扩展提供了便利。如果你不希望将测试类绑定到特定于Spring的类层次结构,则可以使用@ContextConfiguration@TestExecutionListeners等来配置自己的自定义测试类,并通过使用TestContextManager手动检测测试类。有关如何检测测试类的示例,请参见AbstractTestNGSpringContextTests的源代码。

0
青年IT男

个人从事金融行业,就职过易极付、思建科技等重庆一流技术团队,目前就职于某网约车平台负责整个支付系统建设。自身对金融行业有强烈的爱好。同时也实践大数据、数据存储、自动化集成和部署、分布式微服务、响应式编程、人工智能等领域。

评论已关闭。

This site is protected by wp-copyrightpro.com