测试

1. Spring测试介绍

测试是企业软件开发不可或缺的一部分。本章重点讨论IoC原则为单元测试增加的价值,以及Spring框架对集成测试支持的优势。(对企业级测试的全面探讨超出了本参考手册的范围。)spring-doc.cadn.net.cn

2. 单元测试

依赖注入应使您的代码比传统Java EE开发更少依赖于容器。组成应用程序的POJO(普通Java对象)应能在JUnit或TestNG测试中进行测试,通过使用new操作符实例化对象,无需Spring或其他容器。您可以运用模拟对象(结合其他有价值的测试技术)对代码进行隔离测试。若遵循Spring的架构建议,代码库最终形成的清晰层次结构和组件化将有利于简化单元测试。例如,您可以通过桩模块或模拟DAO/仓储接口来测试服务层对象,在运行单元测试时无需访问持久化数据。spring-doc.cadn.net.cn

真正的单元测试通常运行极其迅速,因为无需启动 运行时基础设施。将真正的单元测试作为开发方法论的一部分,可以显著提升 生产效率。您可能无需借助测试章节的本部分内容,即可为基于IoC的应用程序 编写有效的单元测试。然而,对于特定的单元测试场景, Spring框架提供了本章节描述的模拟对象和测试支持类。spring-doc.cadn.net.cn

2.1. 模拟对象

Spring 包含多个专门用于模拟的包:spring-doc.cadn.net.cn

2.1.1. 环境

org.springframework.mock.env包包含了EnvironmentPropertySource抽象的模拟实现(请参阅 Bean定义配置文件PropertySource抽象)。 MockEnvironmentMockPropertySource对于开发依赖于环境特定属性的代码的容器外测试非常有用。spring-doc.cadn.net.cn

2.1.2. JNDI

org.springframework.mock.jndi 包包含 JNDI SPI 的部分实现,您可以使用它来为测试套件或独立应用程序设置简单的 JNDI 环境。例如,如果在测试代码中 JDBC DataSource 实例绑定的 JNDI 名称与 Java EE 容器中的相同,您就可以在测试场景中无需修改地复用应用程序代码和配置。spring-doc.cadn.net.cn

自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-doc.cadn.net.cn

自Spring Framework 5.0起,org.springframework.mock.web中的模拟对象基于Servlet 4.0 API。

Spring MVC 测试框架基于模拟的 Servlet API 对象,为 Spring MVC 提供了一个集成测试框架。请参见 MockMvcspring-doc.cadn.net.cn

2.1.4. Spring Web响应式

org.springframework.mock.http.server.reactive 包包含用于 WebFlux 应用的 ServerHttpRequestServerHttpResponse 的模拟实现。 org.springframework.mock.web.server 包包含依赖于这些模拟请求和响应对象的模拟 ServerWebExchangespring-doc.cadn.net.cn

MockServerHttpRequestMockServerHttpResponse 都从相同的抽象 基类扩展而来,就像特定于服务器的实现一样,并且与它们共享行为。例如, 模拟请求一旦创建就是不可变的,但你可以使用 ServerHttpRequest 中的 mutate() 方法 来创建一个修改后的实例。spring-doc.cadn.net.cn

为使模拟响应正确实现写入约定并返回写入完成句柄(即Mono<Void>),其默认使用Fluxcache().then()组合,该组合会缓冲数据使其可用于测试断言。 应用程序可设置自定义写入函数(例如用于测试无限流场景)。spring-doc.cadn.net.cn

WebTestClient基于模拟请求和响应构建,为测试WebFlux应用程序提供了无需HTTP服务器的支持。该客户端也可用于在运行服务器上进行端到端测试。spring-doc.cadn.net.cn

2.2. 单元测试支持类

Spring 包含了许多有助于单元测试的类。它们主要分为两大类:spring-doc.cadn.net.cn

2.2.1. 通用测试工具

org.springframework.test.util 包包含一些用于单元测试和集成测试的通用工具。spring-doc.cadn.net.cn

AopTestUtils 是AOP相关实用方法的集合。您可以使用这些方法获取隐藏在Spring代理背后的底层目标对象引用。例如,如果您通过EasyMock或Mockito等库将bean配置为动态模拟,且该模拟对象被Spring代理包装时,可能需要直接访问底层模拟对象以配置期望行为并执行验证。关于Spring核心AOP实用工具,请参阅AopUtilsAopProxyUtilsspring-doc.cadn.net.cn

ReflectionTestUtils 是一组基于反射的实用方法。您可以在测试场景中使用这些方法,例如需要更改常量的值、设置非public字段、调用非public的setter方法,或在测试应用程序代码时调用非public的配置或生命周期回调方法,适用于以下用例:spring-doc.cadn.net.cn

  • ORM框架(如JPA和Hibernate)允许在领域实体属性中使用privateprotected级字段访问,而非public级setter方法。spring-doc.cadn.net.cn

  • Spring对注解的支持(例如@Autowired@Inject@Resource), 可为privateprotected字段、setter方法和配置方法提供依赖注入。spring-doc.cadn.net.cn

  • 使用诸如 @PostConstruct@PreDestroy 之类的注解作为生命周期回调方法。spring-doc.cadn.net.cn

TestSocketUtils 是一个简单的工具,用于在 localhost 上查找可用的 TCP 端口,以便在集成测试场景中使用。spring-doc.cadn.net.cn

TestSocketUtils 可以在集成测试中使用,这些测试会在一个可用的随机端口上启动外部服务器。然而,这些工具无法保证后续某个端口的可用性,因此不可靠。与其使用 TestSocketUtils 来查找服务器的可用本地端口,建议您依赖服务器自身选择或由操作系统分配的随机临时端口的能力。要与该服务器进行交互,您应该查询服务器当前使用的端口。spring-doc.cadn.net.cn

2.2.2. Spring MVC测试工具

org.springframework.test.web 包包含 ModelAndViewAssert,您可将其 与 JUnit、TestNG 或其他任何测试框架结合使用,用于处理 Spring MVC ModelAndView 对象的 单元测试。spring-doc.cadn.net.cn

单元测试 Spring MVC 控制器
对Spring MVC进行单元测试Controller将类作为POJO,使用ModelAndViewAssert与……结合MockHttpServletRequest, MockHttpSession,等等由Spring提供的Servlet API模拟要对您的 Spring MVC 和 REST 进行全面的集成测试Controller类配合您的WebApplicationContext用于Spring MVC的配置,使用Spring MVC测试框架 instead.

3. 集成测试

本节(本章大部分剩余内容)涵盖 Spring 应用程序的集成测试。它包括以下主题:spring-doc.cadn.net.cn

3.1. 概述

能够在无需部署到应用服务器或连接其他企业基础设施的情况下进行集成测试,这一点很重要。 这样,您可以测试以下内容:spring-doc.cadn.net.cn

Spring框架在spring-test模块中为集成测试提供了一流的支持。实际JAR文件的名称可能包含发行版本号,也可能采用长org.springframework.test名称形式,具体取决于获取来源(解释说明请参见依赖管理章节)。该库包含org.springframework.test包,其中提供了用于Spring容器集成测试的有价值的类。此类测试不依赖应用服务器或其他部署环境,其运行速度虽慢于单元测试,但远快于需要部署到应用服务器的等效Selenium测试或远程测试。spring-doc.cadn.net.cn

单元测试和集成测试支持以注解驱动的 Spring TestContext Framework形式提供。该测试上下文框架与实际使用的测试框架无关,允许在各种环境(包括JUnit、TestNG等)中进行测试工具化。spring-doc.cadn.net.cn

3.2. 集成测试的目标

Spring的集成测试支持具有以下主要目标:spring-doc.cadn.net.cn

接下来的几个章节将分别阐述每个目标, 并提供相关实现与配置详情的链接。spring-doc.cadn.net.cn

3.2.1. 上下文管理和缓存

Spring测试上下文框架提供了Spring ApplicationContext实例和WebApplicationContext实例的一致性加载功能,并支持这些上下文的缓存。对已加载上下文进行缓存的支持至关重要,因为启动时间可能成为问题——问题不在于Spring本身的开销,而是Spring容器实例化的对象需要时间进行初始化。例如,一个包含50至100个Hibernate映射文件的项目可能需要10到20秒来加载这些映射文件。若在每个测试套件的每个测试执行前都承担此开销,将导致整体测试运行速度变慢,从而降低开发人员生产力。spring-doc.cadn.net.cn

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

默认情况下,一旦加载完成,配置好的ApplicationContext将在每个测试中复用。 因此每个测试套件仅产生一次设置成本,后续测试执行速度会显著提升。此处的"测试套件"指在同一个JVM中运行的所有测试——例如通过Ant、Maven或Gradle为特定项目或模块运行的全部测试。若测试意外破坏应用程序上下文并需重新加载(如修改Bean定义或应用程序对象状态),可配置TestContext框架在执行下个测试前重新加载配置并重建应用程序上下文。spring-doc.cadn.net.cn

请参阅使用TestContext框架的上下文管理上下文缓存spring-doc.cadn.net.cn

3.2.2. 测试装置的依赖注入

当TestContext框架加载您的应用程序上下文时,它可以选择性地 通过依赖注入配置测试类的实例。这为使用 预配置的应用程序上下文中的Bean来设置测试固件提供了便捷机制。 一个显著优势在于您可以在不同测试场景中复用应用程序上下文 (例如用于配置Spring管理的对象图、 事务代理、DataSource实例等),从而避免 在单个测试用例中重复复杂的测试固件设置。spring-doc.cadn.net.cn

例如,考虑这样一个场景:我们有一个实现 Title 领域实体数据访问逻辑的类(HibernateTitleRepository)。我们希望编写集成测试来验证以下方面:spring-doc.cadn.net.cn

  • Spring配置:基本上,所有与HibernateTitleRepository bean相关的配置是否正确且完整存在?spring-doc.cadn.net.cn

  • Hibernate映射文件配置:所有内容是否已正确映射,且延迟加载设置是否配置正确?spring-doc.cadn.net.cn

  • HibernateTitleRepository的逻辑:此类的配置实例行为是否如预期?spring-doc.cadn.net.cn

请参阅通过 TestContext框架 实现的测试固件依赖注入。spring-doc.cadn.net.cn

3.2.3. 事务管理

在访问真实数据库的测试中,一个常见问题是它们对持久化存储状态的影响。即使使用开发数据库,状态变更也可能影响后续测试。此外,许多操作——例如插入或修改持久化数据——无法在事务之外执行(或验证)。spring-doc.cadn.net.cn

TestContext框架解决了这个问题。默认情况下,该框架会为每个测试创建并回滚事务。您可以编写假定事务存在的代码。如果在测试中调用事务代理对象,它们会根据配置的事务语义正确运行。此外,如果测试方法在测试管理的事务执行期间删除选定表的内容,事务将默认回滚,数据库会恢复到测试执行前的状态。事务支持通过测试应用上下文中定义的PlatformTransactionManager bean提供给测试。spring-doc.cadn.net.cn

如果您希望事务提交(这种情况较为罕见,但有时在需要特定测试填充或修改数据库时很有用),可以通过使用@Commit注解,指示TestContext框架将事务设置为提交而非回滚。spring-doc.cadn.net.cn

了解使用 TestContext框架 的事务管理。spring-doc.cadn.net.cn

3.2.4. 集成测试支持类

Spring TestContext 框架提供了若干 abstract 支持类,可简化集成测试的编写。这些基础测试类提供了定义完善的钩子(hooks)以及便捷的实例变量和方法,以便访问:spring-doc.cadn.net.cn

  • 用于执行显式bean查找或测试整个上下文的状态的ApplicationContextspring-doc.cadn.net.cn

  • 一个 JdbcTemplate,用于执行SQL语句查询数据库。您可以使用此类查询来确认执行数据库相关应用代码前后的数据库状态,Spring确保此类查询在与应用代码相同的事务范围内运行。与ORM工具结合使用时,请注意避免误报spring-doc.cadn.net.cn

此外,您可能希望创建一个自定义的、整个应用程序范围内的超类,其中包含特定于您项目的实例变量和方法。spring-doc.cadn.net.cn

3.3. JDBC测试支持

org.springframework.test.jdbc 包包含 JdbcTestUtils,这是一个用于简化标准数据库测试场景的JDBC相关实用函数集合。具体而言,JdbcTestUtils 提供以下静态实用方法。spring-doc.cadn.net.cn

AbstractTransactionalJUnit4SpringContextTestsAbstractTransactionalTestNGSpringContextTests 提供了便捷方法,这些方法委托给 JdbcTestUtils 中的上述方法。spring-doc.cadn.net.cn

spring-jdbc模块提供了配置和启动嵌入式数据库的支持,您可以在与数据库交互的集成测试中使用它。有关详细信息,请参见嵌入式数据库支持使用嵌入式数据库测试数据访问逻辑spring-doc.cadn.net.cn

3.4. 注解

本节介绍在测试 Spring 应用程序时可使用的注解。 包含以下主题:spring-doc.cadn.net.cn

3.4.1. Spring 测试注解

Spring框架提供以下一组Spring专属注解,您可以在单元测试和集成测试中结合TestContext框架使用这些注解。 有关详细信息(包括默认属性值、属性别名及其他细节),请参阅相应的javadoc文档。spring-doc.cadn.net.cn

Spring的测试注解包括以下内容:spring-doc.cadn.net.cn

@BootstrapWith

@BootstrapWith是一个类级别的注解,您可以用它来配置Spring TestContext Framework的引导方式。具体来说,您可以使用@BootstrapWith来指定一个自定义的TestContextBootstrapper。有关更多详细信息,请参阅引导TestContext框架一节。spring-doc.cadn.net.cn

@ContextConfiguration

@ContextConfiguration 定义了用于确定如何为集成测试加载和配置 ApplicationContext 的类级别元数据。具体而言,@ContextConfiguration 声明了用于加载上下文的应用程序上下文资源 locations 或组件类 classesspring-doc.cadn.net.cn

资源位置通常是位于类路径中的XML配置文件或Groovy脚本,而组件类通常是@Configuration类。然而, 资源位置也可以引用文件系统中的文件和脚本,并且组件类可以是@Component类、 @Service类等等。更多详细信息请参阅 组件类spring-doc.cadn.net.cn

以下示例展示了一个引用XML文件的@ContextConfiguration注解:spring-doc.cadn.net.cn

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注解,它引用了一个类:spring-doc.cadn.net.cn

Java
@ContextConfiguration(classes = TestConfig.class) (1)
class ConfigClassApplicationContextTests {
    // class body...
}
1 引用某个类。
Kotlin
@ContextConfiguration(classes = [TestConfig::class]) (1)
class ConfigClassApplicationContextTests {
    // class body...
}
1 引用某个类。

作为声明资源位置或组件类的替代或补充方案,您可以使用 @ContextConfiguration 来声明 ApplicationContextInitializer 类。以下示例展示了这种情况:spring-doc.cadn.net.cn

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

您也可以选择使用@ContextConfiguration来声明ContextLoader策略。但请注意,通常不需要显式配置加载器,因为默认加载器支持initializers以及资源locations或组件classesspring-doc.cadn.net.cn

以下示例同时使用了位置和加载器:spring-doc.cadn.net.cn

Java
@ContextConfiguration(locations = "/test-context.xml", loader = CustomContextLoader.class) (1)
class CustomLoaderXmlApplicationContextTests {
    // class body...
}
1 配置位置和自定义加载器。
Kotlin
@ContextConfiguration("/test-context.xml", loader = CustomContextLoader::class) (1)
class CustomLoaderXmlApplicationContextTests {
    // class body...
}
1 配置位置和自定义加载器。
@ContextConfiguration 支持继承资源位置或配置类,以及由超类或封闭类声明的上下文初始化器。

查看 上下文管理@Nested 测试类配置,以及 @ContextConfiguration javadocs 以获取更多详细信息。spring-doc.cadn.net.cn

@WebAppConfiguration

@WebAppConfiguration 是一个类级别的注解,可用于声明为集成测试加载的 ApplicationContext 应为 WebApplicationContext。在测试类上仅存在 @WebAppConfiguration 即可确保 为测试加载 WebApplicationContext,并使用 "file:src/main/webapp" 的默认值作为 Web 应用程序根目录的路径(即资源基础路径)。 该资源基础路径在后台用于创建 MockServletContext,该容器将作为测试 WebApplicationContextServletContextspring-doc.cadn.net.cn

以下示例展示如何使用 @WebAppConfiguration 注解:spring-doc.cadn.net.cn

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

要覆盖默认设置,您可以使用隐式 value 属性指定不同的基础资源路径。同时支持 classpath:file: 资源前缀。 若未提供资源前缀,则该路径将被视为文件系统资源。以下示例展示了如何指定类路径资源:spring-doc.cadn.net.cn

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 的Javadoc文档。spring-doc.cadn.net.cn

@ContextHierarchy

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

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

如果您需要在测试类层次结构中合并或覆盖上下文层次结构的给定级别的配置,您必须通过在类层次结构中的每个相应级别为@ContextConfiguration中的name属性提供相同的值来显式命名该级别。有关更多示例,请参见上下文层次结构@ContextHierarchy javadoc。spring-doc.cadn.net.cn

@ActiveProfiles

@ActiveProfiles 是一个类级注解,用于声明在加载集成测试的 ApplicationContext 时应激活哪些Bean定义配置文件。spring-doc.cadn.net.cn

以下示例表明dev配置文件应处于激活状态:spring-doc.cadn.net.cn

Java
@ContextConfiguration
@ActiveProfiles("dev") (1)
class DeveloperTests {
    // class body...
}
1 表示dev配置文件应该处于激活状态。
Kotlin
@ContextConfiguration
@ActiveProfiles("dev") (1)
class DeveloperTests {
    // class body...
}
1 表示dev配置文件应该处于激活状态。

以下示例表明 devintegration 配置环境都应处于激活状态:spring-doc.cadn.net.cn

Java
@ContextConfiguration
@ActiveProfiles({"dev", "integration"}) (1)
class DeveloperIntegrationTests {
    // class body...
}
1 表示 devintegration 配置文件应处于激活状态。
Kotlin
@ContextConfiguration
@ActiveProfiles(["dev", "integration"]) (1)
class DeveloperIntegrationTests {
    // class body...
}
1 表示 devintegration 配置文件应处于激活状态。
@ActiveProfiles 默认支持继承由超类和封闭类声明的活动 bean 定义配置文件。您还可以通过实现自定义 ActiveProfilesResolver 并通过使用 resolver 属性注册它来以编程方式解决活动 bean 定义配置文件。

查看 使用环境配置的上下文配置@Nested 测试类配置,以及 @ActiveProfiles 的 javadoc 以获取 示例和更多详细信息。spring-doc.cadn.net.cn

@TestPropertySource

@TestPropertySource 是一个类级别注解,你可以使用它来配置属性文件的位置以及内联属性,这些属性将被添加到为集成测试加载的 ApplicationContextEnvironment 中的 PropertySources 集合中。spring-doc.cadn.net.cn

以下示例展示了如何从类路径声明属性文件:spring-doc.cadn.net.cn

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获取属性。

以下示例演示如何声明内联属性:spring-doc.cadn.net.cn

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 是一个方法级别的注解,可用于向集成测试加载的 ApplicationContextEnvironment 中的 PropertySources 集合注册动态属性。当您无法提前确定属性值时(例如,属性由外部资源管理,例如由 Testcontainers 项目管理的容器),动态属性非常有用。spring-doc.cadn.net.cn

