本章介绍了 Spring 对集成测试以及单元测试的最佳实践. Spring 团队提倡测试驱动开发 (TDD) . Spring 团队发现正确使用控制反转 (IoC) 确实使单元测试和集成测试更加容易 (因为在类上存在 setter 方法和适当的构造函数,使得它们在测试中更容易连接在一起,而不必设置服务注册等类似结构) .

1. Spring 测试简介

测试是企业软件开发的组成部分.本章重点介绍 IoC 为单元测试带来的附加价值,以及 Spring 框架对集成测试的支持所带来的好处. (对企业中测试的彻底处理不在本参考手册的范围之内. )

2. 单元测试

与传统的 Java EE 开发相比,依赖注入应该使您的代码对容器的依赖程度降低. 组成应用程序的 POJO 应该可以在 JUnit 或 TestNG 测试中进行测试,并且可以使用 new 运算符实例化对象,而无需使用 Spring 或任何其他容器. 您可以使用 Mock Objects(模拟对象) (结合其他有价值的测试技术) 来单独测试代码. 如果您遵循 Spring 的体系结构建议,那么代码库的最终分层和组件化将使单元测试更加容易. 例如,您需要在进行单元测试时不访问持久性数据,您可以通过 stubbing 或 mocking DAO 或 repository 接口来测试服务层对象.

真正的单元测试通常运行非常快,因为没有设置运行时基础架构. 将真正的单元测试作为开发方法的一部分可以提高生产率. 您可能不需要测试章节的这一部分来帮助您为基于 IoC 的应用程序编写有效的单元测试. 但是,对于某些单元测试方案,Spring 框架提供了模拟对象和测试支持类,本章对此进行了介绍.

2.1. 模拟对象

Spring 包含许多专用于模拟的软件包:

2.1.1. 环境

org.springframework.mock.env 包包含 EnvironmentPropertySource 抽象的模拟实现 (请参阅Bean 定义配置文件PropertySource 抽象) . MockEnvironmentMockPropertySource 对于编写那些依赖于特定环境或属性的测试非常有用.

2.1.2. JNDI

org.springframework.mock.jndi 包包含 JNDI SPI 的部分实现,可用于为测试套件或独立应用程序设置简单的 JNDI 环境. 例如,如果 JDBC DataSource 实例在测试代码中与在 Java EE 容器中绑定到相同的 JNDI 名称,则可以在测试场景中重用应用程序代码和配置,而无需进行修改.

从 Spring Framework 5.2 开始,正式弃用了 org.springframework.mock.jndi 包中对模拟 JNDI 支持,而希望使用第三方的完整解决方案,例如 Simple-JNDI.

2.1.3. Servlet API

org.springframework.mock.web 包包含一组全面的 Servlet API 模拟对象,这些对象对于测试 Web 上下文,控制器和过滤器非常有用. 这些模拟对象是针对 Spring 的 Web MVC 框架使用的,通常比动态模拟对象 (例如 EasyMock) 或其他 Servlet API 模拟对象 (例如 MockObjects) 更方便使用.

从 Spring Framework 5.0 开始,org.springframework.mock.web 中的模拟对象基于 Servlet 4.0 API.

Spring MVC 测试框架建立在模拟 Servlet API 对象的基础上,为 Spring MVC 提 供了集成测试框架. 参见Spring MVC 测试框架.

2.1.4. Spring Web Reactive

org.springframework.mock.http.server.reactive 包包含 ServerHttpRequestServerHttpResponse 的模拟实现,供WebFlux应用程序使用. org.springframework.mock.web.server 包包含一个模拟 ServerWebExchange,它依赖于那些模拟请求和响应对象.

MockServerHttpRequestMockServerHttpResponse 都从与特定于服务器的实现相同的抽象基类扩展,并与它们共享行为. 例如,模拟请求一旦创建便是不可变的,但是您可以使用 ServerHttpRequest 中的 mutate() 方法来创建修改后的实例.

为了使模拟响应正确实现写约定并返回写完成句柄 (即 Mono<Void>) ,默认情况下,它使用带有 Flux 的 cache(). then() 来对数据进行缓冲并使其 可用于测试中的断言. 应用程序可以设置自定义写入功能 (例如,测试无限流) .

WebTestClient 建立在模拟请求和响应的基础上,为不使用 HTTP 服务器的 WebFlux 应用程序测试提供支持. 客户端还可以用于正在运行的服务器的端到端测试.

2.2. 单元测试支持的类

Spring 包含许多可以帮助进行单元测试的类. 它们分为两类:

2.2.1. 通用测试工具

org.springframework.test.util 包包含几个通用的实用程序,用于单元测试和集成测试.

ReflectionTestUtils 是基于反射的实用程序方法的集合. 您可以在测试场景中使用这些方法,在这些场景中,当测试应用程序代码时,需要更改常量的值,设置非公共字段,调用非公共 setter 方法或调用非公共配置或生命周期回调方法. 用例如下:

  • ORM 框架 (例如 JPA 和 Hibernate) 提供了 privateprotected 的字段访问,而不是实体属性的 public setter 方法.

  • Spring 支持注解 (例如 @Autowired,@Inject@Resource) ,这些注解为 privateprotected 的字段,setter 方法和配置方法提供依赖注入.

  • 将诸如 @PostConstruct@PreDestroy 之类的注解用于生命周期回调方法

AopTestUtils 是与 AOP 相关的实用程序方法的集合. 您可以使用这些方法来获取对隐藏在一个或多个 Spring 代理后面的目标对象的引用. 例如,如果您已通过使用 EasyMock 或 Mockito 之类的库将 bean 配置为动态模拟,并且该模拟包装在 Spring 代理中,则可能需要直接访问基础模拟以在其上配置期望并执行验证 . 有关 Spring 的核心 AOP 实用程序,请参阅 AopUtilsAopProxyUtils.

2.2.2. Spring MVC 测试实用程序

org.springframework.test.web 包包含 ModelAndViewAssert,您可以将其与 JUnit,TestNG 或任何其他测试框架结合使用,以进行处理 Spring MVC ModelAndView 对象的单元测试.

单元测试 Spring MVC 控制器
要对作为 POJO 的 Spring MVC Controller 类进行单元测试,请将 ModelAndViewAssert 与 Spring 的Servlet API mocks模拟中的 MockHttpServletRequest,MockHttpSession 等结合使用. 为了与 Spring MVC 的 WebApplicationContext 配置一起对 Spring MVC 和 REST Controller 类进行全面的集成测试,请改用Spring MVC Test Framework.

3. 集成测试

本节 (本章其余部分) 涵盖了 Spring 应用程序的集成测试. 它包括以下主题:

3.1. 概述

无需将应用程序部署到服务器或链接到其他企业结构,能够执行一些测试.这一点很重要.这样可以测试以下内容:

  • Spring IoC 容器上下文的正确装配.

  • 使用 JDBC 或 ORM 工具进行数据访问.这可以包括诸如 SQL 语句的正确性,Hibernate 查询,JPA 实体映射之类的东西.

Spring 框架为 spring-test 模块中的集成测试提供了一流的支持. 实际的 JAR 文件的名称可能包括发行版,也可能采用长 org.springframework.test 格式,具体取决于您从何处获取 该库包含 org.springframework.test 包,其中包含用于与 Spring 容器进行集成测试的有价值的类. 此测试不依赖于应用程序服务器或其他部署环境. 此类测试的运行速度比单元测试慢,但比依赖于部署到应用程序服务器的等效 Selenium 测试或远程测试快.

单元和集成测试支持以注解驱动的Spring TestContext Framework的形式提供. TestContext 框架与实际使用的测试框架无关,该框架允许在各种环境 (包括 JUnit,TestNG 和其他环境) 中对测试进行检测.

3.2. 集成测试的目标

Spring 的集成测试支持的主要目标如下:

接下来的几节描述了每个目标,并提供了有关实现和配置详细信息的链接.

3.2.1. 上下文管理和缓存

Spring TestContext Framework 提供了 Spring ApplicationContext 实例和 WebApplicationContext 实例的一致加载以及这些上下文的缓存. 支持加载上下文的缓存很重要,因为启动时间可能会成为一个问题-不是因为 Spring 本身的开销,而是因为 Spring 容器实例化的对象需要时间才能实例化. 例如,具有 50 到 100 个 Hibernate 映射文件的项目可能需要 10 到 20 秒来加载映射文件,并且在每个测试夹具中运行每个测试之前都要承担该消耗,这会导致整体测试运行速度变慢,从而降低了开发人员的工作效率.

测试类通常声明 XML 或 Groovy 配置元数据的资源位置数组 (通常是在类路径中) 或用于配置应用程序的组件类的数组. 这些位置或类与 web.xml 或其他用于生产部署的配置文件中指定的位置或类相同或相似.

默认情况下,加载后,已配置的 ApplicationContext 将重新用于每个测试. 因此,每个测试套件仅产生一次安装成本,并且随后的测试执行要快得多. 在这种情况下,术语 "测试套件" 是指所有测试都在同一 JVM 中运行,例如,所有测试都从给定项目或模块的 Ant,Maven 或 Gradle 构建运行. 在不太可能的情况下,测试破坏了应用程序上下文并需要重新加载 (例如,通过修改 bean 定义或应用程序对象的状态) ,可以将 TestContext 框架配置为重新加载配置并重建应用程序上下文,然后再执行下一个 测试.

请参见使用 TestContext 框架进行上下文管理上下文缓存.

3.2.2. 测试夹具实例的依赖注入

当 TestContext 框架加载您的应用程序上下文时,可以选择使用依赖注入来配置测试类的实例. 这提供了一种方便的机制,可以通过在应用程序上下文中使用预配置的 bean 来设置测试装置. 此处的一个强大好处是您可以在各种测试场景中重用应用程序上下文 (例如,用于配置 Spring 管理的对象图,事务代理,DataSource 实例等) ,从而避免了为单个测试复制复杂的测试夹具设置的需要条件.

例如,考虑一个场景,其中我们有一个类 (HibernateTitleRepository) ,该类为 Title 域实体实现数据访问逻辑. 我们要编写集成测试来测试以下方面:

  • Spring 配置: 基本上,与 HibernateTitleRepository bean 的配置有关的所有内容是否正确并存在?

  • Hibernate 映射文件配置: 是否正确映射了所有内容,并且是否有正确的延迟加载设置?

  • HibernateTitleRepository 的逻辑: 此类的配置实例是否按预期执行?

请参见使用TestContext framework 进行测试夹具的依赖注入.

3.2.3. 事务管理

访问真实数据库的测试中的一个常见问题是它们对持久性存储状态的影响. 即使使用开发数据库,对状态的更改也可能会影响以后的测试. 同样,许多操作 (例如插入或修改持久数据) 无法在事务之外执行 (或验证) .

TestContext 框架解决了这个问题. 默认情况下,框架为每个测试创建并回滚事务. 您可以编写存在事务的代码. 如果在测试中调用事务代理对象,则对象将根据其配置的事务语义正确运行. 此外,如果测试方法在测试管理的事务中运行时删除了选定表的内容,则该事务将默认回滚,并且数据库将返回到执行测试之前的状态. 通过使用在测试的应用程序上下文中定义的 PlatformTransactionManager bean,可以为测试提供事务支持.

如果您要提交事务 (不常见,但在希望特定测试填充或修改数据库时偶尔有用) ,则可以使用@Commit 注解告诉 TestContext 框架使事务提交而不是回滚.

请参阅使用 TestContext framework进行事务管理.

3.2.4. 集成测试支持类

Spring TestContext Framework 提供了几个抽象支持类,这些类简化了集成测试的编写. 这些测试类为测试框架提供了定义明确的钩子,以及方便的实例变量和方法,可用于访问以下内容:

  • ApplicationContext,用于执行显式的 bean 查找或测试整个上下文的状态.

  • 一个 JdbcTemplate,用于执行 SQL 语句来查询数据库. 您可以在执行与数据库相关的应用程序代码之前和之后使用此类查询来确认数据库状态,并且 Spring 确保此类查询在与应用程序代码相同的事务范围内运行. 与 ORM 工具一起使用时,请确保避免误报.

另外,您可能希望使用针对您的项目的实例变量和方法创建自己的自定义,应用程序范围的超类.

请参阅TestContext framework的支持类.

3.3. JDBC 测试支持

org.springframework.test.jdbc 包中包含 JdbcTestUtils,它是一个JDBC相关的工具方法集,旨在简化标准数据库测试方案. 具体来说,JdbcTestUtils 提供以下静态实用程序方法.

  • countRowsInTable(..): 统计给定表的行数.

  • countRowsInTableWhere(..): 使用提供的 where 语句进行筛选统计给定表的行数.

  • deleteFromTables(..): 删除特定表的全部数据.

  • deleteFromTableWhere(..): 使用提供的 where 语句进行筛选并删除给定表的数据.

  • dropTables(..): 删除指定的表.

AbstractTransactionalJUnit4SpringContextTestsAbstractTransactionalTestNGSpringContextTests 提供了委托给前面所述的 JdbcTestUtils 中的方法的简便方法.

spring-jdbc 模块提供了配置和启动嵌入式数据库的支持,可用于与数据库交互的集成测试中.

3.4. 注解

本节介绍了在测试 Spring 应用程序时可以使用的注解.它包括以下主题:

3.4.1. Spring Testing Annotations

Spring 框架提供以下 Spring 特定的注解集合,你可以在单元和集成测试中结合 TestContext 框架使用它们. 请参考相应的 JAVA 帮助文档作进一步了解,包括默认的属性,属性别名等等.

@BootstrapWith

@BootstrapWith 是一个用于配置 Spring TestContext 框架如何引导的类级别的注解. 具体地说,@BootstrapWith 用于指定一个自定义的 TestContextBootstrapper. 请查看引导 TestContext 框架作进一步了解.

@ContextConfiguration

@ContextConfiguration 定义了类级别的元数据来决定如何为集成测试来加载和配置应用程序上下文. 具体地说,@ContextConfiguration 声明了用于加载上下文的应用程序上下文资源路径和注解类.

资源路径通常是类路径中的 XML 配置文件或者 Groovy 脚本; 而注解类通常是使用 @Configuration 注解的类. 但是,资源路径也可以指向文件系统中的文件和脚本,而组件类可以是 @Component 类,@Service 类,等等. 有关更多详细信息,请参见组件类 .

下面的示例显示了引用 XML 文件的 @ContextConfiguration 注解:

Java
@ContextConfiguration("/test-config.xml") (1)
class XmlApplicationContextTests {
    // class body...
}
1 引用 XML 文件.
Kotlin
@ContextConfiguration("/test-config.xml") (1)
class XmlApplicationContextTests {
    // class body...
}
1 引用 XML 文件.

以下示例显示了一个 @ContextConfiguration 注解,该注解引用了一个类:

Java
@ContextConfiguration(classes = TestConfig.class) (1)
class ConfigClassApplicationContextTests {
    // class body...
}
1 指向 class.
Kotlin
@ContextConfiguration(classes = [TestConfig::class]) (1)
class ConfigClassApplicationContextTests {
    // class body...
}
1 指向 class.

作为声明资源位置或组件类的替代方法或补充,可以使用 @ContextConfiguration 声明 ApplicationContextInitializer 类. 以下示例显示了这种情况:

Java
@ContextConfiguration(initializers = CustomContextInitializer.class) (1)
class ContextInitializerTests {
    // class body...
}
1 声明一个初始化器类
Kotlin
@ContextConfiguration(initializers = [CustomContextInitializer::class]) (1)
class ContextInitializerTests {
    // class body...
}
1 声明一个初始化器类

@ContextConfiguration 偶尔也被用作声明 ContextLoader 策略. 但注意,通常你不需要显示的配置加载器,因为默认的加载器已经支持资源路径或者注解类以及初始化器.

Java
@ContextConfiguration(locations = "/test-context.xml", loader = CustomContextLoader.class) (1)
class CustomLoaderXmlApplicationContextTests {
    // class body...
}
1 以下示例同时 location 和 custom loader.
Kotlin
@ContextConfiguration("/test-context.xml", loader = CustomContextLoader::class) (1)
class CustomLoaderXmlApplicationContextTests {
    // class body...
}
1 以下示例同时 location 和 custom loader.
@ContextConfiguration 默认对继承父类定义的资源路径或者配置类以及上下文初始化或封闭类提供支持.

参阅上下文管理 , @Nested 测试类配置, 和 @ContextConfiguration 帮助文档作进一步了解.

@WebAppConfiguration

@WebAppConfiguration 是一个用于声明集成测试所加载的 ApplicationContext 须是 WebApplicationContext 的类级别的注解. 测试类的 @WebAppConfiguration 注解只是为了保证用于测试的 WebApplicationContext 会被加载, 它使用 "file:src/main/webapp" 路径默认值作为 web 应用的根路径 (即,资源路径) . 资源基路径用于幕后创建一个 MockServletContext 作为测试的 WebApplicationContextServletContext.

以下示例显示了如何使用 @WebAppConfiguration 注解:

Java
@ContextConfiguration
@WebAppConfiguration (1)
class WebAppTests {
    // class body...
}
Kotlin
@ContextConfiguration
@WebAppConfiguration (1)
class WebAppTests {
    // class body...
}
1 @WebAppConfiguration 注解.

要覆盖默认值,可以使用 value 属性指定其他资源路径. classpath:file: 资源前缀均受支持. 如果未提供资源前缀,则假定该路径是文件系统资源. 以下示例显示如何指定类路径资源:

Java
@ContextConfiguration
@WebAppConfiguration("classpath:test-web-resources") (1)
class WebAppTests {
    // class body...
}
1 指定类路径资源.
Kotlin
@ContextConfiguration
@WebAppConfiguration("classpath:test-web-resources") (1)
class WebAppTests {
    // class body...
}
1 指定类路径资源.

注意 @WebAppConfiguration 必须和 @ContextConfiguration 一起使用,或者在同一个测试类,或者在测试类层次结构中. 请参阅 @WebAppConfiguration 帮助文档作进一步了解.

@ContextHierarchy

@ContextHierarchy 是一个类级别的注解,用于定义用于集成测试的 ApplicationContext 实例的层次结构. @ContextHierarchy 应该用一个或多个 @ContextConfiguration 实例的列表声明,每个实例定义上下文层次结构中的一个级别. 以下示例演示了在单个测试类中使用 @ContextHierarchy (也可以在测试类层次结构中使用 @ContextHierarchy) :

Java
@ContextHierarchy({
    @ContextConfiguration("/parent-config.xml"),
    @ContextConfiguration("/child-config.xml")
})
class ContextHierarchyTests {
    // class body...
}
Kotlin
@ContextHierarchy(
    ContextConfiguration("/parent-config.xml"),
    ContextConfiguration("/child-config.xml"))
class ContextHierarchyTests {
    // class body...
}
Java
@WebAppConfiguration
@ContextHierarchy({
    @ContextConfiguration(classes = AppConfig.class),
    @ContextConfiguration(classes = WebConfig.class)
})
class WebIntegrationTests {
    // class body...
}
Kotlin
@WebAppConfiguration
@ContextHierarchy(
        ContextConfiguration(classes = [AppConfig::class]),
        ContextConfiguration(classes = [WebConfig::class]))
class WebIntegrationTests {
    // class body...
}

如果需要合并或覆盖测试类层次结构中给定级别的上下文层次结构的配置,你就必须在类层次中的每一个相应的层次通过为 @ContextConfigurationname 属性提供与该层次相同的值的方式来显示地指定这个层次. 请参阅上下文层次结构@ContextHierarchy 帮助文档来获得更多的示例.

@ActiveProfiles

@ActiveProfiles 是一个类级别的注解,用于声明在为集成测试加载 ApplicationContext 时应启用哪些 bean 定义配置文件.

以下示例表明 dev 配置文件应处于激活状态:

Java
@ContextConfiguration
@ActiveProfiles("dev") (1)
class DeveloperTests {
    // class body...
}
1 Indicate that the dev profile should be active.
Kotlin
@ContextConfiguration
@ActiveProfiles("dev") (1)
class DeveloperTests {
    // class body...
}
1 Indicate that the dev profile should be active.

下面的示例 devintegration profiles 被激活

Java
@ContextConfiguration
@ActiveProfiles({"dev", "integration"}) (1)
class DeveloperIntegrationTests {
    // class body...
}
1 Indicate that the dev and integration profiles should be active.
Kotlin
@ContextConfiguration
@ActiveProfiles(["dev", "integration"]) (1)
class DeveloperIntegrationTests {
    // class body...
}
1 Indicate that the dev and integration profiles should be active.
@ActiveProfiles 默认支持继承超类和封闭类声明的 active profile. 通过实现一个自定义的 ActiveProfilesResolver 并通过 @ActiveProfilesresolver 属性对其进行注册, 以编程方式解析 active bean 定义配置文件.
@TestPropertySource

@TestPropertySource 是一个用于为集成测试加载 ApplicationContext 时配置属性文件的位置和增加到 Environment 中的 PropertySources 集中的内联属性的类级别的注解.

下面的例子展示了如何从类路径中声明属性文件.

Java
@ContextConfiguration
@TestPropertySource("/test.properties") (1)
class MyIntegrationTests {
    // class body...
}
1 从类路径根目录中的 test.properties 获取属性.
Kotlin
@ContextConfiguration
@TestPropertySource("/test.properties") (1)
class MyIntegrationTests {
    // class body...
}
1 从类路径根目录中的 test.properties 获取属性.

下面的示例演示如何声明内联属性:

Java
@ContextConfiguration
@TestPropertySource(properties = { "timezone = GMT", "port: 4242" }) (1)
class MyIntegrationTests {
    // class body...
}
1 声明 timezoneport 属性.
Kotlin
@ContextConfiguration
@TestPropertySource(properties = ["timezone = GMT", "port: 4242"]) (1)
class MyIntegrationTests {
    // class body...
}
1 声明 timezoneport 属性.

有关示例和更多详细信息,请参见具有测试属性源的上下文配置 .

@DynamicPropertySource

@DynamicPropertySource 是方法级别的注解,可用于注册动态属性,在集成测试加载 ApplicationContext 时,将动态属性添加到 EnvironmentPropertySources 集中. 当您不预先知道属性的值时,例如,如果属性是由外部资源管理的,例如由 Testcontainers 项目管理的容器,则动态属性很有用.

下面的示例演示如何注册动态属性:

Java
@ContextConfiguration
class MyIntegrationTests {

    static MyExternalServer server = // ...

    @DynamicPropertySource (1)
    static void dynamicProperties(DynamicPropertyRegistry registry) { (2)
        registry.add("server.port", server::getPort); (3)
    }

    // tests ...
}
1 使用 @DynamicPropertySource 注解一个 static 方法.
2 接收 DynamicPropertyRegistry 作为参数.
3 注册一个动态的延迟加载的 server.port 属性.
Kotlin
@ContextConfiguration
class MyIntegrationTests {

    companion object {

        @JvmStatic
        val server: MyExternalServer = // ...

        @DynamicPropertySource (1)
        @JvmStatic
        fun dynamicProperties(registry: DynamicPropertyRegistry) { (2)
            registry.add("server.port", server::getPort) (3)
        }
    }

    // tests ...
}
1 使用 @DynamicPropertySource 注解一个 static 方法.
2 接收 DynamicPropertyRegistry 作为参数.
3 注册一个动态的延迟加载的 server.port 属性.

查看 具有动态属性源的上下文配置 获取更多的细节.

@DirtiesContext

@DirtiesContext 指明测试执行期间该 Spring 应用程序上下文已经被改变 (也就是说通过某种方式被更改或者破坏——比如,更改单例 bean 的状态) . 当应用程序上下文被标为 "dirty",它将从测试框架缓存中被移除并关闭. 因此,如果后续的测试需要同样的元数据配置,Spring 容器将被重建.

@DirtiesContext 可以在同一个类或者类层次结构中的类级别和方法级别中使用. 在这个场景下,应用程序上下文将在任意此注解的方法之前或之后以及当前测试类之前或之后被标为 "dirty",这取决于配置的 methodModeclassMode.

下面的例子解释了在多种配置场景下什么时候上下文会被标为 "dirty".

  • 当在一个类中声明并将类模式设为 BEFORE_CLASS,则在当前测试类之前.

    Java
    @DirtiesContext(classMode = BEFORE_CLASS) (1)
    class FreshContextTests {
        // some tests that require a new Spring container
    }
    
    1 在当前测试类之前改变上下文.
    Kotlin
    @DirtiesContext(classMode = BEFORE_CLASS) (1)
    class FreshContextTests {
        // some tests that require a new Spring container
    }
    
    1 在当前测试类之前改变上下文.
  • 当在一个类中声明并将类模式设为 AFTER_CLASS (也就是,默认的类模式) ,则在当前测试类之后.

    Java
    @DirtiesContext (1)
    class ContextDirtyingTests {
        // some tests that result in the Spring container being dirtied
    }
    
    1 当前测试类后改变的上下文.
    Kotlin
    @DirtiesContext (1)
    class ContextDirtyingTests {
        // some tests that result in the Spring container being dirtied
    }
    
    1 当前测试类后改变的上下文.
  • 当在一个类中声明并将类模式设为 BEFORE_EACH_TEST_METHOD,则在当前测试类的每个方法之前.

    Java
    @DirtiesContext(classMode = BEFORE_EACH_TEST_METHOD) (1)
    class FreshContextTests {
        // some tests that require a new Spring container
    }
    
    1 Dirty the context before each test method.
    Kotlin
    @DirtiesContext(classMode = BEFORE_EACH_TEST_METHOD) (1)
    class FreshContextTests {
        // some tests that require a new Spring container
    }
    
    1 Dirty the context before each test method.
  • 当在一个类中声明并将类模式设为 AFTER_EACH_TEST_METHOD,则在当前测试类的每个方法之后.

    Java
    @DirtiesContext(classMode = AFTER_EACH_TEST_METHOD) (1)
    class ContextDirtyingTests {
        // some tests that result in the Spring container being dirtied
    }
    
    1 Dirty the context after each test method.
    Kotlin
    @DirtiesContext(classMode = AFTER_EACH_TEST_METHOD) (1)
    class ContextDirtyingTests {
        // some tests that result in the Spring container being dirtied
    }
    
    1 Dirty the context after each test method.
  • 当在一个方法中声明并将方法模式设为 BEFORE_METHOD,则在当前方法之前.

    Java
    @DirtiesContext(methodMode = BEFORE_METHOD) (1)
    @Test
    void testProcessWhichRequiresFreshAppCtx() {
        // some logic that requires a new Spring container
    }
    
    1 Dirty the context before the current test method.
    Kotlin
    @DirtiesContext(methodMode = BEFORE_METHOD) (1)
    @Test
    fun testProcessWhichRequiresFreshAppCtx() {
        // some logic that requires a new Spring container
    }
    
    1 Dirty the context before the current test method.
  • 当在一个方法中声明并将方法模式设为 AFTER_METHOD(也就是说,默认的方法模式) ,则在当前方法之后.

    Java
    @DirtiesContext (1)
    @Test
    void testProcessWhichDirtiesAppCtx() {
        // some logic that results in the Spring container being dirtied
    }
    
    1 Dirty the context after the current test method.
    Kotlin
    @DirtiesContext (1)
    @Test
    fun testProcessWhichDirtiesAppCtx() {
        // some logic that results in the Spring container being dirtied
    }
    
    1 Dirty the context after the current test method.

如果 @DirtiesContext 被用于上下文被配置为通过 @ContextHierarchy 定义的上下文层次中的一部分的测试中,则 hierarchyMode 标志可用于控制如何声明上下文缓存. 默认将使用一个穷举算法用于清除包括不仅当前层次而且与当前测试拥有共同祖先的其它上下文层次的缓存. 所有在拥有共同祖先上下文的子层次的应用程序上下文都会从上下文中被移除并关闭. 如果穷举算法对于特定的使用场景显得有点威力过猛,那么你可以指定一个更简单的当前层算法来代替,如下所.

Java
@ContextHierarchy({
    @ContextConfiguration("/parent-config.xml"),
    @ContextConfiguration("/child-config.xml")
})
class BaseTests {
    // class body...
}

class ExtendedTests extends BaseTests {

    @Test
    @DirtiesContext(hierarchyMode = CURRENT_LEVEL) (1)
    void test() {
        // some logic that results in the child context being dirtied
    }
}
1 使用当前级别的算法.
Kotlin
@ContextHierarchy(
    ContextConfiguration("/parent-config.xml"),
    ContextConfiguration("/child-config.xml"))
open class BaseTests {
    // class body...
}

class ExtendedTests : BaseTests() {

    @Test
    @DirtiesContext(hierarchyMode = CURRENT_LEVEL) (1)
    fun test() {
        // some logic that results in the child context being dirtied
    }
}
1 使用当前级别的算法.