以下示例演示如何注册动态属性:spring-doc.cadn.net.cn

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 ApplicationContext 在测试执行期间已被污染(即测试以某种方式修改或破坏了它——例如,通过更改单例 bean 的状态),因此应被关闭。当应用上下文被标记为污染状态时,它将被从测试框架的缓存中移除并关闭。因此,对于任何需要相同配置元数据的后续测试,底层的 Spring 容器将被重建。spring-doc.cadn.net.cn

您可以在同一个类或类层级中,将@DirtiesContext同时用作类级别和方法级别的注解。在此类场景中,ApplicationContext会在任何此类带注解方法之前或之后被标记为dirty,并在当前测试类之前或之后被标记,具体取决于配置的methodModeclassModespring-doc.cadn.net.cn

以下示例解释了针对不同配置场景,何时会将上下文标记为脏:spring-doc.cadn.net.cn

  • 在当前测试类运行之前,当在类上声明且类模式设置为 BEFORE_CLASS时。spring-doc.cadn.net.cn

    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(即默认类模式)的类上声明时。spring-doc.cadn.net.cn

    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. 的类上时spring-doc.cadn.net.cn

    Java
    @DirtiesContext(classMode = BEFORE_EACH_TEST_METHOD) (1)
    class FreshContextTests {
        // some tests that require a new Spring container
    }
    1 在每个测试方法之前污染上下文。
    Kotlin
    @DirtiesContext(classMode = BEFORE_EACH_TEST_METHOD) (1)
    class FreshContextTests {
        // some tests that require a new Spring container
    }
    1 在每个测试方法之前污染上下文。
  • 在当前测试类中的每个测试方法之后,当声明在类上且类模式设置为AFTER_EACH_TEST_METHOD.spring-doc.cadn.net.cn

    Java
    @DirtiesContext(classMode = AFTER_EACH_TEST_METHOD) (1)
    class ContextDirtyingTests {
        // some tests that result in the Spring container being dirtied
    }
    1 在每个测试方法之后污染上下文。
    Kotlin
    @DirtiesContext(classMode = AFTER_EACH_TEST_METHOD) (1)
    class ContextDirtyingTests {
        // some tests that result in the Spring container being dirtied
    }
    1 在每个测试方法之后污染上下文。
  • 在当前测试之前,当在方法模式设置为BEFORE_METHOD的方法上声明时。spring-doc.cadn.net.cn

    Java
    @DirtiesContext(methodMode = BEFORE_METHOD) (1)
    @Test
    void testProcessWhichRequiresFreshAppCtx() {
        // some logic that requires a new Spring container
    }
    1 在当前测试方法之前污染上下文。
    Kotlin
    @DirtiesContext(methodMode = BEFORE_METHOD) (1)
    @Test
    fun testProcessWhichRequiresFreshAppCtx() {
        // some logic that requires a new Spring container
    }
    1 在当前测试方法之前污染上下文。
  • 在当前测试之后,当在一个方法上声明,且方法模式设置为 AFTER_METHOD(即默认的方法模式)。spring-doc.cadn.net.cn

    Java
    @DirtiesContext (1)
    @Test
    void testProcessWhichDirtiesAppCtx() {
        // some logic that results in the Spring container being dirtied
    }
    1 污染当前测试方法之后的应用上下文。
    Kotlin
    @DirtiesContext (1)
    @Test
    fun testProcessWhichDirtiesAppCtx() {
        // some logic that results in the Spring container being dirtied
    }
    1 污染当前测试方法之后的应用上下文。

如果在上下文中配置为@ContextHierarchy的上下文层次结构的一部分的测试中使用@DirtiesContext,可以使用hierarchyMode标志位来控制上下文缓存的清除方式。默认情况下,会采用详尽算法清除上下文缓存,该算法不仅会清除当前层级,还会清除与当前测试共享公共祖先上下文的所有其他上下文层次结构。所有位于公共祖先上下文子层次结构中的ApplicationContext实例都将从上下文缓存中移除并关闭。若详尽算法在特定用例中显得过度,可指定更简单的当前层级算法,如下例所示。spring-doc.cadn.net.cn

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 使用当前层级算法。

有关EXHAUSTIVECURRENT_LEVEL算法的更多详细信息,请参阅 DirtiesContext.HierarchyMode javadoc。spring-doc.cadn.net.cn

@TestExecutionListeners

@TestExecutionListeners 用于为特定的测试类、其子类和嵌套类注册监听器。如果您希望全局注册监听器,应该通过在TestExecutionListener 配置中描述的自动发现机制进行注册。spring-doc.cadn.net.cn

以下示例展示了如何注册两个TestExecutionListener实现:spring-doc.cadn.net.cn

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 提供了从超类或封闭类继承监听器的支持。请参阅 @Nested 测试类配置@TestExecutionListeners javadoc 以获取示例和更多详细信息。如果您发现需要切换回使用默认的 TestExecutionListener 实现,请参阅 注册 TestExecutionListener 实现 中的说明。spring-doc.cadn.net.cn

@RecordApplicationEvents

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

记录的事件可以通过测试中的 ApplicationEvents API 访问。spring-doc.cadn.net.cn

查看 应用程序事件@RecordApplicationEvents javadoc 以获取示例和更多详细信息。spring-doc.cadn.net.cn

@Commit

@Commit 表示事务性测试方法的事务应该在测试方法完成后提交。您可以使用 @Commit 直接替换 @Rollback(false) 来更明确地传达代码的意图。类似于 @Rollback@Commit 也可以声明为类级别或方法级别的注解。spring-doc.cadn.net.cn

以下示例展示如何使用 @Commit 注解:spring-doc.cadn.net.cn

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

@Rollback 表示事务性测试方法完成后是否应回滚事务。若值为 true,则事务将回滚;否则事务将提交(另请参阅 @Commit)。在 Spring TestContext 框架中,集成测试的回滚默认值为 true,即使未显式声明 @Rollbackspring-doc.cadn.net.cn

当作为类级注解声明时,@Rollback为测试类层次结构中的所有测试方法定义了默认回滚 语义。当作为方法级注解声明时,@Rollback为特定测试方法定义回滚语义, 可能覆盖类级别的@Rollback@Commit语义。spring-doc.cadn.net.cn

以下示例会导致测试方法的结果不回滚(即结果会被提交到数据库):spring-doc.cadn.net.cn

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 的接口默认方法上。spring-doc.cadn.net.cn

以下示例展示如何使用 @BeforeTransaction 注解:spring-doc.cadn.net.cn

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,可以声明在基于Java8的接口默认方法上。spring-doc.cadn.net.cn

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脚本。以下示例展示了如何使用它:spring-doc.cadn.net.cn

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 为此测试运行两个脚本。
@SqlConfig

@SqlConfig 定义了用于确定如何解析和运行配置有 @Sql 注解的 SQL 脚本的元数据。以下示例展示了其用法:spring-doc.cadn.net.cn

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 声明。spring-doc.cadn.net.cn

请注意,方法级的@SqlMergeMode声明会覆盖类级声明。spring-doc.cadn.net.cn

以下示例展示如何在类级别使用@SqlMergeModespring-doc.cadn.net.cn

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

以下示例展示了如何在方法级别使用@SqlMergeModespring-doc.cadn.net.cn

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 注解,也可以结合 Java 8 的可重复注解支持来使用它,此时 @Sql 可以在同一个类或方法上多次声明,隐式生成此容器注解。以下示例展示了如何声明 SQL 组:spring-doc.cadn.net.cn

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测试上下文框架的所有配置中均受支持,并具有标准语义定义。请注意,这些注解不仅限于测试场景,可在Spring框架的任何位置使用。spring-doc.cadn.net.cn

JSR-250 生命周期注解

在Spring TestContext框架中,您可以在ApplicationContext中配置的任何应用程序组件上使用@PostConstruct@PreDestroy的标准语义。 然而,这些生命周期注解在实际测试类中的使用存在一定限制。spring-doc.cadn.net.cn

如果测试类中的某个方法被注解为@PostConstruct,该方法会在底层测试框架的所有前置方法(例如JUnit Jupiter的@BeforeEach注解方法)之前运行,且该规则适用于测试类中的每个测试方法。反之,若测试类中的方法被注解为@PreDestroy,该方法则永远不会执行。因此,在测试类中,我们建议您直接使用底层测试框架的生命周期回调机制,而非采用@PostConstruct@PreDestroy注解。spring-doc.cadn.net.cn

3.4.3. Spring JUnit 4测试注解

以下注解仅在结合使用SpringRunnerSpring的JUnit 4规则Spring的JUnit 4支持类时才受支持:spring-doc.cadn.net.cn

@IfProfileValue

@IfProfileValue 表示带注解的测试在特定测试环境下被启用。如果配置的 ProfileValueSource 为提供的 name 返回匹配的 value,则测试被启用。否则测试将被禁用且实际上被忽略。spring-doc.cadn.net.cn

您可以在类级别、方法级别或两者同时应用@IfProfileValue。 类级别的@IfProfileValue用法对该类或其子类中的任何方法具有高于方法级别的优先级。 具体而言,只有在类级别和方法级别同时启用时,测试才会被启用。 缺少@IfProfileValue意味着测试会被隐式启用。 这类似于JUnit 4的@Ignore注解的语义,但不同的是:只要存在@Ignore就始终会禁用测试。spring-doc.cadn.net.cn

以下示例展示了一个带有@IfProfileValue注解的测试用例:spring-doc.cadn.net.cn

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" 时运行此测试。

或者,您可以通过配置一个包含@IfProfileValuevalues列表(具有OR语义)来实现类似TestNG的测试组支持,该功能适用于JUnit 4环境。 请看以下示例:spring-doc.cadn.net.cn

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 注解配置的配置文件值时使用何种类型的 ProfileValueSource。如果测试中未声明 @ProfileValueSourceConfiguration,则默认使用 SystemProfileValueSource。以下示例展示了如何使用 @ProfileValueSourceConfigurationspring-doc.cadn.net.cn

Java
@ProfileValueSourceConfiguration(CustomProfileValueSource.class) (1)
public class CustomProfileValueSourceTests {
    // class body...
}
1 使用自定义配置值来源。
Kotlin
@ProfileValueSourceConfiguration(CustomProfileValueSource::class) (1)
class CustomProfileValueSourceTests {
    // class body...
}
1 使用自定义配置值来源。
@Timed

@Timed 表示带注解的测试方法必须在指定时间(毫秒)内完成执行。若测试执行时间超出指定时限,则测试失败。spring-doc.cadn.net.cn

该时间段包括运行测试方法本身、测试的任意重复次数(参见@Repeat),以及测试装置的设置或拆除。以下示例展示了其使用方法:spring-doc.cadn.net.cn

Java
@Timed(millis = 1000) (1)
public void testProcessWithOneSecondTimeout() {
    // some logic that should not take longer than 1 second to run
}
1 将测试的时间段设置为一秒。
Kotlin
@Timed(millis = 1000) (1)
fun testProcessWithOneSecondTimeout() {
    // some logic that should not take longer than 1 second to run
}
1 将测试的时间段设置为一秒。

Spring的@Timed注解与JUnit 4的@Test(timeout=…​) 支持具有不同的语义。具体而言,由于JUnit 4处理测试执行超时的方式 (即在单独的Thread中执行测试方法),如果测试耗时过长,@Test(timeout=…​) 会抢先使测试失败。而Spring的@Timed 则不会抢先使测试失败,而是等待测试完成 再判定失败。spring-doc.cadn.net.cn

@Repeat

@Repeat 表示带注解的测试方法必须重复运行。该测试方法的运行次数已在注解中指定。spring-doc.cadn.net.cn

要重复的执行范围包括测试方法本身的执行以及测试夹具的任何设置或拆除操作。当与 SpringMethodRule一起使用时,该范围还包括通过TestExecutionListener实现来准备测试实例。 以下示例展示了如何使用@Repeat注解:spring-doc.cadn.net.cn

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

3.4.4. Spring JUnit Jupiter测试注解

当与SpringExtension和JUnit Jupiter一起使用时,支持以下注解 (即JUnit 5中的编程模型):spring-doc.cadn.net.cn

@SpringJUnitConfig

@SpringJUnitConfig 是一个组合注解,它将JUnit Jupiter的 @ExtendWith(SpringExtension.class) 与Spring TestContext框架的 @ContextConfiguration 相结合。它可在类级别用作 @ContextConfiguration 的直接替代品。在配置选项方面, @ContextConfiguration@SpringJUnitConfig 之间的唯一区别是, 组件类可以在 @SpringJUnitConfig 中使用 value 属性声明。spring-doc.cadn.net.cn

以下示例演示如何使用 @SpringJUnitConfig 注解指定配置类:spring-doc.cadn.net.cn

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

以下示例展示如何使用 @SpringJUnitConfig 注解来指定配置文件的位置:spring-doc.cadn.net.cn

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了解更多详情。spring-doc.cadn.net.cn

@SpringJUnitWebConfig

@SpringJUnitWebConfig 是一个组合注解,它结合了来自 JUnit Jupiter 的 @ExtendWith(SpringExtension.class) 以及来自 Spring TestContext Framework 的 @ContextConfiguration@WebAppConfiguration。 你可以在类级别上使用它,作为 @ContextConfiguration@WebAppConfiguration 的直接替代。 关于配置选项,@ContextConfiguration@SpringJUnitWebConfig 之间的唯一区别是,你可以通过使用 @SpringJUnitWebConfig 中的 value 属性来声明组件类。 此外,你只能通过使用 @SpringJUnitWebConfig 中的 resourcePath 属性来覆盖来自 @WebAppConfigurationvalue 属性。spring-doc.cadn.net.cn

以下示例演示如何使用 @SpringJUnitWebConfig 注解指定配置类:spring-doc.cadn.net.cn

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

以下示例展示如何使用 @SpringJUnitWebConfig 注解来指定配置文件的位置:spring-doc.cadn.net.cn

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

@TestConstructor 是一个类型级别的注解,用于配置测试类构造函数的参数如何从测试的 ApplicationContext 中的组件自动注入。spring-doc.cadn.net.cn

如果测试类中不存在 @TestConstructor 或元呈现(meta-present),将使用默认的测试构造函数自动装配模式。有关如何更改默认模式的详细信息,请参阅下面的提示。但请注意,构造函数上 @Autowired 的本地声明优先级高于 @TestConstructor 和默认模式。spring-doc.cadn.net.cn

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

默认的测试构造函数自动装配模式可通过将 spring.test.constructor.autowire.mode JVM系统属性设置为all来修改。此外,也可通过 SpringProperties机制设置默认模式。spring-doc.cadn.net.cn

从 Spring Framework 5.3 开始,默认模式也可以作为 JUnit Platform 配置参数进行配置。spring-doc.cadn.net.cn

如果未设置spring.test.constructor.autowire.mode属性,测试类构造函数将不会自动装配。spring-doc.cadn.net.cn

自 Spring Framework 5.2 起,@TestConstructor 仅在结合 SpringExtension 用于 JUnit Jupiter 时受支持。请注意,SpringExtension 通常会为您自动注册——例如,在使用诸如 @SpringJUnitConfig@SpringJUnitWebConfig 等注解或 Spring Boot Test 提供的各种测试相关注解时。
@NestedTestConfiguration

@NestedTestConfiguration 是一个类型注解,用于配置在内部测试类的封闭类层次结构中如何处理 Spring 测试配置注解。spring-doc.cadn.net.cn

如果在测试类、其超类层次结构或其封闭类层次结构中没有出现@NestedTestConfiguration,则将使用默认的封闭配置继承模式。有关如何更改默认模式的详细信息,请参阅下面的提示。spring-doc.cadn.net.cn

更改默认封装配置继承模式

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

Spring TestContext Framework 会遵循以下注解的 @NestedTestConfiguration 语义。spring-doc.cadn.net.cn

使用 @NestedTestConfiguration 通常只在与 JUnit Jupiter 中的 @Nested 个测试类结合时才有意义;然而,可能还有其他支持 Spring 和嵌套测试类的测试框架会使用此注解。

查看 @Nested 测试类配置 以获取示例和更多详细信息。spring-doc.cadn.net.cn

@EnabledIf

@EnabledIf 用于表示:当提供的 expression 表达式求值为 true 时,带注解的 JUnit Jupiter 测试类或测试方法应被启用并执行。 具体而言,若表达式求值为 Boolean.TRUE 或等于 true(不区分大小写)的 String,则启用该测试。 在类级别应用时,该类中的所有测试方法默认也会自动启用。spring-doc.cadn.net.cn

表达式可以是以下任意一种:spring-doc.cadn.net.cn

请注意,非属性占位符动态解析结果的文本字面量在实际应用中毫无价值,因为 @EnabledIf("false") 等同于 @Disabled,而 @EnabledIf("true") 在逻辑上无意义。spring-doc.cadn.net.cn

您可以使用@EnabledIf作为元注解来创建自定义组合注解。例如,您可以按如下方式创建自定义@EnabledOnMac注解:spring-doc.cadn.net.cn

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) 支持。spring-doc.cadn.net.cn

从 JUnit 5.7 开始,JUnit Jupiter 还有一个名为 @EnabledIf 的条件注解。因此, 如果您希望使用 Spring 的 @EnabledIf 支持,请确保从正确的包中导入该注解类型。spring-doc.cadn.net.cn

@DisabledIf

@DisabledIf 用于表示被注解的 JUnit Jupiter 测试类或测试方法已被禁用。如果提供的 expression 表达式结果为 true(具体来说,若表达式结果为 Boolean.TRUE 或等于 trueString(不区分大小写)),则该测试不应运行。当应用于类级别时,该类中的所有测试方法也将自动被禁用。spring-doc.cadn.net.cn

表达式可以是以下任意一种:spring-doc.cadn.net.cn

请注意,非属性占位符动态解析结果的文本字面量在实际应用中毫无价值,因为 @DisabledIf("true") 等同于 @Disabled,而 @DisabledIf("false") 在逻辑上无意义。spring-doc.cadn.net.cn

您可以使用@DisabledIf作为元注解来创建自定义组合注解。例如,您可以按如下方式创建自定义@DisabledOnMac注解:spring-doc.cadn.net.cn

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 {}

@DisabledOnMac 仅作为可能的示例。如果您有相同的使用场景,请使用 JUnit Jupiter 中的内置 @DisabledOnOs(MAC) 支持。spring-doc.cadn.net.cn

从 JUnit 5.7 开始,JUnit Jupiter 还有一个名为 @DisabledIf 的条件注解。因此, 如果您希望使用 Spring 的 @DisabledIf 支持,请确保从正确的包中导入该注解类型。spring-doc.cadn.net.cn

3.4.5. 元注解测试支持

您可以将大多数测试相关注解用作 元注解来创建自定义组合 注解,从而减少整个测试套件中的配置重复。spring-doc.cadn.net.cn

你可以将以下每个注解作为元注解与 TestContext框架配合使用。spring-doc.cadn.net.cn

请考虑以下示例:spring-doc.cadn.net.cn

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 的通用测试配置,具体示例如下:spring-doc.cadn.net.cn

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的单个测试类的配置,如下所示:spring-doc.cadn.net.cn

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中的注解也可作为元注解使用。考虑以下示例:spring-doc.cadn.net.cn

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 的通用测试配置,如下所示:spring-doc.cadn.net.cn

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的单个测试类的配置,如下所示:spring-doc.cadn.net.cn

Java
@TransactionalDevTestConfig
class OrderRepositoryTests { }

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

@TransactionalDevTestConfig
class UserRepositoryTests { }

由于JUnit Jupiter支持将@Test@RepeatedTestParameterizedTest等作为元注解使用, 您也可以在测试方法级别创建自定义组合注解。例如,如果我们希望创建一个组合注解, 将JUnit Jupiter的@Test@Tag注解与Spring的@Transactional注解结合, 可以创建@TransactionalIntegrationTest注解,如下所示:spring-doc.cadn.net.cn

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的单个测试方法的配置,如下所示:spring-doc.cadn.net.cn

Java
@TransactionalIntegrationTest
void saveOrder() { }

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

@TransactionalIntegrationTest
fun deleteOrder() { }

如需更多详细信息,请参阅 Spring 注解编程模型 wiki 页面。spring-doc.cadn.net.cn

3.5. Spring测试上下文框架

Spring TestContext框架(位于org.springframework.test.context包中)提供了通用的、基于注解的单元测试和集成测试支持,该支持与所使用的测试框架无关。TestContext框架还高度重视约定优于配置的原则,提供合理的默认值,您可以通过基于注解的配置进行覆盖。spring-doc.cadn.net.cn

除了通用的测试基础设施外,TestContext框架还为JUnit 4、JUnit Jupiter(亦称JUnit 5)和TestNG提供了明确的支持。对于JUnit 4和TestNG,Spring提供了abstract支持类。此外,Spring为JUnit 4提供了自定义的JUnitRunnerRules,为JUnit Jupiter提供了自定义的Extension,使你可以编写所谓的POJO测试类。POJO测试类无需继承特定的类层次结构,例如abstract支持类。spring-doc.cadn.net.cn

以下部分概述了TestContext框架的内部机制。 如果您仅关注框架的使用,无意通过自定义监听器或加载器进行扩展, 请直接参阅配置(上下文管理依赖注入事务管理)、 支持类以及 注解支持相关章节。spring-doc.cadn.net.cn

3.5.1. 关键抽象概念

框架的核心由TestContextManager类以及TestContextTestExecutionListenerSmartContextLoader接口组成。为每个测试类(例如,在JUnit Jupiter中执行单个测试类内的所有测试方法)创建TestContextManagerTestContextManager则管理着一个保存当前测试上下文的TestContextTestContextManager会随着测试进度更新TestContext的状态,并委托给TestExecutionListener实现,这些实现通过提供依赖注入、管理事务等方式实际执行测试操作。SmartContextLoader负责为给定测试类加载ApplicationContext。有关各种实现的更多信息和示例,请参阅javadoc和Spring测试套件。spring-doc.cadn.net.cn

TestContext

TestContext 封装了运行测试的上下文(独立于实际使用的测试框架),并为所负责的测试实例提供上下文管理和缓存支持。TestContext 还会委托 SmartContextLoader 在请求时加载 ApplicationContextspring-doc.cadn.net.cn

TestContextManager

TestContextManager 是 Spring TestContext 框架的主要入口点,负责管理单个 TestContext 并向每个已注册的 TestExecutionListener 在明确定义的测试执行点发出信号事件:spring-doc.cadn.net.cn

TestExecutionListener

TestExecutionListener 定义了用于响应由 TestContextManager 发布的测试执行事件的 API,监听器与之注册。参见 TestExecutionListener 配置spring-doc.cadn.net.cn

上下文加载器

ContextLoader 是 Spring TestContext 框架用于加载集成测试所需 ApplicationContext 的策略接口。您应当实现 SmartContextLoader 而非此接口,以提供对组件类、活动 bean 定义配置文件、测试属性源、上下文层次结构以及 WebApplicationContext 的支持。spring-doc.cadn.net.cn

SmartContextLoaderContextLoader 接口的扩展,它取代了原始的最小 ContextLoader SPI。具体来说,一个 SmartContextLoader 可以选择处理资源位置、组件类或上下文初始化器。此外,一个 SmartContextLoader 可以在它加载的上下文中设置活动的bean定义配置文件和测试属性源。spring-doc.cadn.net.cn

Spring 提供以下实现:spring-doc.cadn.net.cn

  • DelegatingSmartContextLoader:两个默认加载器之一,它根据测试类声明的配置或默认位置/默认配置类的存在情况, 内部委托给AnnotationConfigContextLoaderGenericXmlContextLoaderGenericGroovyXmlContextLoader实现。 仅当类路径中存在Groovy时才会启用Groovy支持。spring-doc.cadn.net.cn

  • WebDelegatingSmartContextLoader: 两个默认加载器之一,它会根据测试类声明的配置或默认位置/默认配置类的存在情况,在内部委托给AnnotationConfigWebContextLoaderGenericXmlWebContextLoaderGenericGroovyXmlWebContextLoader。仅当测试类上存在@WebAppConfiguration时才会使用WebContextLoader。仅当类路径中存在Groovy时才会启用Groovy支持。spring-doc.cadn.net.cn

  • AnnotationConfigContextLoader: 从组件类加载标准ApplicationContext配置。spring-doc.cadn.net.cn

  • AnnotationConfigWebContextLoader: 从组件类加载WebApplicationContextspring-doc.cadn.net.cn

  • GenericGroovyXmlContextLoader: 从资源位置加载标准ApplicationContext,资源位置可以是Groovy脚本或XML配置文件。spring-doc.cadn.net.cn

  • GenericGroovyXmlWebContextLoader: 从资源位置加载一个WebApplicationContext,这些资源位置可以是Groovy脚本或XML配置文件。spring-doc.cadn.net.cn

  • GenericXmlContextLoader: 从XML资源位置加载标准的ApplicationContextspring-doc.cadn.net.cn

  • GenericXmlWebContextLoader: 从XML资源位置加载一个WebApplicationContextspring-doc.cadn.net.cn

3.5.2. 引导TestContext框架

Spring TestContext 框架的内部实现默认配置足以满足所有常见用例场景。然而,开发团队或第三方框架有时可能需要修改默认的 ContextLoader、实现自定义的 TestContextContextCache、扩展默认的 ContextCustomizerFactoryTestExecutionListener 实现集合等。为了实现对 TestContext 框架运行方式的底层控制,Spring 提供了一套引导策略。spring-doc.cadn.net.cn

TestContextBootstrapper 定义了用于引导 TestContext 框架的 SPI(服务提供接口)。TestContextManager 使用 TestContextBootstrapper 来加载当前测试的 TestExecutionListener 实现,并构建其管理的 TestContext。您可以通过直接使用 @BootstrapWith 或将其作为元注解,为测试类(或测试类层次结构)配置自定义引导策略。若未使用 @BootstrapWith 显式配置引导程序,则将根据 @WebAppConfiguration 的存在情况,选用 DefaultTestContextBootstrapperWebTestContextBootstrapperspring-doc.cadn.net.cn

由于未来的 TestContextBootstrapper SPI 可能会发生变化(以适应新的需求),我们强烈建议实现者不要直接实现此接口,而是继承 AbstractTestContextBootstrapper 或其具体子类之一。spring-doc.cadn.net.cn

3.5.3. TestExecutionListener 配置

Spring 提供了以下 TestExecutionListener 个默认注册的实现,严格按照以下顺序:spring-doc.cadn.net.cn

注册 TestExecutionListener 个实现

您可以使用 @TestExecutionListeners 注解为测试类、其子类和嵌套类显式注册 TestExecutionListener 个实现。请参阅 注解支持@TestExecutionListeners 的 javadoc 以获取详细信息和示例。spring-doc.cadn.net.cn

切换到默认 TestExecutionListener 实现

如果您扩展了一个用 @TestExecutionListeners 注解的类,并且需要切换到使用默认的监听器集,您可以使用以下注解来注解您的类。spring-doc.cadn.net.cn

Java
// Switch to default listeners
@TestExecutionListeners(
    listeners = {},
    inheritListeners = false,
    mergeMode = MERGE_WITH_DEFAULTS)
class MyTest extends BaseTest {
    // class body...
}
Kotlin
// Switch to default listeners
@TestExecutionListeners(
    listeners = [],
    inheritListeners = false,
    mergeMode = MERGE_WITH_DEFAULTS)
class MyTest : BaseTest {
    // class body...
}
自动发现默认的TestExecutionListener实现

通过使用@TestExecutionListeners注册TestExecutionListener个实现的方式,适用于在受限测试场景中使用的自定义监听器。然而,若需在整个测试套件中使用自定义监听器,这种方式会变得繁琐。此问题可通过SpringFactoriesLoader机制支持自动发现默认TestExecutionListener实现的功能来解决。spring-doc.cadn.net.cn

具体来说,spring-test模块在其META-INF/spring.factories属性文件中通过org.springframework.test.context.TestExecutionListener键声明了所有核心默认TestExecutionListener实现。 第三方框架和开发者可以通过各自的META-INF/spring.factories属性文件,以相同方式向默认监听器列表提供自己的TestExecutionListener实现。spring-doc.cadn.net.cn

排序 TestExecutionListener 实现

当TestContext框架通过SpringFactoriesLoader机制发现默认的TestExecutionListener实现时,实例化的监听器会使用Spring的AnnotationAwareOrderComparator进行排序,该机制遵循Spring的Ordered接口和用于排序的@Order注解。AbstractTestExecutionListener及Spring提供的所有默认TestExecutionListener实现均通过适当的数值实现Ordered。因此第三方框架和开发者应确保其默认TestExecutionListener实现通过实现Ordered或声明@Order来按正确顺序注册。有关各核心监听器赋值细节,请参阅核心默认TestExecutionListener实现的getOrder()方法javadoc。spring-doc.cadn.net.cn

合并 TestExecutionListener 个实现方案

如果通过@TestExecutionListeners注册了自定义TestExecutionListener,则不会注册默认监听器。在大多数常见测试场景中,这实际上会强制开发人员除了声明任何自定义监听器外,还必须手动声明所有默认监听器。以下配置示例演示了这种风格:spring-doc.cadn.net.cn

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

这种方法的挑战在于,它要求开发者必须精确知晓默认注册了哪些监听器。此外,默认监听器集合会随版本更新而变化——例如,SqlScriptsTestExecutionListener在Spring Framework 4.1中引入,而DirtiesContextBeforeModesTestExecutionListener则是在Spring Framework 4.2版本中新增的。不仅如此,像Spring Boot和Spring Security这样的第三方框架,也通过前述的自动发现机制注册了它们自己的默认TestExecutionListener实现。spring-doc.cadn.net.cn

为了避免需要关注并重新声明所有默认监听器,您可以将mergeMode@TestExecutionListeners属性设置为MergeMode.MERGE_WITH_DEFAULTSMERGE_WITH_DEFAULTS表示应将本地声明的监听器与默认监听器合并。该合并算法确保从列表中移除重复项, 并根据TestExecutionListener实现类的排序》中描述的AnnotationAwareOrderComparator语义对合并后的监听器集合进行排序。 若监听器实现了Ordered或使用@Order注解,则其可影响与默认监听器合并时的位置。 否则,在合并时本地声明的监听器将被追加到默认监听器列表的末尾。spring-doc.cadn.net.cn

例如,若前例中的 MyCustomTestExecutionListener 类将其 order 值(例如 500)配置为低于 ServletTestExecutionListener 的顺序(该值恰好为 1000),则 MyCustomTestExecutionListener 可自动与 ServletTestExecutionListener 之前的默认列表合并,此时前例可替换为:spring-doc.cadn.net.cn

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. 应用程序事件

从 Spring Framework 5.3.3 开始,TestContext 框架提供了对记录在 ApplicationContext 中发布的 应用事件 的支持,以便可以在测试中对这些事件进行断言。在单个测试执行期间发布的所有事件都可以通过 ApplicationEvents API 获得,这允许您将这些事件作为 java.util.Stream 进行处理。spring-doc.cadn.net.cn

要在您的测试中使用 ApplicationEvents,请执行以下操作。spring-doc.cadn.net.cn

  • 确保您的测试类使用@RecordApplicationEvents进行注解或元注解。spring-doc.cadn.net.cn

  • 确保 ApplicationEventsTestExecutionListener 已被注册。但是请注意, ApplicationEventsTestExecutionListener 默认已被注册,只有在通过 @TestExecutionListeners 进行自定义配置且不包含默认监听器时才需要手动注册。spring-doc.cadn.net.cn

  • 将类型为ApplicationEvents的字段标注为@Autowired,并在您的测试和生命周期方法(如JUnit Jupiter中的@BeforeEach@AfterEach方法)中使用该实例。spring-doc.cadn.net.cn

以下测试类使用 SpringExtension 用于 JUnit Jupiter 和 AssertJ 来断言在调用 Spring 管理的组件中的方法时发布的应用事件类型:spring-doc.cadn.net.cn

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 javadoc 以获取有关 ApplicationEvents API 的更多详细信息。spring-doc.cadn.net.cn

3.5.5. 测试执行事件

Spring Framework 5.2 引入的 EventPublishingTestExecutionListener 提供了一种实现自定义 TestExecutionListener 的替代方案。测试中的 ApplicationContext 组件可以监听 EventPublishingTestExecutionListener 发布的以下事件,每个事件对应 TestExecutionListener API 中的一个方法。spring-doc.cadn.net.cn

这些事件可以被消费用于多种目的,例如重置模拟bean或追踪测试执行。消费测试执行事件相较于实现自定义TestExecutionListener的一个优势在于:测试执行事件可由注册在测试ApplicationContext中的任何Spring bean消费,且这些bean能直接受益于ApplicationContext的依赖注入及其他特性。相对地,TestExecutionListener并非ApplicationContext中的bean。spring-doc.cadn.net.cn

默认情况下,EventPublishingTestExecutionListener会被注册;但是,只有当ApplicationContext已加载时才会发布事件。这可以防止ApplicationContext被不必要的或过早地加载。spring-doc.cadn.net.cn

因此,BeforeTestClassEvent将不会被发布,直到另一个ApplicationContext被加载。例如,使用默认的TestExecutionListener实现集时,对于第一个使用特定测试ApplicationContext的测试类,将不会发布BeforeTestClassEvent,但任何后续在相同测试套件中使用相同测试ApplicationContext的测试类将发布BeforeTestClassEvent,因为当后续测试类运行时,上下文已经加载(只要上下文没有通过@DirtiesContextContextCache中移除或最大大小驱逐策略)。spring-doc.cadn.net.cn

如果您希望确保每个测试类都始终发布一个 BeforeTestClassEvent,则需要注册一个 TestExecutionListener,该注册会在 beforeTestClass 回调中加载 ApplicationContext,并且该 TestExecutionListener 必须在 EventPublishingTestExecutionListener 之前注册。spring-doc.cadn.net.cn

同样,如果在给定测试类的最后一个测试方法后使用 @DirtiesContext 从上下文缓存中移除 ApplicationContext,则 AfterTestClassEvent 将不会为该测试类发布。spring-doc.cadn.net.cn

为了监听测试执行事件,Spring bean 可以选择实现 org.springframework.context.ApplicationListener 接口。或者,监听器方法可以使用 @EventListener 注解,并配置为监听上述列出的特定事件类型之一(参见 基于注解的事件监听器)。 鉴于此方法的流行,Spring 提供了以下专用 @EventListener 注解以简化测试执行事件监听器的注册。 这些注解位于 org.springframework.test.context.event.annotation 包中。spring-doc.cadn.net.cn

异常处理

默认情况下,如果测试执行事件监听器在消费事件时抛出异常,该异常将传播到当前使用的底层测试框架(例如JUnit或TestNG)。例如,如果消费BeforeTestMethodEvent时发生异常,对应的测试方法将因该异常而执行失败。相比之下,如果是异步测试执行事件监听器抛出异常,该异常则不会传播到底层测试框架。有关异步异常处理的更多细节,请查阅@EventListener的类级别Javadoc。spring-doc.cadn.net.cn

异步监听器

如果您希望特定的测试执行事件监听器异步处理事件, 可以使用Spring的常规 @Async支持。更多详细信息,请参阅 @EventListener的类级别javadoc文档。spring-doc.cadn.net.cn

3.5.6. 上下文管理

每个 TestContext 为其负责的测试实例提供上下文管理和缓存支持。测试实例不会自动获得对已配置 ApplicationContext 的访问权限。但是,如果测试类实现了 ApplicationContextAware 接口,则会向测试实例提供对 ApplicationContext 的引用。请注意,AbstractJUnit4SpringContextTestsAbstractTestNGSpringContextTests 实现了 ApplicationContextAware 接口,因此会自动提供对 ApplicationContext 的访问权限。spring-doc.cadn.net.cn

@Autowired ApplicationContext

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

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应用上下文注入测试中,如下所示:spring-doc.cadn.net.cn

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提供 (详见测试夹具的依赖注入)。spring-doc.cadn.net.cn

使用TestContext框架的测试类不需要继承任何特定类或实现特定接口来配置其应用上下文。相反,通过类级别声明@ContextConfiguration注解即可实现配置。若测试类未显式声明应用上下文资源位置或组件类,则配置的ContextLoader将决定如何从默认位置或默认配置类加载上下文。除上下文资源位置和组件类外,应用上下文还可通过应用上下文初始化器进行配置。spring-doc.cadn.net.cn

以下章节将介绍如何使用 Spring 的 @ContextConfiguration 注解,通过 XML 配置文件、Groovy 脚本、 组件类(通常是 @Configuration 类)或上下文初始化器来配置测试 ApplicationContext。 对于高级使用场景,您也可以实现并配置自定义的 SmartContextLoaderspring-doc.cadn.net.cn

使用XML资源的上下文配置

要通过XML配置文件为您的测试加载一个ApplicationContext,请使用@ContextConfiguration注解您的测试类, 并将locations属性配置为包含XML配置元数据资源位置的数组。普通路径或相对路径(例如context.xml) 将被视为类路径资源,该资源相对于定义测试类的包。以斜杠开头的路径被视为绝对类路径位置(例如/org/example/config.xml)。 表示资源URL的路径(即前缀为classpath:file:http:等的路径)将原样使用spring-doc.cadn.net.cn

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 value 属性支持 locations 属性的别名功能。因此,如果您不需要在 @ContextConfiguration 中声明额外属性,可以省略 locations 属性名称的声明,并采用以下示例所示的简写格式来声明资源位置:spring-doc.cadn.net.cn

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注解中同时省略locationsvalue属性, TestContext框架将尝试检测默认的XML资源位置。具体而言, GenericXmlContextLoaderGenericXmlWebContextLoader会根据测试类的名称检测默认位置。 如果您的类名为com.example.MyTestGenericXmlContextLoader将从"classpath:com/example/MyTest-context.xml" 加载您的应用上下文。以下示例展示了具体实现方式:spring-doc.cadn.net.cn

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脚本加载ApplicationContext进行测试,这些脚本基于 Groovy Bean定义DSL,您可以使用@ContextConfiguration注解 您的测试类,并通过locationsvalue属性 配置一个包含Groovy脚本资源位置的数组。Groovy脚本的 资源查找语义与XML配置文件中描述的规则相同。spring-doc.cadn.net.cn