参阅 DirtiesContext.HierarchyMode帮助文档以获得 EXHAUSTIVE (穷举) 和 CURRENT_LEVEL (当前层算法)更详细的了解.

@TestExecutionListeners

@TestExecutionListeners 定义了一个类级别的元数据,用于配置需要用 TestContextManager 进行注册的 TestExecutionListener 实现. 通常,@TestExecutionListeners@ContextConfiguration 一起使用.

Java
@ContextConfiguration
@TestExecutionListeners({CustomTestExecutionListener.class, AnotherTestExecutionListener.class}) (1)
class CustomTestExecutionListenerTests {
    // class body...
}
1 注册两个 TestExecutionListener 实现.
Kotlin
@ContextConfiguration
@TestExecutionListeners(CustomTestExecutionListener::class, AnotherTestExecutionListener::class) (1)
class CustomTestExecutionListenerTests {
    // class body...
}
1 注册两个 TestExecutionListener 实现.

@TestExecutionListeners 默认支持监听器继承. 参阅 javadoc 获得示例和更详细的了解.

默认情况下, @TestExecutionListeners 支持从超类或封闭类继承监听器. 有关示例和更多详细信息, 请参见 @Nested 测试类配置@TestExecutionListeners javadoc.

@RecordApplicationEvents

@RecordApplicationEvents 是一个类级别的注解, 用于指示 Spring TestContext Framework 记录在单个测试执行期间在 ApplicationContext 中发布的所有应用程序事件.

可以在测试中通过 ApplicationEvents API 访问记录的事件.

有关示例和更多详细信息, 请参见 Application Events@RecordApplicationEvents javadoc.

@Commit

@Commit 指定具有事务的测试方法在测试方法执行完成后对事务进行提交. @Commit 可以用作 @Rollback(false) 的直接替代,以更好的传达代码的意图. 和 @Rollback 一样,@Commit 可以在类层次或者方法层级声明.

Java
@Commit (1)
@Test
void testProcessWithoutRollback() {
    // ...
}
1 将测试结果提交到数据库.
Kotlin
@Commit (1)
@Test
fun testProcessWithoutRollback() {
    // ...
}
1 将测试结果提交到数据库.
@Rollback

@Rollback 指明当测试方法执行完毕的时候是否对事务性方法中的事务进行回滚. 如果为 true,则进行回滚; 否则,则提交 (请参阅 @Commit) . 在 Spring TestContext 框架中,集成测试默认的 Rollbacktrue.

当声明为类级注解时,@Rollback 定义测试类层次结构中所有测试方法的默认回滚语义. 当声明为方法级别的注解时,@Rollback 定义特定测试方法的回滚语义,从而可能覆盖类级别的 @Rollback@Commit 语义.

以下示例使测试方法的结果不回滚 (即,结果已提交到数据库) :

Java
@Rollback(false) (1)
@Test
void testProcessWithoutRollback() {
    // ...
}
1 不要回滚结果.
Kotlin
@Rollback(false) (1)
@Test
fun testProcessWithoutRollback() {
    // ...
}
1 不要回滚结果.
@BeforeTransaction

@BeforeTransaction 表示使用 Spring 的 @Transactional 注解在事务内运行的测试方法,注解的 void 方法应在事务开始之前运行. @BeforeTransaction 方法不需要声明为 public,可以在基于 Java 8 的接口默认方法中声明.

以下示例显示了如何使用 @BeforeTransaction 注解:

Java
@BeforeTransaction (1)
void beforeTransaction() {
    // logic to be run before a transaction is started
}
1 在事务之前运行此方法.
Kotlin
@BeforeTransaction (1)
fun beforeTransaction() {
    // logic to be run before a transaction is started
}
1 在事务之前运行此方法.
@AfterTransaction

@AfterTransaction 表示使用 Spring 的 @Transactional 注解在事务内运行的测试方法,注解的 void 方法应在事务结束后运行. @AfterTransaction 方法不需要声明为 public,可以在基于 Java 8 的接口默认方法中声明.

Java
@AfterTransaction (1)
void afterTransaction() {
    // logic to be run after a transaction has ended
}
1 事务后运行此方法.
Kotlin
@AfterTransaction (1)
fun afterTransaction() {
    // logic to be run after a transaction has ended
}
1 事务后运行此方法.
@Sql

@Sql 注解用于测试类或者测试方法,可以让在集成测试过程中配置的 SQL 脚本能够在给定的的数据库中得到执行.

Java
@Test
@Sql({"/test-schema.sql", "/test-user-data.sql"}) (1)
void userTest() {
    // run code that relies on the test schema and test data
}
1 运行此测试的两个脚本.
Kotlin
@Test
@Sql("/test-schema.sql", "/test-user-data.sql") (1)
fun userTest() {
    // run code that relies on the test schema and test data
}
1 运行此测试的两个脚本.

请参阅通过 @sql 声明执行的 SQL 脚本作进一步了解.

@SqlConfig

@SqlConfig 定义了用于决定如何解析和执行通过 @Sql 注解配置的 SQL 脚本.

Java
@Test
@Sql(
    scripts = "/test-user-data.sql",
    config = @SqlConfig(commentPrefix = "`", separator = "@@") (1)
)
void userTest() {
    // run code that relies on the test data
}
1 在 SQL 脚本中设置注解前缀和分隔符.
Kotlin
@Test
@Sql("/test-user-data.sql", config = SqlConfig(commentPrefix = "`", separator = "@@")) (1)
fun userTest() {
    // run code that relies on the test data
}
1 在 SQL 脚本中设置注解前缀和分隔符.
@SqlMergeMode

@SqlMergeMode 注解用于测试类或测试方法,以配置是否将方法级 @Sql 声明与类级 @Sql 声明合并. 如果在测试类或测试方法上未声明 @SqlMergeMode,则默认情况下将使用 OVERRIDE 合并模式. 在 OVERRIDE 模式下,方法级别的 @Sql 声明将覆盖类级别的 @Sql 声明.

请注意,方法级别的 @SqlMergeMode 声明将覆盖类级别的声明.

下面的示例演示如何在类级别使用 @SqlMergeMode.

Java
@SpringJUnitConfig(TestConfig.class)
@Sql("/test-schema.sql")
@SqlMergeMode(MERGE) (1)
class UserTests {

    @Test
    @Sql("/user-test-data-001.sql")
    void standardUserProfile() {
        // run code that relies on test data set 001
    }
}
1 将类中的所有测试方法的 @Sql 合并模式设置为 MERGE.
Kotlin
@SpringJUnitConfig(TestConfig::class)
@Sql("/test-schema.sql")
@SqlMergeMode(MERGE) (1)
class UserTests {

    @Test
    @Sql("/user-test-data-001.sql")
    fun standardUserProfile() {
        // run code that relies on test data set 001
    }
}
1 将类中的所有测试方法的 @Sql 合并模式设置为 MERGE.

下面的示例演示如何在方法级别使用 @SqlMergeMode.

Java
@SpringJUnitConfig(TestConfig.class)
@Sql("/test-schema.sql")
class UserTests {

    @Test
    @Sql("/user-test-data-001.sql")
    @SqlMergeMode(MERGE) (1)
    void standardUserProfile() {
        // run code that relies on test data set 001
    }
}
1 将特定测试方法的 @Sql 合并模式设置为 MERGE.
Kotlin
@SpringJUnitConfig(TestConfig::class)
@Sql("/test-schema.sql")
class UserTests {

    @Test
    @Sql("/user-test-data-001.sql")
    @SqlMergeMode(MERGE) (1)
    fun standardUserProfile() {
        // run code that relies on test data set 001
    }
}
1 将特定测试方法的 @Sql 合并模式设置为 MERGE.
@SqlGroup

@SqlGroup 是一个用于聚合几个 @Sql 注解的容器注解. @SqlGroup 可以直接使用,通过声明几个嵌套的 @Sql 注解,也可以与 Java8 的 repeatable 注解结合使用,即简单地在同一个类或方法上声明几个 @Sql 注解,隐式地产生这个容器注解.

Java
@Test
@SqlGroup({ (1)
    @Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`")),
    @Sql("/test-user-data.sql")
)}
void userTest() {
    // run code that uses the test schema and test data
}
1 声明一组 SQL 脚本.
Kotlin
@Test
@SqlGroup( (1)
    Sql("/test-schema.sql", config = SqlConfig(commentPrefix = "`")),
    Sql("/test-user-data.sql"))
fun userTest() {
    // run code that uses the test schema and test data
}
1 声明一组 SQL 脚本.

3.4.2. 标准注解支持

以下注解为 Spring TestContext 框架所有的配置提供标准语义支持. 注意这些注解不仅限于测试,可以用在 Spring 框架的任意地方.

  • @Autowired

  • @Qualifier

  • @Value

  • @Resource (javax.annotation) if JSR-250 is present

  • @ManagedBean (javax.annotation) if JSR-250 is present

  • @Inject (javax.inject) if JSR-330 is present

  • @Named (javax.inject) if JSR-330 is present

  • @PersistenceContext (javax.persistence) if JPA is present

  • @PersistenceUnit (javax.persistence) if JPA is present

  • @Required

  • @Transactional (org.springframework.transaction.annotation) with limited attribute support

JSR-250 生命周期注解

在Spring TestContext 框架中,@PostConstruct@PreDestroy 可以通过标准语义在配置于应用程序上下文的任意应用程序组件中使用; 但是, 这些生命周期注解在实际测试类中只有很有限的作用.

如果一个测试类的方法被注解为 @PostConstruct,这个方法将在 test 框架中的任何 before 方法 (例如,使用JUnit Jupiter的 @BeforeEach 注解的方法) 调用之前被执行, 这个规则将被应用于测试类的每个方法. 另一方面,如果一个测试类的方法被注解为 @PreDestroy,这个方法将永远不会被执行. 因为建议在测试类中使用 test 框架的测试生命周期回调来代替使用 @PostConstruct@PreDestroy.

3.4.3. Spring JUnit 4 测试注解

以下注解仅在与SpringRunner,Spring的JUnit 4规则或 Spring 的JUnit 4 支持类结合使用时才受支持:

@IfProfileValue

@IfProfileValue 指明该测试只在特定的测试环境中被启用. 如果 ProfileValueSource 配置的 value 属性与此注解配置的 name 属性一致,这该测试将被启用. 否则,该测试将被禁用并忽略.

@IfProfileValue 可以用在类级别、方法级别或者两个同时. 使用类级别的 @IfProfileValue 注解优先于当前类或其子类的任意方法的使用方法级别的注解. 有 @IfProfileValue 注解意味着则测试被隐式开启. 这与 JUnit4 的 @Ignore 注解是相类似的,除了使用 @Ignore 注解是用于禁用测试的之外.

以下示例显示了具有 @IfProfileValue 注解的测试:

Java
@IfProfileValue(name="java.vendor", value="Oracle Corporation") (1)
@Test
public void testProcessWhichRunsOnlyOnOracleJvm() {
    // some logic that should run only on Java VMs from Oracle Corporation
}
1 仅当 Java 供应商是 "Oracle Corporation" 时才运行此测试.
Kotlin
@IfProfileValue(name="java.vendor", value="Oracle Corporation") (1)
@Test
fun testProcessWhichRunsOnlyOnOracleJvm() {
    // some logic that should run only on Java VMs from Oracle Corporation
}
1 仅当 Java 供应商是 "Oracle Corporation" 时才运行此测试.

另外,您可以使用 values 列表 (带有 OR 语义) 配置 @IfProfileValue,以在 JUnit 4 环境中实现对 Test Group 的类似于 TestNG 的支持. 考虑以下示例:

Java
@IfProfileValue(name="test-groups", values={"unit-tests", "integration-tests"}) (1)
@Test
public void testProcessWhichRunsForUnitOrIntegrationTestGroups() {
    // some logic that should run only for unit and integration test groups
}
1 对单元测试和集成测试运行此测试.
Kotlin
@IfProfileValue(name="test-groups", values=["unit-tests", "integration-tests"]) (1)
@Test
fun testProcessWhichRunsForUnitOrIntegrationTestGroups() {
    // some logic that should run only for unit and integration test groups
}
1 对单元测试和集成测试运行此测试.
@ProfileValueSourceConfiguration

@ProfileValueSourceConfiguration 是类级别注解,用于当获取通过 @IfProfileValue 配置的 profile 值时指定使用什么样的 ProfileValueSource 类型. 如果一个测试没有指定 @ProfileValueSourceConfiguration,那么默认使用 SystemProfileValueSource.

Java
@ProfileValueSourceConfiguration(CustomProfileValueSource.class) (1)
public class CustomProfileValueSourceTests {
    // class body...
}
1 Use a custom profile value source.
Kotlin
@ProfileValueSourceConfiguration(CustomProfileValueSource::class) (1)
class CustomProfileValueSourceTests {
    // class body...
}
1 Use a custom profile value source.
@Timed

@Timed 用于指明被注解的测试必须在指定的时限 (毫秒) 内结束. 如果测试超过指定时限,就当作测试失败.

时限包括测试方法本身所耗费的时间,包括任何重复 (请查看 @Repeat) 及任意初始化和销毁所用的时间.

Java
@Timed(millis = 1000) (1)
public void testProcessWithOneSecondTimeout() {
    // some logic that should not take longer than 1 second to run
}
1 Set the time period for the test to one second.
Kotlin
@Timed(millis = 1000) (1)
fun testProcessWithOneSecondTimeout() {
    // some logic that should not take longer than 1 second to run
}
1 Set the time period for the test to one second.

Spring的 @Timed 注解与 JUnit 4 的 @Test(timeout=…) 支持相比具有不同的语义. 确切地说,由于在 JUnit 4 中处理方法执行超时的方式 (也就是,在独立纯程中执行该测试方法) ,如果一个测试方法执行时间太长,@Test(timeout=…) 将直接判定该测试失败. 而 Spring 的 @Timed 则不直接判定失败而是等待测试完成.

@Repeat

@Repeat 指明该测试方法需被重复执行. 注解指定该测试方法被重复的次数. 重复的范围包括该测试方法自身也包括相应的初始化和销毁方法.

重复执行的范围包括测试方法本身的执行以及测试夹具的任何安装或拆除. 当与 SpringMethodRule 一起使用时,范围还包括通过 TestExecutionListener 实现准备测试实例。 以下示例显示了如何使用 @Repeat 注解:

Java
@Repeat(10) (1)
@Test
public void testProcessRepeatedly() {
    // ...
}
1 重复此测试十次.
Kotlin
@Repeat(10) (1)
@Test
fun testProcessRepeatedly() {
    // ...
}
1 重复此测试十次.

3.4.4. Spring JUnit Jupiter Testing Annotations

以下注解仅在与SpringExtension 和 JUnit Jupiter (即 JUnit 5 中的编程模型) 结合使用时才受支持:

@SpringJUnitConfig

@SpringJUnitConfig 是一个组合注解,它将 JUnit Jupiter 的 @ExtendWith(SpringExtension.class) 与 Spring TestContext Framework 的 @ContextConfiguration 组合在一起. 它可以在类级别用作 @ContextConfiguration 的直接替代. 关于配置选项,@ContextConfiguration@SpringJUnitConfig 之间的唯一区别是可以使用 @SpringJUnitConfig 中的 value 属性声明组件类.

以下示例显示如何使用 @SpringJUnitConfig 注解指定配置类:

Java
@SpringJUnitConfig(TestConfig.class) (1)
class ConfigurationClassJUnitJupiterSpringTests {
    // class body...
}
1 指定配置类.
Kotlin
@SpringJUnitConfig(TestConfig::class) (1)
class ConfigurationClassJUnitJupiterSpringTests {
    // class body...
}
1 指定配置类.

以下示例显示如何使用 @SpringJUnitConfig 注解指定配置文件的位置:

Java
@SpringJUnitConfig(locations = "/test-config.xml") (1)
class XmlJUnitJupiterSpringTests {
    // class body...
}
1 指定配置文件的位置.
Kotlin
@SpringJUnitConfig(locations = ["/test-config.xml"]) (1)
class XmlJUnitJupiterSpringTests {
    // class body...
}
1 指定配置文件的位置.

有关更多详细信息,请参见上下文管理以及 @SpringJUnitConfig@ContextConfiguration 的javadoc.

@SpringJUnitWebConfig

@SpringJUnitWebConfig 是一个组合的注解,它将来自 JUnit Jupiter 的 @ExtendWith(SpringExtension.class) 与来自 Spring TestContext Framework 的 @ContextConfiguration@WebAppConfiguration 组合在一起. 您可以在类级别使用它来替代 @ContextConfiguration@WebAppConfiguration. 关于配置选项,@ContextConfiguration@SpringJUnitWebConfig 之间的唯一区别是,您可以使用 @SpringJUnitWebConfig 中的 value 属性来声明组件类. 此外,仅通过使用 @SpringJUnitWebConfig 中的 resourcePath 属性,可以覆盖 @WebAppConfiguration 中的 value 属性.

以下示例显示如何使用 @SpringJUnitWebConfig 注解指定配置类:

Java
@SpringJUnitWebConfig(TestConfig.class) (1)
class ConfigurationClassJUnitJupiterSpringWebTests {
    // class body...
}
1 指定配置类.
Kotlin
@SpringJUnitWebConfig(TestConfig::class) (1)
class ConfigurationClassJUnitJupiterSpringWebTests {
    // class body...
}
1 指定配置类.

以下示例显示如何使用 @SpringJUnitWebConfig 注解指定配置文件的位置:

Java
@SpringJUnitWebConfig(locations = "/test-config.xml") (1)
class XmlJUnitJupiterSpringWebTests {
    // class body...
}
1 指定配置文件的位置.
Kotlin
@SpringJUnitWebConfig(locations = ["/test-config.xml"]) (1)
class XmlJUnitJupiterSpringWebTests {
    // class body...
}
1 指定配置文件的位置.

有关更多详细信息,请参见上下文管理 以及 @SpringJUnitWebConfig, @ContextConfiguration@WebAppConfiguration的javadoc.

@TestConstructor

@TestConstructor 是类型级别的注解,用于配置如何从测试的 ApplicationContext 中的组件自动连接测试类构造函数的参数.

如果在测试类上不存在 @TestConstructor 或不存在 meta-test,则将使用默认的测试构造函数自动装配模式. 有关如何更改默认模式的详细信息,请参见下面的提示. 但是请注意,构造函数上的 @Autowired 本地声明优先于 @TestConstructor 和默认模式.

更改默认的测试构造函数自动装配模式

可以通过将 JVM 系统属性 spring.test.constructor.autowire.mode 设置为 all 来更改默认的测试构造函数自动装配模式. 或者,可以通过 SpringProperties 机制更改默认模式.

从 Spring Framework 5.3 开始, 默认模式也可以配置为 JUnit Platform configuration parameter.

如果未设置 spring.test.constructor.autowire.mode 属性,则测试类构造函数将不会自动进行自动装配.

从Spring Framework 5.2 开始,仅将 @TestConstructorSpringExtension 结合使用以与 JUnit Jupiter 一起使用. 请注意,SpringExtension 通常会自动为您注册-例如,在使用 @SpringJUnitConfig@SpringJUnitWebConfig 之类的注解或 Spring Boot Test 中与测试相关的各种注解时.
@NestedTestConfiguration

@NestedTestConfiguration 是类型级别的注解, 用于配置内部测试类的封闭类层次结构中如何处理 Spring 测试配置注解.

如果 @NestedTestConfiguration 在测试类, 其超级类型层次结构或其封闭类层次结构中不存在或不存在于测试类中, 则将使用默认的封闭配置继承模式. 有关如何更改默认模式的详细信息, 请参见下面的提示.

更改默认的封闭配置继承模式

默认的封闭配置继承模式是 INHERIT, 但是可以通过将 spring.test.enclosing.configuration JVM 系统属性设置为 OVERRIDE 来更改. 或者, 可以通过 SpringProperties 机制设置默认模式.

Spring TestContext Framework 使用以下注解的 @NestedTestConfiguration 语义.

Spring TestContext 框架 对于以下注解尊重 @NestedTestConfiguration 语义.

通常, 仅将 @NestedTestConfiguration 与 JUnit Jupiter 中的 @Nested 测试类结合使用才有意义. 但是, 可能存在其他支持此注解的测试框架, 这些框架都支持 Spring 和嵌套测试类.

有关示例和更多详细信息, 请参见 @Nested 测试类配置.

@EnabledIf

@EnabledIf 用于表示已注解的 JUnit Jupiter 测试类或测试方法已启用,如果提供的表达式的值为 true,则应运行 @EnabledIf . 具体来说,如果表达式的计算结果为 Boolean.TRUE 或等于 true 的字符串 (忽略大小写) ,则启用测试. 在类级别应用时,默认情况下也会自动启用该类中的所有测试方法.

表达式可以是以下任意一种:

  • Spring Expression Language (SpEL) 表达式: @EnabledIf("#{systemProperties['os.name'].toLowerCase().contains('mac')}")

  • Spring Environment中可用属性的占位符. 例如: @EnabledIf("${smoke.tests.enabled}")

  • 文本文字: @EnabledIf("true")

但是请注意,不是属性占位符的动态解析结果的文本文字的实际值为零,因为 @EnabledIf("false") 等效于 @Disabled,而 @EnabledIf("true") 在逻辑上是没有意义的 .

您可以使用 @EnabledIf 作为元注解来创建自定义的组合注解. 例如,您可以创建一个自定义 @EnabledOnMac 注解,如下所示:

Java
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@EnabledIf(
    expression = "#{systemProperties['os.name'].toLowerCase().contains('mac')}",
    reason = "Enabled on Mac OS"
)
public @interface EnabledOnMac {}
Kotlin
@Target(AnnotationTarget.TYPE, AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@EnabledIf(
        expression = "#{systemProperties['os.name'].toLowerCase().contains('mac')}",
        reason = "Enabled on Mac OS"
)
annotation class EnabledOnMac {}

@EnabledOnMac 仅作为示例。 如果你有那个确切的用例,请使用 JUnit Jupiter 中内置的 @EnabledOnOs(MAC) 支持。

从 JUnit 5.7 开始,JUnit Jupiter 也有一个名为 @EnabledIf 的条件注解。 因此,如果您希望使用 Spring 的 @EnabledIf 支持,请确保从正确的包中导入注释类型。

@DisabledIf

@DisabledIf 用于表示已注解的 JUnit Jupiter 测试类或测试方法已禁用,并且如果提供的表达式求值为 true,则不应执行该操作. 具体来说,如果表达式的计算结果为 Boolean.TRUE 或等于 true 的 String (忽略大小写) ,则测试将被禁用. 当在类级别应用时,该类中的所有测试方法也会自动禁用.

表达式可以是以下任意一种:

  • Spring Expression Language (SpEL)表达式: @DisabledIf("#{systemProperties['os.name'].toLowerCase().contains('mac')}")

  • Spring Environment中可用属性的占位符. 例如:: @DisabledIf("${smoke.tests.disabled}")

  • 文本文字: @DisabledIf("true")

但是请注意,不是属性占位符动态解析的结果的文本文字的实际值为零,因为 @DisabledIf("true") 等效于 @Disabled,而 @DisabledIf("false") 在逻辑上是没有意义的 .

您可以使用 @DisabledIf 作为元注解来创建自定义的组合注解. 例如,您可以创建一个自定义 @DisabledOnMac 注解,如下所示:

Java
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@DisabledIf(
    expression = "#{systemProperties['os.name'].toLowerCase().contains('mac')}",
    reason = "Disabled on Mac OS"
)
public @interface DisabledOnMac {}
Kotlin
@Target(AnnotationTarget.TYPE, AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@DisabledIf(
        expression = "#{systemProperties['os.name'].toLowerCase().contains('mac')}",
        reason = "Disabled on Mac OS"
)
annotation class DisabledOnMac {}

@EnabledOnMac 仅作为示例。 如果你有那个确切的用例,请使用 JUnit Jupiter 中内置的 @EnabledOnOs(MAC) 支持。

从 JUnit 5.7 开始,JUnit Jupiter 也有一个名为 @EnabledIf 的条件注解。 因此,如果您希望使用 Spring 的 @EnabledIf 支持,请确保从正确的包中导入注释类型。

3.4.5. Meta-Annotation Support for Testing

可以将大部分测试相关的注解当作meta-annotations使用,以创建自定义组合注解来减少测试集中的重复配置.

下面的每个都可以在TestContext 框架中被当作 meta-annotations 使用.

  • @BootstrapWith

  • @ContextConfiguration

  • @ContextHierarchy

  • @ActiveProfiles

  • @TestPropertySource

  • @DirtiesContext

  • @WebAppConfiguration

  • @TestExecutionListeners

  • @Transactional

  • @BeforeTransaction

  • @AfterTransaction

  • @Commit

  • @Rollback

  • @Sql

  • @SqlConfig

  • @SqlMergeMode

  • @SqlGroup

  • @Repeat (only supported on JUnit 4)

  • @Timed (only supported on JUnit 4)

  • @IfProfileValue (only supported on JUnit 4)

  • @ProfileValueSourceConfiguration (only supported on JUnit 4)

  • @SpringJUnitConfig (only supported on JUnit Jupiter)

  • @SpringJUnitWebConfig (only supported on JUnit Jupiter)

  • @TestConstructor (only supported on JUnit Jupiter)

  • @NestedTestConfiguration (only supported on JUnit Jupiter)

  • @EnabledIf (only supported on JUnit Jupiter)

  • @DisabledIf (only supported on JUnit Jupiter)

考虑以下示例:

Java
@RunWith(SpringRunner.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
public class OrderRepositoryTests { }

@RunWith(SpringRunner.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
public class UserRepositoryTests { }
Kotlin
@RunWith(SpringRunner::class)
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
@ActiveProfiles("dev")
@Transactional
class OrderRepositoryTests { }

@RunWith(SpringRunner::class)
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
@ActiveProfiles("dev")
@Transactional
class UserRepositoryTests { }

如果发现我们在基于 JUnit 4 的测试套件中重复了前面的配置,则可以通过引入一个自定义的组合注解来减少重复,该注解集中了 Spring 的通用测试配置,如下所示:

Java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
public @interface TransactionalDevTestConfig { }
Kotlin
@Target(AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
@ActiveProfiles("dev")
@Transactional
annotation class TransactionalDevTestConfig { }

然后,我们可以使用我们的自定义 @TransactionalDevTestConfig 注解来简化基于单个 JUnit 4 的测试类的配置,如下所示:

Java
@RunWith(SpringRunner.class)
@TransactionalDevTestConfig
public class OrderRepositoryTests { }

@RunWith(SpringRunner.class)
@TransactionalDevTestConfig
public class UserRepositoryTests { }
Kotlin
@RunWith(SpringRunner::class)
@TransactionalDevTestConfig
class OrderRepositoryTests

@RunWith(SpringRunner::class)
@TransactionalDevTestConfig
class UserRepositoryTests

如果我们编写使用 JUnit Jupiter 的测试,则可以进一步减少代码重复,因为 JUnit 5 中的注解也可以用作元注解. 考虑以下示例:

Java
@ExtendWith(SpringExtension.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
class OrderRepositoryTests { }

@ExtendWith(SpringExtension.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
class UserRepositoryTests { }
Kotlin
@ExtendWith(SpringExtension::class)
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
@ActiveProfiles("dev")
@Transactional
class OrderRepositoryTests { }

@ExtendWith(SpringExtension::class)
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
@ActiveProfiles("dev")
@Transactional
class UserRepositoryTests { }

如果我们发现要在基于 JUnit Jupiter 的测试套件中重复上述配置,则可以通过引入一个自定义的组合注解来减少重复,该注解集中了 Spring 和 JUnit Jupiter 的通用测试配置,如下所示:

Java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(SpringExtension.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
public @interface TransactionalDevTestConfig { }
Kotlin
@Target(AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@ExtendWith(SpringExtension::class)
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
@ActiveProfiles("dev")
@Transactional
annotation class TransactionalDevTestConfig { }

然后,我们可以使用我们的自定义 @TransactionalDevTestConfig 注解来简化基于单个 JUnit Jupiter 的测试类的配置,如下所示:

Java
@TransactionalDevTestConfig
class OrderRepositoryTests { }

@TransactionalDevTestConfig
class UserRepositoryTests { }
Kotlin
@TransactionalDevTestConfig
class OrderRepositoryTests { }

@TransactionalDevTestConfig
class UserRepositoryTests { }

由于 JUnit Jupiter 支持使用 @Test,@RepeatedTest,ParameterizedTest 和其他作为元注解,因此您还可以在测试方法级别创建自定义的组合注解. 例如,如果我们希望创建一个组合的注解,将 JUnit Jupiter 的 @Test@Tag 注解与 Spring 的 @Transactional 注解相结合,则可以创建一个 @TransactionalIntegrationTest 注解,如下所示:

Java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Transactional
@Tag("integration-test") // org.junit.jupiter.api.Tag
@Test // org.junit.jupiter.api.Test
public @interface TransactionalIntegrationTest { }
Kotlin
@Target(AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@Transactional
@Tag("integration-test") // org.junit.jupiter.api.Tag
@Test // org.junit.jupiter.api.Test
annotation class TransactionalIntegrationTest { }

然后,我们可以使用我们的自定义 @TransactionalIntegrationTest 注解来简化基于JUnit Jupiter的各个测试方法的配置,如下所示:

Java
@TransactionalIntegrationTest
void saveOrder() { }

@TransactionalIntegrationTest
void deleteOrder() { }
Kotlin
@TransactionalIntegrationTest
fun saveOrder() { }

@TransactionalIntegrationTest
fun deleteOrder() { }

有关更多详细信息,请参见 Spring Annotation编程模型Wiki页面.

3.5. Spring TestContext 框架

Spring TestContext Framework (位于 org.springframework.test.context 包中) 提供了通用的,注解驱动的单元和集成测试支持,这些支持与所使用的测试框架无关. TestContext 框架还非常重视约定优于配置,您可以通过基于注解的配置覆盖合理的默认值.

除了通用测结构之外,TestContext 框架还为 JUnit 4,JUnit Jupiter (AKA JUnit 5) 和 TestNG 提供了显式支持. 对于 JUnit 4 和 TestNG,Spring 提供了抽象支持类. 此外,Spring 为 JUnit 4 提供了自定义 JUnit Runner 和自定义 JUnit Rules ,以及 JUnit Jupiter 的自定义扩展, 可让您编写所谓的 POJO 测试类. 不需要 POJO 测试类来扩展特定的类层次结构,例如抽象支持类.

以下部分概述了 TestContext 框架的内部. 如果您只对使用框架感兴趣,而对使用自己的自定义监听器或自定义加载程序进行扩展不感兴趣,请直接转到配置 (上下文管理, 依赖注入, 事务管理) ,支持类注解支持部分.

3.5.1. Key 抽象

该框架的核心由 TestContextManager 类和 TestContext,TestExecutionListenerSmartContextLoader 接口组成. 为每个测试类创建一个 TestContextManager (例如,用于在 JUnit Jupiter 中的单个测试类中执行所有测试方法) . 反过来,TestContextManager 管理一个 TestContext,它保存当前测试的上下文. 随着测试的进行,TestContextManager 还更新了 TestContext 的状态,并委托给 TestExecutionListener 实现,该实现通过提供依赖注入,管理事务等来检测实际的测试执行. SmartContextLoader 负责为给定的测试类加载 ApplicationContext. 有关更多信息和各种实现的示例,请参见 javadoc和 Spring 测试套件.

TestContext

TestContext 封装了在其中执行测试的上下文 (与使用中的实际测试框架无关) ,并为其负责的测试实例提供了上下文管理和缓存支持. 如果需要,TestContext 还委托给 SmartContextLoader 来加载 ApplicationContext.

TestContextManager

TestContextManager 是 Spring TestContext Framework 的主要入口点,并负责管理单个 TestContext 并在定义良好的测试执行点向每个注册的 TestExecutionListener 发出事件信号:

  • 在特定测试框架的任何 “before class” “before all” 方法之前. 测试实例后处理.

  • 测试实例后处理.

  • 在特定测试框架的任何 “before” 或 “before each” 方法之前.

  • 在执行测试方法之前但在测试设置之后.

  • 在执行测试方法之后但立即将测试拆解.

  • 在特定测试框架的任何 “after” 或 “after each” 方法之后.

  • 在特定测试框架的任何 “after class” 或 “after all” 方法之后

TestExecutionListener

TestExecutionListener 定义用于对由注册监听器的 TestContextManager 发布的测试执行事件做出响应的 API. 请参阅 TestExecutionListener 配置.

Context Loaders

ContextLoader 是一个策略接口,用于为 Spring TestContext Framework 管理的集成测试加载 ApplicationContext. 您应该实现 SmartContextLoader 而不是此接口,以提供对组件类, active bean 定义配置文件,测试属性源,上下文层次结构和 WebApplicationContext 支持的支持.

SmartContextLoaderContextLoader 接口的扩展,它取代了原始的最小 ContextLoader SPI. 具体来说,SmartContextLoader 可以选择处理资源位置,组件类或上下文初始化程序. 此外,SmartContextLoader 可以在其加载的上下文中设置 active 的 Bean 定义配置文件并测试属性源.

Spring提供了以下实现:

  • DelegatingSmartContextLoader: 这是两个默认加载器之一,它在内部委派给 AnnotationConfigContextLoader,GenericXmlContextLoaderGenericGroovyXmlContextLoader,具体取决于为测试类声明的配置或默认位置或默认配置类的存在. 仅当 Groovy 在类路径上时才启用Groovy支持.

  • WebDelegatingSmartContextLoader: 这是两个默认加载器之一,它在内部委派给 AnnotationConfigWebContextLoader,GenericXmlWebContextLoaderGenericGroovyXmlWebContextLoader,具体取决于为测试类声明的配置或默认位置或默认配置类的存在. 仅当测试类上存在 @WebAppConfiguration 时,才使用 Web ContextLoader. 仅当 Groovy 在类路径上时才启用 Groovy 支持.

  • AnnotationConfigContextLoader: 从组件类加载标准 ApplicationContext.

  • AnnotationConfigWebContextLoader: 从组件类加载 WebApplicationContext.

  • GenericGroovyXmlContextLoader: 从 Groovy 脚本或 XML 配置文件的资源位置加载标准 ApplicationContext.

  • GenericGroovyXmlWebContextLoader: 从 Groovy 脚本或 XML 配置文件的资源位置加载 WebApplicationContext.

  • GenericXmlContextLoader: 从 XML 资源位置加载标准 ApplicationContext.

  • GenericXmlWebContextLoader: 从 XML 资源位置加载 WebApplicationContext.

  • GenericPropertiesContextLoader: 从 Java 属性文件加载标准 ApplicationContext.

3.5.2. 引导TestContext框架

Spring TestContext Framework 内部的默认配置足以满足所有常见用例. 但是,有时开发团队或第三方框架希望更改默认的 ContextLoader,实现自定义的 TestContextContextCache,扩展默认的 ContextCustomizerFactoryTestExecutionListener 实现集,等等. 为了对 TestContext 框架的运行方式进行低级控制,Spring 提供了自举策略.

TestContextBootstrapper 定义了用于引导 TestContext 框架的 SPI. TestContextManager 使用 TestContextBootstrapper 加载当前测试的 TestExecutionListener 实现,并构建它管理的 TestContext. 您可以直接使用 @BootstrapWith 或作为元注解,为测试类 (或测试类层次结构) 配置自定义引导策略. 如果没有使用 @BootstrapWith 显式配置引导程序,则根据 @WebAppConfiguration 的存在,使用 DefaultTestContextBootstrapperWebTestContextBootstrapper.

由于 TestContextBootstrapper SPI 将来可能会更改 (以适应新要求) ,因此我们强烈建议实现者不要直接实现此接口,而应扩展 AbstractTestContextBootstrapper 或其具体子类之一.

3.5.3. TestExecutionListener 配置

Spring 提供了以下 TestExecutionListener 实现,这些实现默认情况下按以下顺序注册:

  • ServletTestExecutionListener: 为 WebApplicationContext 配置 Servlet API 模拟.

  • DirtiesContextBeforeModesTestExecutionListener: 处理 @DirtiesContext 注解的 "before" 模式.

  • ApplicationEventsTestExecutionListener: 为 ApplicationEvents 提供支持.

  • DependencyInjectionTestExecutionListener: 为测试实例提供依赖注入.

  • DirtiesContextTestExecutionListener: 处理 "after" 模式的 @DirtiesContext 注解.

  • TransactionalTestExecutionListener: 提供具有默认回滚语义的事务测试执行.

  • SqlScriptsTestExecutionListener: 运行使用 @Sql 注解配置的 SQL 脚本.

  • EventPublishingTestExecutionListener: 将测试执行事件发布到测试的 ApplicationContext 中 (请参阅测试执行事件) .

注册 TestExecutionListener 实现

您可以使用 @TestExecutionListeners 注解为测试类及其子类注册 TestExecutionListener 实现. 有关详细信息和示例,请参见注解支持@TestExecutionListeners 的javadoc.

自动发现默认的 TestExecutionListener 实现

通过使用 @TestExecutionListeners 注册 TestExecutionListener 实现适用于在有限的测试方案中使用的自定义监听器. 但是,如果需要在整个测试套件中使用自定义监听器,则可能会变得很麻烦. 通过支持通过 SpringFactoriesLoader 机制自动发现默认的 TestExecutionListener 实现,解决了此问题.

具体来说,spring-test 模块在其 META-INF/spring.factories 属性文件中的 org.springframework.test.context.TestExecutionListener 项下声明所有核心默认 TestExecutionListener 实现. 第三方框架和开发人员可以通过自己的 META-INF/spring.factories 属性文件以相同的方式将自己的 TestExecutionListener 实现贡献到默认监听器列表中.

排序 TestExecutionListener 实现

当 TestContext 框架通过上述 SpringFactoriesLoader 机制发现默认的 TestExecutionListener 实现时,实例化的监听器将使用 Spring 的 AnnotationAwareOrderComparator 进行排序,该类将使用Spring的 Ordered 接口和 @Order 注解进行排序. Spring 提供的 AbstractTestExecutionListener 和所有默认的 TestExecutionListener 实现以适当的值实现 Ordered. 因此,第三方框架和开发人员应通过实施 Ordered 或声明 @Order 来确保以默认顺序注册其默认的 TestExecutionListener 实现. 请参阅 javadoc 以获取核心默认 TestExecutionListener 实现的 getOrder() 方法,以获取有关为每个核心监听器分配哪些值的详细信息.

合并 TestExecutionListener 实现

如果通过 @TestExecutionListeners 注册了自定义 TestExecutionListener,则不会注册默认监听器. 在大多数常见的测试方案中,这有效地迫使开发人员手动声明除任何自定义监听器之外的所有默认监听器. 下面的清单演示了这种配置样式:

Java
@ContextConfiguration
@TestExecutionListeners({
    MyCustomTestExecutionListener.class,
    ServletTestExecutionListener.class,
    DirtiesContextBeforeModesTestExecutionListener.class,
    DependencyInjectionTestExecutionListener.class,
    DirtiesContextTestExecutionListener.class,
    TransactionalTestExecutionListener.class,
    SqlScriptsTestExecutionListener.class
})
class MyTest {
    // class body...
}
Kotlin
@ContextConfiguration
@TestExecutionListeners(
    MyCustomTestExecutionListener::class,
    ServletTestExecutionListener::class,
    DirtiesContextBeforeModesTestExecutionListener::class,
    DependencyInjectionTestExecutionListener::class,
    DirtiesContextTestExecutionListener::class,
    TransactionalTestExecutionListener::class,
    SqlScriptsTestExecutionListener::class
)
class MyTest {
    // class body...
}

这种方法的挑战在于,它要求开发人员确切地知道默认情况下注册了哪些监听器. 此外,默认的监听器集可以随版本的不同而变化-例如,Spring Framework 4.1 中引入了 SqlScriptsTestExecutionListener, 而 Spring Framework 4.2 中引入了 DirtiesContextBeforeModesTestExecutionListener. 此外,诸如 Spring Boot 和 Spring Security 之类的第三方框架通过使用上述自动发现机制注册了自己的默认 TestExecutionListener 实现.

为避免必须了解并重新声明所有默认监听器,可以将 @TestExecutionListenersmergeMode 属性设置为 MergeMode.MERGE_WITH_DEFAULTS. MERGE_WITH_DEFAULTS指示应将本地声明的监听器与默认监听器合并. 合并算法可确保从列表中删除重复项,并确保根据 AnnotationAwareOrderComparator 的语义对合并的监听器集进行排序,如 排序 TestExecutionListener 实现中所述. 如果监听器实现 Ordered 或使用 @Order 进行注解,则它可以影响将其与默认值合并的位置. 否则,合并时,本地声明的监听器将追加到默认监听器列表中.

例如,如果上一个示例中的 MyCustomTestExecutionListener 类将其顺序值 (例如 500) 配置为小于 ServletTestExecutionListener 的顺序 (恰好是 1000) ,则可以将 MyCustomTestExecutionListener 自动与默认列表合并. 在 ServletTestExecutionListener 前面,并且前面的示例可以替换为以下内容:

Java
@ContextConfiguration
@TestExecutionListeners(
    listeners = MyCustomTestExecutionListener.class,
    mergeMode = MERGE_WITH_DEFAULTS
)
class MyTest {
    // class body...
}
Kotlin
@ContextConfiguration
@TestExecutionListeners(
        listeners = [MyCustomTestExecutionListener::class],
        mergeMode = MERGE_WITH_DEFAULTS
)
class MyTest {
    // class body...
}

3.5.4. Application Events

从 Spring Framework 5.3.3 开始, TestContext 框架提供了对记录在 ApplicationContext 中发布的 application events 的支持, 以便可以针对测试中的那些事件执行断言. 可以通过 ApplicationEvents API 获得在单个测试执行过程中发布的所有事件, 该事件使您可以将事件作为 java.util.Stream 进行处理.

要在测试中使用 ApplicationEvents, 请执行以下操作.

  • 确保使用 @RecordApplicationEvents 对测试类进行注解或进行元注解.

  • 确保已注册 ApplicationEventsTestExecutionListener. 但是请注意 ApplicationEventsTestExecutionListener 默认情况下已注册, 并且只有通过 @TestExecutionListeners 进行自定义配置 (不包括默认监听器) 时, 才需要手动注册.

  • @Autowired 注解类型为 ApplicationEvents 的字段, 并在测试和生命周期方法 (例如 JUnit Jupiter 中的 @BeforeEach@AfterEach 方法) 中使用 ApplicationEvents 的该实例.

    • JUnit Jupiter的SpringExtension 用于 JUnit Jupiter 时, 可以在测试或生命周期方法中声明 ApplicationEvents 类型的方法参数, 以替代测试类中的 @Autowired 字段.

下面的测试类使用 JUnit Jupiter 和 AssertJSpringExtension 来断言在调用 spring 管理组件中的方法时发布的应用事件的类型:

Java
@SpringJUnitConfig(/* ... */)
@RecordApplicationEvents (1)
class OrderServiceTests {

    @Autowired
    OrderService orderService;

    @Autowired
    ApplicationEvents events; (2)

    @Test
    void submitOrder() {
        // Invoke method in OrderService that publishes an event
        orderService.submitOrder(new Order(/* ... */));
        // Verify that an OrderSubmitted event was published
        long numEvents = events.stream(OrderSubmitted.class).count(); (3)
        assertThat(numEvents).isEqualTo(1);
    }
}
1 @RecordApplicationEvents 注解测试类.
2 为当前测试注入 ApplicationEvents 实例.
3 使用 ApplicationEvents API 计算发布了多少 OrderSubmitted 事件.
Kotlin
@SpringJUnitConfig(/* ... */)
@RecordApplicationEvents (1)
class OrderServiceTests {

    @Autowired
    lateinit var orderService: OrderService

    @Autowired
    lateinit var events: ApplicationEvents (2)

    @Test
    fun submitOrder() {
        // Invoke method in OrderService that publishes an event
        orderService.submitOrder(Order(/* ... */))
        // Verify that an OrderSubmitted event was published
        val numEvents = events.stream(OrderSubmitted::class).count() (3)
        assertThat(numEvents).isEqualTo(1)
    }
}
1 @RecordApplicationEvents 注解测试类.
2 为当前测试注入 ApplicationEvents 实例.
3 使用 ApplicationEvents API 计算发布了多少 OrderSubmitted 事件.

有关 ApplicationEvents API 的更多详细信息, 请参见 ApplicationEvents javadoc.

3.5.5. 测试执行事件

Spring Framework 5.2 中引入的 EventPublishingTestExecutionListener 提供了一种实现自定义 TestExecutionListener 的替代方法. 测试的 ApplicationContext 中的组件可以监听 EventPublishingTestExecutionListener 发布的以下事件, 每个事件都与 TestExecutionListener API中的方法相对应.

  • BeforeTestClassEvent

  • PrepareTestInstanceEvent

  • BeforeTestMethodEvent

  • BeforeTestExecutionEvent

  • AfterTestExecutionEvent

  • AfterTestMethodEvent

  • AfterTestClassEvent

这些事件可能由于各种原因而被消耗,例如重置模拟 bean 或跟踪测试执行. 使用测试执行事件而不是实现自定义 TestExecutionListener 的一个优点是,测试执行事件可以被测试 ApplicationContext 中注册的任何 Spring bean 所消耗,并且此类 bean 可以直接受益于依赖注入和 ApplicationContext 的其他功能. 相反,TestExecutionListener 不是 ApplicationContext 中的 bean.

EventPublishingTestExecutionListener 默认注册; 但是,它仅在 ApplicationContext 已经加载 时发布事件。 这可以防止 ApplicationContext 被不必要或过早地加载。

因此,在 ApplicationContext 被另一个 TestExecutionListener 加载之前,不会发布 BeforeTestClassEvent。 例如,在注册了默认的 TestExecutionListener 实现集后,将不会为使用特定测试 ApplicationContext 的第一个测试类发布 BeforeTestClassEvent,但会为任何后续测试类发布 BeforeTestClassEvent 使用相同测试 ApplicationContext 的相同测试套件,因为在后续测试类运行时已经加载了上下文(只要上下文没有通过 @DirtiesContext 或 max-size 从 ContextCache 中删除 驱逐政策)。

如果您希望确保始终为每个测试类发布 BeforeTestClassEvent,则需要在 beforeTestClass 回调中注册一个加载 ApplicationContextTestExecutionListener,并且必须在`EventPublishingTestExecutionListener` 注册之前注册 TestExecutionListener

同样,如果在给定测试类中的最后一个测试方法之后使用 @DirtiesContext 从上下文缓存中删除 ApplicationContext,则不会为该测试类发布 AfterTestClassEvent

为了监听测试执行事件,Spring bean 可以选择实现 org.springframework.context.ApplicationListener 接口. 另外,可以使用 @EventListener 注解监听器方法,并将监听器方法配置为监听上面列出的特定事件类型之一 (请参阅基于注解的事件监听器) . 由于这种方法的流行,Spring 提供了以下专用的 @EventListener 注解,以简化测试执行事件监听器的注册. 这些注解驻留在 org.springframework.test.context.event.annotation 包中.

  • @BeforeTestClass

  • @PrepareTestInstance

  • @BeforeTestMethod

  • @BeforeTestExecution

  • @AfterTestExecution

  • @AfterTestMethod

  • @AfterTestClass

异常处理

默认情况下,如果测试执行事件监听器在使用事件时抛出异常,则该异常将传播到使用中的基础测试框架 (例如 JUnit 或 TestNG) . 例如,如果使用 BeforeTestMethodEvent 导致异常,则相应的测试方法将由于异常而失败. 相反,如果异步测试执行事件监听器引发异常,则该异常不会传播到基础测试框架. 有关异步异常处理的更多详细信息,请查阅 @EventListener 的类级 javadoc.

异步监听器

如果您希望特定的测试执行事件监听器异步处理事件,则可以使用 Spring 的常规 @Async 支持. 有关更多详细信息,请查阅 @EventListener 的类级javadoc.

3.5.6. 上下文管理

每个 TestContext 为其负责的测试实例提供上下文管理和缓存支持. 测试实例不会自动接收对配置的 ApplicationContext 的访问. 但是,如果测试类实现 ApplicationContextAware 接口,则将对 ApplicationContext 的引用提供给测试实例. 请注意,AbstractJUnit4SpringContextTestsAbstractTestNGSpringContextTests 实现了 ApplicationContextAware,因此可以自动提供对 ApplicationContext 的访问.

@Autowired ApplicationContext

作为实现 ApplicationContextAware 接口的替代方法,您可以通过字段或 setter 方法上的 @Autowired 注解为测试类注入应用程序上下文,如以下示例所示:

Java
@SpringJUnitConfig
class MyTest {

    @Autowired (1)
    ApplicationContext applicationContext;

    // class body...
}
1 注入 ApplicationContext.
Kotlin
@SpringJUnitConfig
class MyTest {

    @Autowired (1)
    lateinit var applicationContext: ApplicationContext

    // class body...
}
1 注入 ApplicationContext.

同样,如果将测试配置为加载 WebApplicationContext,则可以将 Web 应用程序上下文注入到测试中,如下所示:

Java
@SpringJUnitWebConfig (1)
class MyWebAppTest {

    @Autowired (2)
    WebApplicationContext wac;

    // class body...
}
1 配置 WebApplicationContext.
2 注入 WebApplicationContext.
Kotlin
@SpringJUnitWebConfig (1)
class MyWebAppTest {

    @Autowired (2)
    lateinit var wac: WebApplicationContext
    // class body...
}
1 配置 WebApplicationContext.
2 注入 WebApplicationContext.

使用 @Autowired 的依赖注入由 DependencyInjectionTestExecutionListener 提供,它是默认配置的 (请参阅测试夹具的依赖注入) .

使用 TestContext 框架的测试类不需要扩展任何特定的类或实现特定的接口来配置其应用程序上下文. 而是通过在类级别声明 @ContextConfiguration 注解来实现配置. 如果您的测试类未明确声明应用程序上下文资源位置或组件类,则配置的 ContextLoader 决定如何从默认位置或默认配置类加载上下文. 除了上下文资源位置和组件类之外,还可以通过应用程序上下文初始化程序配置应用程序上下文.

以下各节说明如何使用 Spring 的 @ContextConfiguration 注解通过XML配置文件,Groovy 脚本,组件类 (通常为 @Configuration 类) 或上下文初始化器来配置测试 ApplicationContext. 另外,您可以为高级用例实现和配置自己的自定义 SmartContextLoader.

使用XML资源进行上下文配置

若要使用 XML 配置文件为测试加载 ApplicationContext,请使用 @ContextConfiguration 注解测试类,并使用包含XML配置元数据的资源位置的数组配置 locations 属性. 普通或相对路径 (例如 context.xml) 被视为相对于定义测试类的程序包的类路径资源. 以斜杠开头的路径被视为绝对类路径位置 (例如, /org/example/config.xml) . 照原样使用表示资源URL的路径 (即,以 classpath:, file:,http: 等开头的路径) .

Java
@ExtendWith(SpringExtension.class)
// ApplicationContext will be loaded from "/app-config.xml" and
// "/test-config.xml" in the root of the classpath
@ContextConfiguration(locations={"/app-config.xml", "/test-config.xml"}) (1)
class MyTest {
    // class body...
}
1 locations 属性设置为 XML 文件列表.
Kotlin
@ExtendWith(SpringExtension::class)
// ApplicationContext will be loaded from "/app-config.xml" and
// "/test-config.xml" in the root of the classpath
@ContextConfiguration("/app-config.xml", "/test-config.xml") (1)
class MyTest {
    // class body...
}
1 locations 属性设置为XML文件列表.

@ContextConfiguration 通过标准 Java 值属性为 locations 属性支持别名. 因此,如果不需要在 @ContextConfiguration 中声明其他属性,则可以使用以下示例中演示的速记格式,省略 locations 属性名称的声明并声明资源位置:

Java
@ExtendWith(SpringExtension.class)
@ContextConfiguration({"/app-config.xml", "/test-config.xml"}) (1)
class MyTest {
    // class body...
}
1 在不使用 location 属性的情况下指定 XML 文件.
Kotlin
@ExtendWith(SpringExtension::class)
@ContextConfiguration("/app-config.xml", "/test-config.xml") (1)
class MyTest {
    // class body...
}
1 在不使用 location 属性的情况下指定XML文件.

如果您从 @ContextConfiguration 注解中省略了位置和值属性,则 TestContext 框架将尝试检测默认的 XML 资源位置. 具体来说,GenericXmlContextLoaderGenericXmlWebContextLoader 根据测试类的名称检测默认位置. 如果您的类名为 com.example.MyTest,则 GenericXmlContextLoader 将从 "classpath:com/example/MyTest-context.xml" 加载应用程序上下文. 以下示例显示了如何执行此操作:

Java
@ExtendWith(SpringExtension.class)
// ApplicationContext will be loaded from
// "classpath:com/example/MyTest-context.xml"
@ContextConfiguration (1)
class MyTest {
    // class body...
}
1 从默认位置加载配置.
Kotlin
@ExtendWith(SpringExtension::class)
// ApplicationContext will be loaded from
// "classpath:com/example/MyTest-context.xml"
@ContextConfiguration (1)
class MyTest {
    // class body...
}
1 从默认位置加载配置.
使用Groovy脚本进行上下文配置

要通过使用使用Groovy Bean 定义 DSL的 Groovy 脚本为测试加载 ApplicationContext,可以使用 @ContextConfiguration 注解测试类,并使用包含 Groovy 脚本资源位置的数组配置 locationvalue 属性. Groovy 脚本的资源查找语义与针对XML 配置文件描述的语义相同.

启用 Groovy 脚本支持
如果 Groovy 位于类路径上,则会自动启用对使用 Groovy 脚本在 Spring TestContext Framework 中加载 ApplicationContext 的支持.

下面的示例显示如何指定 Groovy 配置文件:

Java
@ExtendWith(SpringExtension.class)
// ApplicationContext will be loaded from "/AppConfig.groovy" and
// "/TestConfig.groovy" in the root of the classpath
@ContextConfiguration({"/AppConfig.groovy", "/TestConfig.Groovy"}) (1)
class MyTest {
    // class body...
}
Kotlin
@ExtendWith(SpringExtension::class)
// ApplicationContext will be loaded from "/AppConfig.groovy" and
// "/TestConfig.groovy" in the root of the classpath
@ContextConfiguration("/AppConfig.groovy", "/TestConfig.Groovy") (1)
class MyTest {
    // class body...
}
1 指定 Groovy 配置文件的位置.

如果您从 @ContextConfiguration 注解中省略了 locationvalue 属性,则 TestContext 框架将尝试检测默认的 Groovy 脚本. 具体来说,GenericGroovyXmlContextLoaderGenericGroovyXmlWebContextLoader 根据测试类的名称检测默认位置. 如果您的类名为 com.example.MyTest,则 Groovy 上下文加载器将从 "classpath:com/example/MyTestContext.groovy" 加载应用程序上下文. 以下示例显示如何使用默认值:

Java
@ExtendWith(SpringExtension.class)
// ApplicationContext will be loaded from
// "classpath:com/example/MyTestContext.groovy"
@ContextConfiguration (1)
class MyTest {
    // class body...
}
1 从默认位置加载配置.
Kotlin
@ExtendWith(SpringExtension::class)
// ApplicationContext will be loaded from
// "classpath:com/example/MyTestContext.groovy"
@ContextConfiguration (1)
class MyTest {
    // class body...
}
1 从默认位置加载配置.
同时声明 XML 配置和 Groovy 脚本

您可以使用 @ContextConfigurationlocationvalue 属性同时声明 XML 配置文件和 Groovy 脚本. 如果到已配置资源位置的路径以 .xml 结尾,则使用 XmlBeanDefinitionReader 加载该路径. 否则,将使用 GroovyBeanDefinitionReader 加载它.

以下清单显示了如何在集成测试中将两者结合在一起:

Java
@ExtendWith(SpringExtension.class)
// ApplicationContext will be loaded from
// "/app-config.xml" and "/TestConfig.groovy"
@ContextConfiguration({ "/app-config.xml", "/TestConfig.groovy" })
class MyTest {
    // class body...
}
Kotlin
@ExtendWith(SpringExtension::class)
// ApplicationContext will be loaded from
// "/app-config.xml" and "/TestConfig.groovy"
@ContextConfiguration("/app-config.xml", "/TestConfig.groovy")
class MyTest {
    // class body...
}
使用组件类进行上下文配置

若要使用组件类 (请参见基于 Java 的容器配置) 为测试加载 ApplicationContext,可以使用 @ContextConfiguration 注解测试类,并使用包含对组件类的引用的数组来配置 classes 属性. 以下示例显示了如何执行此操作:

Java
@ExtendWith(SpringExtension.class)
// ApplicationContext will be loaded from AppConfig and TestConfig
@ContextConfiguration(classes = {AppConfig.class, TestConfig.class}) (1)
class MyTest {
    // class body...
}
1 指定组件类.
Kotlin
@ExtendWith(SpringExtension::class)
// ApplicationContext will be loaded from AppConfig and TestConfig
@ContextConfiguration(classes = [AppConfig::class, TestConfig::class]) (1)
class MyTest {
    // class body...
}
1 指定组件类.
组件类

术语 “组件类” 可以指以下任何一种:

  • 一个带有 @Configuration 注解的类.

  • 一个组件(也就是说,一个用 @Component, @Service, @Repository,或者其他 stereotype 注解的类).

  • 一个 JSR-330 兼容的类,用 javax.inject 注解.

  • 包含 @bean -方法的任何类.

  • 打算注册为 Spring 组件的任何其他类 (即 ApplicationContext 中的Spring bean) ,可能利用单个自动构造函数的自动自动装配而无需使用Spring注解.

有关组件类的配置和语义的更多信息,请参见 @Configuration@Bean 的javadoc,尤其要注意 @Bean Lite Mode 的讨论.

如果从 @ContextConfiguration 注解中省略了 classes 属性,则 TestContext 框架将尝试检测默认配置类的存在. 具体来说,AnnotationConfigContextLoaderAnnotationConfigWebContextLoader 将检测测试类的所有静态嵌套类,这些静态嵌套类满足配置类实现的要求, 如 @Configuration javadoc 中所指定. 请注意,配置类的名称是任意的. 此外,如果需要,测试类可以包含多个静态嵌套配置类. 在以下示例中,OrderServiceTest 类声明一个名为 Config 的静态嵌套配置类,该配置类将自动用于为测试类加载 ApplicationContext:

Java
@SpringJUnitConfig (1)
// ApplicationContext will be loaded from the
// static nested Config class
class OrderServiceTest {

    @Configuration
    static class Config {

        // this bean will be injected into the OrderServiceTest class
        @Bean
        OrderService orderService() {
            OrderService orderService = new OrderServiceImpl();
            // set properties, etc.
            return orderService;
        }
    }

    @Autowired
    OrderService orderService;

    @Test
    void testOrderService() {
        // test the orderService
    }

}
1 从嵌套的 Config 类中加载配置信息.
Kotlin
@SpringJUnitConfig (1)
// ApplicationContext will be loaded from the nested Config class
class OrderServiceTest {

    @Autowired
    lateinit var orderService: OrderService

    @Configuration
    class Config {

        // this bean will be injected into the OrderServiceTest class
        @Bean
        fun orderService(): OrderService {
            // set properties, etc.
            return OrderServiceImpl()
        }
    }

    @Test
    fun testOrderService() {
        // test the orderService
    }
}
1 从嵌套的 Config 类中加载配置信息.
混合XML,Groovy脚本和组件类

有时可能需要混合使用 XML 配置文件,Groovy 脚本和组件类 (通常为 @Configuration 类) 来为测试配置 ApplicationContext. 例如,如果您在生产中使用 XML 配置,则可以决定要使用 @Configuration 类为测试配置特定的 Spring 托管组件,反之亦然.

此外,某些第三方框架 (例如 Spring Boot) 提供了一流的支持,可以同时从不同类型的资源 (例如 XML 配置文件,Groovy 脚本和 @Configuration 类) 中加载 ApplicationContext. 过去,Spring 框架不支持此标准部署. 因此,Spring 框架在 spring-test 模块中提供的大多数 SmartContextLoader 实现对于每个测试上下文仅支持一种资源类型. 但是,这并不意味着您不能同时使用两者. 通用规则的一个例外是 GenericGroovyXmlContextLoaderGenericGroovyXmlWebContextLoader 同时支持 XML 配置文件和 Groovy 脚本. 此外,第三方框架可以选择通过 @ContextConfiguration 支持位置和类的声明,并且,借助 TestContext 框架中的标准测试支持,您可以选择以下选项.

如果要使用资源位置 (例如 XML 或 Groovy) 和 @Configuration 类来配置测试,则必须选择一个作为入口点,并且其中一个必须包含或导入另一个. 例如,在 XML 或 Groovy 脚本中,可以通过使用组件扫描或将它们定义为普通的 Spring bean 来包括 @Configuration 类,而在 @Configuration 类中, 可以使用 @ImportResource 导入 XML 配置文件或 Groovy 脚本. 请注意,此行为在语义上等同于您在生产环境中配置应用程序的方式: 在生产配置中,您定义了一组 XML 或 Groovy 资源位置或一组 @Configuration 类, 从中加载了生产 ApplicationContext,但是您仍然拥有 包含或导入其他类型配置的自由.

使用上下文初始化器进行上下文配置

若要使用上下文初始化程序为测试配置 ApplicationContext,请使用 @ContextConfiguration 注解测试类,并使用包含对实现 ApplicationContextInitializer 的类的引用的数组配置初始化程序属性. 然后,使用声明的上下文初始值设定项来初始化为测试加载的 ConfigurableApplicationContext. 请注意,每个声明的初始化程序支持的具体 ConfigurableApplicationContext 类型必须与使用中的 SmartContextLoader 创建的 ApplicationContext 类型 (通常是 GenericApplicationContext) 兼容. 此外,初始化程序的调用顺序取决于它们是实现 Spring 的 Ordered 接口还是用Spring的 @Order 注解或标准的 @Priority 注解进行注解. 以下示例显示如何使用初始化程序:

Java
@ExtendWith(SpringExtension.class)
// ApplicationContext will be loaded from TestConfig
// and initialized by TestAppCtxInitializer
@ContextConfiguration(
    classes = TestConfig.class,
    initializers = TestAppCtxInitializer.class) (1)
class MyTest {
    // class body...
}
1 使用配置类和初始化程序指定配置.
Kotlin
@ExtendWith(SpringExtension::class)
// ApplicationContext will be loaded from TestConfig
// and initialized by TestAppCtxInitializer
@ContextConfiguration(
        classes = [TestConfig::class],
        initializers = [TestAppCtxInitializer::class]) (1)
class MyTest {
    // class body...
}
1 使用配置类和初始化程序指定配置.

您还可以完全省略 @ContextConfiguration 中的 XML 配置文件,Groovy 脚本或组件类的声明,而仅声明 ApplicationContextInitializer 类,然后这些类负责在上下文中注册Bean (例如,通过编程方式从 XML 文件加载 Bean 定义) 或配置类. 以下示例显示了如何执行此操作:

Java
@ExtendWith(SpringExtension.class)
// ApplicationContext will be initialized by EntireAppInitializer
// which presumably registers beans in the context
@ContextConfiguration(initializers = EntireAppInitializer.class) (1)
class MyTest {
    // class body...
}
1 仅使用初始化程序来指定配置.
Kotlin
@ExtendWith(SpringExtension::class)
// ApplicationContext will be initialized by EntireAppInitializer
// which presumably registers beans in the context
@ContextConfiguration(initializers = [EntireAppInitializer::class]) (1)
class MyTest {
    // class body...
}
1 仅使用初始化程序来指定配置.
上下文配置继承

@ContextConfiguration 支持布尔值继承位置和 InheritInitializers 属性,这些属性指示是否应继承资源位置或组件类以及超类声明的上下文初始化器. 这两个标志的默认值为 true. 这意味着测试类将继承资源位置或组件类以及任何超类声明的上下文初始化器. 具体地说,将测试类的资源位置或组件类附加到由超类声明的资源位置或带注解的类的列表中. 同样,将给定测试类的初始化程序添加到由测试超类定义的初始化程序集. 因此,子类可以选择扩展资源位置,组件类或上下文初始化程序.

如果 @ContextConfiguration 中的 InheritLocationsInheritInitializers 属性设置为 false,则测试类的影子的资源位置或组件类以及上下文初始化器分别有效地替换超类定义的配置.

从 Spring Framework 5.3 开始, 测试配置也可以从封闭类继承. 有关详细信息, 请参见 @Nested 测试类配置.

在下一个使用 XML 资源位置的示例中,从 Base-config.xmlExtended-config.xml 依次加载 ExtendedTestApplicationContext. 因此,extended-config.xml 中定义的 Bean 可以覆盖 (即替换) base-config.xml 中定义的 Bean. 以下示例显示了一个类如何扩展另一个类并使用其自己的配置文件和超类的配置文件:

Java
@ExtendWith(SpringExtension.class)
// ApplicationContext will be loaded from "/base-config.xml"
// in the root of the classpath
@ContextConfiguration("/base-config.xml") (1)
class BaseTest {
    // class body...
}

// ApplicationContext will be loaded from "/base-config.xml" and
// "/extended-config.xml" in the root of the classpath
@ContextConfiguration("/extended-config.xml") (2)
class ExtendedTest extends BaseTest {
    // class body...
}
1 在超类中定义的配置文件.
2 子类中定义的配置文件.
Kotlin
@ExtendWith(SpringExtension::class)
// ApplicationContext will be loaded from "/base-config.xml"
// in the root of the classpath
@ContextConfiguration("/base-config.xml") (1)
open class BaseTest {
    // class body...
}

// ApplicationContext will be loaded from "/base-config.xml" and
// "/extended-config.xml" in the root of the classpath
@ContextConfiguration("/extended-config.xml") (2)
class ExtendedTest : BaseTest() {
    // class body...
}
1 在超类中定义的配置文件.
2 子类中定义的配置文件.

同样,在下一个使用组件类的示例中,从 BaseConfigExtendedConfig 类按该顺序加载 ExtendedTestApplicationContext. 因此,在ExtendedConfig 中定义的 Bean 可以覆盖 (即替换) 在 BaseConfig 中定义的那些. 以下示例显示了一个类如何扩展另一个类并使用其自己的配置类和超类的配置类:

Java
// ApplicationContext will be loaded from BaseConfig
@SpringJUnitConfig(BaseConfig.class) (1)
class BaseTest {
    // class body...
}

// ApplicationContext will be loaded from BaseConfig and ExtendedConfig
@SpringJUnitConfig(ExtendedConfig.class) (2)
class ExtendedTest extends BaseTest {
    // class body...
}
1 在超类中定义的配置文件.
2 子类中定义的配置文件.
Kotlin
// ApplicationContext will be loaded from BaseConfig
@SpringJUnitConfig(BaseConfig::class) (1)
open class BaseTest {
    // class body...
}

// ApplicationContext will be loaded from BaseConfig and ExtendedConfig
@SpringJUnitConfig(ExtendedConfig::class) (2)
class ExtendedTest : BaseTest() {
    // class body...
}
1 在超类中定义的配置文件.
2 子类中定义的配置文件.

在使用上下文初始化程序的下一个示例中,通过使用 BaseInitializerExtendedInitializer 初始化 ExtendedTestApplicationContext. 但是请注意,初始化程序的调用顺序取决于它们是实现 Spring 的 Ordered 接口还是以 Spring 的 @Order 注解或标准的 @Priority 注解进行注解. 以下示例显示了一个类如何扩展另一个类并同时使用其自己的初始化程序和超类的初始化程序:

Java
// ApplicationContext will be initialized by BaseInitializer
@SpringJUnitConfig(initializers = BaseInitializer.class) (1)
class BaseTest {
    // class body...
}

// ApplicationContext will be initialized by BaseInitializer
// and ExtendedInitializer
@SpringJUnitConfig(initializers = ExtendedInitializer.class) (2)
class ExtendedTest extends BaseTest {
    // class body...
}
1 在超类中定义的初始化程序.
2 子类中定义的初始化程序.
Kotlin
// ApplicationContext will be initialized by BaseInitializer
@SpringJUnitConfig(initializers = [BaseInitializer::class]) (1)
open class BaseTest {
    // class body...
}

// ApplicationContext will be initialized by BaseInitializer
// and ExtendedInitializer
@SpringJUnitConfig(initializers = [ExtendedInitializer::class]) (2)
class ExtendedTest : BaseTest() {
    // class body...
}
1 在超类中定义的初始化程序.
2 子类中定义的初始化程序.
使用环境配置文件进行上下文配置

Spring 框架对环境和概要文件 (AKA "bean 定义概要文件") 的概念提供了一流的支持,并且可以将集成测试配置为针对各种测试场景激活特定的 bean 定义概要文件. 这是通过使用 @ActiveProfiles 注解测试类并提供在加载测试的 ApplicationContext 时应激活的配置文件列表来实现的.

您可以将 @ActiveProfilesSmartContextLoader SPI的任何实现一起使用,但较早的 ContextLoader SPI 的实现不支持 @ActiveProfiles.

考虑两个带有 XML 配置和 @Configuration 类的示例:

<!-- app-config.xml -->
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    xmlns:jee="http://www.springframework.org/schema/jee"
    xsi:schemaLocation="...">

    <bean id="transferService"
            class="com.bank.service.internal.DefaultTransferService">
        <constructor-arg ref="accountRepository"/>
        <constructor-arg ref="feePolicy"/>
    </bean>

    <bean id="accountRepository"
            class="com.bank.repository.internal.JdbcAccountRepository">
        <constructor-arg ref="dataSource"/>
    </bean>

    <bean id="feePolicy"
        class="com.bank.service.internal.ZeroFeePolicy"/>

    <beans profile="dev">
        <jdbc:embedded-database id="dataSource">
            <jdbc:script
                location="classpath:com/bank/config/sql/schema.sql"/>
            <jdbc:script
                location="classpath:com/bank/config/sql/test-data.sql"/>
        </jdbc:embedded-database>
    </beans>

    <beans profile="production">
        <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
    </beans>

    <beans profile="default">
        <jdbc:embedded-database id="dataSource">
            <jdbc:script
                location="classpath:com/bank/config/sql/schema.sql"/>
        </jdbc:embedded-database>
    </beans>

</beans>
Java
@ExtendWith(SpringExtension.class)
// ApplicationContext will be loaded from "classpath:/app-config.xml"
@ContextConfiguration("/app-config.xml")
@ActiveProfiles("dev")
class TransferServiceTest {

    @Autowired
    TransferService transferService;

    @Test
    void testTransferService() {
        // test the transferService
    }
}
Kotlin
@ExtendWith(SpringExtension::class)
// ApplicationContext will be loaded from "classpath:/app-config.xml"
@ContextConfiguration("/app-config.xml")
@ActiveProfiles("dev")
class TransferServiceTest {

    @Autowired
    lateinit var transferService: TransferService

    @Test
    fun testTransferService() {
        // test the transferService
    }
}

运行 TransferServiceTest 时,会从类路径根目录中的 app-config.xml 配置文件中加载其 ApplicationContext. 如果检查 app-config.xml,可以看到 accountRepository bean对 dataSource bean有依赖性. 但是,dataSource 没有定义为顶级 bean. 相反,dataSource 定义了三次: 在生产配置文件中,在开发配置文件中以及在默认配置文件中.

通过使用 @ActiveProfiles("dev") 注解 TransferServiceTest,我们指示 Spring TestContext Framework 加载具有设置为 {"dev"} 的 active 配置文件的 ApplicationContext. 结果,创建了一个嵌入式数据库,并用测试数据填充了该数据库, 并用对开发 DataSource 的引用连接了 accountRepository bean. 这可能是我们在集成测试中想要的.

有时将 bean 分配给默认概要文件很有用. 仅当没有专门激活其他配置文件时,才包含默认配置文件中的 Bean. 您可以使用它来定义要在应用程序默认状态下使用的 “fallback” bean. 例如,您可以显式提供开发和生产配置文件的数据源,但是当两者都不处于 active 状态时,将内存中数据源定义为默认数据源.

以下代码清单演示了如何使用 @Configuration 类而不是 XML 实现相同的配置和集成测试:

Java
@Configuration
@Profile("dev")
public class StandaloneDataConfig {

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("classpath:com/bank/config/sql/schema.sql")
            .addScript("classpath:com/bank/config/sql/test-data.sql")
            .build();
    }
}
Kotlin
@Configuration
@Profile("dev")
class StandaloneDataConfig {

    @Bean
    fun dataSource(): DataSource {
        return EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.HSQL)
                .addScript("classpath:com/bank/config/sql/schema.sql")
                .addScript("classpath:com/bank/config/sql/test-data.sql")
                .build()
    }
}
Java
@Configuration
@Profile("production")
public class JndiDataConfig {

    @Bean(destroyMethod="")
    public DataSource dataSource() throws Exception {
        Context ctx = new InitialContext();
        return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
    }
}
Kotlin
@Configuration
@Profile("production")
class JndiDataConfig {

    @Bean(destroyMethod = "")
    fun dataSource(): DataSource {
        val ctx = InitialContext()
        return ctx.lookup("java:comp/env/jdbc/datasource") as DataSource
    }
}
Java
@Configuration
@Profile("default")
public class DefaultDataConfig {

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("classpath:com/bank/config/sql/schema.sql")
            .build();
    }
}
Kotlin
@Configuration
@Profile("default")
class DefaultDataConfig {

    @Bean
    fun dataSource(): DataSource {
        return EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.HSQL)
                .addScript("classpath:com/bank/config/sql/schema.sql")
                .build()
    }
}
Java
@Configuration
public class TransferServiceConfig {

    @Autowired DataSource dataSource;

    @Bean
    public TransferService transferService() {
        return new DefaultTransferService(accountRepository(), feePolicy());
    }

    @Bean
    public AccountRepository accountRepository() {
        return new JdbcAccountRepository(dataSource);
    }

    @Bean
    public FeePolicy feePolicy() {
        return new ZeroFeePolicy();
    }
}
Kotlin
@Configuration
class TransferServiceConfig {

    @Autowired
    lateinit var dataSource: DataSource

    @Bean
    fun transferService(): TransferService {
        return DefaultTransferService(accountRepository(), feePolicy())
    }

    @Bean
    fun accountRepository(): AccountRepository {
        return JdbcAccountRepository(dataSource)
    }

    @Bean
    fun feePolicy(): FeePolicy {
        return ZeroFeePolicy()
    }
}
Java
@SpringJUnitConfig({
        TransferServiceConfig.class,
        StandaloneDataConfig.class,
        JndiDataConfig.class,
        DefaultDataConfig.class})
@ActiveProfiles("dev")
class TransferServiceTest {

    @Autowired
    TransferService transferService;

    @Test
    void testTransferService() {
        // test the transferService
    }
}
Kotlin
@SpringJUnitConfig(
        TransferServiceConfig::class,
        StandaloneDataConfig::class,
        JndiDataConfig::class,
        DefaultDataConfig::class)
@ActiveProfiles("dev")
class TransferServiceTest {

    @Autowired
    lateinit var transferService: TransferService

    @Test
    fun testTransferService() {
        // test the transferService
    }
}

在此变体中,我们将 XML 配置分为四个独立的 @Configuration 类:

  • TransferServiceConfig: 使用 @Autowired 通过依赖注入获取 dataSource.

  • StandaloneDataConfig: 为适合开发人员测试的嵌入式数据库定义 dataSource.

  • JndiDataConfig: 定义在生产环境中从 JNDI 检索的 dataSource.

  • DefaultDataConfig: 如果没有配置文件处于 active 状态,则为默认的嵌入式数据库定义一个 dataSource.

与基于 XML 的配置示例一样,我们仍然使用 @ActiveProfiles("dev") 注解 TransferServiceTest,但是这次我们使用 @ContextConfiguration 注解指定所有四个配置类. 测试类的主体本身保持完全不变.

通常,在给定项目中的多个测试类之间使用一组概要文件. 因此,为避免 @ActiveProfiles 注解的重复声明,您可以在基类上声明一次 @ActiveProfiles,子类会自动从基类继承 @ActiveProfiles 配置. 在以下示例中,@ActiveProfiles 的声明 (以及其他注解) 已移至抽象超类 AbstractIntegrationTest:

从 Spring Framework 5.3 开始, 测试配置也可以从封闭的类继承. 有关详细信息, 请参见 @Nested 测试类配置.
Java
@SpringJUnitConfig({
        TransferServiceConfig.class,
        StandaloneDataConfig.class,
        JndiDataConfig.class,
        DefaultDataConfig.class})
@ActiveProfiles("dev")
abstract class AbstractIntegrationTest {
}
Kotlin
@SpringJUnitConfig(
        TransferServiceConfig::class,
        StandaloneDataConfig::class,
        JndiDataConfig::class,
        DefaultDataConfig::class)
@ActiveProfiles("dev")
abstract class AbstractIntegrationTest {
}
Java
// "dev" profile inherited from superclass
class TransferServiceTest extends AbstractIntegrationTest {

    @Autowired
    TransferService transferService;

    @Test
    void testTransferService() {
        // test the transferService
    }
}
Kotlin
// "dev" profile inherited from superclass
class TransferServiceTest : AbstractIntegrationTest() {

    @Autowired
    lateinit var transferService: TransferService

    @Test
    fun testTransferService() {
        // test the transferService
    }
}

@ActiveProfiles 还支持可用于禁用 active 配置文件的继承的 InheritedProfiles 属性,如以下示例所示:

Java
// "dev" profile overridden with "production"
@ActiveProfiles(profiles = "production", inheritProfiles = false)
class ProductionTransferServiceTest extends AbstractIntegrationTest {
    // test body
}
Kotlin
// "dev" profile overridden with "production"
@ActiveProfiles("production", inheritProfiles = false)
class ProductionTransferServiceTest : AbstractIntegrationTest() {
    // test body
}

此外,有时有必要以编程方式而不是以声明方式来解析测试的 active 配置文件,例如,基于:

  • 当前的操作系统.

  • 是否在持续集成构建服务器上执行测试.

  • 存在某些环境变量.

  • 自定义类级别注解的存在.

  • 其他问题.

要以编程方式解析 active bean 定义概要文件,可以实现自定义 ActiveProfilesResolver 并使用 @ActiveProfilesresolver 属性对其进行注册. 有关更多信息,请参见相应的 javadoc. 下面的示例演示如何实现和注册自定义 OperatingSystemActiveProfilesResolver:

Java
// "dev" profile overridden programmatically via a custom resolver
@ActiveProfiles(
        resolver = OperatingSystemActiveProfilesResolver.class,
        inheritProfiles = false)
class TransferServiceTest extends AbstractIntegrationTest {
    // test body
}
Kotlin
// "dev" profile overridden programmatically via a custom resolver
@ActiveProfiles(
        resolver = OperatingSystemActiveProfilesResolver::class,
        inheritProfiles = false)
class TransferServiceTest : AbstractIntegrationTest() {
    // test body
}
Java
public class OperatingSystemActiveProfilesResolver implements ActiveProfilesResolver {

    @Override
    public String[] resolve(Class<?> testClass) {
        String profile = ...;
        // determine the value of profile based on the operating system
        return new String[] {profile};
    }
}
Kotlin
class OperatingSystemActiveProfilesResolver : ActiveProfilesResolver {

    override fun resolve(testClass: Class<*>): Array<String> {
        val profile: String = ...
        // determine the value of profile based on the operating system
        return arrayOf(profile)
    }
}
具有测试属性源的上下文配置

Spring 框架对具有属性源层次结构的环境的概念提供了一流的支持,您可以使用特定于测试的属性源配置集成测试. 与 @Configuration 类上使用的 @PropertySource 注解相反,可以在测试类上声明 @TestPropertySource 注解,以声明测试属性文件或内联属性的资源位置. 将这些测试属性源添加到环境中针对为注解集成测试加载的 ApplicationContextPropertySources 集中.

您可以将 @TestPropertySourceSmartContextLoader SPI 的任何实现一起使用,但是较早的 ContextLoader SPI的实现不支持 @TestPropertySource.

SmartContextLoader 的实现可通过 MergedContextConfiguration 中的 getPropertySourceLocations()getPropertySourceProperties() 方法访问合并的测试属性源值.

声明测试属性源

您可以使用 @TestPropertySourcelocationvalue 属性来配置测试属性文件.

支持传统属性和基于 XML 的属性文件格式,例如 "classpath:/com/example/test.properties""file:///path/to/file.xml".

每个路径都被解释为 Spring 资源. 纯路径 (例如,"test.properties") 被视为相对于定义测试类的程序包的类路径资源. 以斜杠开头的路径被视为绝对类路径资源 (例如: "/org/example/test.xml") . 通过使用指定的资源协议加载引用URL的路径 (例如,以 classpath:, file:, or http: 开头的路径) . 不允许使用资源位置通配符 (例如 */.properties) : 每个位置都必须精确评估为一个 .properties.xml 资源.

以下示例使用测试属性文件:

Java
@ContextConfiguration
@TestPropertySource("/test.properties") (1)
class MyIntegrationTests {
    // class body...
}
1 指定具有绝对路径的属性文件.
Kotlin
@ContextConfiguration
@TestPropertySource("/test.properties") (1)
class MyIntegrationTests {
    // class body...
}
1 指定具有绝对路径的属性文件.

您可以使用 @TestPropertySourceproperties 属性以键-值对的形式配置内联属性,如下例所示. 所有键值对都作为优先级最高的单个测试 PropertySource 添加到封闭环境中.

键值对支持的语法与为Java属性文件中的条目定义的语法相同:

  • key=value

  • key:value

  • key value

以下示例设置两个内联属性:

Java
@ContextConfiguration
@TestPropertySource(properties = {"timezone = GMT", "port: 4242"}) (1)
class MyIntegrationTests {
    // class body...
}
1 通过使用键值语法的两种变体来设置两个属性.
Kotlin
@ContextConfiguration
@TestPropertySource(properties = ["timezone = GMT", "port: 4242"]) (1)
class MyIntegrationTests {
    // class body...
}
1 通过使用键值语法的两种变体来设置两个属性.

从 Spring Framework 5.2 开始,@TestPropertySource 可以用作可重复注解. 这意味着您可以在单个测试类上具有 @TestPropertySource 的多个声明,其后的 @TestPropertySource 注解中的位置和属性将覆盖先前的 @TestPropertySource 注解中的位置和属性.

另外,您可以在一个测试类上声明多个组成的注解,每个注解都使用 @TestPropertySource 进行元注解,所有这些 @TestPropertySource 声明都将有助于您的测试属性源.

直接存在的 @TestPropertySource 注解始终优先于元存在的 @TestPropertySource 注解. 换句话说,直接存在的 @TestPropertySource 注解中的位置和属性将覆盖 @TestPropertySource 注解中用作元注解的位置和属性.

默认属性文件检测

如果 @TestPropertySource 声明为空注解 (即,没有位置或属性属性的显式值) ,则尝试检测相对于声明该注解的类的默认属性文件. 例如,如果带注解的测试类是 com.example.MyTest,则相应的默认属性文件是 classpath:com/example/MyTest.properties. 如果无法检测到默认值,则抛出 IllegalStateException.

优先顺序

测试属性的优先级高于操作系统环境,Java 系统属性或应用程序通过使用 @PropertySource 以声明式或者编程式的方法添加的属性源中定义的属性. 因此,测试属性源可用于选择性覆盖系统和应用程序属性源中定义的属性. 此外,内联属性比从资源位置加载的属性具有更高的优先级. 但是请注意,通过 @DynamicPropertySource 注册的属性比通过 @TestPropertySource 加载的属性具有更高的优先级.

在下一个示例中, timezoneport 属性以及在 "/test.properties" 中定义的任何属性都将覆盖在系统和应用程序属性源中定义的具有相同名称的所有属性. 此外,如果 "/test.properties" 文件定义了 timezoneport 属性,则这些条目将被使用 properties 属性声明的内联属性所覆盖. 以下示例显示如何在文件和内联中指定属性:

Java
@ContextConfiguration
@TestPropertySource(
    locations = "/test.properties",
    properties = {"timezone = GMT", "port: 4242"}
)
class MyIntegrationTests {
    // class body...
}
Kotlin
@ContextConfiguration
@TestPropertySource("/test.properties",
        properties = ["timezone = GMT", "port: 4242"]
)
class MyIntegrationTests {
    // class body...
}
继承和覆盖测试属性源

@TestPropertySource 支持 booleanInheritLocationsInheritProperties 属性,这些属性指示是否应继承属性文件和超类声明的内联属性的资源位置. 这两个标志的默认值为 true. 这意味着测试类将继承任何超类声明的位置和内联属性. 具体来说,将测试类的位置和内联属性附加到超类声明的位置和内联属性中. 因此,子类可以选择扩展位置和内联属性. 请注意,稍后出现的属性会阴影 (即,覆盖) 之前出现的相同名称的属性. 此外,上述优先规则也适用于继承的测试属性源.

如果 @TestPropertySource 中的 InheritLocationsInheritProperties 属性设置为 false,则分别为测试类阴影设置位置或内联属性,并有效替换超类定义的配置.

在下一个示例中,仅通过将 base.properties 文件用作测试属性源来加载 BaseTestApplicationContext. 相反,通过使用 base.propertiesextended.properties 文件作为测试属性源位置来加载 ExtendedContextApplicationContext. 下面的示例显示如何通过使用属性文件在子类及其父类中定义属性:

Java
@TestPropertySource("base.properties")
@ContextConfiguration
class BaseTest {
    // ...
}

@TestPropertySource("extended.properties")
@ContextConfiguration
class ExtendedTest extends BaseTest {
    // ...
}
Kotlin
@TestPropertySource("base.properties")
@ContextConfiguration
open class BaseTest {
    // ...
}

@TestPropertySource("extended.properties")
@ContextConfiguration
class ExtendedTest : BaseTest() {
    // ...
}
从 Spring Framework 5.3 开始, 测试配置也可以从封闭的类继承. 有关详细信息, 请参见 @Nested 测试类配置.

在下一个示例中,仅使用内联的 key1 属性加载 BaseTestApplicationContext. 相反,通过使用内联的 key1key2 属性来加载 ExtendedTestApplicationContext. 下面的示例演示如何通过使用内联属性在子类及其父类中定义属性:

Java
@TestPropertySource(properties = "key1 = value1")
@ContextConfiguration
class BaseTest {
    // ...
}

@TestPropertySource(properties = "key2 = value2")
@ContextConfiguration
class ExtendedTest extends BaseTest {
    // ...
}
Kotlin
@TestPropertySource(properties = ["key1 = value1"])
@ContextConfiguration
open class BaseTest {
    // ...
}

@TestPropertySource(properties = ["key2 = value2"])
@ContextConfiguration
class ExtendedTest : BaseTest() {
    // ...
}
具有动态属性源的上下文配置

从 Spring Framework 5.2.5 开始,TestContext 框架通过 @DynamicPropertySource 注解提供对动态属性的支持. 此注解可用于集成测试, 集成测试中加载的 ApplicationContext 需要将具有动态属性的值添加到 EnvironmentPropertySources 集中.

@DynamicPropertySource 注解及其支持的基础结构涉及之初的目的是使 Testcontainers 的测试中的属性易于暴露于 Spring 集成测试. 但是,此功能也可以 ApplicationContext 生命周期之外的外部资源一起使用.

在类级别应用 @TestPropertySource 注解则相反,必须将 @DynamicPropertySource 应用于接受单个 DynamicPropertyRegistry 参数的静态方法,该参数用于向环境添加键值对. 值是动态的,并通过 Supplier 提供,只有在解析属性后才调用该 Supplier. 通常,方法引用用于提供值,如以下示例所示,该示例使用 Testcontainers 项目在 Spring ApplicationContext 外部管理 Redis 容器. 通过 redis.hostredis.port 属性,测试的 ApplicationContext 中的组件可以使用托管 Redis 容器的IP地址和端口. 这些属性可以通过 Spring 的环境抽象来访问,也可以直接注入到 Spring 管理的组件中,例如分别通过 @Value("${redis.host}")@Value("${redis.port}").

如果您在基类中使用 @DynamicPropertySource 并发现子类中的测试由于动态属性在子类之间变化而失败, 则可能需要使用 @DirtiesContext 注解您的基类, 以确保每个子类获得具有正确动态属性的自己的 ApplicationContext.

Java
@SpringJUnitConfig(/* ... */)
@Testcontainers
class ExampleIntegrationTests {

    @Container
    static RedisContainer redis = new RedisContainer();

    @DynamicPropertySource
    static void redisProperties(DynamicPropertyRegistry registry) {
        registry.add("redis.host", redis::getContainerIpAddress);
        registry.add("redis.port", redis::getMappedPort);
    }

    // tests ...

}
Kotlin
@SpringJUnitConfig(/* ... */)
@Testcontainers
class ExampleIntegrationTests {

    companion object {

        @Container
        @JvmStatic
        val redis: RedisContainer = RedisContainer()

        @DynamicPropertySource
        @JvmStatic
        fun redisProperties(registry: DynamicPropertyRegistry) {
            registry.add("redis.host", redis::getContainerIpAddress)
            registry.add("redis.port", redis::getMappedPort)
        }
    }

    // tests ...

}
优先顺序

动态属性的优先级高于从 @TestPropertySource 加载的属性,操作系统的环境,Java 系统属性或应用程序通过使用 @PropertySource 声明性地或以编程方式添加的属性. 因此,动态属性可用于有选择地覆盖通过 @TestPropertySource,系统属性源和应用程序属性源加载的属性.

加载 WebApplicationContext

若要指示 TestContext 框架加载 WebApplicationContext 而不是标准 ApplicationContext,可以使用 @WebAppConfiguration 注解各自的测试类.

测试类上 @WebAppConfiguration 的存在指示 TestContext 框架 (TCF) 应该为集成测试加载 WebApplicationContext (WAC) . TCF 在后台确保创建了 MockServletContext 并将其提供给测试的 WAC. 默认情况下,您的 MockServletContext 的基本资源路径设置为 src/main/webapp. 这被解释为相对于 JVM 根目录的路径 (通常是项目的路径) . 如果您熟悉 Maven 项目中 Web 应用程序的目录结构,则知道 src/main/webapp 是WAR根目录的默认位置. 如果需要覆盖此默认值, 则可以提供 @WebAppConfiguration 注解的备用路径 (例如, @WebAppConfiguration(“src/test/webapp”) ) . 如果您希望从类路径而不是文件系统中引用基本资源路径,则可以使用 Spring 的 classpath: 前缀.

请注意,Spring 对 WebApplicationContext 实现的测试支持与其对标准 ApplicationContext 实现的支持相当. 使用 WebApplicationContext 进行测试时,可以使用 @ContextConfiguration 声明XML配置文件,Groovy 脚本或 @Configuration 类. 您还可以自由使用任何其他测试注解,例如 @ActiveProfiles,@TestExecutionListeners,@Sql,@Rollback 等.

本节中的其余示例显示了用于加载 WebApplicationContext 的各种配置选项. 以下示例显示了 TestContext 框架对配置约定的支持:

Java
@ExtendWith(SpringExtension.class)

// defaults to "file:src/main/webapp"
@WebAppConfiguration

// detects "WacTests-context.xml" in the same package
// or static nested @Configuration classes
@ContextConfiguration
class WacTests {
    //...
}
Kotlin
@ExtendWith(SpringExtension::class)

// defaults to "file:src/main/webapp"
@WebAppConfiguration

// detects "WacTests-context.xml" in the same package
// or static nested @Configuration classes
@ContextConfiguration
class WacTests {
    //...
}

如果使用 @WebAppConfiguration 注解测试类而未指定资源基本路径,则资源路径实际上默认为 file:src/main/webapp. 同样,如果在声明 @ContextConfiguration 时未指定资源位置,组件类或上下文初始化器,则 Spring 会尝试使用约定 (即 WacTests-context.xmlWacTests 类或静态包放在同一包中) 来检测配置的存在. 嵌套的 @Configuration 类) .

以下示例显示如何使用 @WebAppConfiguration 显式声明资源基础路径和使用 @ContextConfiguration 显式声明XML资源位置:

Java
@ExtendWith(SpringExtension.class)

// file system resource
@WebAppConfiguration("webapp")

// classpath resource
@ContextConfiguration("/spring/test-servlet-config.xml")
class WacTests {
    //...
}
Kotlin
@ExtendWith(SpringExtension::class)

// file system resource
@WebAppConfiguration("webapp")

// classpath resource
@ContextConfiguration("/spring/test-servlet-config.xml")
class WacTests {
    //...
}

这里要注意的重要一点是具有这两个注解的路径的语义不同. 默认情况下,@WebAppConfiguration 资源路径基于文件系统,而 @ContextConfiguration 资源位置基于类路径.

下面的示例显示,我们可以通过指定 Spring 资源前缀来覆盖两个注解的默认资源语义:

Java
@ExtendWith(SpringExtension.class)

// classpath resource
@WebAppConfiguration("classpath:test-web-resources")

// file system resource
@ContextConfiguration("file:src/main/webapp/WEB-INF/servlet-config.xml")
class WacTests {
    //...
}
Kotlin
@ExtendWith(SpringExtension::class)

// classpath resource
@WebAppConfiguration("classpath:test-web-resources")

// file system resource
@ContextConfiguration("file:src/main/webapp/WEB-INF/servlet-config.xml")
class WacTests {
    //...
}

将本示例中的注解与上一个示例进行对比.

Working with Web Mocks

为了提供全面的 Web 测试支持,TestContext 框架具有默认启用的 ServletTestExecutionListener. 在针对 WebApplicationContext 进行测试时,此 TestExecutionListener 会在每个测试方法之前使用 Spring Web 的 RequestContextHolder 来设置默认的线程本地状态,并根据通过 @WebAppConfiguration 配置的基本资源路径创建 MockHttpServletRequest,MockHttpServletResponseServletWebRequest. ServletTestExecutionListener 还确保可以将 MockHttpServletResponseServletWebRequest 注入到测试实例中,并且一旦测试完成,它将清除线程本地状态.

一旦为测试加载了 WebApplicationContext,您可能会发现您需要与 Web 模拟进行交互,例如,在调用 Web 组件后设置测试夹具或执行断言. 以下示例显示可以将哪些模拟自动连接到您的测试实例. 请注意,WebApplicationContextMockServletContext 都缓存在测试套件中,而其他模拟则由 ServletTestExecutionListener 针对每个测试方法进行管理.

Java
@SpringJUnitWebConfig
class WacTests {

    @Autowired
    WebApplicationContext wac; // cached

    @Autowired
    MockServletContext servletContext; // cached

    @Autowired
    MockHttpSession session;

    @Autowired
    MockHttpServletRequest request;

    @Autowired
    MockHttpServletResponse response;

    @Autowired
    ServletWebRequest webRequest;

    //...
}
Kotlin
@SpringJUnitWebConfig
class WacTests {

    @Autowired
    lateinit var wac: WebApplicationContext // cached

    @Autowired
    lateinit var servletContext: MockServletContext // cached

    @Autowired
    lateinit var session: MockHttpSession

    @Autowired
    lateinit var request: MockHttpServletRequest

    @Autowired
    lateinit var response: MockHttpServletResponse

    @Autowired
    lateinit var webRequest: ServletWebRequest

    //...
}
上下文缓存

一旦 TestContext 框架为测试加载了 ApplicationContext (或 WebApplicationContext) ,该上下文将被缓存并重新用于在同一测试套件中声明相同唯一上下文配置的所有后续测试. 要了解缓存的工作原理,重要的是要了解 "唯一" 和 "测试套件" 的含义.

可以通过用于加载它的配置参数的组合来唯一标识 ApplicationContext. 因此,配置参数的唯一组合用于生成一个密钥,在该密钥下缓存上下文. TestContext 框架使用以下配置参数来构建上下文缓存键:

  • locations (from @ContextConfiguration)

  • classes (from @ContextConfiguration)

  • contextInitializerClasses (from @ContextConfiguration)

  • contextCustomizers (from ContextCustomizerFactory)-这包括 @DynamicPropertySource 方法以及 Spring Boot 的各种功能测试支持,例如 @MockBean@SpyBean.

  • contextLoader (from @ContextConfiguration)

  • parent (from @ContextHierarchy)

  • activeProfiles (from @ActiveProfiles)

  • propertySourceLocations (from @TestPropertySource)

  • propertySourceProperties (from @TestPropertySource)

  • resourceBasePath (from @WebAppConfiguration)

例如,如果 TestClassA@ContextConfigurationlocation (或 value) 属性指定 {"app-config.xml", "test-config.xml"},则TestContext框架将加载相应的 ApplicationContext 并将其存储在静态上下文缓存中 仅基于那些位置的密钥下. 因此,如果 TestClassB 还为其位置 (通过继承显式或隐式) 定义了 {"app-config.xml", "test-config.xml"} ,但未定义 @WebAppConfiguration,不同的 ContextLoader,不同的 active 配置文件,不同的 上下文初始化程序,不同的测试属性源或不同的父上下文,则两个测试类将共享相同的 ApplicationContext. 这意味着加载应用程序上下文的设置成本仅发生一次 (每个测试套件) ,并且随后的测试执行要快得多.

Test suites and forked processes

Spring TestContext 框架将应用程序上下文存储在静态缓存中. 这意味着上下文实际上是存储在 static 变量中的. 换句话说,如果测试是在单独的进程中执行的,则在每次测试执行之间都会清除静态缓存,从而有效地禁用了缓存机制.

为了从缓存机制中受益,所有测试必须在同一进程或测试套件中运行. 这可以通过在 IDE 中以组的形式执行所有测试来实现. 同样,在使用诸如 Ant,Maven 或 Gradle 之类的构建框架执行测试时,确保该构建框架不会在测试之间进行扩展很重要. 例如,如果将 Maven Surefire 插件的 forkMode设置为 alwayspertest,则 TestContext 框架将无法在测试类之间缓存应用程序上下文,因此,构建过程的运行速度将大大降低.

上下文缓存的大小以默认的最大大小 32 为界. 只要达到最大大小,就会使用最近最少使用 (LRU) 驱逐策略来驱逐和关闭陈旧的上下文. 您可以通过设置名为 spring.test.context.cache.maxSize 的JVM系统属性,从命令行或构建脚本中配置最大大小. 或者,您可以使用 SpringProperties API 以编程方式设置相同的属性.

由于在给定的测试套件中加载大量应用程序上下文会导致套件花费不必要的长时间执行,因此准确地知道已加载和缓存了多少个上下文通常是有益的. 要查看基础上下文缓存的统计信息,可以将 org.springframework.test.context.cache 日志记录类别的日志级别设置为 DEBUG.

万一测试破坏了应用程序上下文并需要重新加载 (例如,通过修改 Bean 定义或应用程序对象的状态) ,则可以使用 @DirtiesContext 注解测试类或测试方法 (请参阅的讨论 Spring Testing Annotations 中的 DirtiesContext) . 这指示 Spring 在运行下一个需要相同应用程序上下文的测试之前,从缓存中删除上下文并重建应用程序上下文. 请注意,@DirtiesContext 注解的支持由 DirtiesContextBeforeModesTestExecutionListenerDirtiesContextTestExecutionListener 默认启用.

ApplicationContext lifecycle and console logging

当您需要调试使用 Spring TestContext Framework 执行的测试时, 分析控制台输出 (即, 输出到 SYSOUTSYSERR 流) 可能很有用. 一些构建工具和 IDE 能够将控制台输出与给定的测试相关联. 但是, 某些控制台输出无法轻松地与给定测试关联.

关于由 Spring 框架本身或由 ApplicationContext 中注册的组件触发的控制台日志记录, 了解由 Spring TestContext Framework 在测试套件中加载的 ApplicationContext 的生命周期非常重要.

通常在准备测试类的实例时加载测试的 ApplicationContext, 例如, 将依赖项注入到测试实例的 @Autowired 字段中. 这意味着在 ApplicationContext 初始化期间触发的任何控制台日志记录通常都不能与单个测试方法相关联. 但是, 如果根据 @DirtiesContext 语义在执行测试方法之前立即关闭上下文, 则将在执行测试方法之前立即加载该上下文的新实例. 在后一种情况下, IDE 或构建工具可能会将控制台日志记录与单独的测试方法相关联.

可以通过以下方案之一关闭测试的 ApplicationContext.

  • 根据 @DirtiesContext 语义关闭上下文.

  • 上下文已关闭, 因为已根据 LRU 淘汰策略自动将其从缓存中淘汰.

  • 当测试套件的 JVM 终止时, 通过 JVM 关闭钩子关闭上下文. .

如果在特定测试方法之后根据 @DirtiesContext 语义关闭了上下文, 则 IDE 或构建工具可能会将控制台日志记录与单个测试方法相关联. 如果在测试类之后根据 @DirtiesContext 语义关闭了上下文, 则在 ApplicationContext 关闭期间触发的任何控制台日志记录都不能与单个测试方法相关联. 同样, 在关闭阶段通过 JVM 关闭钩子触发的任何控制台日志记录都不能与单独的测试方法相关联.

当通过 JVM 关机钩子关闭 Spring ApplicationContext 时, 在关机阶段执行的回调将在名为 SpringContextShutdownHook 的线程上执行. 因此, 如果您希望禁用通过 JVM 关闭钩子关闭 ApplicationContext 时触发的控制台日志记录, 则可以在日志记录框架中注册自定义过滤器, 从而允许您忽略该线程启动的任何日志记录.

上下文层次结构

在编写依赖于已加载的 Spring ApplicationContext 的集成测试时,通常足以针对单个上下文进行测试. 但是,有时需要对 ApplicationContext 实例的层次结构进行测试是有益的甚至是必要的. 例如,如果您正在开发 Spring MVC Web 应用程序, 则通常具有由 Spring 的 ContextLoaderListener 加载的根 WebApplicationContext 和由Spring的 DispatcherServlet 加载的子 WebApplicationContext. 这导致父子上下文层次结构,其中共享组件和基础结构配置在根上下文中声明,并在特定于 Web 的组件的子上下文中使用. 在 Spring Batch 应用程序中可以找到另一个用例,在该应用程序中,您经常有一个父上下文为共享批处理基础结构提供配置,而子上下文为特定批处理作业的配置提供配置.

您可以通过在单个测试类上或在测试类层次结构中使用 @ContextHierarchy 注解声明上下文配置来编写使用上下文层次结构的集成测试. 如果在测试类层次结构中的多个类上声明了上下文层次结构,则还可以合并或覆盖上下文层次结构中特定命名级别的上下文配置. 合并层次结构中给定级别的配置时,配置资源类型 (即XML配置文件或组件类) 必须一致. 否则,在使用不同资源类型配置的上下文层次结构中具有不同级别是完全可以接受的.

本节中其余的基于 JUnit Jupiter 的示例显示了需要使用上下文层次结构的集成测试的常见配置方案.

具有上下文层次结构的单个测试类

ControllerIntegrationTests 通过声明一个上下文层次结构来表示 Spring MVC Web 应用程序的典型集成测试场景,该上下文层次结构包含两个级别,一个层次用于根 WebApplicationContext (通过使用 TestAppConfig @Configuration 类加载) ,一个层次用于调度程序 Servlet WebApplicationContext (通过使用 WebConfig @Configuration 类 加载) . 自动连接到测试实例的 WebApplicationContext 是用于子上下文 (即,层次结构中的最低上下文) 的 WebApplicationContext. 以下清单显示了此配置方案:

Java
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextHierarchy({
    @ContextConfiguration(classes = TestAppConfig.class),
    @ContextConfiguration(classes = WebConfig.class)
})
class ControllerIntegrationTests {

    @Autowired
    WebApplicationContext wac;

    // ...
}
Kotlin
@ExtendWith(SpringExtension::class)
@WebAppConfiguration
@ContextHierarchy(
    ContextConfiguration(classes = [TestAppConfig::class]),
    ContextConfiguration(classes = [WebConfig::class]))
class ControllerIntegrationTests {

    @Autowired
    lateinit var wac: WebApplicationContext

    // ...
}
具有隐式父上下文的类层次结构

本示例中的测试类在测试类层次结构中定义了上下文层次结构. AbstractWebTests 在Spring驱动的Web应用程序中声明根 WebApplicationContext 的配置. 但是请注意,AbstractWebTests 不会声明 @ContextHierarchy. 因此,AbstractWebTests 的子类可以选择参与上下文层次结构或遵循 @ContextConfiguration 的标准语义. SoapWebServiceTestsRestWebServiceTests 都扩展了 AbstractWebTests 并使用 @ContextHierarchy 定义了上下文层次结构. 结果是,加载了三个应用程序上下文 (每个 @ContextConfiguration 声明一个) ,并且基于 AbstractWebTests 中的配置加载的应用程序上下文被设置为为具体子类加载的每个上下文的父上下文. 以下清单显示 了此配置方案:

Java
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration("file:src/main/webapp/WEB-INF/applicationContext.xml")
public abstract class AbstractWebTests {}

@ContextHierarchy(@ContextConfiguration("/spring/soap-ws-config.xml"))
public class SoapWebServiceTests extends AbstractWebTests {}

@ContextHierarchy(@ContextConfiguration("/spring/rest-ws-config.xml"))
public class RestWebServiceTests extends AbstractWebTests {}
Kotlin
@ExtendWith(SpringExtension::class)
@WebAppConfiguration
@ContextConfiguration("file:src/main/webapp/WEB-INF/applicationContext.xml")
abstract class AbstractWebTests

@ContextHierarchy(ContextConfiguration("/spring/soap-ws-config.xml"))
class SoapWebServiceTests : AbstractWebTests()

@ContextHierarchy(ContextConfiguration("/spring/rest-ws-config.xml"))
class RestWebServiceTests : AbstractWebTests()
具有合并上下文层次结构配置的类层次结构

此示例中的类显示了使用命名层次结构级别的目的,以便合并上下文层次结构中特定级别的配置. BaseTests 在层次结构中定义了两个级别,parent 级别和 child 级别. ExtendedTests 扩展 BaseTests 并指示 Spring TestContext Framework 合并子层次结构级别的上下文配置, 方法是确保在 @ContextConfigurationname 属性中声明的名称均为 child 元素. 结果是加载了三个应用程序上下文: 一个用于 /app-config.xml,一个用于 /user-config.xml,一个用于 {"/user-config.xml", "/order-config.xml"}. 与前面的示例一样,将从 /app-config.xml 加载的应用程序上下文设置为从 /user-config.xml{"/user-config.xml", "/order-config.xml"}. 以下清单显示了此配置方案:

Java
@ExtendWith(SpringExtension.class)
@ContextHierarchy({
    @ContextConfiguration(name = "parent", locations = "/app-config.xml"),
    @ContextConfiguration(name = "child", locations = "/user-config.xml")
})
class BaseTests {}

@ContextHierarchy(
    @ContextConfiguration(name = "child", locations = "/order-config.xml")
)
class ExtendedTests extends BaseTests {}
Kotlin
@ExtendWith(SpringExtension::class)
@ContextHierarchy(
    ContextConfiguration(name = "parent", locations = ["/app-config.xml"]),
    ContextConfiguration(name = "child", locations = ["/user-config.xml"]))
open class BaseTests {}

@ContextHierarchy(
    ContextConfiguration(name = "child", locations = ["/order-config.xml"])
)
class ExtendedTests : BaseTests() {}
具有覆盖的上下文层次结构配置的类层次结构

与前面的示例相反,此示例演示了如何通过将 @ContextConfiguration 中的 InheritLocations 标志设置为 false 来覆盖上下文层次结构中给定命名级别的配置. 因此,ExtendedTests 的应用程序上下文仅从 /test-user-config.xml 加载,并且其父级设置为从 /app-config.xml 加载的上下文. 以下清单显示了此配置方案:

Java
@ExtendWith(SpringExtension.class)
@ContextHierarchy({
    @ContextConfiguration(name = "parent", locations = "/app-config.xml"),
    @ContextConfiguration(name = "child", locations = "/user-config.xml")
})
class BaseTests {}

@ContextHierarchy(
    @ContextConfiguration(
        name = "child",
        locations = "/test-user-config.xml",
        inheritLocations = false
))
class ExtendedTests extends BaseTests {}
Kotlin
@ExtendWith(SpringExtension::class)
@ContextHierarchy(
    ContextConfiguration(name = "parent", locations = ["/app-config.xml"]),
    ContextConfiguration(name = "child", locations = ["/user-config.xml"]))
open class BaseTests {}

@ContextHierarchy(
        ContextConfiguration(
                name = "child",
                locations = ["/test-user-config.xml"],
                inheritLocations = false
        ))
class ExtendedTests : BaseTests() {}
污染上下文层次结构中的上下文
如果在上下文被配置为上下文层次结构一部分的测试中使用 @DirtiesContext,则可以使用 hierarchyMode 标志控制清除上下文缓存的方式. 有关更多详细信息,请参见 Spring Testing Annotations中的 @DirtiesContext@DirtiesContext javadoc 的讨论.

3.5.7. 测试夹具的依赖注入

当使用 DependencyInjectionTestExecutionListener (默认配置) 时,测试实例的依赖是从使用 @ContextConfiguration 或相关注解配置的应用程序上下文中的 bean 注入的. 您可以使用 setter 注入,字段注入,或同时使用这两种方法,具体取决于您选择的注解以及是否将它们放置在 setter 方法或字段中. 如果使用的是 JUnit Jupiter,则还可以选择使用构造函数注入 (请参阅带有 SpringExtension 的依赖注入) . 为了与 Spring 基于注解的注入支持保持一致,您还可以将 Spring 的 @Autowired 注解或 JSR-330 中的 @Inject 注解用于字段注入和设置器注入.

对于 JUnit Jupiter 以外的测试框架,TestContext 框架不参与测试类的实例化. 因此,将 @Autowired@Inject 用于构造函数对测试类无效.
尽管在生产代码中不鼓励使用字段注入,但是在测试代码中字段注入实际上是很自然的. 区别的理由是,您永远不会直接实例化测试类. 因此,不需要能够在测试类上调用 public 构造函数或 setter 方法.

因为 @Autowired 用于按类型执行自动装配,所以如果您具有相同类型的多个 bean 定义,那么对于那些特定的 bean,您将不能依靠这种方法. 在这种情况下,您可以将 @Autowired@Qualifier 结合使用. 您也可以选择将 @Inject@Named 结合使用. 另外,如果您的测试类可以访问其 ApplicationContext,则可以通过使用 (例如) 对 applicationContext.getBean("titleRepository", TitleRepository.class) 的调用来执行显式查找.

如果您不希望将依赖注入应用于测试实例,请不要使用 @Autowired@Inject 注解字段或设置方法. 另外,您可以通过使用 @TestExecutionListeners 显式配置您的类并从监听器列表中省 略DependencyInjectionTestExecutionListener.class 来完全禁用依赖注入.

考虑一下Goals部分概述的测试 HibernateTitleRepository 类的场景. 接下来的两个代码清单演示了 @Autowired 在字段和setter方法上的用法. 在所有示例代码清单之后显示了应用程序上下文配置.

以下代码清单中的依赖注入行为并非特定于 JUnit Jupiter. 相同的DI技术可以与任何受支持的测试框架结合使用.

以下示例对静态断言方法 (例如 assertNotNull()) 进行了调用,但没有在声明前添加断言. 在这种情况下,假定该方法是通过示例中未显示的 import static 声明正确导入的.

第一个代码清单显示了使用 @Autowired 进行字段注入的测试类的基于 JUnit Jupiter 的实现:

Java
@ExtendWith(SpringExtension.class)
// specifies the Spring configuration to load for this test fixture
@ContextConfiguration("repository-config.xml")
class HibernateTitleRepositoryTests {

    // this instance will be dependency injected by type
    @Autowired
    HibernateTitleRepository titleRepository;

    @Test
    void findById() {
        Title title = titleRepository.findById(new Long(10));
        assertNotNull(title);
    }
}
Kotlin
@ExtendWith(SpringExtension::class)
// specifies the Spring configuration to load for this test fixture
@ContextConfiguration("repository-config.xml")
class HibernateTitleRepositoryTests {

    // this instance will be dependency injected by type
    @Autowired
    lateinit var titleRepository: HibernateTitleRepository

    @Test
    fun findById() {
        val title = titleRepository.findById(10)
        assertNotNull(title)
    }
}

或者,您可以将类配置为使用 @Autowired 进行 setter 注入,如下所示:

Java
@ExtendWith(SpringExtension.class)
// specifies the Spring configuration to load for this test fixture
@ContextConfiguration("repository-config.xml")
class HibernateTitleRepositoryTests {

    // this instance will be dependency injected by type
    HibernateTitleRepository titleRepository;

    @Autowired
    void setTitleRepository(HibernateTitleRepository titleRepository) {
        this.titleRepository = titleRepository;
    }

    @Test
    void findById() {
        Title title = titleRepository.findById(new Long(10));
        assertNotNull(title);
    }
}
Kotlin
@ExtendWith(SpringExtension::class)
// specifies the Spring configuration to load for this test fixture
@ContextConfiguration("repository-config.xml")
class HibernateTitleRepositoryTests {

    // this instance will be dependency injected by type
    lateinit var titleRepository: HibernateTitleRepository

    @Autowired
    fun setTitleRepository(titleRepository: HibernateTitleRepository) {
        this.titleRepository = titleRepository
    }

    @Test
    fun findById() {
        val title = titleRepository.findById(10)
        assertNotNull(title)
    }
}

前面的代码清单使用 @ContextConfiguration 注解引用的相同 XML 上下文文件 (即, repository-config.xml) . 下面显示了此配置:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- this bean will be injected into the HibernateTitleRepositoryTests class -->
    <bean id="titleRepository" class="com.foo.repository.hibernate.HibernateTitleRepository">
        <property name="sessionFactory" ref="sessionFactory"/>
    </bean>

    <bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
        <!-- configuration elided for brevity -->
    </bean>

</beans>

如果您是从 Spring 提供的测试基类扩展而来的,而该基类恰巧在其 setter 方法之一上使用 @Autowired,则可能在应用程序上下文中定义了多个受影响类型的 Bean (例如,多个 DataSource Bean) .在这种情况下,您可以重写 setter 方法,并使用 @Qualifier 注解指示特定的目标 bean,如下所示 (但请确保也委托给超类中的重写方法) :

Java
// ...

    @Autowired
    @Override
    public void setDataSource(@Qualifier("myDataSource") DataSource dataSource) {
        super.setDataSource(dataSource);
    }

// ...
Kotlin
// ...

    @Autowired
    override fun setDataSource(@Qualifier("myDataSource") dataSource: DataSource) {
        super.setDataSource(dataSource)
    }

// ...

指定的限定符值指示要注入的特定 DataSource Bean,从而将类型匹配的范围缩小到特定 Bean. 其值与相应的 <bean> 定义中的 <qualifier> 声明匹配. Bean 名称用作后备限定符值,因此您也可以在该名称中有效地指向特定的 Bean (如先前所示,假设 myDataSource 是 Bean id) .

3.5.8. 测试请求和会话作用域的Bean

Spring 从早期开始就支持请求和会话作用域的 Bean,您可以按照以下步骤测试请求和会话作用域的 Bean:

  • 通过使用 @WebAppConfiguration 注解测试类,确保为测试加载 WebApplicationContext.

  • 将模拟请求或会话注入到测试实例中,并适当地准备测试夹具.

  • 调用从配置的 WebApplicationContext 中检索到的 Web 组件 (带有依赖注入) .

  • 对模拟执行断言.

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

Request-scoped bean configuration
<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. 然后,我们可以根据用户名和密码的已知输入对结果进行断言. 以下清单显示了如何执行此操作:

Java
@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
    }
}
Kotlin
@SpringJUnitWebConfig
class RequestScopedBeanTests {

    @Autowired lateinit var userService: UserService
    @Autowired lateinit var request: MockHttpServletRequest

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

        val results = userService.loginUser()
        // assert results
    }
}

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

Session-scoped bean configuration
<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,并且可以基于配置的主题对结果执行断言. 以下示例显示了如何执行此操作:

Java
@SpringJUnitWebConfig
class SessionScopedBeanTests {

    @Autowired UserService userService;
    @Autowired MockHttpSession session;

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

        Results results = userService.processUserPreferences();
        // assert results
    }
}
Kotlin
@SpringJUnitWebConfig
class SessionScopedBeanTests {

    @Autowired lateinit var userService: UserService
    @Autowired lateinit var session: MockHttpSession

    @Test
    fun sessionScope() {
        session.setAttribute("theme", "blue")

        val results = userService.processUserPreferences()
        // assert results
    }
}

3.5.9. 事务管理

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

测试-管理事务

测试管理的事务是通过使用 TransactionalTestExecutionListener 声明式管理的事务,或者是通过使用 TestTransaction 以编程方式管理的事务 (稍后描述) . 您不应将此类事务与 Spring 托管的事务 (由 Spring 在加载的 ApplicationContext 中直接管理以进行测试的事务) 或应用程序托管的事务 (在测试所调用的应用程序代码中以编程方式管理的事务) 相混淆. 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_SUPPORTEDNEVER 的测试不会在事务中传播.

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

value and transactionManager

yes

propagation

仅支持 Propagation.NOT_SUPPORTEDPropagation.NEVER

isolation

no

timeout

no

readOnly

no

rollbackFor and rollbackForClassName

no: use TestTransaction.flagForRollback() instead

noRollbackFor and noRollbackForClassName

no: use TestTransaction.flagForCommit() instead

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

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

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

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

Java
@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"));
    }
}
Kotlin
@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 自动回滚.