启用Groovy脚本支持
支持使用Groovy脚本来加载一个ApplicationContext如果类路径中存在 Groovy,Spring TestContext 框架中的相关功能将自动启用。

以下示例演示如何指定Groovy配置文件:spring-doc.cadn.net.cn

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注解中同时省略locationsvalue属性,TestContext框架会尝试检测默认的Groovy脚本。具体而言,GenericGroovyXmlContextLoaderGenericGroovyXmlWebContextLoader会基于测试类的名称检测默认位置。如果您的类名为com.example.MyTest,Groovy上下文加载器将从"classpath:com/example/MyTestContext.groovy"加载您的应用程序上下文。以下示例展示了默认方式的使用:spring-doc.cadn.net.cn

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脚本

您可以同时声明XML配置文件和Groovy脚本,方法是使用@ContextConfigurationlocationsvalue属性。如果配置的资源位置路径以.xml结尾,则通过XmlBeanDefinitionReader加载;否则将通过GroovyBeanDefinitionReader加载。spring-doc.cadn.net.cn

以下清单展示了如何在集成测试中组合使用两者:spring-doc.cadn.net.cn

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...
}
基于组件类的上下文配置

要为测试加载ApplicationContext(使用组件类,参见 基于Java的容器配置),您可以为测试类添加@ContextConfiguration注解, 并通过classes属性配置包含组件类引用的数组。 以下示例演示了如何操作:spring-doc.cadn.net.cn

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 指定组件类。
组件类

术语“组件类”可指以下任意一种:spring-doc.cadn.net.cn

请参阅 @Configuration@Bean 的javadoc文档, 以获取关于组件类配置和语义的更多信息,特别要注意 @Bean 精简模式(Lite Mode)的讨论。spring-doc.cadn.net.cn

如果您在@ContextConfiguration注解中省略classes属性,TestContext框架会尝试检测默认配置类的存在。 具体来说,AnnotationConfigContextLoaderAnnotationConfigWebContextLoader 会检测测试类中所有符合配置类实现要求的static嵌套类, 如@Configuration的javadoc所规定。 请注意配置类的名称是任意的。此外,如果需要, 一个测试类可以包含多个static嵌套配置类。 在以下示例中,OrderServiceTest类声明了一个名为Configstatic嵌套配置类, 该配置类将自动用于加载测试类的ApplicationContextspring-doc.cadn.net.cn

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-doc.cadn.net.cn

此外,部分第三方框架(如Spring Boot)提供了一流的支持,允许从不同类型的资源(例如XML配置文件、Groovy脚本和@Configuration类)同时加载ApplicationContext。从历史角度看,Spring框架在标准部署中并不支持此功能。因此,Spring框架在spring-test模块中提供的大多数SmartContextLoader实现,每个测试上下文仅支持一种资源类型。然而,这并不意味着您不能同时使用两者。常规规则的一个例外是:GenericGroovyXmlContextLoaderGenericGroovyXmlWebContextLoader可同时支持XML配置文件与Groovy脚本。此外,第三方框架可以选择通过@ContextConfiguration同时支持locationsclasses的声明,结合TestContext框架的标准测试支持,您将拥有以下选项。spring-doc.cadn.net.cn

如果您希望同时使用资源位置(例如XML或Groovy)和@Configuration类来配置测试,必须选择其中一个作为入口点,且该入口点必须包含或导入另一个配置方式。例如,在XML或Groovy脚本中,可以通过组件扫描或将其定义为常规Spring bean来包含@Configuration类;而在@Configuration类中,可以使用@ImportResource注解导入XML配置文件或Groovy脚本。请注意,这种配置方式在语义上等同于生产环境中的应用程序配置:在生产配置中,您可以定义一组XML/Groovy资源位置或一组用于加载生产环境ApplicationContext@Configuration类,同时仍可自由包含或导入另一种配置类型。spring-doc.cadn.net.cn

上下文配置与上下文初始化器

要为您的测试配置一个ApplicationContext,可通过使用上下文初始化器实现, 在测试类上添加@ContextConfiguration注释,并在initializers属性中配置 一个包含实现了ApplicationContextInitializer的类的引用数组。然后,声明的上下文初始化器将用于 初始化为您的测试加载的ConfigurableApplicationContext。请注意, 每个声明的初始化器所支持的具体ConfigurableApplicationContext类型必须与当前使用的 SmartContextLoader创建的ApplicationContext类型兼容(通常是一个GenericApplicationContext)。此外, 初始化器的调用顺序取决于它们是否实现了Spring的Ordered接口, 或是否标注了Spring的@Order注解或标准@Priority注解。以下示例展示了如何使用初始化器:spring-doc.cadn.net.cn

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定义。以下示例展示了具体操作方法:spring-doc.cadn.net.cn

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 支持布尔值属性 inheritLocationsinheritInitializers,用于声明是否应继承超类声明的资源位置或组件类及上下文初始化器。这两个标志的默认值均为 true。这意味着测试类会继承所有超类声明的资源位置、组件类以及上下文初始化器。 具体来说,测试类的资源位置或组件类会被追加到超类声明的资源位置列表或注解类列表中。同样地,给定测试类的初始化器会被添加到测试超类定义的初始化器集合中。因此,子类可以选择扩展资源位置、组件类或上下文初始化器。spring-doc.cadn.net.cn

如果inheritLocationsinheritInitializers属性在@ContextConfiguration中被设置为false,则测试类的资源位置或组件类以及上下文初始化器,将分别覆盖并有效替换父类定义的配置。spring-doc.cadn.net.cn

从 Spring Framework 5.3 开始,测试配置也可以从外围类继承。有关详细信息,请参阅 @Nested 测试类配置

在以下使用XML资源位置的示例中,ApplicationContextExtendedTest按此顺序从base-config.xmlextended-config.xml加载。 因此在extended-config.xml中定义的bean可以覆盖(即替换)base-config.xml中定义的bean。 以下示例展示了某个类如何扩展另一个类,并使用自己的配置文件及父类的配置文件:spring-doc.cadn.net.cn

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 配置文件定义于子类中。

同样地,在下一个使用组件类的示例中,ExtendedTestApplicationContext将按顺序从BaseConfigExtendedConfig类加载。 因此,在ExtendedConfig中定义的Bean可以覆盖(即替换)BaseConfig中定义的Bean。以下示例展示了如何扩展一个类, 并同时使用其自身的配置类及父类的配置类:spring-doc.cadn.net.cn

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 在子类中定义的配置类。

在以下使用上下文初始化器的示例中,ApplicationContextExtendedTest 通过 BaseInitializerExtendedInitializer 进行初始化。但需注意,初始化器的调用顺序取决于它们是否实现了 Spring 的 Ordered 接口,或标注了 Spring 的 @Order 注解,亦或是标准的 @Priority 注解。以下示例展示了如何通过扩展一个类,同时使用其自身的初始化器及超类的初始化器:spring-doc.cadn.net.cn

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框架对环境和配置文件(又称"bean定义配置文件")提供了一流支持, 集成测试可以配置为在各种测试场景中激活特定的bean定义配置文件。 这可以通过在测试类上添加@ActiveProfiles注解并指定加载测试ApplicationContext时应激活的配置文件列表来实现。spring-doc.cadn.net.cn

您可以将@ActiveProfiles与任何实现了SmartContextLoader SPI的版本一起使用,但在较旧的ContextLoader SPI实现中不支持@ActiveProfiles

考虑两个使用XML配置和@Configuration个类的示例:spring-doc.cadn.net.cn

<!-- 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时,其ApplicationContext会从类路径根目录下的app-config.xml配置文件中加载。如果检查app-config.xml,可以看到accountRepository bean依赖于dataSource bean。然而,dataSource并未被定义为顶级bean。相反,dataSource被定义了三次:分别在production配置文件中、dev配置文件以及default配置文件中。spring-doc.cadn.net.cn

通过使用@ActiveProfiles("dev")注解TransferServiceTest,我们指示Spring TestContext框架加载ApplicationContext并将激活配置文件设置为{"dev"}。因此,系统会创建一个嵌入式数据库并填充测试数据,同时accountRepository bean将自动装配开发环境DataSource的引用。这通常是集成测试中所需的效果。spring-doc.cadn.net.cn

有时将bean分配给default profile会很有用。只有当没有其他profile被显式激活时,默认profile中的bean才会被包含。您可以使用此功能定义应用程序默认状态下使用的“回退”bean。例如,您可以显式地为devproduction profile提供数据源,但当两者均未激活时,可将内存数据源定义为默认选项。spring-doc.cadn.net.cn

下面的代码清单演示了如何不使用XML而改用@Configuration个类来实现相同的配置和集成测试:spring-doc.cadn.net.cn

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类:spring-doc.cadn.net.cn

  • TransferServiceConfig: 通过使用@Autowired,依赖注入获取dataSourcespring-doc.cadn.net.cn

  • StandaloneDataConfig: 定义适用于开发者测试的嵌入式数据库的dataSourcespring-doc.cadn.net.cn

  • JndiDataConfig: 定义一个在生产环境中从JNDI获取的dataSourcespring-doc.cadn.net.cn

  • DefaultDataConfig: 定义一个dataSource用于默认嵌入式数据库,当没有激活配置文件时。spring-doc.cadn.net.cn

与基于XML的配置示例类似,我们仍然使用@ActiveProfiles("dev")TransferServiceTest进行注解, 但这次我们通过@ContextConfiguration注解指定了所有四个配置类。 测试类本身的主体部分则保持完全不变。spring-doc.cadn.net.cn

在给定项目中,多个测试类共用一组配置文件是常见场景。因此,为避免重复声明@ActiveProfiles注解,可在基类上声明@ActiveProfiles一次,子类便会自动从基类继承@ActiveProfiles配置。以下示例将@ActiveProfiles(及其他注解)的声明移至抽象基类AbstractIntegrationTestspring-doc.cadn.net.cn

从 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 同样支持一个 inheritProfiles 属性,该属性可用于禁用激活配置集的继承关系,如下示例所示:spring-doc.cadn.net.cn

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
}

此外,有时需要通过编程方式而非声明式方式解析测试的活动配置文件——例如基于:spring-doc.cadn.net.cn

要以编程方式解析活动bean定义配置文件,您可以实现一个自定义的ActiveProfilesResolver,并通过使用@ActiveProfilesresolver属性进行注册。更多信息,请参阅相应的 javadoc。 以下示例演示了如何实现和注册自定义的 OperatingSystemActiveProfilesResolverspring-doc.cadn.net.cn

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框架对环境概念提供了一流的支持,支持属性源的层次结构,并且您可以使用特定于测试的属性源配置集成测试。与在@PropertySource类上使用的@Configuration注解不同,您可以在测试类上声明@TestPropertySource注解,以声明测试属性文件的资源位置或内联属性。这些测试属性源将被添加到为带注解的集成测试加载的ApplicationContextEnvironment中的PropertySources集合中。spring-doc.cadn.net.cn

您可以将@TestPropertySource与任何实现了SmartContextLoader SPI的版本一起使用,但在较旧的ContextLoader SPI实现中不支持@TestPropertySourcespring-doc.cadn.net.cn

Implementations of SmartContextLoader gain access to merged test property source values through the getPropertySourceLocations() and getPropertySourceProperties() methods in MergedContextConfiguration.spring-doc.cadn.net.cn

声明测试属性源

您可以通过使用locationsvalue属性来配置测试属性文件。spring-doc.cadn.net.cn

同时支持传统的和基于XML的属性文件格式——例如,"classpath:/com/example/test.properties""file:///path/to/file.xml"spring-doc.cadn.net.cn

每个路径被解释为Spring Resource。一个普通的路径(例如,"test.properties") 被视为相对于定义测试类的包的类路径资源。以斜杠开头的路径被视为绝对类路径资源(例如:"/org/example/test.xml")。引用URL的路径(例如,以classpath:file:http: 开头的路径)使用指定的资源协议进行加载。不允许使用资源位置通配符(如*/.properties):每个位置必须解析为一个确切的.properties.xml资源。spring-doc.cadn.net.cn

以下示例使用了一个测试属性文件:spring-doc.cadn.net.cn

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

您可以通过使用@TestPropertySourceproperties属性以键值对的形式配置内联属性,如下例所示。所有键值对都将作为具有最高优先级的单个测试PropertySource添加到外围的Environment中。spring-doc.cadn.net.cn

支持的键值对语法与 Java 属性文件中定义的条目语法相同:spring-doc.cadn.net.cn

以下示例设置了两个内联属性:spring-doc.cadn.net.cn

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 注解的 locationsproperties 会覆盖先前 @TestPropertySource 注解中的值。spring-doc.cadn.net.cn

此外,您可以在测试类上声明多个组合注解,每个注解都使用@TestPropertySource元注解,所有这些@TestPropertySource声明都将为您的测试属性源做出贡献。spring-doc.cadn.net.cn

直接呈现的@TestPropertySource注解总是优先于元呈现的@TestPropertySource注解。换句话说,直接呈现的@TestPropertySource注解中的locationsproperties将覆盖作为元注解使用的@TestPropertySource注解中的locationspropertiesspring-doc.cadn.net.cn

默认属性文件检测

如果 @TestPropertySource 被声明为一个空注解(即,没有显式指定 locationsproperties 属性的值),将尝试检测相对于声明该注解的类的默认属性文件。例如,如果被注解的测试类是 com.example.MyTest,对应的默认属性文件是 classpath:com/example/MyTest.properties。如果无法检测到默认值,则会抛出 IllegalStateExceptionspring-doc.cadn.net.cn

优先级

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

在下一个示例中,timezoneport 属性以及在 "/test.properties" 中定义的任何属性将覆盖在系统和应用程序属性源中定义的同名属性。此外,如果 "/test.properties" 文件为 timezoneport 属性定义了条目,那么这些条目将被使用 properties 属性声明的内联属性覆盖。以下示例展示了如何同时在文件和内联中指定属性:spring-doc.cadn.net.cn

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 支持布尔 inheritLocationsinheritProperties 属性,这些属性表示是否应继承超类声明的属性文件的位置和内联属性。这两个标志的默认值都是 true。这意味着测试类会继承任何超类声明的位置和内联属性。具体来说,测试类的位置和内联属性将附加到超类声明的位置和内联属性中。因此,子类可以选择扩展位置和内联属性。请注意,较后出现的属性会覆盖同名的较早出现的属性。此外,上述优先级规则也适用于继承的测试属性源。spring-doc.cadn.net.cn

如果在@TestPropertySource中将inheritLocationsinheritProperties属性设置为false,测试类的locations或内联属性将覆盖并有效替换超类定义的配置。spring-doc.cadn.net.cn

从 Spring Framework 5.3 开始,测试配置也可以从外围类继承。有关详细信息,请参阅 @Nested 测试类配置

在下一个示例中,ApplicationContext 对于 BaseTest 仅使用 base.properties 文件作为测试属性源进行加载。相比之下,ApplicationContext 对于 ExtendedTest 使用 base.propertiesextended.properties 文件作为测试属性源位置进行加载。以下示例展示了如何通过使用 properties 文件在子类和超类中定义属性:spring-doc.cadn.net.cn

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() {
    // ...
}

在下一个示例中,使用仅内联的key1属性加载ApplicationContext对于BaseTest。相反,使用内联的key1key2属性加载ApplicationContext对于ExtendedTest。以下示例展示了如何在子类及其超类中定义属性,方法是使用内联属性:spring-doc.cadn.net.cn

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中的Environment添加具有动态值的属性的集成测试。spring-doc.cadn.net.cn

The @DynamicPropertySource 注解及其支持基础设施最初设计用于允许来自 Testcontainers 基于的测试中的属性轻松暴露给 Spring 集成测试。但是,此功能也可与任何其生命周期在测试的 ApplicationContext 之外管理的外部资源一起使用。spring-doc.cadn.net.cn

与应用于类级别的 @TestPropertySource 注解不同,@DynamicPropertySource 必须应用于接受单个 static 参数的 DynamicPropertyRegistry 方法,该参数用于向 Environment 添加 名称-值 对。值是动态的,并通过 Supplier 提供,该方法仅在解析属性时调用。通常,方法引用用于提供值,如以下示例所示,该示例使用 Testcontainers 项目在 Spring ApplicationContext 外管理 Redis 容器。Redis 容器的 IP 地址和端口通过 ApplicationContextredis.host 属性提供给组件。这些属性可以通过 Spring 的 redis.port 抽象访问或直接注入到 Spring 管理的组件中——例如,分别通过 @Value("${redis.host}")@Value("${redis.port}")spring-doc.cadn.net.cn

如果您在基类中使用 @DynamicPropertySource,并发现子类中的测试失败,因为动态属性在子类之间发生变化,您可能需要使用 @DirtiesContext 注解您的基类,以确保每个子类都拥有自己的 ApplicationContext,并且具有正确的动态属性。spring-doc.cadn.net.cn

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

    @Container
    static GenericContainer redis =
        new GenericContainer("redis:5.0.3-alpine").withExposedPorts(6379);

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

    // tests ...

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

    companion object {

        @Container
        @JvmStatic
        val redis: GenericContainer =
            GenericContainer("redis:5.0.3-alpine").withExposedPorts(6379)

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

    // tests ...

}
优先级

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

Loading a WebApplicationContext

要指示TestContext框架加载WebApplicationContext而不是标准的ApplicationContext,你可以在相应的测试类上使用注解@WebAppConfigurationspring-doc.cadn.net.cn

The presence of @WebAppConfiguration on your test class instructs the TestContext framework (TCF) that a WebApplicationContext (WAC) should be loaded for your integration tests. In the background, the TCF makes sure that a MockServletContext is created and supplied to your test’s WAC. By default, the base resource path for your MockServletContext is set to src/main/webapp. This is interpreted as a path relative to the root of your JVM (normally the path to your project). If you are familiar with the directory structure of a web application in a Maven project, you know that src/main/webapp is the default location for the root of your WAR. If you need to override this default, you can provide an alternate path to the @WebAppConfiguration annotation (for example, @WebAppConfiguration("src/test/webapp")). If you wish to reference a base resource path from the classpath instead of the file system, you can use Spring’s classpath: prefix.spring-doc.cadn.net.cn

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

本节中的剩余示例展示了一些不同的配置选项,用于加载WebApplicationContext。以下示例展示了TestContext框架对约定优于配置的支持:spring-doc.cadn.net.cn

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但没有指定资源locations、组件classes或上下文initializers,Spring会尝试通过约定检测你的配置(即,在WacTests类所在的包中查找WacTests-context.xml,或者查找静态嵌套的@Configuration类)。spring-doc.cadn.net.cn

以下示例展示了如何显式声明资源基础路径使用@WebAppConfiguration以及XML资源位置使用@ContextConfigurationspring-doc.cadn.net.cn

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-doc.cadn.net.cn

以下示例说明了我们可以通过指定 Spring 资源前缀来覆盖这两个注解的默认资源语义:spring-doc.cadn.net.cn

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 {
    //...
}

将此示例中的注释与前一个示例进行对比。spring-doc.cadn.net.cn

与Web Mocks一起工作