事务回滚和提交行为

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

程序化事务管理

您可以使用 TestTransaction 中的静态方法以编程方式与测试管理的事务进行交互. 例如,您可以在测试方法中,方法之前和方法之后使用 TestTransaction 来启动或结束当前的测试管理的事务,或配置当前的测试管理的事务以进行回滚或提交. 每当启用 TransactionalTestExecutionListener 时,都会自动提供对 TestTransaction 的支持.

下面的示例演示了 TestTransaction 的某些功能. 有关更多详细信息,请参见 javadoc 中的 TestTransaction.

Java
@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"));
    }
}
Kotlin
@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 确保您的 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 脚本执行. 以下示例显示了相关的注解:

Java
@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
    }

}
Kotlin
@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 持久性上下文状态的应用程序代码时,请确保在运行该代码的测试方法中刷新基础工作单元. 未能刷新基础工作单元可能会产生误报: 您的测试通过了,但是相同的代码在实际的生产环境中引发异常. 请注意,这适用于任何维护内存中工作单元的 ORM 框架. 在下面的基于 Hibernate 的示例测试用例中,一种方法演示了误报,另一种方法正确显示了刷新会话的结果:

Java
// ...

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

// ...
Kotlin
// ...

@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 的匹配方法:

Java
// ...

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

// ...
Kotlin
// ...

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

// ...
Testing ORM entity lifecycle callbacks

类似于在测试 ORM 代码时避免 false positives 的注释,如果您的应用程序使用实体生命周期回调(也称为实体侦听器),请确保刷新底层单元在运行该代码的测试方法中工作。未能 flushclear 底层工作单元可能会导致某些生命周期回调未被调用。

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

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

Java
// ...

@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...
}

// ...
Kotlin
// ...

@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...
}

// ...

github.com/spring-projects/spring-framework/blob/5.3.x/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/orm/JpaEntityListenerTests.java Spring Framework 测试套件中的 [JpaEntityListenerTests] 用于使用所有 JPA 生命周期回调的工作示例。

3.5.10. 执行 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 可能比稍后介绍的其他一些替代方法更适合您的需求. 有关更多详细信息,请参见 ScriptUtils 中各个方法的 javadoc .

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

Java
@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);
    // run code that uses the test schema and data
}
Kotlin
@Test
fun databaseTest() {
    val populator = ResourceDatabasePopulator()
    populator.addScripts(
            ClassPathResource("test-schema.sql"),
            ClassPathResource("test-data.sql"))
    populator.setSeparator("@@")
    populator.execute(dataSource)
    // run code that uses the test schema and data
}

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

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

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

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