为了提供全面的Web测试支持,TestContext框架默认启用了ServletTestExecutionListener。在针对WebApplicationContext进行测试时,这个TestExecutionListener通过使用Spring Web的RequestContextHolder在每个测试方法之前设置默认的线程本地状态,并根据使用@WebAppConfiguration配置的基本资源路径创建MockHttpServletRequestMockHttpServletResponseServletWebRequestServletTestExecutionListener还确保MockHttpServletResponseServletWebRequest可以注入到测试实例中,并且在测试完成后清理线程本地状态。spring-doc.cadn.net.cn

一旦你为测试加载了WebApplicationContext,你可能会发现需要与网络模拟进行交互——例如,为了设置测试环境或在调用你的网络组件后执行断言。下面的例子展示了哪些模拟可以自动装配到你的测试实例中。请注意,WebApplicationContextMockServletContext在整个测试套件中都是缓存的,而其他的模拟则由ServletTestExecutionListener按测试方法进行管理。spring-doc.cadn.net.cn

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),该上下文将被缓存并在同一测试套件中所有声明相同唯一上下文配置的后续测试中重用。要理解缓存的工作原理,重要的是要理解“唯一”和“测试套件”的含义。spring-doc.cadn.net.cn

一个 ApplicationContext 可以通过用于加载它的配置参数组合唯一标识。因此,唯一的配置参数组合用于生成上下文缓存的键。TestContext 框架使用以下配置参数来构建上下文缓存键:spring-doc.cadn.net.cn

例如,如果TestClassA 指定了 {"app-config.xml", "test-config.xml"} 用于 locations(或 value)属性的 @ContextConfiguration,TestContext 框架将加载相应的 ApplicationContext 并将其存储在基于这些位置的键的 static 上下文缓存中。因此,如果 TestClassB 还为它的位置定义了 {"app-config.xml", "test-config.xml"}(无论是显式还是通过继承隐式地),但没有定义 @WebAppConfiguration,则不同的 ContextLoader、不同的活动配置文件、不同的上下文初始化器、不同的测试属性源或不同的父上下文,则两个测试类共享相同的 ApplicationContext。这意味着加载应用程序上下文的设置成本仅在一次(每个测试套件)中发生,并且后续的测试执行速度要快得多。spring-doc.cadn.net.cn

测试套件和分离进程

Spring TestContext框架将应用程序上下文存储在静态缓存中。这意味着上下文实际上是存储在一个static变量中。换句话说,如果测试在单独的进程中运行,静态缓存会在每次测试执行之间被清除,这实际上禁用了缓存机制。spring-doc.cadn.net.cn

要从缓存机制中受益,所有测试必须在同一进程或测试套件中运行。这可以通过在IDE中将所有测试作为一个组执行来实现。同样,在使用构建框架(如Ant、Maven或Gradle)执行测试时,确保构建框架在测试之间不进行fork非常重要。例如,如果Maven Surefire插件的 forkMode 设置为alwayspertest,TestContext框架无法在测试类之间缓存应用程序上下文,结果构建过程会显著变慢。spring-doc.cadn.net.cn

上下文缓存的大小是有限制的,默认的最大大小为32。每当达到最大大小时,将使用最近最少使用(LRU)策略来驱逐和关闭过期的上下文。您可以通过设置名为spring.test.context.cache.maxSize的JVM系统属性从命令行或构建脚本中配置最大大小。作为替代方案,您也可以通过SpringProperties机制设置相同的属性。spring-doc.cadn.net.cn

由于在一个给定的测试套件中加载大量的应用程序上下文可能会导致套件运行时间过长,因此通常有益于知道已加载和缓存了多少个上下文。要查看底层上下文缓存的统计信息,可以将org.springframework.test.context.cache日志类别设置为DEBUGspring-doc.cadn.net.cn

在测试破坏应用程序上下文并需要重新加载的情况下(例如,通过修改 bean 定义或应用程序对象的状态),您可以使用 @DirtiesContext 注解您的测试类或测试方法(参见 Spring 测试注解 中对 @DirtiesContext 的讨论)。这会指示 Spring 从缓存中删除上下文,并在运行下一个需要相同应用程序上下文的测试之前重新构建应用程序上下文。请注意,@DirtiesContext 注解的支持由 DirtiesContextBeforeModesTestExecutionListenerDirtiesContextTestExecutionListener 提供,默认情况下这些功能已启用。spring-doc.cadn.net.cn

ApplicationContext生命周期和控制台日志记录

当您需要调试使用 Spring TestContext Framework 执行的测试时,分析控制台输出(即输出到 SYSOUTSYSERR 流的输出)可能会很有帮助。一些构建工具和 IDE 可以将控制台输出与特定测试关联;然而,一些控制台输出无法轻松与特定测试关联。spring-doc.cadn.net.cn

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

ApplicationContext 在测试中通常在测试类的实例被准备时加载 — 例如,为了将依赖项注入到测试实例的 @Autowired 字段中。这意味着在 ApplicationContext 初始化期间触发的任何控制台日志通常无法与单个测试方法相关联。然而,如果根据 @DirtiesContext 语义在测试方法执行前立即关闭上下文,则会在测试方法执行前加载一个新的上下文实例。在后一种情况下,IDE 或构建工具可能会将控制台日志与单个测试方法相关联。spring-doc.cadn.net.cn

可以通过以下任一情况关闭测试的 ApplicationContextspring-doc.cadn.net.cn

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

当通过JVM关闭钩子关闭Spring ApplicationContext时,关闭阶段执行的回调是在名为SpringContextShutdownHook的线程上执行的。因此,如果您希望在通过JVM关闭钩子关闭ApplicationContext时禁用控制台日志记录,您可以向日志框架注册一个自定义过滤器,以忽略由该线程发起的任何日志记录。spring-doc.cadn.net.cn

上下文层次结构

在编写依赖于加载的Spring ApplicationContext的集成测试时,通常测试单个上下文就足够了。但是,有时测试ApplicationContext实例的层次结构是有益的,甚至是必要的。例如,如果你正在开发一个Spring MVC Web应用程序,你通常会有一个由Spring的ContextLoaderListener加载的根WebApplicationContext和一个由Spring的DispatcherServlet加载的子WebApplicationContext。这将导致父-子上下文层次结构,在该层次结构中,共享组件和基础设施配置在根上下文中声明,并由Web特定组件在子上下文中使用。另一个用例可以在Spring Batch应用程序中找到,其中你经常有一个提供共享批处理基础结构配置的父WebApplicationContext和一个由Spring的DispatcherServlet加载的子WebApplicationContext。这将导致一个父-子上下文层次结构,其中共享组件和基础设施配置在根上下文中声明,并由Web特定组件在子上下文中消费。spring-doc.cadn.net.cn

你可以编写使用上下文层次结构的集成测试,通过在单个测试类上或在测试类层次结构内声明上下文配置并使用@注解。如果在测试类层次结构内的多个类上声明了上下文层次结构,你还可以为上下文层次结构中的特定命名级别合并或覆盖上下文配置。在合并层次结构中给定级别的配置时,配置资源类型(即XML配置文件或组件类)必须保持一致。否则,在上下文层次结构的不同级别使用不同的资源类型进行配置是完全可以接受的。spring-doc.cadn.net.cn

本节中剩余的基于 JUnit Jupiter 的示例展示了需要使用上下文层次结构的集成测试的常见配置场景。spring-doc.cadn.net.cn

单个测试类与上下文层次结构

ControllerIntegrationTests 代表一个典型的Spring MVC Web应用程序的集成测试场景,通过声明一个包含两个级别的上下文层次结构:一个用于根 WebApplicationContext(使用 TestAppConfig @Configuration 类加载)和一个用于调度程序Servlet WebApplicationContext(使用 WebConfig @Configuration 类加载)。自动装配到测试实例中的 WebApplicationContext 是子上下文(即层次结构中最低的上下文)。以下列表显示了这种配置场景:spring-doc.cadn.net.cn

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 中的配置加载的应用程序上下文被设置为每个具体子类加载的上下文的父上下文。以下列表显示了这种配置场景:spring-doc.cadn.net.cn

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 定义了层次结构中的两个级别,parentchildExtendedTests 扩展了 BaseTests,并指示 Spring TestContext Framework 合并 child 层级的上下文配置,确保在 @ContextConfiguration 中声明的名称都为 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"} 的上下文配置。以下列表显示了这种配置场景:spring-doc.cadn.net.cn

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() {}
类层次结构与覆盖的上下文层次配置

与前一个示例不同,此示例演示了如何通过在上下文层次结构中为给定的命名级别设置inheritLocations标志来覆盖配置。因此,ExtendedTests的应用上下文仅从/test-user-config.xml加载,并将其父级设置为从/app-config.xml加载的上下文。以下列出了这种配置场景:spring-doc.cadn.net.cn

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标志位用于控制如何清除上下文缓存。有关详细信息,请参阅关于的讨论。@DirtiesContext in Spring测试注解@DirtiesContext javadoc.

3.5.7. 测试夹具的依赖注入

当你使用 DependencyInjectionTestExecutionListener(默认配置为DependencyInjectionTestExecutionListener),你的测试实例的依赖项将从你在@ContextConfiguration或相关注解中配置的应用上下文中注入。你可以使用属性注入、字段注入,或者两者都使用,这取决于你选择的注解以及你是否将它们放在setter方法或字段上。如果你正在使用JUnit Jupiter,你还可以选择性地使用构造器注入(参见使用SpringExtension进行依赖注入)。为了与Spring的注解式注入支持保持一致,你也可以使用Spring的@Autowired注解或JSR-330的@Inject注解来进行字段和setter注入。spring-doc.cadn.net.cn

For testing frameworks other than JUnit Jupiter, the TestContext framework does not participate in instantiation of the test class. Thus, the use of @Autowired or @Inject for constructors has no effect for test classes.
虽然字段注入在生产代码中不被推荐,但在测试代码中字段注入实际上是相当自然的。这种差异的原因是你永远不会直接实例化你的测试类。因此,没有必要能够在你的测试类上调用无参构造函数或setter方法。

因为@Autowired用于按类型自动装配,如果你有多个相同类型的bean定义,你不能依赖这种方法。在这种情况下,你可以使用@Autowired@Qualifier一起。你也可以选择使用@Inject@Named一起。或者,如果你的测试类可以访问其ApplicationContext,你可以通过显式查找来执行此操作,例如调用applicationContext.getBean("titleRepository", TitleRepository.class)spring-doc.cadn.net.cn

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

考虑测试 HibernateTitleRepository 类的情景,如在目标部分所述。接下来的两个代码示例展示了在字段和setter方法中使用 @Autowired 的情况。所有示例代码列出后将展示应用程序上下文配置。spring-doc.cadn.net.cn

以下代码示例中的依赖注入行为并不特定于JUnit Jupiter。相同的DI技术可以与任何受支持的测试框架结合使用。spring-doc.cadn.net.cn

以下示例调用了静态断言方法,例如assertNotNull(), 但没有在调用前加上Assertions。在这种情况下,假设该方法 已通过未在示例中显示的import static声明正确导入。spring-doc.cadn.net.cn

第一个代码示例展示了一个基于JUnit Jupiter的测试类实现,该测试类使用@Autowired进行字段注入:spring-doc.cadn.net.cn

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注入,如下所示:spring-doc.cadn.net.cn

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)。以下是此配置:spring-doc.cadn.net.cn

<?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,如下所示(但请确保同时调用超类中的被覆盖的方法):spring-doc.cadn.net.cn

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名称将用作默认的限定符值,因此你也可以通过名称(如前面所示,假设myDataSource是beanid)有效地指向一个具体的bean。spring-doc.cadn.net.cn

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

Spring 从早期版本开始就支持请求和会话作用域的bean,你可以通过以下步骤测试你的请求作用域和会话作用域的bean:spring-doc.cadn.net.cn

以下代码片段展示了登录用例的XML配置。请注意,userService bean依赖于一个请求范围的loginAction bean。此外,LoginAction 是通过使用SpEL表达式实例化的,这些表达式从当前HTTP请求中检索用户名和密码。在我们的测试中,我们希望使用TestContext框架管理的mock来配置这些请求参数。spring-doc.cadn.net.cn

请求范围的bean配置
<beans>

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

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

</beans>

RequestScopedBeanTests 中,我们注入了 UserService(即测试主题)和 MockHttpServletRequest 到我们的测试实例中。在我们的 requestScope() 测试方法中,我们通过设置提供的 MockHttpServletRequest 中的请求参数来设置测试环境。当在我们的 userService 上调用 loginUser() 方法时,我们可以确保用户服务可以访问当前 MockHttpServletRequest 的请求作用域内的 loginAction。然后我们可以根据已知的用户名和密码输入对结果进行断言。spring-doc.cadn.net.cn

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。请注意,UserPreferences bean是通过使用SpEL表达式从当前HTTP会话中检索主题来实例化的。在我们的测试中,我们需要在TestContext框架管理的模拟会话中配置一个主题。下面的例子展示了如何做到这一点:spring-doc.cadn.net.cn

会话作用域的bean配置
<beans>

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

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

</beans>

SessionScopedBeanTests 中,我们注入了 UserServiceMockHttpSession 到我们的测试实例中。在我们的 sessionScope() 测试方法中,我们通过设置提供的 MockHttpSession 中的预期 theme 属性来设置测试环境。当在我们的 userService 上调用 processUserPreferences() 方法时,我们可以确保用户服务可以访问当前 MockHttpSession 的会话作用域 userPreferences,并且我们可以根据配置的主题对结果进行断言。spring-doc.cadn.net.cn

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。为了启用事务支持,你必须在用@ContextConfiguration语义加载的ApplicationContext中配置一个PlatformTransactionManager bean。此外,你必须在类或方法级别为你的测试声明Spring的@Transactional注解。spring-doc.cadn.net.cn

测试管理的事务

Test-managed transactions 是通过使用 TransactionalTestExecutionListener 声明式管理的事务,或者通过使用 TestTransaction 程序式管理的事务(稍后描述)。你不应该将这些事务与 Spring 管理的事务(直接由 Spring 在为测试加载的 ApplicationContext 中管理的事务)或应用程序管理的事务(在被测试调用的应用程序代码中程序式管理的事务)混淆。Spring 管理的事务和应用程序管理的事务通常参与 Test-managed transactions。但是,如果你配置了任何传播类型而不是 REQUIREDSUPPORTS(请参阅事务传播 的讨论以获取详细信息)。spring-doc.cadn.net.cn

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

在使用任何形式的预设超时机制(来自测试框架)与Spring的测试管理事务结合时必须小心。spring-doc.cadn.net.cn

Specifically, Spring 的测试支持在调用当前测试方法之前将事务状态绑定到当前线程(通过 java.lang.ThreadLocal 变量)。如果测试框架为了支持抢占式超时而在新线程中调用当前测试方法,则在当前测试方法中执行的任何操作都不会在测试管理的事务中执行。因此,这些操作的结果不会随着测试管理的事务回滚。相反,这些操作将被提交到持久存储中——例如,关系数据库——即使测试管理的事务被 Spring 正确回滚。spring-doc.cadn.net.cn

这种情况包括但不限于以下几种。spring-doc.cadn.net.cn

启用和禁用事务

使用 @Transactional 注解测试方法会导致该测试在事务中运行,该事务默认在测试完成后自动回滚。 如果一个测试类使用 @Transactional 注解,则该类层次结构中的每个测试方法都在事务中运行。未使用 @Transactional 注解的测试方法(在类级别或方法级别)不在事务中运行。请注意,@Transactional 不适用于测试生命周期方法——例如,使用 JUnit Jupiter 的 @BeforeAll@BeforeEach 等注解的方法。此外,使用 @Transactional 注解但将 propagation 属性设置为 NOT_SUPPORTEDNEVER 的测试不会在事务中运行。spring-doc.cadn.net.cn

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

valuetransactionManagerspring-doc.cadn.net.cn

spring-doc.cadn.net.cn

propagationspring-doc.cadn.net.cn

仅支持 Propagation.NOT_SUPPORTEDPropagation.NEVERspring-doc.cadn.net.cn

isolationspring-doc.cadn.net.cn

nospring-doc.cadn.net.cn

timeoutspring-doc.cadn.net.cn

nospring-doc.cadn.net.cn

readOnlyspring-doc.cadn.net.cn

nospring-doc.cadn.net.cn

rollbackForrollbackForClassNamespring-doc.cadn.net.cn

使用 TestTransaction.flagForRollback() 替代spring-doc.cadn.net.cn

noRollbackFornoRollbackForClassNamespring-doc.cadn.net.cn

使用 TestTransaction.flagForCommit() 替代spring-doc.cadn.net.cn

方法级别的生命周期方法——例如,使用JUnit Jupiter的@BeforeEach@AfterEach注解的方法——在测试管理的事务中运行。另一方面,套件级别和类级别的生命周期方法——例如,使用JUnit Jupiter的@BeforeAll@AfterAll注解的方法以及使用TestNG的@BeforeSuite@AfterSuite@BeforeClass@AfterClass注解的方法——不在测试管理的事务中运行。spring-doc.cadn.net.cn

如果你需要在事务中运行套件级或类级的生命周期方法中的代码,你可能希望将相应的PlatformTransactionManager注入到你的测试类中,然后使用TransactionTemplate进行程序化事务管理。spring-doc.cadn.net.cn

以下示例演示了为基于Hibernate的UserRepository编写集成测试的常见场景:spring-doc.cadn.net.cn

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

Transaction Rollback and Commit Behavior中所述,无需在createUser()方法运行后清理数据库,因为对数据库所做的任何更改都会被TransactionalTestExecutionListener自动回滚。spring-doc.cadn.net.cn

事务回滚和提交行为

默认情况下,测试事务将在测试完成后自动回滚;但是,可以通过@Commit@Rollback注解声明式配置事务的提交和回滚行为。有关详细信息,请参阅注解支持部分。spring-doc.cadn.net.cn

程序化事务管理

你可以通过使用TestTransaction中的静态方法来编程式地与测试管理的事务进行交互。例如,你可以在测试方法、前置方法和后置方法中使用TestTransaction来开始或结束当前的测试管理事务,或者配置当前的测试管理事务以回滚或提交。每当启用TransactionalTestExecutionListener时,对TestTransaction的支持会自动可用。spring-doc.cadn.net.cn

以下示例演示了TestTransaction的一些功能。有关详细信息,请参阅TestTransaction的Javadoc。spring-doc.cadn.net.cn

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 确保您的前置事务方法或后置事务方法在适当的时间运行。spring-doc.cadn.net.cn

任何前置方法(例如使用JUnit Jupiter的@BeforeEach注解的方法) 和任何后置方法(例如使用JUnit Jupiter的@AfterEach注解的方法)都在事务中运行。此外,使用@BeforeTransaction@AfterTransaction注解的方法不会为未配置在事务中运行的测试方法执行。
配置事务管理器

TransactionalTestExecutionListener 期望在 Spring ApplicationContext 中定义一个 PlatformTransactionManager bean 用于测试。如果在测试的 ApplicationContext 中有多个 PlatformTransactionManager 实例,你可以通过使用 @Transactional("myTxMgr")@Transactional(transactionManager = "myTxMgr"),或实现 TransactionManagementConfigurer 接口的 @Configuration 类来声明限定符。 请参阅 javadoc for TestContextTransactionUtils.retrieveTransactionManager() 以获取在测试的 ApplicationContext 中查找事务管理器所使用的算法的详细信息。spring-doc.cadn.net.cn

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

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

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的示例测试用例中,一个方法展示了假阳性,而另一个方法正确地暴露了刷新会话的结果:spring-doc.cadn.net.cn

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的匹配方法:spring-doc.cadn.net.cn

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

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

与在测试ORM代码时避免出现 误报 的注意事项类似,如果您的应用程序使用了实体生命周期回调(也称为实体监听器),请确保在运行该代码的测试方法中刷新底层的工作单元。未能 刷新清除 底层的工作单元可能导致某些生命周期回调未被调用。spring-doc.cadn.net.cn

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

以下示例说明如何将 EntityManager 刷新,以确保在实体被持久化时调用 @PostPersist 回调方法。已为示例中使用的 Person 实体注册了一个具有 @PostPersist 回调方法的实体监听器。spring-doc.cadn.net.cn

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

// ...

查看 JpaEntityListenerTests 中使用所有JPA生命周期回调的Spring框架测试套件中的示例。spring-doc.cadn.net.cn

3.5.10. 执行 SQL 脚本

在编写针对关系数据库的集成测试时,通常有益于运行SQL脚本来修改数据库模式或将测试数据插入表中。spring-jdbc模块提供了通过执行SQL脚本初始化嵌入式或现有数据库的支持。当Spring ApplicationContext 加载时。请参阅嵌入式数据库支持嵌入式数据库支持以及使用嵌入式数据库测试数据访问逻辑以获取详细信息。spring-doc.cadn.net.cn

虽然在加载 ApplicationContext 时一次性初始化数据库以进行测试非常有用,但有时在集成测试期间能够修改数据库是必不可少的。以下部分解释了如何在集成测试期间程序化和声明性地运行SQL脚本。spring-doc.cadn.net.cn

通过编程方式执行SQL脚本

Spring 提供了以下选项,用于在集成测试方法中程序化地执行 SQL 脚本。spring-doc.cadn.net.cn

ScriptUtils 提供了一组静态实用方法,用于处理 SQL 脚本,并主要用于框架内部。但是,如果你需要完全控制 SQL 脚本的解析和运行方式,ScriptUtils 可能比后面描述的一些其他替代方案更适合你的需求。有关 ScriptUtils 中各个方法的详细信息,请参阅 javadocspring-doc.cadn.net.cn

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

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 脚本。有关各种 executeSqlScript(..) 方法的详细信息,请参阅 Javadoc。spring-doc.cadn.net.cn

声明式执行SQL脚本与@Sql

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

Method-level @Sql 声明默认会覆盖类级别的声明。然而,从 Spring Framework 5.2 开始,这种行为可以通过 @SqlMergeMode 按照测试类或测试方法进行配置。有关详细信息,请参阅 使用 @SqlMergeMode 合并和覆盖配置
路径资源语义

每个路径被解释为Spring Resource。一个普通的路径(例如,"schema.sql") 被视为相对于定义测试类的包的类路径资源。以斜杠开头的路径被视为绝对类路径资源(例如,"/org/example/schema.sql")。引用URL的路径(例如,以classpath:, file:, http: 开头的路径)使用指定的资源协议进行加载。spring-doc.cadn.net.cn

以下示例展示了如何在基于JUnit Jupiter的集成测试类中在类级别和方法级别使用@Sqlspring-doc.cadn.net.cn

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脚本或语句,则会尝试检测default 脚本,具体取决于@Sql的声明位置。如果无法检测到默认值,则会抛出 IllegalStateExceptionspring-doc.cadn.net.cn

  • 类级别声明:如果注解的测试类是com.example.MyTest,则对应的默认脚本是classpath:com/example/MyTest.sqlspring-doc.cadn.net.cn

  • 方法级别的声明:如果注解的测试方法名为 testMethod() 并且在类 com.example.MyTest 中定义,则对应的默认脚本是 classpath:com/example/MyTest.testMethod.sqlspring-doc.cadn.net.cn

声明多个@Sql集合

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

以下示例展示了如何在Java 8中使用@Sql作为可重复注解:spring-doc.cadn.net.cn

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

在前面示例中呈现的情景下,test-schema.sql 脚本使用了不同的语法来表示单行注释。spring-doc.cadn.net.cn

以下示例与前面的示例相同,只是将 @Sql 声明组合在一起放在 @SqlGroup 中。从 Java 8 及更高版本开始,使用 @SqlGroup 是可选的,但您可能需要使用 @SqlGroup 以与其他 JVM 语言(如 Kotlin)兼容。spring-doc.cadn.net.cn

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脚本会在对应的测试方法之前运行。但是,如果你需要在测试方法之后运行特定的一组脚本(例如,清理数据库状态),你可以使用executionPhase属性在@Sql中,如下例所示:spring-doc.cadn.net.cn

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
}

请注意 ISOLATEDAFTER_TEST_METHOD 分别从 Sql.TransactionModeSql.ExecutionPhase 静态导入。spring-doc.cadn.net.cn

脚本配置与@SqlConfig

您可以通过使用@SqlConfig注解来配置脚本解析和错误处理。 当作为集成测试类上的类级注解声明时,@SqlConfig 作为测试类层次结构中所有SQL脚本的全局配置。当 直接使用@Sql注解的config属性声明时,@SqlConfig 作为封装的@Sql注解内声明的SQL脚本的本地配置。@SqlConfig中的每个属性都有一个隐式默认值,这在相应属性的Javadoc中有文档说明。由于Java语言规范中定义的注解属性规则,不幸的是,无法将null值分配给注解属性。因此,为了支持继承的全局配置的覆盖,@SqlConfig属性具有显式的默认值""(对于字符串)、{}(对于数组)或DEFAULT(对于枚举)。当 使用@Sql注解的config属性直接声明时,@SqlConfig 作为本地配置。因此,为了支持继承的全局配置的覆盖,@SqlConfig属性具有""(对于字符串)、{}(对于数组)或DEFAULT(对于枚举)的显式默认值。这种做法使得@SqlConfig注解内的SQL脚本的本地配置。@SqlConfig注解内的SQL脚本的本地配置。每个属性在@SqlConfig中都有一个隐式默认值,这在相应属性的Javadoc中有文档说明。因此,@SqlConfig注解内的SQL脚本的本地配置。每个属性在@SqlConfig中都有一个隐式默认值。全局配置@SqlConfig 注解内的SQL脚本的本地配置。@SqlConfig注解内的SQL脚本的本地配置。@SqlConfig注解内的SQL脚本的本地配置。""注解内的SQL脚本的本地配置。{}注解内的SQL脚本的本地配置。DEFAULT注解内的SQL脚本的本地配置。22注解内的SQL脚本的本地配置。23注解内的SQL脚本的本地配置。24注解内的SQL脚本的本地配置。25注解内的SQL脚本的本地配置。26注解内的SQL脚本的本地配置。27注解内的SQL脚本的本地配置。28注解内的SQL脚本的本地配置。29注解内的SQL脚本的本地配置。30注解内的SQL脚本的本地配置。31注解内的SQL脚本的本地配置。32注解内的SQL脚本的本地配置。33注解内的SQL脚本的本地配置。34注解内的SQL脚本的本地配置。35注解内的SQL脚本的本地配置。36注解内的SQL脚本的本地配置。37注解内的SQL脚本的本地配置。38注解内的SQL脚本的本地配置。39注解内的SQL脚本的本地配置。40注解内的SQL脚本的本地配置。41注解内的SQL脚本的本地配置。42注解内的SQL脚本的本地配置。43注解内的SQL脚本的本地配置。44注解内的SQL脚本的本地配置。45注解内的SQL脚本的本地配置。46注解内的SQL脚本的本地配置。47注解内的SQL脚本的本地配置。48注解内的SQL脚本的本地配置。49注解内的SQL脚本的本地配置。50注解内的SQL脚本的本地配置。51注解内的SQL脚本的本地配置。52注解内的SQL脚本的本地配置。53注解内的SQL脚本的本地配置。54注解内的SQL脚本的本地配置。55注解内的SQL脚本的本地配置。56注解内的SQL脚本的本地配置。57注解内的SQL脚本的本地配置。58注解内的SQL脚本的本地配置。59注解内的SQL脚本的本地配置。60注解内的SQL脚本的本地配置。61注解内的SQL脚本的本地配置。62注解内的SQL脚本的本地配置。63注解内的SQL脚本的本地配置。64注解内的SQL脚本的本地配置。65注解内的SQL脚本的本地配置。66注解内的SQL脚本的本地配置。67注解内的SQL脚本的本地配置。68注解内的SQL脚本的本地配置。69注解内的SQL脚本的本地配置。70注解内的SQL脚本的本地配置。71注解内的SQL脚本的本地配置。72注解内的SQL脚本的本地配置。73注解内的SQL脚本的本地配置。74注解内的SQL脚本的本地配置。75注解内的SQL脚本的本地配置。76注解内的SQL脚本的本地配置。77注解内的SQL脚本的本地配置。78注解内的SQL脚本的本地配置。79注解内的SQL脚本的本地配置。80注解内的SQL脚本的本地配置。81注解内的SQL脚本的本地配置。82注解内的SQL脚本的本地配置。83注解内的SQL脚本的本地配置。84注解内的SQL脚本的本地配置。85注解内的SQL脚本的本地配置。86注解内的SQL脚本的本地配置。87注解内的SQL脚本的本地配置。88注解内的SQL脚本的本地配置。89注解内的SQL脚本的本地配置。90注解内的SQL脚本的本地配置。91注解内的SQL脚本的本地配置。92注解内的SQL脚本的本地配置。93注解内的SQL脚本的本地配置。94注解内的SQL脚本的本地配置。95注解内的SQL脚本的本地配置。96注解内的SQL脚本的本地配置。97注解内的SQL脚本的本地配置。98注解内的SQL脚本的本地配置。99注解内的SQL脚本的本地配置。100注解内的SQL脚本的本地配置。101注解内的SQL脚本的本地配置。102注解内的SQL脚本的本地配置。103注解内的SQL脚本的本地配置。104注解内的SQL脚本的本地配置。105注解内的SQL脚本的本地配置。106注解内的SQL脚本的本地配置。107注解内的SQL脚本的本地配置。108注解内的SQL脚本的本地配置。109注解内的SQL脚spring-doc.cadn.net.cn

The configuration options provided by @Sql and @SqlConfig are equivalent to those supported by ScriptUtils and ResourceDatabasePopulator but are a superset of those provided by the <jdbc:initialize-database/> XML namespace element. See the javadoc of individual attributes in @Sql and @SqlConfig for details.spring-doc.cadn.net.cn

事务管理用于@Sqlspring-doc.cadn.net.cn

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

如果用于检测DataSourcePlatformTransactionManager并推断事务语义的算法不满足您的需求,您可以通过设置@SqlConfigdataSourcetransactionManager属性来指定明确的名称。此外,您可以通过设置@SqlConfigtransactionMode属性来控制事务传播行为(例如,脚本是否应在隔离事务中运行)。虽然详细讨论所有支持的事务管理选项超出了本参考手册的范围,但@SqlConfigSqlScriptsTestExecutionListenertransactionMode属性来控制事务传播行为(例如,脚本是否应在一个隔离的事务中运行)。尽管使用@Sql进行事务管理的所有支持选项的详尽讨论超出了本参考手册的范围,但@SqlConfigSqlScriptsTestExecutionListener的Javadoc提供了详细信息,以下示例展示了一个典型的测试场景,该场景使用JUnit Jupiter和@Sql的事务性测试:spring-doc.cadn.net.cn

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-data.sql脚本中)都会被TransactionalTestExecutionListener自动回滚(有关详细信息,请参见事务管理)。spring-doc.cadn.net.cn

合并和覆盖配置与@SqlMergeMode

从Spring Framework 5.2开始,可以在方法级别合并@Sql声明与类级别声明。例如,这允许您为测试类提供一次数据库模式或一些通用测试数据的配置,然后为每个测试方法提供额外的、特定于用例的测试数据。要启用@Sql合并,请使用@SqlMergeMode(MERGE)注解标记您的测试类或测试方法。要为特定测试方法(或特定测试子类)禁用合并,可以通过@SqlMergeMode(OVERRIDE)切换回默认模式。有关@SqlMergeMode注解文档部分的示例和详细信息,请参阅@SqlMergeMode注解文档部分spring-doc.cadn.net.cn

3.5.11. 并行测试执行

Spring Framework 5.0 引入了基本支持,可以在使用 Spring TestContext Framework 时在单个 JVM 内并行执行测试。一般来说,这意味着大多数测试类或测试方法可以在不更改测试代码或配置的情况下并行运行。spring-doc.cadn.net.cn

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

请注意,将并发引入您的测试套件可能会导致意外的副作用、奇怪的运行时行为,以及间歇性或看似随机失败的测试。因此,Spring 团队提供了以下一般指导原则,说明何时不应并行运行测试。spring-doc.cadn.net.cn

如果测试满足以下条件,请勿并行运行测试:spring-doc.cadn.net.cn

  • 使用Spring框架的@DirtiesContext支持。spring-doc.cadn.net.cn

  • 使用Spring Boot的@MockBean@SpyBean支持。spring-doc.cadn.net.cn

  • 使用JUnit 4的@FixMethodOrder支持或任何测试框架功能 以确保测试方法按特定顺序运行。请注意, 如果整个测试类并行运行,则此规则不适用。spring-doc.cadn.net.cn

  • 更改共享服务或系统的状态,例如数据库、消息代理、文件系统等。这适用于嵌入式和外部系统。spring-doc.cadn.net.cn

如果并行测试执行失败,并且异常表明当前测试的 ApplicationContext 不再处于活动状态,这通常意味着在另一个线程中 ApplicationContext 已从 ContextCache 中移除。spring-doc.cadn.net.cn

这可能是由于使用了 @DirtiesContext 或者是由于自动从 ContextCache 中移除。如果 @DirtiesContext 是罪魁祸首,您需要找到一种方法来避免使用 @DirtiesContext 或将此类测试排除在并行执行之外。如果 ContextCache 的最大大小已被超出,您可以增加缓存的最大大小。有关详细信息,请参见关于 上下文缓存 的讨论。spring-doc.cadn.net.cn

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

3.5.12. TestContext 框架支持类

本节描述了支持 Spring TestContext 框架的各种类。spring-doc.cadn.net.cn

Spring JUnit 4 Runner

Spring TestContext框架通过自定义运行器(在JUnit 4.12或更高版本中支持)提供了与JUnit 4的全面集成。通过使用@RunWith(SpringJUnit4ClassRunner.class)或较短的@RunWith(SpringRunner.class)注解测试类,开发人员可以实现标准的基于JUnit 4的单元和集成测试,并同时享受TestContext框架的好处,例如支持加载应用程序上下文、测试实例的依赖注入、事务性测试方法执行等。如果你想使用Spring TestContext框架与替代运行器(如JUnit 4的Parameterized运行器)或第三方运行器(如MockitoJUnitRunner),你可以选择使用Spring对JUnit规则的支持spring-doc.cadn.net.cn

以下代码示例展示了配置一个使用自定义Spring Runner运行的测试类的最低要求:spring-doc.cadn.net.cn

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 配置一个 ApplicationContextspring-doc.cadn.net.cn

Spring JUnit 4 规则

The org.springframework.test.context.junit4.rules 包提供了以下 JUnit 4 规则(支持 JUnit 4.12 或更高版本):spring-doc.cadn.net.cn

SpringClassRule 是一个 JUnit TestRule,它支持 Spring TestContext Framework 的类级别特性,而 SpringMethodRule 是一个 JUnit MethodRule,它支持 Spring TestContext Framework 的实例级别和方法级别特性。spring-doc.cadn.net.cn

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

要支持TestContext框架的全部功能,您必须将SpringClassRuleSpringMethodRule结合使用。以下示例展示了在集成测试中正确声明这些规则的方式:spring-doc.cadn.net.cn

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 支持类

The org.springframework.test.context.junit4 包提供了以下支持类,用于基于 JUnit 4 的测试用例(在 JUnit 4.12 或更高版本中受支持):spring-doc.cadn.net.cn

AbstractJUnit4SpringContextTests 是一个抽象的基础测试类,它将 Spring TestContext Framework 与显式的 ApplicationContext 测试支持集成在一个 JUnit 4 环境中。当你扩展 AbstractJUnit4SpringContextTests 时,你可以访问一个 protected applicationContext 实例变量,你可以使用它来执行显式的 bean 查找或测试上下文的整体状态。spring-doc.cadn.net.cn

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

这些类是为了方便扩展。如果你不希望你的测试类依赖于Spring特定的类层次结构,你可以通过使用@RunWith(SpringRunner.class)Spring的JUnit规则来配置你自己的自定义测试类。
Spring扩展用于JUnit Jupiter

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

此外,由于 JUnit Jupiter 中丰富的扩展 API,Spring 提供了以下功能,这些功能超出了 Spring 对 JUnit 4 和 TestNG 支持的功能集:spring-doc.cadn.net.cn

以下代码示例展示了如何配置一个测试类以使用SpringExtension@ContextConfiguration结合使用:spring-doc.cadn.net.cn

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

Since you can also use annotations in JUnit 5 as meta-annotations, Spring provides the @SpringJUnitConfig and @SpringJUnitWebConfig composed annotations to simplify the configuration of the test ApplicationContext and JUnit Jupiter.spring-doc.cadn.net.cn

以下示例使用 @SpringJUnitConfig 来减少前一个示例中使用的配置量:spring-doc.cadn.net.cn

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

Similarly, the following example uses @SpringJUnitWebConfig to create a WebApplicationContext for use with JUnit Jupiter: 同样,以下示例使用 @SpringJUnitWebConfig 来创建一个 WebApplicationContext 用于 JUnit Jupiter:spring-doc.cadn.net.cn

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

请参阅 @SpringJUnitConfig@SpringJUnitWebConfigSpring JUnit Jupiter 测试注解 中的文档以获取更多详细信息。spring-doc.cadn.net.cn

依赖注入与SpringExtension

SpringExtension 实现了 ParameterResolver JUnit Jupiter 扩展 API,这使得 Spring 可以为测试构造函数、测试方法和测试生命周期回调方法提供依赖注入。spring-doc.cadn.net.cn

Specifically, SpringExtension 可以从测试的 ApplicationContext 注入依赖到使用 @BeforeAll, @AfterAll, @BeforeEach, @AfterEach, @Test, @RepeatedTest, @ParameterizedTest, 以及其他注解标注的测试构造函数和方法中。spring-doc.cadn.net.cn

构造器注入

如果JUnit Jupiter测试类的构造函数中的特定参数是类型 ApplicationContext(或其子类型)或被注解或元注解为 @Autowired@Qualifier@Value,Spring会将该特定参数的值注入相应的bean或从测试的ApplicationContext中获取的值。spring-doc.cadn.net.cn

Spring 也可以配置为自动装配测试类构造函数的所有参数,如果该构造函数被认为是可自动装配的。一个构造函数被认为是可自动装配的,如果满足以下条件之一(按优先级顺序)。spring-doc.cadn.net.cn

请参阅 @TestConstructor 了解有关使用 @TestConstructor 的详细信息以及如何更改全局 测试构造器自动装配模式spring-doc.cadn.net.cn

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