每个路径都被解释为 Spring 资源. 普通路径 (例如 "schema.sql" ) 被视为相对于定义测试类的程序包的类路径资源. 以斜杠开头的路径被视为绝对类路径资源 (例如, "/org/example/schema.sql") . 通过使用指定的资源协议加载引用URL的路径 (例如,前缀为 classpath:, file:, http: 的路径) .

以下示例显示如何在基于 JUnit Jupiter 的集成测试类中的类级别和方法级别使用 @Sql :

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

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

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

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

    @Test
    @Sql("/test-schema.sql", "/test-user-data.sql")
    fun userTest() {
        // run code that uses the test schema and test data
    }
}
默认脚本检测

如果未指定任何 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 的可重复注解:

Java
@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
}
Kotlin
// Repeatable annotations with non-SOURCE retention are not yet supported by Kotlin

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

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

Java
@Test
@SqlGroup({
    @Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`")),
    @Sql("/test-user-data.sql")
)}
void userTest() {
    // run code that uses the test schema and test data
}
Kotlin
@Test
@SqlGroup(
    Sql("/test-schema.sql", config = SqlConfig(commentPrefix = "`")),
    Sql("/test-user-data.sql"))
fun userTest() {
    // run code that uses the test schema and test data
}
脚本执行阶段

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

Java
@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() {
    // run code that needs the test data to be committed
    // to the database outside of the test's transaction
}
Kotlin
@Test
@SqlGroup(
    Sql("create-test-data.sql",
        config = SqlConfig(transactionMode = ISOLATED)),
    Sql("delete-test-data.sql",
        config = SqlConfig(transactionMode = ISOLATED),
        executionPhase = AFTER_TEST_METHOD))
fun userTest() {
    // run 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 提供的配置选项与 ScriptUtils 和 ResourceDatabasePopulator 支持的配置选项等效,但是 <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 提供了详细信息,并且以下示例显示了使用 @Sql JUnit Jupiter 和事务性测试的典型测试方案:

Java
@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);
        // run 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.");
    }
}
Kotlin
@SpringJUnitConfig(TestDatabaseConfig::class)
@Transactional
class TransactionalSqlScriptsTests @Autowired constructor(dataSource: DataSource) {

    val jdbcTemplate: JdbcTemplate = JdbcTemplate(dataSource)

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

    fun countRowsInTable(tableName: String): Int {
        return JdbcTestUtils.countRowsInTable(jdbcTemplate, tableName)
    }

    fun assertNumUsers(expected: Int) {
        assertEquals(expected, countRowsInTable("user"),
                "Number of rows in the [user] table.")
    }
}

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

使用 @SqlMergeMode 合并和覆盖配置

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

3.5.11. 并行测试执行

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

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

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

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

  • 使用 Spring Framework 的 @DirtiesContext 支持.

  • 使用 Spring Boot 的 @MockBean@SpyBean 支持.

  • 使用 JUnit 4 的 @FixMethodOrder 支持或旨在确保测试方法按特定顺序运行的任何测试框架功能. 但是请注意,如果整个测试类是并行执行的,则此方法不适用.

  • 更改共享服务或系统 (如数据库,消息代理,文件系统等) 的状态. 这适用于嵌入式和外部系统.

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

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

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

3.5.12. TestContext Framework支持类

本节描述了支持 Spring TestContext Framework 的各种类.

Spring JUnit 4 Runner

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

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

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

    @Test
    public void testMethod() {
        // test logic...
    }
}
Kotlin
@RunWith(SpringRunner::class)
@TestExecutionListeners
class SimpleTest {

    @Test
    fun testMethod() {
        // test logic...
    }
}

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

Spring JUnit 4 规则

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

  • SpringClassRule

  • SpringMethodRule

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

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

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

Java
// 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() {
        // test logic...
    }
}
Kotlin
// Optionally specify a non-Spring Runner via @RunWith(...)
@ContextConfiguration
class IntegrationTest {

    @Rule
    val springMethodRule = SpringMethodRule()

    @Test
    fun testMethod() {
        // test logic...
    }

    companion object {
        @ClassRule
        val springClassRule = SpringClassRule()
    }
}
JUnit 4支持类

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

  • AbstractJUnit4SpringContextTests

  • AbstractTransactionalJUnit4SpringContextTests

AbstractJUnit4SpringContextTests 是抽象的基础测试类,该类将 Spring TestContext Framework 与 JUnit 4 环境中的显式 ApplicationContext 测试支持集成在一起. 扩展 AbstractJUnit4SpringContextTests 时,可以访问 protectedapplicationContext 实例变量, 该变量可用于执行显式 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 Framework 提供了与 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 结合使用:

Java
// 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() {
        // test logic...
    }
}
Kotlin
// 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
    fun testMethod() {
        // test logic...
    }
}

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

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

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

    @Test
    void testMethod() {
        // test logic...
    }
}
Kotlin
// Instructs Spring to register the SpringExtension with JUnit
// Jupiter and load an ApplicationContext from TestConfig.class
@SpringJUnitConfig(TestConfig::class)
class SimpleTests {

    @Test
    fun testMethod() {
        // test logic...
    }
}

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

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

    @Test
    void testMethod() {
        // test logic...
    }
}
Kotlin
// Instructs Spring to register the SpringExtension with JUnit
// Jupiter and load a WebApplicationContext from TestWebConfig::class
@SpringJUnitWebConfig(TestWebConfig::class)
class SimpleWebTests {

    @Test
    fun testMethod() {
        // 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 构造函数中.

Java
@SpringJUnitConfig(TestConfig.class)
class OrderServiceIntegrationTests {

    private final OrderService orderService;

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

    // tests that use the injected OrderService
}
Kotlin
@SpringJUnitConfig(TestConfig::class)
class OrderServiceIntegrationTests @Autowired constructor(private val orderService: OrderService){
    // tests that use the injected OrderService
}

请注意,此功能使测试依赖是 final 的,因此是不可变的.

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

Java
@SpringJUnitConfig(TestConfig.class)
class OrderServiceIntegrationTests {

    private final OrderService orderService;

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

    // tests that use the injected OrderService
}
Kotlin
@SpringJUnitConfig(TestConfig::class)
class OrderServiceIntegrationTests(val orderService:OrderService) {
    // tests that use the injected OrderService
}
方法注入

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

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

Java
@SpringJUnitConfig(TestConfig.class)
class OrderServiceIntegrationTests {

    @Test
    void deleteOrder(@Autowired OrderService orderService) {
        // use orderService from the test's ApplicationContext
    }
}
Kotlin
@SpringJUnitConfig(TestConfig::class)
class OrderServiceIntegrationTests {

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

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

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

Java
@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
    }
}
Kotlin
@SpringJUnitConfig(TestConfig::class)
class OrderServiceIntegrationTests {

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

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

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

@Nested 测试类配置

从 Spring Framework 5.0 开始, Spring TestContext Framework 支持在 JUnit Jupiter 中的 @Nested 测试类上使用与测试相关的注解; 但是, 直到 Spring Framework 5.3 类级测试配置注解才从封闭类继承而来, 就像它们是从超类继承的一样.

Spring Framework 5.3 引入了对从封闭类继承测试类配置的一流支持, 并且默认情况下将继承此类配置. 要将默认的 INHERIT 模式更改为 OVERRIDE 模式, 可以使用 @NestedTestConfiguration(EnclosingConfiguration.OVERRIDE) 注解单个 @Nested 测试类. 一个显式的 @NestedTestConfiguration 声明将应用于带注解的测试类及其任何子类和嵌套类. 因此, 您可以使用 @NestedTestConfiguration 注解顶级测试类, 并将其递归应用于其所有嵌套测试类.

为了允许开发团队将默认值更改为 OVERRIDE (例如, 为了与 Spring Framework 5.0 到 5.2 兼容) , 可以通过 JVM 系统属性或类路径根目录中的 spring.properties 文件全局更改默认模式. 有关详细信息, 请参见 "Changing the default enclosing configuration inheritance mode".

尽管下面的 "Hello World" 示例非常简单, 但是它显示了如何在由其 @Nested 测试类继承的顶级类上声明通用配置. 在此特定示例中, 仅继承 TestConfig 配置类. 每个嵌套的测试类提供自己的活动配置文件集, 从而为每个嵌套的测试类提供不同的 ApplicationContext (有关详细信息, 请参见 上下文缓存) . 请查阅 受支持的注解列表 , 以了解可以在 @Nested 测试类中继承哪些注解.

Java
@SpringJUnitConfig(TestConfig.class)
class GreetingServiceTests {

    @Nested
    @ActiveProfiles("lang_en")
    class EnglishGreetings {

        @Test
        void hello(@Autowired GreetingService service) {
            assertThat(service.greetWorld()).isEqualTo("Hello World");
        }
    }

    @Nested
    @ActiveProfiles("lang_de")
    class GermanGreetings {

        @Test
        void hello(@Autowired GreetingService service) {
            assertThat(service.greetWorld()).isEqualTo("Hallo Welt");
        }
    }
}
Kotlin
@SpringJUnitConfig(TestConfig::class)
class GreetingServiceTests {

    @Nested
    @ActiveProfiles("lang_en")
    inner class EnglishGreetings {

        @Test
        fun hello(@Autowired service:GreetingService) {
            assertThat(service.greetWorld()).isEqualTo("Hello World")
        }
    }

    @Nested
    @ActiveProfiles("lang_de")
    inner class GermanGreetings {

        @Test
        fun hello(@Autowired service:GreetingService) {
            assertThat(service.greetWorld()).isEqualTo("Hallo Welt")
        }
    }
}
TestNG 支持类

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

  • AbstractTestNGSpringContextTests

  • AbstractTransactionalTestNGSpringContextTests

AbstractTestNGSpringContextTests 是一个抽象的基础测试类,该类将 Spring TestContext Framework 与 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 的源代码.

3.6. WebTestClient

WebTestClient 是用于测试应用程序服务器的 HTTP 客户端,它里面包含了 WebClient ,可用于执行请求并暴露专用的流式 API 来验证响应. WebTestClient 可以用来 执行端到端 HTTP 测试. 它还可以用于测试 Spring MVC 和 Spring WebFlux,并且不需要通过模拟服务器请求和响应对象运行服务器的应用程序.

Kotlin 用户: 请参阅本节WebTestClient 的使用有关.

3.6.1. 设置

要创建 WebTestClient,您需要选择一个要绑定的服务器设置. 这可以是几个模拟服务器设置选择之一 , 也可以是到正在运行的服务器连接.

绑定到控制器

通过使用模拟请求和响应对象,可以在没有 HTTP 服务器的情况下测试生成的 WebFlux 应用程序.

对于 WebFlux 应用程序, 使用以下命令加载 WebFlux Java 配置 并注册给定的控制器, 并创建一个 WebHandler chain 来处理请求:

Java
WebTestClient client =
        WebTestClient.bindToController(new TestController()).build();
Kotlin
val client = WebTestClient.bindToController(TestController()).build()

对于 Spring MVC, 使用 StandaloneMockMvcBuilder 加载 WebMvc Java config 并注册给定的控制器, 并创建一个 MockMvc 实例来处理请求:

Java
WebTestClient client =
        MockMvcWebTestClient.bindToController(new TestController()).build();
Kotlin
val client = MockMvcWebTestClient.bindToController(TestController()).build()
绑定到 ApplicationContext

通过此设置, 您可以使用 Spring MVC 或 Spring WebFlux 基础结构和控制器声明加载 Spring 配置, 并使用它通过模拟请求和响应对象来处理请求, 而无需运行服务器.

对于 WebFlux, 请使用以下内容, 将 Spring ApplicationContext 传递到 WebHttpHandlerBuilder 中, 以创建 WebHandler chain 处理请求:

Java
@SpringJUnitConfig(WebConfig.class) (1)
class MyTests {

    WebTestClient client;

    @BeforeEach
    void setUp(ApplicationContext context) {  (2)
        client = WebTestClient.bindToApplicationContext(context).build(); (3)
    }
}
1 指定要加载的配置
2 注入配置
3 创建 WebTestClient
Kotlin
@SpringJUnitConfig(WebConfig::class) (1)
class MyTests {

    lateinit var client: WebTestClient

    @BeforeEach
    fun setUp(context: ApplicationContext) { (2)
        client = WebTestClient.bindToApplicationContext(context).build() (3)
    }
}
1 指定要加载的配置
2 注入配置
3 创建 WebTestClient

对于 Spring MVC, 将 Spring 的 ApplicationContext 传递给 MockMvcBuilders.webAppContextSetup 并创建一个 MockMvc 实例来处理请求:

Java
@ExtendWith(SpringExtension.class)
@WebAppConfiguration("classpath:META-INF/web-resources") (1)
@ContextHierarchy({
    @ContextConfiguration(classes = RootConfig.class),
    @ContextConfiguration(classes = WebConfig.class)
})
class MyTests {

    @Autowired
    WebApplicationContext wac; (2)

    WebTestClient client;

    @BeforeEach
    void setUp() {
        client = MockMvcWebTestClient.bindToApplicationContext(this.wac).build(); (3)
    }
}
1 Specify the configuration to load
2 Inject the configuration
3 Create the WebTestClient
Kotlin
@ExtendWith(SpringExtension.class)
@WebAppConfiguration("classpath:META-INF/web-resources") (1)
@ContextHierarchy({
    @ContextConfiguration(classes = RootConfig.class),
    @ContextConfiguration(classes = WebConfig.class)
})
class MyTests {

    @Autowired
    lateinit var wac: WebApplicationContext; (2)

    lateinit var client: WebTestClient

    @BeforeEach
    fun setUp() { (2)
        client = MockMvcWebTestClient.bindToApplicationContext(wac).build() (3)
    }
}
1 Specify the configuration to load
2 Inject the configuration
3 Create the WebTestClient
Bind to Router Function

通过此设置, 您可以在没有运行服务器的情况下通过模拟请求和响应对象测 functional endpoints.

对于 WebFlux, 使用 RouterFunctions.toWebHandler 创建服务器设置以处理请求:

Java
RouterFunction<?> route = ...
client = WebTestClient.bindToRouterFunction(route).build();
Kotlin
val route: RouterFunction<*> = ...
val client = WebTestClient.bindToRouterFunction(route).build()

对于 Spring MVC 目前没有可供测试的选项测试 WebMvc functional endpoints.

绑定到服务器

以下服务器设置选项使您可以连接到正在运行的服务器:

Java
client = WebTestClient.bindToServer().baseUrl("http://localhost:8080").build();
Kotlin
client = WebTestClient.bindToServer().baseUrl("http://localhost:8080").build()
客户端 Config

除了前面描述的服务器设置选项之外,您还可以配置客户端选项,包括基本 URL,默认请求头,客户端过滤器等. 这些选项在 bindToServer 之后很容易获得. 对于所有其他服务器,您需要使用 configureClient() 从服务器配置过渡到客户端配置,如下所示:

Java
client = WebTestClient.bindToController(new TestController())
        .configureClient()
        .baseUrl("/test")
        .build();
Kotlin
client = WebTestClient.bindToController(TestController())
        .configureClient()
        .baseUrl("/test")
        .build()

3.6.2. 编写测试

WebTestClient 提供了与 WebClient 相同的 API,直到使用 exchange() 执行请求为止. 请查看 WebClient 文档获取更多关于如何准备一个包含 form data, multipart data 等内容的请求.

调用 exchange() 之后, WebTestClientWebClient 分开, 继续进行工作流以验证响应.

要声明响应状态和 headers, 请使用以下命令:

Java
client.get().uri("/persons/1")
    .accept(MediaType.APPLICATION_JSON)
    .exchange()
    .expectStatus().isOk()
    .expectHeader().contentType(MediaType.APPLICATION_JSON);
Kotlin
client.get().uri("/persons/1")
    .accept(MediaType.APPLICATION_JSON)
    .exchange()
    .expectStatus().isOk()
    .expectHeader().contentType(MediaType.APPLICATION_JSON)

如果您希望所有期望都被断言,即使其中一个失败,您可以使用 expectAll(..) 而不是使用多个链接的 expect*(..) 。 这个功能是 类似于 AssertJ 中的 soft assertions 支持和 assertAll() 支持 JUnit Jupiter。

Java
client.get().uri("/persons/1")
    .accept(MediaType.APPLICATION_JSON)
    .exchange()
    .expectAll(
        spec -> spec.expectStatus().isOk(),
        spec -> spec.expectHeader().contentType(MediaType.APPLICATION_JSON)
    );

然后, 您可以选择通过以下方式之一对响应主体进行解码:

  • expectBody(Class<T>): 解码为单个对象

  • expectBodyList(Class<T>): 解码并将对象收集到 List<T>.

  • expectBody(): 解码为 byte[]获取 JSON 内容或一个空的正文.

并在生成的更高级别的对象上执行断言:

Java
client.get().uri("/persons")
        .exchange()
        .expectStatus().isOk()
        .expectBodyList(Person.class).hasSize(3).contains(person);
Kotlin
import org.springframework.test.web.reactive.server.expectBodyList

client.get().uri("/persons")
        .exchange()
        .expectStatus().isOk()
        .expectBodyList<Person>().hasSize(3).contains(person)

如果内置断言不足, 则可以改为使用该对象并执行任何其他断言:

Java
import org.springframework.test.web.reactive.server.expectBody

client.get().uri("/persons/1")
        .exchange()
        .expectStatus().isOk()
        .expectBody(Person.class)
        .consumeWith(result -> {
            // custom assertions (e.g. AssertJ)...
        });
Kotlin
client.get().uri("/persons/1")
        .exchange()
        .expectStatus().isOk()
        .expectBody<Person>()
        .consumeWith {
            // custom assertions (e.g. AssertJ)...
        }

您还可以退出工作流程并获得 EntityExchangeResult,如下所示:

Java
EntityExchangeResult<Person> result = client.get().uri("/persons/1")
        .exchange()
        .expectStatus().isOk()
        .expectBody(Person.class)
        .returnResult();
Kotlin
import org.springframework.test.web.reactive.server.expectBody

val result = client.get().uri("/persons/1")
        .exchange()
        .expectStatus().isOk
        .expectBody<Person>()
        .returnResult()
当需要使用泛型解码为目标类型时,请寻找接受 ParameterizedTypeReference 而不是 Class<T> 的重载方法.
无内容

如果响应没有内容(或者您不在乎) ,则可以使用以下断言:

Java
client.post().uri("/persons")
        .body(personMono, Person.class)
        .exchange()
        .expectStatus().isCreated()
        .expectBody().isEmpty();
Kotlin
client.post().uri("/persons")
        .bodyValue(person)
        .exchange()
        .expectStatus().isCreated()
        .expectBody().isEmpty()

如果要忽略响应内容, 则以下操作将释放响应内容, 而不会产生任何断言:

Java
client.get().uri("/persons/123")
        .exchange()
        .expectStatus().isNotFound()
        .expectBody(Void.class);
Kotlin
client.get().uri("/persons/123")
        .exchange()
        .expectStatus().isNotFound
        .expectBody<Unit>()
JSON 内容

当您使用 expectBody() 时,响应以 byte[] 的形式使用. 这对于原始内容声明很有用. 例如,您可以使用 JSONAssert 来验证 JSON 内容,如下所示:

Java
client.get().uri("/persons/1")
        .exchange()
        .expectStatus().isOk()
        .expectBody()
        .json("{\"name\":\"Jane\"}")
Kotlin
client.get().uri("/persons/1")
        .exchange()
        .expectStatus().isOk()
        .expectBody()
        .json("{\"name\":\"Jane\"}")

您还可以使用 JSONPath 验证 JSON content,如下所示:

Java
client.get().uri("/persons")
        .exchange()
        .expectStatus().isOk()
        .expectBody()
        .jsonPath("$[0].name").isEqualTo("Jane")
        .jsonPath("$[1].name").isEqualTo("Jason");
Kotlin
client.get().uri("/persons")
        .exchange()
        .expectStatus().isOk()
        .expectBody()
        .jsonPath("$[0].name").isEqualTo("Jane")
        .jsonPath("$[1].name").isEqualTo("Jason")
流式响应

要测试可能存在的无限流 (例如, "text/event-stream""application/x-ndjson"), 首先需要验证 响应状态和 headers, 之后会获得一个 FluxExchangeResult:

Java
FluxExchangeResult<MyEvent> result = client.get().uri("/events")
        .accept(TEXT_EVENT_STREAM)
        .exchange()
        .expectStatus().isOk()
        .returnResult(MyEvent.class);
Kotlin
import org.springframework.test.web.reactive.server.returnResult

val result = client.get().uri("/events")
        .accept(TEXT_EVENT_STREAM)
        .exchange()
        .expectStatus().isOk()
        .returnResult<MyEvent>()

现在, 您可以使用来自 reactor-test 中的 StepVerifier 来使用响应流了:

Java
Flux<Event> eventFlux = result.getResponseBody();

StepVerifier.create(eventFlux)
        .expectNext(person)
        .expectNextCount(4)
        .consumeNextWith(p -> ...)
        .thenCancel()
        .verify();
Kotlin
val eventFlux = result.getResponseBody()

StepVerifier.create(eventFlux)
        .expectNext(person)
        .expectNextCount(4)
        .consumeNextWith { p -> ... }
        .thenCancel()
        .verify()
MockMvc 断言

WebTestClient 是一个 HTTP 客户端, 因此它只能验证客户端响应中的内容, 包括状态, header 和正文.

当使用 MockMvc 服务器设置测试 Spring MVC 应用程序时, 您可以选择对服务器响应执行进一步的声明. 要做到这一点, 首先要在声明主体后获得一个 ExchangeResult

Java
// For a response with a body
EntityExchangeResult<Person> result = client.get().uri("/persons/1")
        .exchange()
        .expectStatus().isOk()
        .expectBody(Person.class)
        .returnResult();

// For a response without a body
EntityExchangeResult<Void> result = client.get().uri("/path")
        .exchange()
        .expectBody().isEmpty();
Kotlin
// For a response with a body
val result = client.get().uri("/persons/1")
        .exchange()
        .expectStatus().isOk()
        .expectBody(Person.class)
        .returnResult();

// For a response without a body
val result = client.get().uri("/path")
        .exchange()
        .expectBody().isEmpty();

然后切换到 MockMvc 服务器响应断言:

Java
MockMvcWebTestClient.resultActionsFor(result)
        .andExpect(model().attribute("integer", 3))
        .andExpect(model().attribute("string", "a string value"));
Kotlin
MockMvcWebTestClient.resultActionsFor(result)
        .andExpect(model().attribute("integer", 3))
        .andExpect(model().attribute("string", "a string value"));

3.7. MockMvc

Spring MVC 测试框架 (也称为 MockMvc) 为测试 Spring MVC 应用程序提供支持. 它通过模拟请求和响应对象而不是正在运行的服务器执行完整的 Spring MVC 请求处理.

MockMvc 可以单独用于执行请求和验证响应. 也可以通过 WebTestClient 来使用它, 在该 WebTestClient 中插入 MockMvc 作为服务器来处理请求. WebTestClient 的优点是可以处理更高级别的对象而不是原始数据, 并且可以切换到针对实时服务器的完整端到端 HTTP 测试并使用相同的测试 API.

3.7.1. 概述

您可以通过实例化控制器, 向其注入依赖项并调用其方法来为 Spring MVC 编写普通的单元测试. 但是, 此类测试不会验证请求映射, 数据绑定, 消息转换, 类型转换, 验证, 也不会涉及任何支持的 @InitBinder, @ModelAttribute@ExceptionHandler 方法.

Spring MVC Test 框架 (也称为 MockMvc) 旨在在没有运行服务器的情况下为 Spring MVC 控制器提供更完整的测试. 它通过调用 DispatcherServlet 并从 spring-test 模块传递了 Servlet API “mock” 实现 来实现, 该模块在没有运行服务器的情况下复制了完整的 Spring MVC 请求处理.

MockMvc 是一个服务器端测试框架, 使您可以使用轻量级的针对性测试来验证 Spring MVC 应用程序的大多数功能. 您可以单独使用它来执行请求和验证响应, 也可以通过 WebTestClient API (带有作为服务器处理请求的服务器插入的 MockMvc) 来使用它.

静态导入

直接使用 MockMvc 执行请求时, 您需要静态导入才能实现:

  • MockMvcBuilders.*

  • MockMvcRequestBuilders.*

  • MockMvcResultMatchers.*

  • MockMvcResultHandlers.*

一种简单的记住方法是搜索 MockMvc*. 如果使用 Eclipse, 请确保还要在 Eclipse 首选项中将上述内容添加为 “favorite static members”.

通过 WebTestClient 使用 MockMvc 时, 不需要静态导入. WebTestClient 提供了一个流式的 API, 而没有静态导入.

设置选择

MockMvc 可以通过以下两种方式之一进行设置. 一种是直接指向要测试的控制器, 并以编程方式配置 Spring MVC 基础结构. 第二个是指向其中装有 Spring MVC 和控制器基础结构的 Spring 配置.

要设置 MockMvc 以测试特定的控制器, 请使用以下命令:

Java
class MyWebTests {

    MockMvc mockMvc;

    @BeforeEach
    void setup() {
        this.mockMvc = MockMvcBuilders.standaloneSetup(new AccountController()).build();
    }

    // ...

}
Kotlin
class MyWebTests {

    lateinit var mockMvc : MockMvc

    @BeforeEach
    fun setup() {
        mockMvc = MockMvcBuilders.standaloneSetup(AccountController()).build()
    }

    // ...

}

或者, 您也可以在通过 WebTestClient 进行测试时使用此设置, 该测试委托给如上所述的同一构建器.

要通过 Spring 配置设置 MockMvc, 请使用以下命令:

Java
@SpringJUnitWebConfig(locations = "my-servlet-context.xml")
class MyWebTests {

    MockMvc mockMvc;

    @BeforeEach
    void setup(WebApplicationContext wac) {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
    }

    // ...

}
Kotlin
@SpringJUnitWebConfig(locations = ["my-servlet-context.xml"])
class MyWebTests {

    lateinit var mockMvc: MockMvc

    @BeforeEach
    fun setup(wac: WebApplicationContext) {
        mockMvc = MockMvcBuilders.webAppContextSetup(wac).build()
    }

    // ...

}

或者, 您也可以在通过 WebTestClient 进行测试时使用此设置, 该测试委托给如上所述的同一构建器.

您应该使用哪个设置选项?

webAppContextSetup 加载实际的 Spring MVC 配置,从而进行更完整的集成测试. 由于 TestContext 框架缓存了已加载的 Spring 配置,因此即使您在测试套件中引入了更多测试,它也有助于保持测试的快速运行. 此外,您可以通过 Spring 配置将模拟服务注入控制器中,以继续专注于测试 Web 层. 下面的示例使用 Mockito 声明一个模拟服务:

<bean id="accountService" class="org.mockito.Mockito" factory-method="mock">
    <constructor-arg value="org.example.AccountService"/>
</bean>

然后,您可以将模拟服务注入测试中,以设置和验证您的期望,如以下示例所示:

Java
@SpringJUnitWebConfig(locations = "test-servlet-context.xml")
class AccountTests {

    @Autowired
    AccountService accountService;

    MockMvc mockMvc;

    @BeforeEach
    void setup(WebApplicationContext wac) {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
    }

    // ...

}
Kotlin
@SpringJUnitWebConfig(locations = ["test-servlet-context.xml"])
class AccountTests {

    @Autowired
    lateinit var accountService: AccountService

    lateinit mockMvc: MockMvc

    @BeforeEach
    fun setup(wac: WebApplicationContext) {
        mockMvc = MockMvcBuilders.webAppContextSetup(wac).build()
    }

    // ...

}

另一方面,standaloneSetup 更接近于单元测试. 它一次测试一个控制器. 您可以手动注入具有模拟依赖的控制器,并且不涉及加载 Spring 配置. 这样的测试更多地集中在样式上,使查看被测试的控制器,是否需要任何特定的 Spring MVC 配置等工作变得更加容易. standaloneSetup 还是编写临时测试以验证特定行为或调试问题的一种非常方便的方法.

与大多数 "集成与单元测试" 辩论一样,没有正确或错误的答案. 但是,使用 standaloneSetup 确实意味着需要其他 webAppContextSetup 测试,以验证您的 Spring MVC 配置. 另外,您可以使用 webAppContextSetup 编写所有测试,以便始终针对实际的Spring MVC配置进行测试.

设定功能

无论使用哪种 MockMvc 构建器,所有 MockMvcBuilder 实现都提供一些常见且非常有用的功能. 例如,您可以为所有请求声明一个 Accept 请求头,并且期望所有响应中的状态为 200 以及 Content-Type 请求头,如下所示:

Java
// static import of MockMvcBuilders.standaloneSetup

MockMvc mockMvc = standaloneSetup(new MusicController())
    .defaultRequest(get("/").accept(MediaType.APPLICATION_JSON))
    .alwaysExpect(status().isOk())
    .alwaysExpect(content().contentType("application/json;charset=UTF-8"))
    .build();
Kotlin
// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed

此外,第三方框架 (和应用程序) 可以预先打包安装说明,例如 MockMvcConfigurer 中的安装说明. Spring 框架具有一个这样的内置实现,可帮助保存和重用跨请求的 HTTP 会话. 您可以按以下方式使用它:

Java
// static import of SharedHttpSessionConfigurer.sharedHttpSession

MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new TestController())
        .apply(sharedHttpSession())
        .build();

// Use mockMvc to perform requests...
Kotlin
// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed

有关所有 MockMvc 构建器功能的列表,请参阅 ConfigurableMockMvcBuilder 的 javadoc,或使用 IDE 探索可用选项.

执行请求

本节说明如何独自使用 MockMvc 来执行请求和验证响应. 如果通过 WebTestClient 使用 MockMvc, 请参见 编写测试.

您可以使用任何 HTTP 方法执行请求,如以下示例所示:

Java
// static import of MockMvcRequestBuilders.*

mockMvc.perform(post("/hotels/{id}", 42).accept(MediaType.APPLICATION_JSON));
Kotlin
import org.springframework.test.web.servlet.post

mockMvc.post("/hotels/{id}", 42) {
    accept = MediaType.APPLICATION_JSON
}

您还可以执行内部使用 MockMultipartHttpServletRequest 的文件上载请求,以便不对多部分请求进行实际解析. 相反,您必须将其设置为类似于以下示例:

Java
mockMvc.perform(multipart("/doc").file("a1", "ABC".getBytes("UTF-8")));
Kotlin
import org.springframework.test.web.servlet.multipart

mockMvc.multipart("/doc") {
    file("a1", "ABC".toByteArray(charset("UTF8")))
}

您可以使用 URI 模板样式指定查询参数,如以下示例所示:

Java
mockMvc.perform(get("/hotels?thing={thing}", "somewhere"));
Kotlin
mockMvc.get("/hotels?thing={thing}", "somewhere")

您还可以添加代表查询或表单参数的 Servlet 请求参数,如以下示例所示:

Java
mockMvc.perform(get("/hotels").param("thing", "somewhere"));
Kotlin
import org.springframework.test.web.servlet.get

mockMvc.get("/hotels") {
    param("thing", "somewhere")
}

如果应用程序代码依赖 Servlet 请求参数,并且没有显式检查查询字符串 (通常是这种情况) ,则使用哪个选项都没有关系. 但是请记住,URI 模板提供的查询参数已被解码,而通过 param(…​) 方法提供的请求参数预计已被解码.

在大多数情况下,最好将上下文路径和 Servlet 路径保留在请求URI之外. 如果必须使用完整的请求 URI 进行测试,请确保相应地设置 contextPathservletPath,以便请求映射起作用,如以下示例所示:

Java
mockMvc.perform(get("/app/main/hotels/{id}").contextPath("/app").servletPath("/main"))
Kotlin
import org.springframework.test.web.servlet.get

mockMvc.get("/app/main/hotels/{id}") {
    contextPath = "/app"
    servletPath = "/main"
}

在前面的示例中,为每个执行的请求设置 contextPathservletPath 将很麻烦. 相反,您可以设置默认请求属性,如以下示例所示:

Java
class MyWebTests {

    MockMvc mockMvc;

    @BeforeEach
    void setup() {
        mockMvc = standaloneSetup(new AccountController())
            .defaultRequest(get("/")
            .contextPath("/app").servletPath("/main")
            .accept(MediaType.APPLICATION_JSON)).build();
    }
}
Kotlin
// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed

前述属性会影响通过 MockMvc 实例执行的每个请求. 如果在给定请求上也指定了相同的属性,则它将覆盖默认值. 这就是默认请求中的 HTTP 方法和 URI 无关紧要的原因,因为必须在每个请求中都指定它们.

定义期望

您可以通过在执行请求后追加一个或多个 .andExpect(..) 调用来定义期望,如以下示例所示,如果一个失败,则不会执行其他的:

Java
// static import of MockMvcRequestBuilders.* and MockMvcResultMatchers.*

mockMvc.perform(get("/accounts/1")).andExpect(status().isOk());
Kotlin
import org.springframework.test.web.servlet.get

mockMvc.get("/accounts/1").andExpect {
    status { isOk() }
}

您可以通过在执行后附加 andExpectAll(..) 来定义多个期望请求,如下例所示。 与 andExpect(..) 相比, andExpectAll(..) 保证所有提供的期望都将被断言,并且所有故障都将被跟踪和报告。

Java
// static import of MockMvcRequestBuilders.* and MockMvcResultMatchers.*

mockMvc.perform(get("/accounts/1")).andExpectAll(
    status().isOk(),
    content().contentType("application/json;charset=UTF-8"));

MockMvcResultMatchers.* 提供了许多期望,其中一些期望与更详细的期望进一步嵌套.

期望分为两大类. 第一类断言验证响应的属性 (例如,响应状态,header 和内容) . 这些是要断言的最重要的结果.

第二类断言超出了响应范围. 这些断言使您可以检查 Spring MVC 的特定方面,例如哪种控制器方法处理了请求,是否引发和处理了异常,模型的内容是什么,选择了哪种视图,添加了哪些闪存属性,等等. 它们还使您可以检查 Servlet 的特定方面,例如请求和会话属性.

以下测试断言绑定或验证失败:

Java
mockMvc.perform(post("/persons"))
    .andExpect(status().isOk())
    .andExpect(model().attributeHasErrors("person"));
Kotlin
import org.springframework.test.web.servlet.post

mockMvc.post("/persons").andExpect {
    status { isOk() }
    model {
        attributeHasErrors("person")
    }
}

很多时候,编写测试时,转储已执行请求的结果很有用. 您可以按照以下方式进行操作,其中 print() 是从 MockMvcResultHandlers 静态导入的:

Java
mockMvc.perform(post("/persons"))
    .andDo(print())
    .andExpect(status().isOk())
    .andExpect(model().attributeHasErrors("person"));
Kotlin
import org.springframework.test.web.servlet.post

mockMvc.post("/persons").andDo {
        print()
    }.andExpect {
        status { isOk() }
        model {
            attributeHasErrors("person")
        }
    }

只要请求处理不会引起未处理的异常, print() 方法会将所有可用的结果数据打印到 System.out. 还有一个 log() 方法和 print() 方法的两个其他变体,一个变体接受 OutputStream,另一个变体接受 Writer. 例如,调用 print(System.err) 将结果数据打印到 System.err,而调用 print(myWriter) 将结果数据打印到自定义编写器. 如果要记录结果数据而不是打印结果,则可以调用 log() 方法, 该方法将结果数据记录为 org.springframework.test.web.servlet.result 记录类别下的单个 DEBUG 消息.

在某些情况下,您可能需要直接访问结果并验证否则无法验证的内容. 可以通过在所有其他期望之后附加 .andReturn() 来实现,如以下示例所示:

Java
MvcResult mvcResult = mockMvc.perform(post("/persons")).andExpect(status().isOk()).andReturn();
// ...
Kotlin
var mvcResult = mockMvc.post("/persons").andExpect { status { isOk() } }.andReturn()
// ...

如果所有测试都重复相同的期望,则在构建 MockMvc 实例时可以一次设置通用期望,如以下示例所示:

Java
standaloneSetup(new SimpleController())
    .alwaysExpect(status().isOk())
    .alwaysExpect(content().contentType("application/json;charset=UTF-8"))
    .build()
Kotlin
// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed

请注意,通常会应用共同的期望,并且在不创建单独的 MockMvc 实例的情况下不能将其覆盖.

当 JSON 响应内容包含使用 Spring HATEOAS创建的超媒体链接时,可以使用 JsonPath 表达式来验证结果链接,如以下示例所示:

Java
mockMvc.perform(get("/people").accept(MediaType.APPLICATION_JSON))
    .andExpect(jsonPath("$.links[?(@.rel == 'self')].href").value("http://localhost:8080/people"));
Kotlin
mockMvc.get("/people") {
    accept(MediaType.APPLICATION_JSON)
}.andExpect {
    jsonPath("$.links[?(@.rel == 'self')].href") {
        value("http://localhost:8080/people")
    }
}

当 XML 响应内容包含使用 Spring HATEOAS 创建的超媒体链接时,可以使用 XPath 表达式来验证生成的链接:

Java
Map<String, String> ns = Collections.singletonMap("ns", "http://www.w3.org/2005/Atom");
mockMvc.perform(get("/handle").accept(MediaType.APPLICATION_XML))
    .andExpect(xpath("/person/ns:link[@rel='self']/@href", ns).string("http://localhost:8080/people"));
Kotlin
val ns = mapOf("ns" to "http://www.w3.org/2005/Atom")
mockMvc.get("/handle") {
    accept(MediaType.APPLICATION_XML)
}.andExpect {
    xpath("/person/ns:link[@rel='self']/@href", ns) {
        string("http://localhost:8080/people")
    }
}
异步请求

本节说明如何独自使用 MockMvc 来测试异步请求处理. 如果通过 WebTestClient 使用 MockMvc, 则没有什么特别的事情可以使异步请求正常工作, 因为 WebTestClient 会自动执行本节中介绍的操作.

Spring MVC 支持的 Servlet 3.0 异步请求通过退出 Servlet 容器线程并允许应用程序异步计算响应来工作,然后进行异步调度以完成对 Servlet 容器线程的处理.

在 Spring MVC Test 中,可以通过以下方法测试异步请求: 首先声明产生的异步值,然后手动执行异步分派,最后验证响应. 以下是针对返回 DeferredResult,CallableReactor Mono 等响应类型的控制器方法的示例测试:

Java
// static import of MockMvcRequestBuilders.* and MockMvcResultMatchers.*

@Test
void test() throws Exception {
    MvcResult mvcResult = this.mockMvc.perform(get("/path"))
            .andExpect(status().isOk()) (1)
            .andExpect(request().asyncStarted()) (2)
            .andExpect(request().asyncResult("body")) (3)
            .andReturn();

    this.mockMvc.perform(asyncDispatch(mvcResult)) (4)
            .andExpect(status().isOk()) (5)
            .andExpect(content().string("body"));
}
1 检查响应状态仍然不变
2 异步处理必须已经开始
3 等待并声明异步结果
4 手动执行 ASYNC 调度 (因为没有正在运行的容器)
5 验证最终答复
Kotlin
@Test
fun test() {
    var mvcResult = mockMvc.get("/path").andExpect {
        status { isOk() } (1)
        request { asyncStarted() } (2)
        // TODO Remove unused generic parameter
        request { asyncResult<Nothing>("body") } (3)
    }.andReturn()


    mockMvc.perform(asyncDispatch(mvcResult)) (4)
            .andExpect {
                status { isOk() } (5)
                content().string("body")
            }
}
1 检查响应状态仍然不变
2 异步处理必须已经开始
3 等待并声明异步结果
4 手动执行 ASYNC 调度 (因为没有正在运行的容器)
5 验证最终答复
流式响应

测试诸如服务器发送事件之类的流响应的最佳方法是通过 WebTestClient, 它可以用作测试客户端以连接到 MockMvc 实例, 以在不运行服务器的情况下在 Spring MVC 控制器上执行测试. 例如:

Java
WebTestClient client = MockMvcWebTestClient.bindToController(new SseController()).build();

FluxExchangeResult<Person> exchangeResult = client.get()
        .uri("/persons")
        .exchange()
        .expectStatus().isOk()
        .expectHeader().contentType("text/event-stream")
        .returnResult(Person.class);

// Use StepVerifier from Project Reactor to test the streaming response

StepVerifier.create(exchangeResult.getResponseBody())
        .expectNext(new Person("N0"), new Person("N1"), new Person("N2"))
        .expectNextCount(4)
        .consumeNextWith(person -> assertThat(person.getName()).endsWith("7"))
        .thenCancel()
        .verify();

WebTestClient 还可以连接到实时服务器并执行完整的端到端集成测试. Spring Boot 也支持此功能, 您可以在其中测试 {doc-spring-boot}/html/spring-boot-features.html#boot-features-testing-spring-boot-applications-testing-with-running-server[测试正在运行的服务器].

过滤器注册

设置 MockMvc 实例时,可以注册一个或多个Servlet Filter 实例,如以下示例所示:

Java
mockMvc = standaloneSetup(new PersonController()).addFilters(new CharacterEncodingFilter()).build();
Kotlin
// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed

注册的过滤器通过 spring-test 中的 MockFilterChain 调用,最后一个过滤器委托给 DispatcherServlet.

MockMVc 与端到端测试

MockMVc 基于 spring-test 模块的 Servlet API 模拟实现而构建,并且不依赖于运行中的容器. 因此,与使用实际客户端和实时服务器运行的完整端到端集成测试相比,存在一些差异.

考虑这一点的最简单方法是从空白的 MockHttpServletRequest 开始. 您添加到其中的内容就是请求的内容. 可能令您感到惊讶的是,默认情况下没有上下文路径. 没有 jsessionid cookie; 没有转发,错误或异步调度; 因此,没有实际的JSP呈现. 而是将 “forwarded” 和 “redirected” URL 保存在 MockHttpServletResponse 中,并且可以按预期进行声明.

这意味着,如果您使用 JSP,则可以验证将请求转发到的 JSP 页面,但不会呈现 HTML. 换句话说,不调用JSP. 但是请注意,不依赖转发的所有其他渲染技术 (例如 Thymeleaf 和 Freemarker) 都可以按预期将 HTML 渲染到响应主体. 通过 @ResponseBody 方法呈现 JSON,XML 和其他格式时也是如此.

另外,您可以考虑使用 @SpringBootTest 从 Spring Boot 获得完整的端到端集成测试支持. 请参阅 {doc-spring-boot}/html/spring-boot-features.html#boot-features-testing[Spring Boot Reference Guide].

每种方法都各有利弊. 从经典的单元测试到全面的集成测试,Spring MVC Test 中提供的选项在规模上是不同的. 可以肯定的是,Spring MVC Test 中的所有选项都不属于经典单元测试的范畴,但与之接近. 例如,您可以通过将模拟服务注入到控制器中来隔离 Web 层,在这种情况下,您仅通过 DispatcherServlet 并使用实际的 Spring 配置来测试 Web 层,因为您可能会与上一层隔离地测试数据访问层 . 另外,您可以使用独立设置,一次只关注一个控制器,然后手动提供使其工作所需的配置.

使用 Spring MVC Test 时的另一个重要区别是,从概念上讲,此类测试是服务器端的,因此您可以检查使用了哪个处理程序,如果使用 HandlerExceptionResolver 处理了异常,则模型的内容是什么,绑定错误是什么? 还有其他细节. 这意味着编写期望值更容易,因为服务器不是一个透明的盒子,就像通过实际的 HTTP 客户端对其进行测试时那样. 通常,这是经典单元测试的一个优势: 编写,推理和调试更容易,但不能代替完全集成测试的需要. 同时,重要的是不要忽略响应是最重要的检查事实. 简而言之,即使在同一项目中,这里也有多种样式和测试策略的空间.

进一步的例子

测试框架包括 许多示例测试,旨在展示如何使用 MockMvc 或通过 WebTestClient 使用 MockMvc . 您可以浏览这些示例以获取进一步的想法.

3.7.2. HtmlUnit 集成

Spring 提供了MockMvcHtmlUnit之间的集成. 使用基于 HTML 的视图时,这简化了执行端到端测试的过程. 通过此集成,您可以:

  • 使用 HtmlUnit, WebDriverGeb 等工具可以轻松测试HTML页面,而无需部署到 Servlet 容器.

  • 在页面中测试 JavaScript.

  • (可选) 使用模拟服务进行测试以加快测试速度.

  • 在容器内端到端测试和容器外集成测试之间共享逻辑.

MockMvc 使用不依赖 Servlet 容器的模板技术 (例如 Thymeleaf,FreeMarker 等) ,但不适用于 JSP,因为它们依赖 Servlet 容器.
为什么要进行 HtmlUnit 集成?

想到的最明显的问题是 "我为什么需要这个? " 最好的答案是通过探索一个非常基本的示例应用程序来找到的. 假设您有一个 Spring MVC Web 应用程序,该应用程序支持对 Message 对象的 CRUD 操作. 该应用程序还支持所有消息的分页. 您将如何进行测试?

使用Spring MVC Test,我们可以轻松地测试是否能够创建 Message,如下所示:

Java
MockHttpServletRequestBuilder createMessage = post("/messages/")
        .param("summary", "Spring Rocks")
        .param("text", "In case you didn't know, Spring Rocks!");

mockMvc.perform(createMessage)
        .andExpect(status().is3xxRedirection())
        .andExpect(redirectedUrl("/messages/123"));
Kotlin
@Test
fun test() {
    mockMvc.post("/messages/") {
        param("summary", "Spring Rocks")
        param("text", "In case you didn't know, Spring Rocks!")
    }.andExpect {
        status().is3xxRedirection()
        redirectedUrl("/messages/123")
    }
}

如果我们要测试允许我们创建消息的表单视图怎么办? 例如,假设我们的表单类似于以下代码段:

<form id="messageForm" action="/messages/" method="post">
    <div class="pull-right"><a href="/messages/">Messages</a></div>

    <label for="summary">Summary</label>
    <input type="text" class="required" id="summary" name="summary" value="" />

    <label for="text">Message</label>
    <textarea id="text" name="text"></textarea>

    <div class="form-actions">
        <input type="submit" value="Create" />
    </div>
</form>

我们如何确保表单产生正确的请求以创建新消息? 天真的尝试可能类似于以下内容:

Java
mockMvc.perform(get("/messages/form"))
        .andExpect(xpath("//input[@name='summary']").exists())
        .andExpect(xpath("//textarea[@name='text']").exists());
Kotlin
mockMvc.get("/messages/form").andExpect {
    xpath("//input[@name='summary']") { exists() }
    xpath("//textarea[@name='text']") { exists() }
}

此测试有一些明显的缺点. 如果我们更新控制器以使用 message 而不是 text,则即使 HTML 表单与控制器不同步,我们的表单测试也会继续通过. 为了解决这个问题,我们可以结合以下两个测试:

Java
String summaryParamName = "summary";
String textParamName = "text";
mockMvc.perform(get("/messages/form"))
        .andExpect(xpath("//input[@name='" + summaryParamName + "']").exists())
        .andExpect(xpath("//textarea[@name='" + textParamName + "']").exists());

MockHttpServletRequestBuilder createMessage = post("/messages/")
        .param(summaryParamName, "Spring Rocks")
        .param(textParamName, "In case you didn't know, Spring Rocks!");

mockMvc.perform(createMessage)
        .andExpect(status().is3xxRedirection())
        .andExpect(redirectedUrl("/messages/123"));
Kotlin
val summaryParamName = "summary";
val textParamName = "text";
mockMvc.get("/messages/form").andExpect {
    xpath("//input[@name='$summaryParamName']") { exists() }
    xpath("//textarea[@name='$textParamName']") { exists() }
}
mockMvc.post("/messages/") {
    param(summaryParamName, "Spring Rocks")
    param(textParamName, "In case you didn't know, Spring Rocks!")
}.andExpect {
    status().is3xxRedirection()
    redirectedUrl("/messages/123")
}

这样可以减少测试不正确通过的风险,但是仍然存在一些问题:

  • 如果页面上有多个表单怎么办? 诚然,我们可以更新 XPath 表达式,但是由于我们考虑了更多因素,它们变得更加复杂: 字段是正确的类型吗? 是否启用了字段? 等等.

  • 另一个问题是我们正在做我们期望的两倍的工作. 我们必须首先验证视图,然后使用刚刚验证的相同参数提交视图. 理想情况下,可以一次完成所有操作.

  • 最后,我们仍然无法解释某些事情. 例如,如果表单也具有我们希望测试的 JavaScript 验证,该怎么办?

总体问题是,测试网页不涉及单个交互. 相反,它是用户如何与网页交互以及该网页与其他资源交互的组合. 例如,表单视图的结果用作用户创建消息的输入. 另外,我们的表单视图可以潜在地使用影响页面行为的其他资源,例如 JavaScript 验证.

集成测试补救?

为了解决前面提到的问题,我们可以执行端到端集成测试,但这有一些缺点. 考虑测试允许我们翻阅消息的视图. 我们可能需要以下测试:

  • 我们的页面是否向用户显示通知,以指示消息为空时没有可用结果?

  • 我们的页面是否正确显示一条消息?

  • 我们的页面是否正确支持分页?

要设置这些测试,我们需要确保我们的数据库包含正确的消息. 这带来了许多其他挑战:

  • 确保数据库中包含正确的消息可能很繁琐. (考虑外键约束. )

  • 测试可能会变慢,因为每次测试都需要确保数据库处于正确的状态.

  • 由于我们的数据库需要处于特定状态,因此我们无法并行运行测试.

  • 对诸如自动生成的 ID,时间戳等项目进行断言可能很困难.

这些挑战并不意味着我们应该完全放弃端到端集成测试. 相反,我们可以通过重构我们的详细测试以使用运行速度更快,更可靠且没有副作用的模拟服务来减少端到端集成测试的数量. 然后,我们可以实施少量真正的端到端集成测试,以验证简单的工作流程,以确保一切正常工作.

进入HtmlUnit集成

那么,如何在测试页面的交互性之间保持平衡,并在测试套件中保持良好的性能呢? 答案是: "通过将 MockMvc 与 HtmlUnit 集成. "

HtmlUnit 集成选项

要将 MockMvc 与 HtmlUnit 集成时,有很多选择:

  • MockMvc 和 HtmlUnit: 如果要使用原始的 HtmlUnit 库,请使用此选项.

  • MockMvc 和 WebDriver: 使用此选项可简化集成和端到端测试之间的开发和重用代码.