Constructor injection for test classes must not be used in conjunction with JUnit Jupiter’s @TestInstance(PER_CLASS) support if @DirtiesContext is used to close the test’s ApplicationContext before or after test methods.spring-doc.cadn.net.cn

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

要使用 @DirtiesContext 在 "before test method" 或 "after test method" 模式下与 @TestInstance(PER_CLASS) 结合使用,必须配置 Spring 依赖项通过字段或 setter 注入提供,以便它们可以在测试方法调用之间重新注入。spring-doc.cadn.net.cn

在以下示例中,Spring 从 TestConfig.class 加载的 ApplicationContext 中注入 OrderService bean 到 OrderServiceIntegrationTests 构造函数中。spring-doc.cadn.net.cn

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-doc.cadn.net.cn

如果 spring.test.constructor.autowire.mode 属性设置为 all(参见 @TestConstructor),我们可以在之前的示例中省略构造函数中的 @Autowired 声明,结果如下。spring-doc.cadn.net.cn

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-doc.cadn.net.cn

在以下示例中,Spring 从 TestConfig.class 中加载的 ApplicationContext 注入到 deleteOrder() 测试方法中:spring-doc.cadn.net.cn

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-doc.cadn.net.cn

以下示例展示了如何同时让Spring和JUnit Jupiter将依赖项注入到placeOrderRepeatedly()测试方法中。spring-doc.cadn.net.cn

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的访问。spring-doc.cadn.net.cn

@Nested 测试类配置

Spring TestContext Framework 自 Spring Framework 5.0 起已支持在 JUnit Jupiter 的 @Nested 测试类上使用与测试相关的注解;然而,直到 Spring Framework 5.3 之前,从包含此类的类中继承的类级别测试配置注解并未像从超类中那样被 继承spring-doc.cadn.net.cn

Spring Framework 5.3 首次支持从外部类继承测试类配置,并且这种配置默认会得到继承。要从默认的 INHERIT 模式更改为 OVERRIDE 模式,可以使用 @NestedTestConfiguration(EnclosingConfiguration.OVERRIDE) 注解单独的 @Nested 测试类。显式的 @NestedTestConfiguration 声明将适用于注解的测试类以及其任何子类和内部类。因此,您可以使用 @NestedTestConfiguration 注解一个顶层测试类,这将递归地应用于其所有内部测试类。spring-doc.cadn.net.cn

为了允许开发团队将默认值更改为 OVERRIDE - 例如,为了与 Spring Framework 5.0 到 5.2 兼容 - 可以通过 JVM 系统属性或类路径根目录下的 spring.properties 文件在全球范围内更改默认模式。有关详细信息,请参阅 "更改默认封装配置继承模式" 的说明。spring-doc.cadn.net.cn

尽管下面的“Hello World”示例非常简单,但它展示了如何在顶级类中声明常规配置,该配置被其<code>0</code>个测试类继承。在本例中,仅继承了<code>1</code>个配置类。每个嵌套测试类都提供自己的一组激活的profile,从而为每个嵌套测试类生成一个不同的<code>2</code>(有关详细信息,请参见<a t="C4">上下文缓存</a>)。查阅<a t="C5">支持的注解</a>列表,了解哪些注解可以被<code>3</code>测试类继承。spring-doc.cadn.net.cn

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支持类

The org.springframework.test.context.testng 包提供了以下支持类用于基于 TestNG 的测试用例:spring-doc.cadn.net.cn

AbstractTestNGSpringContextTests 是一个抽象基础测试类,它将 Spring TestContext Framework 与显式的 ApplicationContext 测试支持集成在一个 TestNG 环境中。当你扩展 AbstractTestNGSpringContextTests 时,你可以访问一个 protected applicationContext 实例变量,你可以使用它来执行显式的 bean 查找或测试上下文的整体状态。spring-doc.cadn.net.cn

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

这些类是为了方便扩展。如果你不希望你的测试类依赖于Spring特定的类层次结构,你可以通过使用@ContextConfiguration@TestExecutionListeners等,并手动为你的测试类添加一个TestContextManager来配置你自己的自定义测试类。请参阅AbstractTestNGSpringContextTests的源代码,了解如何为你的测试类添加一个TestContextManager的例子。

3.6. WebTestClient

WebTestClient 是一个用于测试服务器应用程序的 HTTP 客户端。它封装了 Spring 的 WebClient,并使用它来执行请求, 但为验证响应提供了测试外观。WebTestClient 可以用于执行端到端的 HTTP 测试。它也可以通过模拟服务器请求和响应对象来测试 Spring MVC 和 Spring WebFlux 应用程序,而无需运行服务器。spring-doc.cadn.net.cn

Kotlin 用户:请参阅 此部分 有关使用 WebTestClient 的内容。

3.6.1. 配置

要设置一个 WebTestClient,您需要选择一个要绑定的服务器设置。这可以是几种模拟服务器设置选项之一,或者连接到一个实时服务器。spring-doc.cadn.net.cn

绑定到控制器

此设置允许你通过模拟请求和响应对象来测试特定的控制器, 而无需运行服务器。spring-doc.cadn.net.cn

对于WebFlux应用程序,请使用以下内容,它会加载等效于 WebFlux Java配置的基础结构, 注册给定的 控制器(s),并创建一个 WebHandler链 来处理请求:spring-doc.cadn.net.cn

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

对于 Spring MVC,请使用以下内容,它将委托给 StandaloneMockMvcBuilder 以加载等效于 WebMvc Java 配置 的基础设施, 注册给定的控制器,并创建一个 MockMvc 实例来处理请求:spring-doc.cadn.net.cn

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

此设置允许你使用 Spring MVC 或 Spring WebFlux 基础设施和控制器声明加载 Spring 配置,并通过模拟请求和响应对象来处理请求,而无需运行服务器。spring-doc.cadn.net.cn

对于 WebFlux,请在将 Spring ApplicationContext 传递到 WebHttpHandlerBuilder 以创建处理 WebHandler 链 的请求时使用以下内容:spring-doc.cadn.net.cn

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 实例的请求时使用以下内容:spring-doc.cadn.net.cn

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 指定要加载的配置
2 注入配置
3 创建 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 指定要加载的配置
2 注入配置
3 创建 WebTestClient
绑定到路由函数

此设置允许您通过模拟请求和响应对象测试 功能端点,而无需运行服务器。spring-doc.cadn.net.cn

对于 WebFlux,请使用以下代码,它会将 RouterFunctions.toWebHandler 委托给以创建一个处理请求的服务器设置:spring-doc.cadn.net.cn

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

对于Spring MVC,目前没有选项来测试 WebMvc功能端点spring-doc.cadn.net.cn

绑定到服务器

此设置连接到运行中的服务器以执行完整的端到端HTTP测试:spring-doc.cadn.net.cn

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

除了前面描述的服务器设置选项外,您还可以配置客户端选项,包括基础URL、默认头、客户端过滤器等。这些选项在bindToServer()之后可以直接使用。对于所有其他配置选项,您需要使用configureClient()从服务器配置切换到客户端配置,如下所示:spring-doc.cadn.net.cn

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 的文档。spring-doc.cadn.net.cn

在调用 exchange() 之后,WebTestClient 会与 WebClient 分离,并继续执行验证响应的工作流程。spring-doc.cadn.net.cn

要断言响应状态和头信息,请使用以下方法:spring-doc.cadn.net.cn

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 中的 软断言 支持以及 JUnit Jupiter 中的 assertAll() 支持。spring-doc.cadn.net.cn

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

然后你可以选择通过以下方式对响应体进行解码:spring-doc.cadn.net.cn

并对生成的高级对象执行断言:spring-doc.cadn.net.cn

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)

如果内置的断言不够用,你可以直接使用该对象并执行任何其他断言:spring-doc.cadn.net.cn

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

或者您可以退出工作流并获取 EntityExchangeResultspring-doc.cadn.net.cn

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> 的重载方法。
没有内容

如果响应预期不包含内容,可以按以下方式断言:spring-doc.cadn.net.cn

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

如果你想忽略响应内容,以下方式会释放内容而没有任何断言:spring-doc.cadn.net.cn

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() 而不指定目标类型,以对原始内容进行断言,而不是通过高级对象进行。spring-doc.cadn.net.cn

要使用 JSONAssert 验证完整的 JSON 内容:spring-doc.cadn.net.cn

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 内容:spring-doc.cadn.net.cn

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",请先验证响应状态和标题,然后 获取一个 FluxExchangeResultspring-doc.cadn.net.cn

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

现在您可以通过 StepVerifierreactor-test 消费响应流:spring-doc.cadn.net.cn

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 客户端,因此它只能验证客户端响应中的内容,包括状态、头信息和正文。spring-doc.cadn.net.cn

在使用 MockMvc 服务器设置测试 Spring MVC 应用程序时,你还可以对服务器响应执行进一步的断言。要做到这一点,请在断言正文后获取一个 ExchangeResultspring-doc.cadn.net.cn

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 服务器响应断言:spring-doc.cadn.net.cn

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 请求处理。spring-doc.cadn.net.cn

MockMvc 可以单独使用来执行请求并验证响应。它也可以通过 WebTestClient 使用,其中 MockMvc 作为服务器来处理请求。WebTestClient 的优势在于可以使用高级对象而不是原始数据,以及能够切换到针对实时服务器的完整端到端 HTTP 测试,并使用相同的测试 API。spring-doc.cadn.net.cn

3.7.1. 概述

你可以通过实例化控制器、为其注入依赖项并调用其方法来为Spring MVC编写普通的单元测试。然而,这样的测试不会验证请求映射、数据绑定、消息转换、类型转换、验证,也不会涉及任何支持的 @InitBinder@ModelAttribute@ExceptionHandler 方法。spring-doc.cadn.net.cn

Spring MVC 测试框架,也称为 MockMvc,旨在在不运行服务器的情况下为 Spring MVC 控制器提供更全面的测试。它是通过调用 DispatcherServlet 并传递来自 spring-test 模块的 Servlet API 的“模拟”实现来完成的,这些实现复制了完整的 Spring MVC 请求处理过程而无需运行服务器。spring-doc.cadn.net.cn

MockMvc 是一个服务器端测试框架,它允许你通过轻量级和针对性的测试来验证 Spring MVC 应用的大部分功能。你可以单独使用它来执行请求并验证响应,或者也可以通过 WebTestClient API 使用 MockMvc 作为服务器来处理请求。spring-doc.cadn.net.cn

静态导入

当直接使用 MockMvc 执行请求时,你需要静态导入以下内容:spring-doc.cadn.net.cn

一个容易记住的方法是搜索 MockMvc*。如果使用 Eclipse,请确保在 Eclipse 首选项中也将上述内容添加为“常用静态成员”。spring-doc.cadn.net.cn

通过 WebTestClient 使用 MockMvc 时不需要静态导入。 WebTestClient 提供了一个无需静态导入的流畅 API。spring-doc.cadn.net.cn

设置选项

MockMvc可以通过两种方式设置。一种是直接指向您要测试的控制器,并以编程方式配置Spring MVC基础设施。另一种是指向包含Spring MVC和控制器基础设施的Spring配置。spring-doc.cadn.net.cn

要为测试特定控制器设置 MockMvc,请使用以下方法:spring-doc.cadn.net.cn

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-doc.cadn.net.cn