  • MockMvc 和 Geb: 如果要使用 Groovy 进行测试,简化开发并在集成和端到端测试之间重用代码,请使用此选项.

MockMvc 和 HtmlUnit

本节介绍如何集成 MockMvc 和 HtmlUnit. 如果要使用原始的 HtmlUnit 库,请使用此选项.

MockMvc 和 HtmlUnit 设置

首先,请确保您已包含对 net.sourceforge.htmlunit:htmlunit 的测试依赖. 为了将HtmlUnit与Apache HttpComponents 4.5+一起使用,您需要使用HtmlUnit 2.18或更高版本.

我们可以使用 MockMvcWebClientBuilder 轻松创建一个与 MockMvc 集成的 HtmlUnit WebClient,如下所示:

Java
WebClient webClient;

@BeforeEach
void setup(WebApplicationContext context) {
    webClient = MockMvcWebClientBuilder
            .webAppContextSetup(context)
            .build();
}
Kotlin
lateinit var webClient: WebClient

@BeforeEach
fun setup(context: WebApplicationContext) {
    webClient = MockMvcWebClientBuilder
            .webAppContextSetup(context)
            .build()
}
这是使用 MockMvcWebClientBuilder 的简单示例. 有关高级用法,请参阅高级 MockMvcWebClientBuilder.

这样可以确保将引用 localhost 作为服务器的所有 URL 定向到我们的 MockMvc 实例,而无需真正的 HTTP 连接. 通常,通过使用网络连接来请求其他任何 URL. 这使我们可以轻松测试 CDN 的使用.

MockMvc 和 HtmlUnit 使用

现在,我们可以像往常一样使用 HtmlUnit,而无需将应用程序部署到 Servlet 容器. 例如,我们可以请求视图创建以下消息:

Java
HtmlPage createMsgFormPage = webClient.getPage("http://localhost/messages/form");
Kotlin
val createMsgFormPage = webClient.getPage("http://localhost/messages/form")
默认上下文路径为 "". 或者,我们可以指定上下文路径,如高级 MockMvcWebClientBuilder中所述.

一旦有了对 HtmlPage 的引用,我们就可以填写该表单并提交以创建一条消息,如以下示例所示:

Java
HtmlForm form = createMsgFormPage.getHtmlElementById("messageForm");
HtmlTextInput summaryInput = createMsgFormPage.getHtmlElementById("summary");
summaryInput.setValueAttribute("Spring Rocks");
HtmlTextArea textInput = createMsgFormPage.getHtmlElementById("text");
textInput.setText("In case you didn't know, Spring Rocks!");
HtmlSubmitInput submit = form.getOneHtmlElementByAttribute("input", "type", "submit");
HtmlPage newMessagePage = submit.click();
Kotlin
val form = createMsgFormPage.getHtmlElementById("messageForm")
val summaryInput = createMsgFormPage.getHtmlElementById("summary")
summaryInput.setValueAttribute("Spring Rocks")
val textInput = createMsgFormPage.getHtmlElementById("text")
textInput.setText("In case you didn't know, Spring Rocks!")
val submit = form.getOneHtmlElementByAttribute("input", "type", "submit")
val newMessagePage = submit.click()

最后,我们可以验证是否成功创建了新消息. 以下断言使用 AssertJ 库:

Java
assertThat(newMessagePage.getUrl().toString()).endsWith("/messages/123");
String id = newMessagePage.getHtmlElementById("id").getTextContent();
assertThat(id).isEqualTo("123");
String summary = newMessagePage.getHtmlElementById("summary").getTextContent();
assertThat(summary).isEqualTo("Spring Rocks");
String text = newMessagePage.getHtmlElementById("text").getTextContent();
assertThat(text).isEqualTo("In case you didn't know, Spring Rocks!");
Kotlin
assertThat(newMessagePage.getUrl().toString()).endsWith("/messages/123")
val id = newMessagePage.getHtmlElementById("id").getTextContent()
assertThat(id).isEqualTo("123")
val summary = newMessagePage.getHtmlElementById("summary").getTextContent()
assertThat(summary).isEqualTo("Spring Rocks")
val text = newMessagePage.getHtmlElementById("text").getTextContent()
assertThat(text).isEqualTo("In case you didn't know, Spring Rocks!")

前面的代码以多种方式改进了我们的MockMvc 测试 . 首先,我们不再需要显式验证表单,然后创建类似于表单的请求. 相反,我们要求表单,将其填写并提交,从而大大减少了开销.

另一个重要因素是 HtmlUnit 使用 Mozilla Rhino 引擎 来评估 JavaScript. 这意味着我们还可以在页面内测试 JavaScript 的行为.

有关使用 HtmlUnit 的其他信息,请参见 HtmlUnit 文档.

高级 MockMvcWebClientBuilder

在到目前为止的示例中,我们已经通过 Spring TestContext Framework 为我们加载的 WebApplicationContext 构建了一个 WebClient,从而以最简单的方式使用了 MockMvcWebClientBuilder. 在以下示例中重复此方法:

Java
WebClient webClient;

@BeforeEach
void setup(WebApplicationContext context) {
    webClient = MockMvcWebClientBuilder
            .webAppContextSetup(context)
            .build();
}
Kotlin
lateinit var webClient: WebClient

@BeforeEach
fun setup(context: WebApplicationContext) {
    webClient = MockMvcWebClientBuilder
            .webAppContextSetup(context)
            .build()
}

我们还可以指定其他配置选项,如以下示例所示:

Java
WebClient webClient;

@BeforeEach
void setup() {
    webClient = MockMvcWebClientBuilder
        // demonstrates applying a MockMvcConfigurer (Spring Security)
        .webAppContextSetup(context, springSecurity())
        // for illustration only - defaults to ""
        .contextPath("")
        // By default MockMvc is used for localhost only;
        // the following will use MockMvc for example.com and example.org as well
        .useMockMvcForHosts("example.com","example.org")
        .build();
}
Kotlin
lateinit var webClient: WebClient

@BeforeEach
fun setup() {
    webClient = MockMvcWebClientBuilder
        // demonstrates applying a MockMvcConfigurer (Spring Security)
        .webAppContextSetup(context, springSecurity())
        // for illustration only - defaults to ""
        .contextPath("")
        // By default MockMvc is used for localhost only;
        // the following will use MockMvc for example.com and example.org as well
        .useMockMvcForHosts("example.com","example.org")
        .build()
}

或者,我们可以通过分别配置 MockMvc 实例并将其提供给 MockMvcWebClientBuilder 来执行完全相同的设置,如下所示:

Java
MockMvc mockMvc = MockMvcBuilders
        .webAppContextSetup(context)
        .apply(springSecurity())
        .build();

webClient = MockMvcWebClientBuilder
        .mockMvcSetup(mockMvc)
        // for illustration only - defaults to ""
        .contextPath("")
        // By default MockMvc is used for localhost only;
        // the following will use MockMvc for example.com and example.org as well
        .useMockMvcForHosts("example.com","example.org")
        .build();
Kotlin
// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed

这比较冗长,但是,通过使用 MockMvc 实例构建 WebClient,我们可以轻而易举地拥有 MockMvc 的全部功能.

有关创建 MockMvc 实例的其他信息,请参见设置选择..
MockMvc 和 WebDriver

在前面的部分中,我们已经了解了如何将 MockMvc 与原始 HtmlUnit API 结合使用. 在本节中,我们在 Selenium WebDriver 中使用其他抽象使事情变得更加容易.

为什么要使用 WebDriver 和 MockMvc?

我们已经可以使用 HtmlUnit 和 MockMvc,那么为什么要使用 WebDriver? Selenium WebDriver 提供了一个非常优雅的 API,使我们可以轻松地组织代码. 为了更好地说明其工作原理,我们在本节中探索一个示例.

尽管是 Selenium 的一部分,WebDriver 并不需要 Selenium Server来运行测试.

假设我们需要确保正确创建一条消息. 测试涉及找到HTML表单输入元素,将其填写并做出各种断言.

这种方法会导致大量单独的测试,因为我们也想测试错误情况. 例如,如果只填写表格的一部分,我们要确保得到一个错误. 如果我们填写整个表格,那么新创建的消息将在之后显示.

如果其中一个字段被命名为 “summary”,我们可能会在测试中的多个位置重复以下内容:

Java
HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary");
summaryInput.setValueAttribute(summary);
Kotlin
val summaryInput = currentPage.getHtmlElementById("summary")
summaryInput.setValueAttribute(summary)

那么,如果我们将 id 更改为 smmry,会发生什么? 这样做将迫使我们更新所有测试以纳入此更改. 这违反了 DRY 原理,因此理想情况下,我们应将此代码提取到其自己的方法中,如下所示:

Java
public HtmlPage createMessage(HtmlPage currentPage, String summary, String text) {
    setSummary(currentPage, summary);
    // ...
}

public void setSummary(HtmlPage currentPage, String summary) {
    HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary");
    summaryInput.setValueAttribute(summary);
}
Kotlin
fun createMessage(currentPage: HtmlPage, summary:String, text:String) :HtmlPage{
    setSummary(currentPage, summary);
    // ...
}

fun setSummary(currentPage:HtmlPage , summary: String) {
    val summaryInput = currentPage.getHtmlElementById("summary")
    summaryInput.setValueAttribute(summary)
}

这样做可以确保在更改 UI 时不必更新所有测试.

我们甚至可以更进一步,将此逻辑放在代表我们当前所在的 HtmlPageObject 中,如以下示例所示:

Java
public class CreateMessagePage {

    final HtmlPage currentPage;

    final HtmlTextInput summaryInput;

    final HtmlSubmitInput submit;

    public CreateMessagePage(HtmlPage currentPage) {
        this.currentPage = currentPage;
        this.summaryInput = currentPage.getHtmlElementById("summary");
        this.submit = currentPage.getHtmlElementById("submit");
    }

    public <T> T createMessage(String summary, String text) throws Exception {
        setSummary(summary);

        HtmlPage result = submit.click();
        boolean error = CreateMessagePage.at(result);

        return (T) (error ? new CreateMessagePage(result) : new ViewMessagePage(result));
    }

    public void setSummary(String summary) throws Exception {
        summaryInput.setValueAttribute(summary);
    }

    public static boolean at(HtmlPage page) {
        return "Create Message".equals(page.getTitleText());
    }
}
Kotlin
    class CreateMessagePage(private val currentPage: HtmlPage) {

        val summaryInput: HtmlTextInput = currentPage.getHtmlElementById("summary")

        val submit: HtmlSubmitInput = currentPage.getHtmlElementById("submit")

        fun <T> createMessage(summary: String, text: String): T {
            setSummary(summary)

            val result = submit.click()
            val error = at(result)

            return (if (error) CreateMessagePage(result) else ViewMessagePage(result)) as T
        }

        fun setSummary(summary: String) {
            summaryInput.setValueAttribute(summary)
        }

        fun at(page: HtmlPage): Boolean {
            return "Create Message" == page.getTitleText()
        }
    }
}

以前,此模式称为 页面对象模式. 虽然我们当然可以使用 HtmlUnit 做到这一点,但 WebDriver 提供了一些我们在以下各节中探讨的工具,以使该模式的实现更加容易.

MockMvc和WebDriver设置

要将 Selenium WebDriver 与 Spring MVC Test 框架一起使用,请确保您的项目包含对 org.seleniumhq.selenium:selenium-htmlunit-driver 的测试依赖.

我们可以使用 MockMvcHtmlUnitDriverBuilder 轻松创建一个与 MockMvc 集成的 Selenium WebDriver,如以下示例所示:

Java
WebDriver driver;

@BeforeEach
void setup(WebApplicationContext context) {
    driver = MockMvcHtmlUnitDriverBuilder
            .webAppContextSetup(context)
            .build();
}
Kotlin
lateinit var driver: WebDriver

@BeforeEach
fun setup(context: WebApplicationContext) {
    driver = MockMvcHtmlUnitDriverBuilder
            .webAppContextSetup(context)
            .build()
}
这是使用 MockMvcHtmlUnitDriverBuilder 的简单示例. 有关更多高级用法,请参见 高级 MockMvcHtmlUnitDriverBuilder.

前面的示例确保将引用 localhost 作为服务器的所有URL定向到我们的 MockMvc 实例,而无需真正的HTTP连接. 通常,通过使用网络连接来请求其他任何 URL. 这使我们可以轻松测试 CDN 的使用.

MockMvc 和 WebDriver 的用法

现在,我们可以像往常一样使用 WebDriver,而无需将应用程序部署到 Servlet 容器. 例如,我们可以请求视图创建以下消息:

Java
CreateMessagePage page = CreateMessagePage.to(driver);
Kotlin
val page = CreateMessagePage.to(driver)

然后,我们可以填写表格并提交以创建一条消息,如下所示:

Java
ViewMessagePage viewMessagePage =
        page.createMessage(ViewMessagePage.class, expectedSummary, expectedText);
Kotlin
val viewMessagePage =
    page.createMessage(ViewMessagePage::class, expectedSummary, expectedText)

通过利用 Page Object Pattern(页面对象模式),这可以改善我们的HtmlUnit 测试的设计. 正如我们在为什么要使用 WebDriver 和 MockMvc? 中提到的那样,我们可以将页面对象模式与 HtmlUnit 一起使用,但使用 WebDriver 则要容易得多. 考虑以下 CreateMessagePage 实现:

Java
public class CreateMessagePage
        extends AbstractPage { (1)

    (2)
    private WebElement summary;
    private WebElement text;

    (3)
    @FindBy(css = "input[type=submit]")
    private WebElement submit;

    public CreateMessagePage(WebDriver driver) {
        super(driver);
    }

    public <T> T createMessage(Class<T> resultPage, String summary, String details) {
        this.summary.sendKeys(summary);
        this.text.sendKeys(details);
        this.submit.click();
        return PageFactory.initElements(driver, resultPage);
    }

    public static CreateMessagePage to(WebDriver driver) {
        driver.get("http://localhost:9990/mail/messages/form");
        return PageFactory.initElements(driver, CreateMessagePage.class);
    }
}
1 CreateMessagePage 扩展 AbstractPage. 我们没有详细介绍 AbstractPage,但是总而言之,它包含了我们所有页面的通用功能. 例如,如果我们的应用程序具有导航栏,全局错误消息和其他功能,则可以将此逻辑放置在共享位置.
2 对于我们感兴趣的 HTML 页面的每个部分,我们都有一个成员变量. 这些是 WebElement 类型. WebDriver的 PageFactory 允许我们通过自动解析每个 WebElement 从 HtmlUnit 版本的 CreateMessagePage 中删除很多代码. PageFactory#initElements(WebDriver,Class<T>)方法通过使用字段名称并通过 HTML 页面中元素的 idname 查找来自动解析每个 WebElement.
3 我们可以使用 @FindBy annotation注解覆盖默认的查找行为. 我们的示例演示了如何使用 @FindBy 注解通过CSS选择器 (input[type=submit]) 查找提交按钮.
Kotlin
class CreateMessagePage(private val driver: WebDriver) : AbstractPage(driver) { (1)

    (2)
    private lateinit var summary: WebElement
    private lateinit var text: WebElement

    (3)
    @FindBy(css = "input[type=submit]")
    private lateinit var submit: WebElement

    fun <T> createMessage(resultPage: Class<T>, summary: String, details: String): T {
        this.summary.sendKeys(summary)
        text.sendKeys(details)
        submit.click()
        return PageFactory.initElements(driver, resultPage)
    }
    companion object {
        fun to(driver: WebDriver): CreateMessagePage {
            driver.get("http://localhost:9990/mail/messages/form")
            return PageFactory.initElements(driver, CreateMessagePage::class.java)
        }
    }
}
1 CreateMessagePage 扩展 AbstractPage. 我们没有详细介绍 AbstractPage,但是总而言之,它包含了我们所有页面的通用功能. 例如,如果我们的应用程序具有导航栏,全局错误消息和其他功能,则可以将此逻辑放置在共享位置.
2 对于我们感兴趣的 HTML 页面的每个部分,我们都有一个成员变量. 这些是 WebElement 类型. WebDriver的 PageFactory 允许我们通过自动解析每个 WebElement 从 HtmlUnit 版本的 CreateMessagePage 中删除很多代码. PageFactory#initElements(WebDriver,Class<T>)方法通过使用字段名称并通过HTML页面中元素的 idname 查找来自动解析每个 WebElement.
3 我们可以使用 @FindBy annotation注解覆盖默认的查找行为. 我们的示例演示了如何使用 @FindBy 注解通过CSS选择器 (input[type=submit]) 查找提交按钮.

最后,我们可以验证是否成功创建了新消息. 以下断言使用 AssertJ 断言库:

Java
assertThat(viewMessagePage.getMessage()).isEqualTo(expectedMessage);
assertThat(viewMessagePage.getSuccess()).isEqualTo("Successfully created a new message");
Kotlin
assertThat(viewMessagePage.message).isEqualTo(expectedMessage)
assertThat(viewMessagePage.success).isEqualTo("Successfully created a new message")

我们可以看到 ViewMessagePage 允许我们与自定义 domain 模型进行交互. 例如,它暴露了一个返回 Message 对象的方法:

Java
public Message getMessage() throws ParseException {
    Message message = new Message();
    message.setId(getId());
    message.setCreated(getCreated());
    message.setSummary(getSummary());
    message.setText(getText());
    return message;
}
Kotlin
fun getMessage() = Message(getId(), getCreated(), getSummary(), getText())

然后,我们可以在声明中使用富域对象.

最后,我们一定不要忘记在测试完成后关闭 WebDriver 实例,如下所示:

Java
@AfterEach
void destroy() {
    if (driver != null) {
        driver.close();
    }
}
Kotlin
@AfterEach
fun destroy() {
    if (driver != null) {
        driver.close()
    }
}

有关使用 WebDriver 的其他信息,请参阅 Selenium WebDriver documentation..

高级 MockMvcHtmlUnitDriverBuilder

在到目前为止的示例中,我们通过基于 Spring TestContext Framework 为我们加载的 WebApplicationContext 构建 WebDriver,以最简单的方式使用了 MockMvcHtmlUnitDriverBuilder. 在此重复此方法,如下所示:

Java
WebDriver driver;

@BeforeEach
void setup(WebApplicationContext context) {
    driver = MockMvcHtmlUnitDriverBuilder
            .webAppContextSetup(context)
            .build();
}
Kotlin
lateinit var driver: WebDriver

@BeforeEach
fun setup(context: WebApplicationContext) {
    driver = MockMvcHtmlUnitDriverBuilder
            .webAppContextSetup(context)
            .build()
}

我们还可以指定其他配置选项,如下所示:

Java
WebDriver driver;

@BeforeEach
void setup() {
    driver = MockMvcHtmlUnitDriverBuilder
            // demonstrates applying a MockMvcConfigurer (Spring Security)
            .webAppContextSetup(context, springSecurity())
            // for illustration only - defaults to ""
            .contextPath("")
            // By default MockMvc is used for localhost only;
            // the following will use MockMvc for example.com and example.org as well
            .useMockMvcForHosts("example.com","example.org")
            .build();
}
Kotlin
lateinit var driver: WebDriver

@BeforeEach
fun setup() {
    driver = MockMvcHtmlUnitDriverBuilder
            // demonstrates applying a MockMvcConfigurer (Spring Security)
            .webAppContextSetup(context, springSecurity())
            // for illustration only - defaults to ""
            .contextPath("")
            // By default MockMvc is used for localhost only;
            // the following will use MockMvc for example.com and example.org as well
            .useMockMvcForHosts("example.com","example.org")
            .build()
}

或者,我们可以通过分别配置 MockMvc 实例并将其提供给 MockMvcHtmlUnitDriverBuilder 来执行完全相同的设置,如下所示:

Java
MockMvc mockMvc = MockMvcBuilders
        .webAppContextSetup(context)
        .apply(springSecurity())
        .build();

driver = MockMvcHtmlUnitDriverBuilder
        .mockMvcSetup(mockMvc)
        // for illustration only - defaults to ""
        .contextPath("")
        // By default MockMvc is used for localhost only;
        // the following will use MockMvc for example.com and example.org as well
        .useMockMvcForHosts("example.com","example.org")
        .build();
Kotlin
// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed

这比较冗长,但是,通过使用 MockMvc 实例构建 WebDriver,我们可以轻而易举地拥有 MockMvc 的全部功能.

有关创建 MockMvc 实例的其他信息,请参见设置选择.
MockMvc 和 Geb

在上一节中,我们了解了如何在 WebDriver 中使用 MockMvc. 在本节中,我们将使用 Geb 进行甚至 Groovy-er 的测试.

为什么选择Geb和MockMvc?

Geb 由 WebDriver 支持,因此它提供了许多与 WebDriver 相同的好处 . 但是,Geb 通过为我们处理一些样板代码使事情变得更加轻松.

MockMvc和Geb设置

我们可以轻松地使用使用 MockMvc 的 Selenium WebDriver 初始化 Geb 浏览器,如下所示:

def setup() {
    browser.driver = MockMvcHtmlUnitDriverBuilder
        .webAppContextSetup(context)
        .build()
}
这是使用 MockMvcHtmlUnitDriverBuilder 的简单示例. 有关更多高级用法,请参见 高级 MockMvcHtmlUnitDriverBuilder.

这样可以确保在服务器上引用 localhost 主机的所有URL都定向到我们的 MockMvc 实例,而无需真正的 HTTP 连接. 通常,通过使用网络连接来请求其他任何 URL. 这使我们可以轻松测试 CDN 的使用.

MockMvc和Geb用法

现在,我们可以像往常一样使用 Geb 了,而无需将应用程序部署到 Servlet 容器中. 例如,我们可以请求视图创建以下消息:

to CreateMessagePage

然后,我们可以填写表格并提交以创建一条消息,如下所示:

when:
form.summary = expectedSummary
form.text = expectedMessage
submit.click(ViewMessagePage)

找不到的所有无法识别的方法调用或属性访问或引用都将转发到当前页面对象. 这消除了我们直接使用 WebDriver 时需要的许多样板代码.

与直接使用 WebDriver 一样,这可以通过使用 Page Object Pattern 改进 HtmlUnit 测试 的设计. 如前所述,我们可以将页面对象模式与 HtmlUnit 和 WebDriver 一起使用,但使用Geb则更加容易. 考虑我们新的基于 Groovy 的 CreateMessagePage 实现:

class CreateMessagePage extends Page {
    static url = 'messages/form'
    static at = { assert title == 'Messages : Create'; true }
    static content =  {
        submit { $('input[type=submit]') }
        form { $('form') }
        errors(required:false) { $('label.error, .alert-error')?.text() }
    }
}

我们的 CreateMessagePage 扩展了 Page. 我们不会详细介绍 Page,但是总而言之,它包含所有页面的通用功能. 我们定义一个可在其中找到此页面的 URL. 这使我们可以导航到页面,如下所示:

to CreateMessagePage

我们还有一个 at 闭包,它确定我们是否在指定页面上. 如果我们在正确的页面上,它应该返回 true. 这就是为什么我们可以断言我们在正确的页面上的原因,如下所示:

then:
at CreateMessagePage
errors.contains('This field is required.')
我们在闭包中使用一个断言,以便我们可以确定在错误的页面上哪里出错了.

接下来,我们创建一个 content 闭包,以指定页面中所有感兴趣的区域. 我们可以使用 jQuery-ish Navigator API 来选择我们感兴趣的内容.

最后,我们可以验证是否成功创建了新消息,如下所示:

then:
at ViewMessagePage
success == 'Successfully created a new message'
id
date
summary == expectedSummary
message == expectedMessage

有关如何充分利用 Geb 的更多详细信息,请参见 The Book of Geb 用户手册.

3.7.3. 测试客户端应用程序

您可以使用客户端测试来测试内部使用 RestTemplate 的代码. 这个想法是声明预期的请求并提供 “stub” 响应,以便您可以专注于隔离测试代码 (即,不运行服务器) . 以下示例显示了如何执行此操作:

Java
RestTemplate restTemplate = new RestTemplate();

MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build();
mockServer.expect(requestTo("/greeting")).andRespond(withSuccess());

// Test code that uses the above RestTemplate ...

mockServer.verify();
Kotlin
val restTemplate = RestTemplate()

val mockServer = MockRestServiceServer.bindTo(restTemplate).build()
mockServer.expect(requestTo("/greeting")).andRespond(withSuccess())

// Test code that uses the above RestTemplate ...

mockServer.verify()

在前面的示例中,MockRestServiceServer (客户端REST测试的中心类) 使用自定义的 ClientHttpRequestFactory 配置 RestTemplate,该 ClientHttpRequestFactory 根据期望断言实际的请求并返回 “stub” 响应. 在这种情况下,我们希望有一个请求 /greeting ,并希望返回一个包含 text/plain 的 200 响应. 我们可以根据需要定义其他预期的请求和存根响应. 当我们定义期望的请求和存根响应时,RestTemplate 可以照常在客户端代码中使用. 在测试结束时,可以使用 mockServer.verify() 来验证是否满足所有期望.

默认情况下,请求应按声明的期望顺序进行. 您可以在构建服务器时设置 ignoreExpectOrder 选项,在这种情况下,将检查所有期望值 (以便) 以找到给定请求的匹配项. 这意味着允许请求以任何顺序出现. 以下示例使用 ignoreExpectOrder:

Java
server = MockRestServiceServer.bindTo(restTemplate).ignoreExpectOrder(true).build();
Kotlin
server = MockRestServiceServer.bindTo(restTemplate).ignoreExpectOrder(true).build()

即使默认情况下无顺序请求,每个请求也只能执行一次. expect 方法提供了一个重载的变量,该变量接受一个 ExpectedCount 参数,该参数指定一个计数范围 (例如,once, manyTimes, max, min,between 等等) . 以下示例使用 times:

Java
RestTemplate restTemplate = new RestTemplate();

MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build();
mockServer.expect(times(2), requestTo("/something")).andRespond(withSuccess());
mockServer.expect(times(3), requestTo("/somewhere")).andRespond(withSuccess());

// ...

mockServer.verify();
Kotlin
val restTemplate = RestTemplate()

val mockServer = MockRestServiceServer.bindTo(restTemplate).build()
mockServer.expect(times(2), requestTo("/something")).andRespond(withSuccess())
mockServer.expect(times(3), requestTo("/somewhere")).andRespond(withSuccess())

// ...

mockServer.verify()

请注意,如果未设置 ignoreExpectOrder (默认设置) ,并且因此要求按声明顺序进行请求,则该顺序仅适用于任何预期请求中的第一个. 例如,如果期望 "/something" 两次,然后是 "/somewhere" 三次,那么在请求 "/somewhere" 之前应该先请求 "/something" , 但是除了随后的 "/something" 和 "/somewhere" ,请求可以随时发出.

作为上述所有方法的替代,客户端测试支持还提供了 ClientHttpRequestFactory 实现,您可以将其配置为 RestTemplate 以将其绑定到 MockMvc 实例. 这样就可以使用实际的服务器端逻辑来处理请求,而无需运行服务器. 以下示例显示了如何执行此操作:

Java
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
this.restTemplate = new RestTemplate(new MockMvcClientHttpRequestFactory(mockMvc));

// Test code that uses the above RestTemplate ...
Kotlin
val mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build()
restTemplate = RestTemplate(MockMvcClientHttpRequestFactory(mockMvc))

// Test code that uses the above RestTemplate ...

3.7.4. 静态导入

与服务器端测试一样,用于客户端测试的流式 API 需要进行一些静态导入. 通过搜索 MockRest* 可以轻松找到这些内容. Eclipse 用户应在 Java → Editor → Content Assist → Favorites 下的 Eclipse 首选项中,将 MockRestRequestMatchers.*MockRestResponseCreators.* 添加为 “favorite static members” . 这样可以在输入静态方法名称的第一个字符后使用内容辅助. 其他IDE (例如IntelliJ) 可能不需要任何其他配置. 检查是否支持静态成员上的代码完成.

3.7.5. 客户端 REST 测试的更多示例

Spring MVC Test 自己的测试包括客户端 REST 测试的 示例测试 .

4. 更多资源

有关测试的更多信息,请参见以下资源:

  • JUnit: "一个程序员友好的 Java 测试框架". 由 Spring Framework 在其测试套件中使用,并在Spring TestContext Framework中得到支持.

  • TestNG: 受 JUnit 启发的测试框架,它对测试组,数据驱动的测试,分布式测试和其他功能提供了额外的支持. 在Spring TestContext Framework中受支持

  • AssertJ: "Java 的有效断言",包括对 Java 8 lambda,流和其他功能的支持.

  • Mock Objects: 维基百科中的文章.

  • MockObjects.com: 专门用于模拟对象的网站,一种用于在测试驱动的开发中改进代码设计的技术.

  • Mockito: 基于 Test Spy 模式的 Java 模拟库. 由 Spring Framework 在其测试套件中使用.

  • EasyMock: Java 库"通过使用 Java 的代理机制动态生成接口,为接口 (以及通过类扩展的对象) 提供 Mock 对象. "

  • JMock: 该库支持使用模拟对象进行 Java 代码的测试驱动开发.

  • DbUnit: JUnit 扩展 (也可与 Ant 和 Maven 一起使用) ,针对数据库驱动的项目,除其他外,它使数据库在测试运行之间进入已知状态.

  • The Grinder: Java 负载测试框架

  • SpringMockK: 支持使用 MockK而不是 Mockito 用 Kotlin 编写的 Spring Boot 集成测试.