通过Spring配置设置MockMvc,请使用以下方式:spring-doc.cadn.net.cn

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

    MockMvc mockMvc;

    @BeforeEach
    void setup(WebApplicationContext wac) {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(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 进行测试时使用此设置, 它会将相同构建器委托给上面所示的内容。spring-doc.cadn.net.cn

你应该使用哪种设置选项?spring-doc.cadn.net.cn

The webAppContextSetup 加载你的实际 Spring MVC 配置,从而实现更完整的集成测试。由于 TestContext 框架缓存了加载的 Spring 配置,因此有助于保持测试运行速度,即使你在测试套件中引入更多测试也是如此。此外,你可以通过 Spring 配置将模拟服务注入到控制器中,以便专注于测试 web 层。以下示例声明了一个使用 Mockito 的模拟服务:spring-doc.cadn.net.cn

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

然后可以将模拟服务注入测试中,以设置和验证您的预期,如下例所示:spring-doc.cadn.net.cn

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

    // ...

}

The standaloneSetup, 另一方面,更接近于单元测试。它一次测试一个控制器。你可以手动注入控制器并使用模拟依赖项,并且不需要加载Spring配置。这样的测试更专注于样式,更容易看到正在测试哪个控制器,是否需要任何特定的Spring MVC配置来工作,等等。standaloneSetup 也是一种非常方便的方式来编写临时测试以验证特定行为或调试问题。spring-doc.cadn.net.cn

与大多数“集成测试与单元测试”的争论一样,没有绝对的对错答案。但是,使用standaloneSetup确实意味着需要额外的webAppContextSetup测试来验证你的Spring MVC配置。或者,你可以使用webAppContextSetup编写所有测试,以便始终针对实际的Spring MVC配置进行测试。spring-doc.cadn.net.cn

设置功能

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

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 会话。您可以如下使用它:spring-doc.cadn.net.cn

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

请参阅javadoc以获取 ConfigurableMockMvcBuilder 中列出的所有MockMvc构建器功能,或使用IDE探索可用选项。spring-doc.cadn.net.cn

执行请求

本节展示如何单独使用MockMvc来执行请求并验证响应。 如果通过 WebTestClient 使用MockMvc,请参阅相应的 编写测试 部分。spring-doc.cadn.net.cn

要执行使用任何HTTP方法的请求,如下例所示:spring-doc.cadn.net.cn

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,因此实际上并不会解析多部分请求。相反,您需要将其设置为类似于以下示例:spring-doc.cadn.net.cn

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 模板样式中指定查询参数,如下例所示:spring-doc.cadn.net.cn

Java
mockMvc.perform(get("/hotels?thing={thing}", "somewhere"));
Kotlin
mockMvc.get("/hotels?thing={thing}", "somewhere")

您还可以添加表示查询或表单参数的Servlet请求参数,如下例所示:spring-doc.cadn.net.cn

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(…​)方法提供的请求参数则需要已经解码。spring-doc.cadn.net.cn

在大多数情况下,最好将上下文路径和Servlet路径排除在请求URI之外。如果你必须使用完整的请求URI进行测试,请确保相应地设置contextPathservletPath,以便请求映射能够正常工作,如下例所示:spring-doc.cadn.net.cn

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 会很繁琐。相反,您可以设置默认的请求属性,如下例所示:spring-doc.cadn.net.cn

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无关紧要,因为它们必须在每个请求中指定。spring-doc.cadn.net.cn

定义期望

您可以通过在执行请求后追加一个或多个andExpect(..)调用来定义预期,如下例所示。一旦某个预期失败,其他预期将不会被断言。spring-doc.cadn.net.cn

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(..) 确保所有提供的预期都会被断言,并且所有失败都会被跟踪并报告。spring-doc.cadn.net.cn

Java
// static import of MockMvcRequestBuilders.* and MockMvcResultMatchers.*

mockMvc.perform(get("/accounts/1")).andExpectAll(
    status().isOk(),
    content().contentType("application/json;charset=UTF-8"));

MockMvcResultMatchers.* 提供了一些期望,其中一些进一步嵌套了更详细的期望。spring-doc.cadn.net.cn

期望大致可以分为两类。第一类断言验证响应的属性(例如,响应状态、标头和内容)。这些是最重要的需要断言的结果。spring-doc.cadn.net.cn

第二类断言超出了响应的范围。这些断言允许你检查Spring MVC特有的方面,例如处理请求的控制器方法、是否引发了异常并被处理、模型的内容是什么、选择了哪个视图、添加了哪些闪存属性等等。它们还允许你检查Servlet特有的方面,例如请求和会话属性。spring-doc.cadn.net.cn

以下测试断言绑定或验证失败:spring-doc.cadn.net.cn

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中静态导入的:spring-doc.cadn.net.cn

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() 方法,该方法将结果数据作为单个 DEBUG 消息在 org.springframework.test.web.servlet.result 日志类别下记录结果数据。spring-doc.cadn.net.cn

在某些情况下,你可能需要直接访问结果并验证一些其他方式无法验证的内容。这可以通过在所有其他期望值之后附加 .andReturn() 来实现,如下例所示:spring-doc.cadn.net.cn

Java
MvcResult mvcResult = mockMvc.perform(post("/persons")).andExpect(status().isOk()).andReturn();
// ...
Kotlin
var mvcResult = mockMvc.post("/persons").andExpect { status { isOk() } }.andReturn()
// ...

如果所有测试都重复相同的预期,你可以在构建MockMvc实例时一次性设置共同的预期,如下例所示:spring-doc.cadn.net.cn

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实例。spring-doc.cadn.net.cn

当JSON响应内容包含使用Spring HATEOAS创建的超媒体链接时,你可以通过使用JsonPath表达式来验证生成的链接,如下例所示:spring-doc.cadn.net.cn

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表达式来验证生成的链接:spring-doc.cadn.net.cn

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-doc.cadn.net.cn

Servlet 3.0 异步请求,在Spring MVC中受支持,通过退出Servlet容器线程并允许应用程序异步计算响应来工作,之后在Servlet容器线程上进行异步调度以完成处理。spring-doc.cadn.net.cn

在Spring MVC测试中,可以通过断言生成的异步值来测试异步请求,然后手动执行异步分发,最后验证响应。以下是针对返回DeferredResultCallable或响应式类型(如Reactor Mono)的控制器方法的测试示例:spring-doc.cadn.net.cn

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 来测试 流式响应 如服务器发送事件。但是,MockMvcWebTestClient 不支持无限流 因为从客户端无法取消服务器流。 要测试无限流,您需要 绑定到一个运行中的服务器, 或者在使用 Spring Boot 时, 使用运行中的服务器进行测试spring-doc.cadn.net.cn

过滤注册

在设置MockMvc实例时,您可以注册一个或多个Servlet Filter实例,如下例所示:spring-doc.cadn.net.cn

Java
mockMvc = standaloneSetup(new PersonController()).addFilters(new CharacterEncodingFilter()).build();
Kotlin
// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed

已注册的过滤器通过 MockFilterChainspring-test 调用,并且最后一个过滤器委托给 DispatcherServletspring-doc.cadn.net.cn

MockMvc 与端到端测试

MockMvc 是基于来自 spring-test 模块的 Servlet API 模拟实现构建的,并不依赖于运行中的容器。因此,与使用实际客户端和运行中的服务器进行完整的端到端集成测试相比,存在一些差异。spring-doc.cadn.net.cn

最简单的方法是从一个空白的MockHttpServletRequest开始。 无论你向它添加什么,请求就会变成什么。可能会让你感到意外的是,默认情况下没有上下文路径;没有jsessionid cookie;没有转发、错误或异步调度;因此,也没有实际的JSP渲染。相反, “转发”和“重定向”的URL会保存在MockHttpServletResponse中,并可以通过预期断言进行验证。spring-doc.cadn.net.cn

这意味着,如果你使用JSP,你可以验证请求被转发到的JSP页面,但不会渲染任何HTML。换句话说,JSP没有被调用。然而,请注意,所有其他不依赖转发的渲染技术,如Thymeleaf和Freemarker,都会如预期地将HTML渲染到响应体中。同样地,通过@ResponseBody方法渲染JSON、XML和其他格式也是如此。spring-doc.cadn.net.cn

Alternatively, you may consider the full end-to-end integration testing support from Spring Boot with @SpringBootTest. See the Spring Boot Reference Guide.spring-doc.cadn.net.cn

每种方法都有其优缺点。Spring MVC Test 提供的选项在经典单元测试和完整集成测试之间有所不同。可以肯定的是,Spring MVC Test 中的任何选项都不属于经典单元测试的范畴,但它们离经典单元测试更近一些。例如,你可以通过向控制器注入模拟服务来隔离 web 层,在这种情况下,你只通过 DispatcherServlet 测试 web 层,但使用实际的 Spring 配置,就像你可能会隔离地测试数据访问层一样。此外,你可以使用独立设置,一次专注于一个控制器,并手动提供使其正常工作的所需配置。spring-doc.cadn.net.cn

使用Spring MVC Test时,另一个重要的区别是,从概念上讲,这些测试是在服务器端进行的,因此你可以检查使用了哪个处理器,是否通过HandlerExceptionResolver处理了异常,模型的内容是什么,有哪些绑定错误,以及其他细节。这意味着编写预期结果更容易,因为服务器不再是通过实际HTTP客户端进行测试时的黑盒。这通常是经典单元测试的一个优势:编写、推理和调试都更容易,但并不能取代全面集成测试的需求。同时,重要的是不要忽视检查响应的重要性。简而言之,在同一个项目中可以采用多种测试风格和策略。spring-doc.cadn.net.cn

更多示例

框架自身的测试包括 许多示例测试,旨在展示如何单独使用MockMvc或通过 WebTestClient 使用。浏览这些示例以获得更多的想法。spring-doc.cadn.net.cn

3.7.2. HtmlUnit 集成

Spring 提供了 MockMvcHtmlUnit 之间的集成。这简化了使用基于 HTML 的视图时进行端到端测试的过程。这种集成使您可以:spring-doc.cadn.net.cn

MockMvc 可以与不依赖 Servlet 容器的模板技术一起工作(例如,Thymeleaf、FreeMarker 和其他),但它不能与 JSP 一起工作,因为 JSP 依赖于 Servlet 容器。
为什么集成HtmlUnit?

最显而易见的问题是“我为什么需要这个?”答案最好通过探索一个非常基本的示例应用程序来找到。假设你有一个支持对Message对象进行CRUD操作的Spring MVC Web应用程序。该应用程序还支持浏览所有消息。你会如何测试它?spring-doc.cadn.net.cn

使用Spring MVC Test,我们可以轻松测试是否能够创建一个Message,如下所示:spring-doc.cadn.net.cn

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

如果我们想测试允许我们创建消息的表单视图呢?例如,假设我们的表单如下所示:spring-doc.cadn.net.cn

<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>

我们如何确保表单生成正确的请求以创建新消息?一个天真的尝试可能如下所示:spring-doc.cadn.net.cn

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表单与控制器不同步。为了解决这个问题,我们可以将这两个测试结合起来,如下所示:spring-doc.cadn.net.cn

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

这会降低我们的测试错误通过的风险,但仍有一些问题:spring-doc.cadn.net.cn

  • 如果我们页面上有多个表单呢?诚然,我们可以更新我们的XPath表达式,但随着我们考虑的因素越来越多,这些表达式会变得越来越复杂:字段类型是否正确?字段是否已启用?等等。spring-doc.cadn.net.cn

  • 另一个问题是,我们做了两倍于预期的工作。我们必须首先验证视图,然后使用刚刚验证的相同参数提交视图。理想情况下,这可以一次性完成。spring-doc.cadn.net.cn

  • 最后,我们仍然无法考虑到一些事情。例如,如果表单有我们希望测试的JavaScript验证该怎么办?spring-doc.cadn.net.cn

总体问题是测试网页不仅仅涉及单一的交互。相反,它是用户与网页交互以及该网页与其他资源交互的组合。例如,表单视图的结果被用作用户创建消息的输入。此外,我们的表单视图可能还会使用其他影响页面行为的资源,如 JavaScript 验证。spring-doc.cadn.net.cn

集成测试来拯救?

为了解决前面提到的问题,我们可以进行端到端的集成测试,但这有一些缺点。考虑测试允许我们分页浏览消息的视图。我们可能需要以下测试:spring-doc.cadn.net.cn

为了设置这些测试,我们需要确保数据库包含正确的消息。这导致了一系列额外的挑战:spring-doc.cadn.net.cn

这些挑战并不意味着我们应该完全放弃端到端集成测试。相反,我们可以通过重构详细的测试用例来使用运行速度更快、更可靠且没有副作用的模拟服务,从而减少端到端集成测试的数量。然后,我们可以实现少量真正的端到端集成测试,验证简单的业务流程,以确保所有组件能够正常协同工作。spring-doc.cadn.net.cn

进入HtmlUnit集成

那么我们如何在测试页面交互的同时还能保持测试套件的良好性能呢?答案是:“通过将MockMvc与HtmlUnit集成。”spring-doc.cadn.net.cn

HtmlUnit集成选项

当你想要将 MockMvc 与 HtmlUnit 集成时,你有多种选择:spring-doc.cadn.net.cn

MockMvc 和 HtmlUnit

本节描述了如何集成 MockMvc 和 HtmlUnit。如果您想使用原始的 HtmlUnit 库,请使用此选项。spring-doc.cadn.net.cn

MockMvc 和 HtmlUnit 配置

首先,确保您已经包含了一个对net.sourceforge.htmlunit:htmlunit的测试依赖。为了使用HtmlUnit与Apache HttpComponents 4.5+一起,您需要使用HtmlUnit 2.18或更高版本。spring-doc.cadn.net.cn

我们可以轻松地创建一个HtmlUnit WebClient,它通过使用MockMvcWebClientBuilder与MockMvc集成,如下所示:spring-doc.cadn.net.cn

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的使用。spring-doc.cadn.net.cn

MockMvc 和 HtmlUnit 的使用

现在我们可以像平常一样使用HtmlUnit,而无需将我们的应用程序部署到Servlet容器中。例如,我们可以请求视图以创建一条消息,如下所示:spring-doc.cadn.net.cn

Java
HtmlPage createMsgFormPage = webClient.getPage("http://localhost/messages/form");
Kotlin
val createMsgFormPage = webClient.getPage("http://localhost/messages/form")
默认的上下文路径是""。或者,我们可以指定上下文路径,如高级MockMvcWebClientBuilder中所述。

一旦我们有了HtmlPage的引用,我们就可以填写表单并提交它来创建消息,如下例所示:spring-doc.cadn.net.cn

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库:spring-doc.cadn.net.cn

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测试。 首先,我们不再需要显式验证表单,然后创建一个看起来像表单的请求。相反,我们请求表单,填写它并提交,从而显著减少了开销。spring-doc.cadn.net.cn

另一个重要因素是HtmlUnit使用Mozilla Rhino引擎来评估JavaScript。这意味着我们还可以测试页面中JavaScript的行为。spring-doc.cadn.net.cn

请参阅HtmlUnit文档以获取有关使用HtmlUnit的更多信息。spring-doc.cadn.net.cn

高级 MockMvcWebClientBuilder

在到目前为止的例子中,我们以最简单的方式使用了MockMvcWebClientBuilder,通过基于Spring TestContext Framework为我们加载的WebApplicationContext来构建一个WebClient。以下示例重复了这种方法:spring-doc.cadn.net.cn

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

我们还可以指定其他配置选项,如下例所示:spring-doc.cadn.net.cn

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来执行完全相同的设置,如下所示:spring-doc.cadn.net.cn

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的全部功能。spring-doc.cadn.net.cn

有关创建MockMvc实例的更多信息,请参阅 设置选项
MockMvc 和 WebDriver

在前几节中,我们已经看到了如何使用MockMvc结合原始的HtmlUnit API。在本节中,我们将使用Selenium中的WebDriver的额外抽象来使事情变得更加简单。spring-doc.cadn.net.cn

为什么使用WebDriver和MockMvc?

我们已经可以使用HtmlUnit和MockMvc,那么为什么还要使用WebDriver呢?Selenium WebDriver提供了一个非常优雅的API,让我们可以轻松地组织代码。为了更好地展示它是如何工作的,我们在本节中探索一个示例。spring-doc.cadn.net.cn

尽管是Selenium的一部分,WebDriver 运行测试不需要 Selenium Server。

假设我们需要确保消息被正确创建。测试涉及找到 HTML 表单输入元素,填写这些元素,并进行各种断言。spring-doc.cadn.net.cn

这种方法会导致大量的单独测试,因为我们还想测试错误条件。例如,我们希望确保如果只填写表单的一部分,会收到错误提示。如果我们填写了整个表单,新创建的消息应该在之后显示。spring-doc.cadn.net.cn

如果其中一个字段被命名为“summary”,我们可能会在测试的多个地方看到类似于以下内容的重复:spring-doc.cadn.net.cn

Java
HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary");
summaryInput.setValueAttribute(summary);
Kotlin
val summaryInput = currentPage.getHtmlElementById("summary")
summaryInput.setValueAttribute(summary)

那么如果我们把 id 改为 smmry 会发生什么?这样做将迫使我们更新所有测试以包含这个更改。这违反了DRY原则,因此我们最好将这段代码提取到自己的方法中,如下所示:spring-doc.cadn.net.cn

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 时不必更新所有测试。spring-doc.cadn.net.cn

我们甚至可以更进一步,将此逻辑放置在一个 Object 中,该 HtmlPage 表示我们当前所在的上下文,如下例所示:spring-doc.cadn.net.cn

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提供了一些我们在接下来的章节中将要探讨的工具,使得实现这种模式变得更加容易。spring-doc.cadn.net.cn

MockMvc 和 WebDriver 配置

要使用Selenium WebDriver与Spring MVC Test框架,请确保您的项目包含对org.seleniumhq.selenium:selenium-htmlunit-driver的测试依赖。spring-doc.cadn.net.cn

我们可以轻松地创建一个与MockMvc集成的Selenium WebDriver,如下例所示:spring-doc.cadn.net.cn

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的使用。spring-doc.cadn.net.cn

MockMvc 和 WebDriver 的使用

现在我们可以像平常一样使用WebDriver,但不需要将我们的应用程序部署到Servlet容器中。例如,我们可以请求视图创建一条消息,如下所示:spring-doc.cadn.net.cn

Java
CreateMessagePage page = CreateMessagePage.to(driver);
Kotlin
val page = CreateMessagePage.to(driver)

然后我们可以填写表单并提交以创建消息,如下所示:spring-doc.cadn.net.cn

Java
ViewMessagePage viewMessagePage =
        page.createMessage(ViewMessagePage.class, expectedSummary, expectedText);
Kotlin
val viewMessagePage =
    page.createMessage(ViewMessagePage::class, expectedSummary, expectedText)

这改进了我们HtmlUnit测试的设计,通过利用页面对象模式。正如我们在为什么选择WebDriver和MockMvc?中提到的,我们可以使用页面对象模式与HtmlUnit一起工作,但使用WebDriver会更容易。考虑以下CreateMessagePage实现:spring-doc.cadn.net.cn

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让我们可以从HtmlUnit版本的CreateMessagePage中删除大量代码,通过自动解析每个WebElement。The PageFactory#initElements(WebDriver,Class<T>) 方法通过使用字段名称并在HTML页面内查找idname来自动解析每个WebElement
3 我们可以使用 @FindBy 注解 来覆盖默认的查找行为。我们的示例展示了如何使用 @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让我们可以从HtmlUnit版本的CreateMessagePage中删除大量代码,通过自动解析每个WebElement。The PageFactory#initElements(WebDriver,Class<T>) 方法通过使用字段名称并在HTML页面内查找idname来自动解析每个WebElement
3 我们可以使用 @FindBy 注解 来覆盖默认的查找行为。我们的示例展示了如何使用 @FindBy 注解通过 css 选择器(input[type=submit])查找我们的提交按钮。

最后,我们可以验证新消息是否成功创建。以下断言使用了AssertJ断言库:spring-doc.cadn.net.cn

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 让我们能够与自定义的领域模型进行交互。例如,它暴露了一个返回 Message 对象的方法:spring-doc.cadn.net.cn

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

然后我们可以在断言中使用丰富的领域对象。spring-doc.cadn.net.cn

最后,我们不能忘记在测试完成时关闭 WebDriver 实例,如下所示:spring-doc.cadn.net.cn

Java
@AfterEach
void destroy() {
    if (driver != null) {
        driver.close();
    }
}
Kotlin
@AfterEach
fun destroy() {
    if (driver != null) {
        driver.close()
    }
}

有关使用WebDriver的更多信息,请参阅Selenium WebDriver文档spring-doc.cadn.net.cn

高级 MockMvcHtmlUnitDriverBuilder

在到目前为止的例子中,我们以最简单的方式使用了MockMvcHtmlUnitDriverBuilder,通过基于Spring TestContext Framework为我们加载的WebApplicationContext来构建一个WebDriver。以下是重复这一方法的示例:spring-doc.cadn.net.cn

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

我们还可以指定其他配置选项,如下:spring-doc.cadn.net.cn

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来执行完全相同的设置,如下所示:spring-doc.cadn.net.cn

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的全部功能。spring-doc.cadn.net.cn

有关创建MockMvc实例的更多信息,请参阅 设置选项
MockMvc 和 Geb

在上一节中,我们看到了如何使用MockMvc与WebDriver。在本节中,我们 使用 Geb 使我们的测试更加Groovy。spring-doc.cadn.net.cn

为什么选择Geb和MockMvc?

Geb 由 WebDriver 支持,因此它提供了许多我们从 WebDriver 获得的相同的好处。然而,Geb 通过为我们处理一些样板代码,使得事情变得更加简单。spring-doc.cadn.net.cn

MockMvc 和 Geb 设置

我们可以轻松地使用MockMvc提供的Selenium WebDriver初始化一个Geb Browser,如下所示:spring-doc.cadn.net.cn

def setup() {
    browser.driver = MockMvcHtmlUnitDriverBuilder
        .webAppContextSetup(context)
        .build()
}
这是一个使用MockMvcHtmlUnitDriverBuilder的简单示例。有关更高级的用法,请参阅高级MockMvcHtmlUnitDriverBuilder

这确保了任何引用localhost作为服务器的URL都会被定向到我们的MockMvc实例,而无需实际的HTTP连接。任何其他URL都像平常一样通过网络连接请求。这让我们可以轻松测试CDN的使用。spring-doc.cadn.net.cn

MockMvc 和 Geb 的使用

现在我们可以像平常一样使用 Geb,而无需将应用程序部署到 Servlet 容器中。例如,我们可以请求视图以创建一条消息,如下所示:spring-doc.cadn.net.cn

to CreateMessagePage

然后我们可以填写表单并提交以创建消息,如下所示:spring-doc.cadn.net.cn

when:
form.summary = expectedSummary
form.text = expectedMessage
submit.click(ViewMessagePage)

任何未识别的方法调用或属性访问或引用如果找不到,都会转发到当前的页面对象。这消除了我们在直接使用WebDriver时需要的大量样板代码。spring-doc.cadn.net.cn

与直接使用WebDriver类似,这通过使用页面对象模式改进了我们的HtmlUnit测试的设计。如前所述,我们可以在HtmlUnit和WebDriver中使用页面对象模式,但在Geb中使用它更加简单。考虑我们新的Groovy基础的CreateMessagePage实现:spring-doc.cadn.net.cn

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,该页面可以在其中找到。这使我们可以导航到该页面,如下所示:spring-doc.cadn.net.cn

to CreateMessagePage

我们还有一个at闭包,用于确定我们是否在指定的页面上。如果我们在正确的页面上,它应该返回true。这就是为什么我们可以断言我们在正确的页面上,如下所示:spring-doc.cadn.net.cn

then:
at CreateMessagePage
errors.contains('This field is required.')
我们在闭包中使用断言,以便在页面错误时确定问题出在哪里。

接下来,我们创建一个 content 闭包,该闭包指定了页面内所有感兴趣的部分。我们可以使用一个 类似jQuery的Navigator API 来选择我们感兴趣的内容。spring-doc.cadn.net.cn

最后,我们可以验证成功创建了一条新消息,如下:spring-doc.cadn.net.cn

then:
at ViewMessagePage
success == 'Successfully created a new message'
id
date
summary == expectedSummary
message == expectedMessage

有关如何充分利用Geb的更多详细信息,请参阅 Geb之书用户手册。spring-doc.cadn.net.cn

3.8. 测试客户端应用程序

您可以使用客户端测试来测试内部使用RestTemplate的代码。这个想法是声明预期的请求并提供“stub”响应,以便您可以专注于隔离测试代码(即,无需运行服务器)。以下示例展示了如何做到这一点:spring-doc.cadn.net.cn

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 断言实际请求与预期相符并返回“模拟”响应。在这种情况下,我们期望对 /greeting 的请求,并希望返回 200 响应和 text/plain 内容。我们可以根据需要定义额外的预期请求和模拟响应。当定义了预期请求和模拟响应后,RestTemplate 可以像往常一样在客户端代码中使用。mockServer.verify() 可用于验证所有预期是否已满足。spring-doc.cadn.net.cn

默认情况下,请求按预期声明的顺序接收。您可以在构建服务器时设置ignoreExpectOrder选项,在这种情况下,所有预期都会被检查(按顺序)以找到与给定请求的匹配项。这意味着请求可以按任何顺序到达。以下示例使用ignoreExpectOrderspring-doc.cadn.net.cn

Java
server = MockRestServiceServer.bindTo(restTemplate).ignoreExpectOrder(true).build();
Kotlin
server = MockRestServiceServer.bindTo(restTemplate).ignoreExpectOrder(true).build()

即使默认情况下请求是无序的,每个请求也只允许运行一次。 expect 方法提供了一个重载变体,接受一个 ExpectedCount 参数,该参数指定了计数范围(例如,oncemanyTimesmaxminbetween 等)。以下示例使用了 timesspring-doc.cadn.net.cn

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" 预期出现三次,则应先有一个请求到 "/something",然后再有一个请求到 "/somewhere",但除此之外,后续的 "/something" 和 "/somewhere" 请求可以在任何时候到达。spring-doc.cadn.net.cn

作为上述所有方法的替代方案,客户端测试支持还提供了一个ClientHttpRequestFactory实现,您可以将其配置到RestTemplate中,以绑定到MockMvc实例。这允许使用实际的服务器端逻辑处理请求,但无需运行服务器。以下示例展示了如何操作:spring-doc.cadn.net.cn

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.8.1. 静态导入

与服务器端测试类似,客户端测试的流畅API也需要几个静态导入。通过搜索MockRest*可以很容易找到这些导入。Eclipse用户应该在Eclipse首选项中的Java → 编辑器 → 内容辅助 → 收藏夹下将MockRestRequestMatchers.*MockRestResponseCreators.*添加为“收藏静态成员”。这允许在输入静态方法名的第一个字符后使用内容辅助。其他IDE(如IntelliJ)可能不需要任何额外配置。检查对静态成员的代码补全支持。spring-doc.cadn.net.cn

3.8.2. 客户端 REST 测试的其他示例

Spring MVC Test 自带的测试包括 示例测试 客户端 REST 测试。spring-doc.cadn.net.cn

4. 进一步的资源

请参阅以下资源以获取有关测试的更多信息:spring-doc.cadn.net.cn