测试

1. Spring 测试简介

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

2. 单元测试

依赖注入应使您的代码对容器的依赖程度低于传统的 Java EE 开发方式。构成您应用程序的 POJO(普通 Java 对象)应当能够在 JUnit 或 TestNG 测试中进行测试,通过使用 new 操作符实例化对象,而无需依赖 Spring 或任何其他容器。您可以使用模拟对象(结合其他有价值的测试技术)来隔离测试您的代码。 如果您遵循 Spring 的架构建议,所得到的清晰分层和组件化代码结构将有助于更轻松地进行单元测试。例如,在运行单元测试时,您可以通过对 DAO 或 Repository 接口进行桩(stubbing)或模拟(mocking),来测试服务层对象,而无需访问持久化数据。spring-doc.cadn.net.cn

真正的单元测试通常运行得非常快,因为无需设置任何运行时基础设施。在开发方法论中强调真正的单元测试可以提高你的生产力。对于基于 IoC 的应用程序编写有效的单元测试,你可能并不需要本测试章节的这一部分内容。然而,在某些单元测试场景下,Spring 框架提供了模拟对象(mock objects)和测试支持类,这些内容将在本章中进行介绍。spring-doc.cadn.net.cn

2.1. 模拟对象

Spring 包含多个专门用于模拟(mocking)的包: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 实例绑定到与 Java EE 容器中相同的 JNDI 名称,那么你就可以在测试场景中无需修改即可重用应用程序代码和配置。spring-doc.cadn.net.cn

org.springframework.mock.jndi 包中的模拟 JNDI 支持自 Spring Framework 5.2 起已正式弃用,建议改用第三方提供的完整解决方案,例如 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 提供了一个集成测试框架。参见 Spring MVC 测试框架spring-doc.cadn.net.cn

2.1.4. Spring Web 响应式

org.springframework.mock.http.server.reactive 包包含用于 WebFlux 应用程序的 ServerHttpRequestServerHttpResponse 的模拟实现。org.springframework.mock.web.server 包包含一个模拟的 ServerWebExchange,它依赖于上述模拟的请求和响应对象。spring-doc.cadn.net.cn

MockServerHttpRequestMockServerHttpResponse 均继承自与服务器特定实现相同的抽象基类,并与其共享行为。 例如,模拟请求一旦创建就是不可变的,但你可以使用 mutate() 中的 ServerHttpRequest 方法来创建一个修改后的实例。spring-doc.cadn.net.cn

为了让模拟响应正确实现写入契约并返回一个写入完成句柄(即 Mono<Void>),它默认使用一个带有 Fluxcache().then(),该方式会缓冲数据,使其在测试中可用于断言。 应用程序可以设置自定义的写入函数(例如,用于测试无限流)。spring-doc.cadn.net.cn

WebTestClient 基于模拟的请求和响应构建,用于在无需 HTTP 服务器的情况下测试 WebFlux 应用程序。该客户端也可用于对运行中的服务器进行端到端测试。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

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

AopTestUtils 是一组与 AOP 相关的工具方法。您可以使用这些方法获取被一个或多个 Spring 代理隐藏的底层目标对象的引用。例如,如果您使用 EasyMock 或 Mockito 等库将某个 bean 配置为动态模拟对象,且该模拟对象被包裹在 Spring 代理中,那么您可能需要直接访问底层的模拟对象,以便对其进行期望设置和执行验证。关于 Spring 核心 AOP 工具类,请参阅 AopUtilsAopProxyUtilsspring-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 以及 Spring Servlet API 模拟对象 中的 MockHttpServletRequestMockHttpSession 等工具。若要对您的 Spring MVC 和 REST Controller 类进行全面的集成测试,并结合您的 Spring MVC WebApplicationContext 配置,请改用 Spring MVC 测试框架

3. 集成测试

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

3.1. 概述

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

  • 正确配置您的 Spring IoC 容器上下文。spring-doc.cadn.net.cn

  • 使用 JDBC 或 ORM 工具进行数据访问。这可能包括 SQL 语句的正确性、Hibernate 查询、JPA 实体映射等方面。spring-doc.cadn.net.cn

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

以注解驱动的Spring TestContext 框架形式提供单元测试和集成测试支持。TestContext 框架与实际使用的测试框架无关,因此可以在各种环境中对测试进行增强,包括 JUnit、TestNG 等。spring-doc.cadn.net.cn

3.2. 集成测试的目标

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

接下来的几节将分别描述每个目标,并提供指向实现和配置细节的链接。spring-doc.cadn.net.cn

3.2.1. 上下文管理与缓存

Spring TestContext 框架提供了一致的 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 来设置测试夹具(test fixtures)。这里的一个显著优势是,您可以在各种测试场景中复用应用程序上下文(例如,用于配置 Spring 管理的对象图、事务代理、DataSource 实例等),从而避免在各个单独的测试用例中重复复杂的测试夹具设置。spring-doc.cadn.net.cn

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

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

  • Hibernate 映射文件配置:所有内容是否都已正确映射?是否设置了正确的懒加载(lazy-loading)配置?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

  • ApplicationContext,用于执行显式的 bean 查找或测试整个上下文的状态。spring-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 框架的引导方式。具体来说,你可以使用 @BootstrapWith 来指定一个自定义的 TestContextBootstrapper。更多详细信息,请参阅引导 TestContext 框架一节。spring-doc.cadn.net.cn

@ContextConfiguration

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

资源位置通常是指类路径(classpath)中的 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 = CustomContextIntializer.class) (1)
class ContextInitializerTests {
    // class body...
}
Kotlin
@ContextConfiguration(initializers = [CustomContextIntializer::class]) (1)
class ContextInitializerTests {
    // class body...
}
1 声明一个初始化器类。

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

以下示例同时使用了位置(location)和加载器(loader):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 提供了对继承资源位置或配置类以及由超类声明的上下文初始化器的支持。

有关更多详细信息,请参见上下文管理@ContextConfiguration的Javadoc。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 @Repository注解。

要覆盖默认设置,您可以使用隐式的 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 并通过 @ActiveProfilesresolver 属性进行注册,以编程方式解析活动 Bean 定义配置文件。

请参阅 使用环境配置文件的上下文配置@ActiveProfiles javadoc,以获取示例和更多详细信息。spring-doc.cadn.net.cn

@TestPropertySource

@TestPropertySource 是一个类级别的注解,可用于配置属性文件的位置以及内联属性,这些属性将被添加到为集成测试加载的 PropertySourcesEnvironment 中的 ApplicationContext 集合中。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 属性。

参见使用测试属性源进行上下文配置以获取示例和更多详细信息。spring-doc.cadn.net.cn

@DynamicPropertySource

@DynamicPropertySource 是一个方法级别的注解,可用于注册动态属性,这些属性将被添加到为集成测试所加载的 PropertySourcesEnvironment 中的 ApplicationContext 集合里。当你事先不知道属性值时,动态属性非常有用——例如,当这些属性由外部资源管理时,比如由 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 使用 static 注解一个 @DynamicPropertySource 方法。
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 使用 static 注解一个 @DynamicPropertySource 方法。
2 接受一个 DynamicPropertyRegistry 作为参数。
3 注册一个动态的 server.port 属性,以便从服务器中延迟获取。
@DirtiesContext

@DirtiesContext 表示在测试执行期间,底层的 Spring ApplicationContext 已被污染(即测试以某种方式修改或破坏了该上下文——例如,更改了单例 bean 的状态),应当将其关闭。当一个应用上下文被标记为已污染时,它会从测试框架的缓存中移除并关闭。因此,对于后续任何需要具有相同配置元数据的上下文的测试,底层的 Spring 容器都会被重新构建。spring-doc.cadn.net.cn

你可以在同一个类或类层次结构中,将 @DirtiesContext 同时用作类级别和方法级别的注解。在此类场景中,ApplicationContext 会根据所配置的 methodModeclassMode,在任何此类注解的方法之前或之后,以及在当前测试类之前或之后,被标记为“脏”(dirty)。spring-doc.cadn.net.cn

以下示例说明了在各种配置场景下,上下文何时会被标记为脏(dirty):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 在当前测试方法之后将上下文标记为“脏”。

如果你在某个测试中使用了 @DirtiesContext,而该测试的上下文是通过 @ContextHierarchy 配置为上下文层次结构的一部分,那么你可以使用 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 定义了类级别的元数据,用于配置应注册到 TestExecutionListenerTestContextManager 实现。通常,@TestExecutionListeners@ContextConfiguration 结合使用。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 支持继承的监听器。有关示例和更多详细信息,请参阅 javadocspring-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 Framework 中,集成测试的回滚默认值为 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 表示被注解的 void 方法应在事务启动之前运行,适用于那些通过使用 Spring 的 @Transactional 注解配置为在事务内执行的测试方法。@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 表示被注解的 void 方法应在事务结束之后执行,适用于那些通过使用 Spring 的 @Transactional 注解配置为在事务内运行的测试方法。@AfterTransaction 方法不要求是 public 的,并且可以声明在基于 Java 8 的接口默认方法中。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 TestContext 框架的所有配置中均支持标准语义。请注意,这些注解并非专用于测试,可以在 Spring 框架的任何地方使用。spring-doc.cadn.net.cn

JSR-250 生命周期注解

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

如果测试类中的某个方法使用了 @PostConstruct 注解,则该方法会在底层测试框架的所有前置方法(例如,使用 JUnit Jupiter 的 @BeforeEach 注解的方法)之前运行,并且针对测试类中的每个测试方法都会如此执行。另一方面,如果测试类中的某个方法使用了 @PreDestroy 注解,则该方法永远不会被执行。因此,在测试类中,我们建议您使用底层测试框架提供的测试生命周期回调方法,而不是 @PostConstruct@PreDestroyspring-doc.cadn.net.cn

3.4.3. Spring JUnit 4 测试注解

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

@IfProfileValue

@IfProfileValue 表示带注解的测试在特定测试环境中启用。如果配置的 ProfileValueSource 为提供的 value 返回匹配的 name,则该测试将被启用;否则,该测试将被禁用,并实际上被忽略。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”时运行此测试。

或者,你也可以将 @IfProfileValue 配置为一个 values 列表(具有 OR 语义),以在 JUnit 4 环境中实现类似 TestNG 的测试组支持。 请看以下示例: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 是一个类级别的注解,用于指定在通过 ProfileValueSource 注解获取配置的 profile 值时应使用哪种类型的 @IfProfileValue。如果测试类未声明 @ProfileValueSourceConfiguration,则默认使用 SystemProfileValueSource。以下示例展示了如何使用 @ProfileValueSourceConfigurationspring-doc.cadn.net.cn

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

@Timed 表示被注解的测试方法必须在指定的时间段(以毫秒为单位)内完成执行。如果测试执行时间超过指定的时间段,则测试失败。spring-doc.cadn.net.cn

该时间段包括执行测试方法本身、测试的任何重复(参见 @Repeat),以及测试夹具(test fixture)的任何设置或清理工作。以下示例展示了如何使用它: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 唯一的区别在于,组件类可以通过 value@SpringJUnitConfig 属性进行声明。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 指定配置文件的位置。

另请参阅 上下文管理 以及以下类的 Javadoc 文档: @SpringJUnitWebConfig@ContextConfiguration@WebAppConfiguration 以获取更多详细信息。spring-doc.cadn.net.cn

@TestConstructor

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

如果测试类上未直接或通过元注解使用 @TestConstructor,则将使用默认的测试构造函数自动装配模式。有关如何更改默认模式的详细信息,请参见下方提示。但请注意,在构造函数上本地声明的 @Autowired 注解优先级高于 @TestConstructor 和默认模式。spring-doc.cadn.net.cn

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

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

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

从 Spring Framework 5.2 起,@TestConstructor 仅支持与 JUnit Jupiter 配合使用时的 SpringExtension。请注意,SpringExtension 通常会自动为您注册——例如,当使用诸如 @SpringJUnitConfig@SpringJUnitWebConfig 等注解,或 Spring Boot Test 中的各种测试相关注解时。
@EnabledIf

@EnabledIf 用于表明被注解的 JUnit Jupiter 测试类或测试方法 在所提供的 expression 表达式求值为 true 时启用并应予以执行。 具体来说,如果该表达式求值结果为 Boolean.TRUE 或一个(忽略大小写的)等于 Stringtrue, 则该测试将被启用。当此注解应用于类级别时,该类中的所有测试方法默认也会自动启用。spring-doc.cadn.net.cn

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

然而请注意,如果一个文本字面量并非属性占位符动态解析的结果,那么它实际上毫无用处,因为 @EnabledIf("false") 等同于 @Disabled,而 @EnabledIf("true") 在逻辑上则毫无意义。spring-doc.cadn.net.cn

你可以将 @EnabledIf 用作元注解(meta-annotation)来创建自定义的组合注解。例如,你可以按如下方式创建一个自定义的 @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 {}
@DisabledIf

@DisabledIf 用于表明被注解的 JUnit Jupiter 测试类或测试方法在所提供的 expression 表达式求值为 true 时应被禁用且不应执行。具体来说,如果该表达式求值结果为 Boolean.TRUE 或一个(忽略大小写的)等于 Stringtrue,则该测试将被禁用。当此注解应用于类级别时,该类中的所有测试方法也会自动被禁用。spring-doc.cadn.net.cn

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

然而请注意,如果一个文本字面量并非属性占位符动态解析的结果,那么它实际上毫无用处,因为 @DisabledIf("true") 等同于 @Disabled,而 @DisabledIf("false") 在逻辑上则毫无意义。spring-doc.cadn.net.cn

你可以将 @DisabledIf 用作元注解(meta-annotation)来创建自定义的组合注解。例如,你可以按如下方式创建一个自定义的 @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 {}

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 以及其他注解用作元注解(meta-annotations),你也可以在测试方法级别创建自定义的组合注解。例如,如果我们希望创建一个组合注解,将 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 注解编程模型 维基页面。spring-doc.cadn.net.cn

3.5. Spring TestContext 框架

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 提供了自定义的 JUnit Runner 和自定义 JUnit Rules,并为 JUnit Jupiter 提供了自定义的Extension,使你可以编写所谓的 POJO 测试类。POJO 测试类无需继承特定的类层次结构(例如上述abstract支持类)。spring-doc.cadn.net.cn

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

3.5.1. 核心抽象

框架的核心由 TestContextManager 类以及 TestContextTestExecutionListenerSmartContextLoader 接口组成。为每个测试类创建一个 TestContextManager(例如,用于在 JUnit Jupiter 中执行单个测试类内的所有测试方法)。TestContextManager 进而管理一个持有当前测试上下文的 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。仅当测试类上存在 ContextLoader 注解时,才会使用 Web @WebAppConfiguration。仅当 Groovy 位于类路径中时,才启用 Groovy 支持。spring-doc.cadn.net.cn

  • AnnotationConfigContextLoader:从组件类加载一个标准的ApplicationContextspring-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

  • GenericPropertiesContextLoader:从 Java 属性文件加载标准的 ApplicationContextspring-doc.cadn.net.cn

3.5.2. 引导测试上下文框架

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

TestContextBootstrapper 定义了用于引导测试上下文框架的 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实现

使用 TestExecutionListener 注册 @TestExecutionListeners 实现类适用于在有限测试场景中使用的自定义监听器。然而,如果某个自定义监听器需要在整个测试套件中使用,这种方式就会变得繁琐。这一问题通过 TestExecutionListener 机制对默认 SpringFactoriesLoader 实现类的自动发现支持得以解决。spring-doc.cadn.net.cn

具体来说,spring-test 模块在其 TestExecutionListener 属性文件中,通过 org.springframework.test.context.TestExecutionListener 键声明了所有核心的默认 META-INF/spring.factories 实现。第三方框架和开发者也可以通过各自独立的 TestExecutionListener 属性文件,以相同的方式将自己的 META-INF/spring.factories 实现添加到默认监听器列表中。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实现

如果通过 TestExecutionListener 注册了一个自定义的 @TestExecutionListeners, 则默认的监听器将不会被注册。在大多数常见的测试场景中,这实际上迫使开发者除了声明自定义监听器外, 还必须手动声明所有默认监听器。以下代码清单展示了这种配置方式: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 这样的第三方框架也会通过上述自动发现机制注册它们自己的默认 #testcontext-tel-config-automatic-discovery 实现。spring-doc.cadn.net.cn

为避免必须知晓并重新声明所有默认监听器,您可以将 @TestExecutionListenersmergeMode 属性设置为 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.2 引入的 EventPublishingTestExecutionListener 提供了一种实现自定义 TestExecutionListener 的替代方法。测试的 ApplicationContext 中的组件可以监听由 EventPublishingTestExecutionListener 发布的以下事件,每个事件分别对应于 TestExecutionListener API 中的一个方法。spring-doc.cadn.net.cn

仅当 ApplicationContext 已经加载完毕时,才会发布这些事件。

这些事件可能会出于各种原因被消费,例如重置模拟(mock)Bean 或跟踪测试执行过程。与实现自定义的 TestExecutionListener 相比,消费测试执行事件的一个优势在于:任何注册在测试 ApplicationContext 中的 Spring Bean 都可以消费这些事件,并且这些 Bean 能够直接受益于依赖注入和 ApplicationContext 提供的其他特性。相比之下,TestExecutionListener 并不是 ApplicationContext 中的一个 Bean。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.5. 上下文管理

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

@Autowired ApplicationContext

作为实现 ApplicationContextAware 接口的替代方案,您可以使用 @Autowired 注解将应用上下文注入到您的测试类中,该注解可应用于字段或 setter 方法,如下例所示: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 将决定如何从默认位置或默认配置类加载上下文。除了上下文资源位置和组件类之外,还可以通过应用上下文初始化器(Application Context Initializers)来配置应用上下文。spring-doc.cadn.net.cn

以下各节说明如何使用 Spring 的 @ContextConfiguration 注解,通过 XML 配置文件、Groovy 脚本、组件类(通常为 ApplicationContext 类)或上下文初始化器来配置测试用的 @Configuration。 此外,对于高级用例,您也可以实现并配置自己的自定义 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 locations 属性为 value 属性提供了一个别名。因此,如果你不需要在 @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 文件。

如果你在 locations 注解中同时省略了 value@ContextConfiguration 属性,TestContext 框架会尝试自动检测一个默认的 XML 资源位置。具体来说,GenericXmlContextLoaderGenericXmlWebContextLoader 会根据测试类的名称来确定默认位置。例如,如果你的类名为 com.example.MyTest,那么 GenericXmlContextLoader 会从 "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 Bean 定义 DSL的 Groovy 脚本为测试加载 core.html#groovy-bean-definition-dsl,您可以在测试类上添加 @ContextConfiguration 注解,并将 locationsvalue 属性配置为包含 Groovy 脚本资源位置的数组。Groovy 脚本的资源查找语义与XML 配置文件中描述的语义相同。spring-doc.cadn.net.cn

启用 Groovy 脚本支持
如果类路径中存在 Groovy,则 Spring TestContext 框架会自动启用使用 Groovy 脚本加载 ApplicationContext 的支持。

以下示例展示了如何指定 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 配置文件的位置。

如果从 locations 注解中同时省略 value@ContextConfiguration 属性,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 脚本

你可以通过使用 locations 注解的 value@ContextConfiguration 属性,同时声明 XML 配置文件和 Groovy 脚本。如果所配置资源路径以 .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...
}
使用组件类进行上下文配置

要使用组件类(参见基于 Java 的容器配置)为您的测试加载 core.html#beans-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 注解的类。spring-doc.cadn.net.cn

  • 一个组件(即,使用 @Component@Service@Repository 或其他构造型注解标注的类)。spring-doc.cadn.net.cn

  • 一个符合 JSR-330 规范并使用 javax.inject 注解进行标注的类。spring-doc.cadn.net.cn

  • 任何包含 @Bean 方法的类。spring-doc.cadn.net.cn

  • 任何其他打算注册为 Spring 组件(即 ApplicationContext 中的 Spring Bean)的类,可能会利用自动装配单一构造函数的功能,而无需使用 Spring 注解。spring-doc.cadn.net.cn

请参阅 @Configuration@Bean 的 Javadoc,以获取有关组件类的配置和语义的更多信息,并特别注意关于 @Bean 轻量模式的讨论。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)提供了一流的支持,可同时从不同类型的资源加载 ApplicationContext(例如 XML 配置文件、Groovy 脚本和 @Configuration 类)。历史上,Spring Framework 在标准部署中并不支持此功能。因此,Spring Framework 在 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 资源位置,或者一组 @Configuration 类中加载生产环境的 ApplicationContext,但你仍然可以自由地包含或导入另一种类型的配置。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

如果 inheritLocations 注解中的 inheritInitializers@ContextConfiguration 属性被设置为 false,则测试类中指定的资源位置(或组件类)和上下文初始化器将覆盖(shadow)并有效替换其父类中定义的配置。spring-doc.cadn.net.cn

在下一个使用 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 在子类中定义的配置文件。

同样地,在下一个使用组件类的示例中,ApplicationContextExtendedTest 将按顺序从 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 的实现一起使用,但 @ActiveProfiles 不支持与旧版 ContextLoader SPI 的实现一起使用。

考虑两个示例,分别使用 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

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

有时将 Bean 分配给 default(默认)配置文件非常有用。只有在未显式激活其他任何配置文件时,才会包含默认配置文件中的 Bean。你可以利用这一点来定义应用程序在默认状态下使用的“后备”Bean。例如,你可以为 dev(开发)和 production(生产)配置文件显式提供数据源,但在这些配置文件均未激活时,将内存数据源作为默认选项。spring-doc.cadn.net.cn

以下代码示例演示了如何使用 @Configuration 类而非 XML 来实现相同的配置和集成测试: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:通过使用 dataSource 注解,以依赖注入的方式获取一个 @Autowiredspring-doc.cadn.net.cn

  • StandaloneDataConfig:为嵌入式数据库定义一个dataSource,适用于开发人员测试。spring-doc.cadn.net.cn

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

  • DefaultDataConfig:在没有激活任何配置文件的情况下,为默认的嵌入式数据库定义一个 dataSourcespring-doc.cadn.net.cn

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

在给定项目中,通常会在多个测试类之间使用同一组配置文件(profiles)。因此,为了避免重复声明 @ActiveProfiles 注解,你可以将其一次性声明在基类上,子类会自动从基类继承 @ActiveProfiles 的配置。在以下示例中,@ActiveProfiles 注解(以及其他注解)已被移到一个抽象超类 @ActiveProfiles 中:spring-doc.cadn.net.cn

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,并通过 resolver 注解的 @ActiveProfiles 属性进行注册。更多信息,请参阅相应的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 注解,以指定测试属性文件的资源位置或内联属性。这些测试属性源会被添加到为带注解的集成测试所加载的 PropertySourcesEnvironment 中的 ApplicationContext 集合里。spring-doc.cadn.net.cn

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

SmartContextLoader 的实现类通过 getPropertySourceLocations() 中的 getPropertySourceProperties()MergedContextConfiguration 方法来访问合并后的测试属性源值。spring-doc.cadn.net.cn

声明测试属性源

你可以通过使用 locations 注解的 value@TestPropertySource 属性来配置测试属性文件。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 使用绝对路径指定属性文件。

你可以通过使用 properties 注解的 @TestPropertySource 属性,以键值对的形式配置内联属性,如下一个示例所示。所有键值对都会作为单个测试 Environment 添加到外围的 PropertySource 中,并具有最高优先级。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, 其中后面声明的 locations 注解中的 properties@TestPropertySource 会覆盖前面声明的 @TestPropertySource 注解中的相应内容。spring-doc.cadn.net.cn

此外,你可以在测试类上声明多个组合注解,每个组合注解都使用 @TestPropertySource 进行元注解,所有这些 @TestPropertySource 声明都会贡献到你的测试属性源中。spring-doc.cadn.net.cn

直接声明的 @TestPropertySource 注解始终优先于作为元注解(meta-annotation)使用的 @TestPropertySource 注解。换句话说,直接声明的 locations 注解中的 properties@TestPropertySource 将覆盖作为元注解使用的 locations 注解中的 properties@TestPropertySourcespring-doc.cadn.net.cn

默认属性文件检测

如果 @TestPropertySource 被声明为一个空注解(即未显式指定 locationsproperties 属性的值),则会尝试在声明该注解的类所在位置相对路径下查找默认的属性文件。例如,如果被注解的测试类是 com.example.MyTest,则对应的默认属性文件为 classpath:com/example/MyTest.properties。如果无法检测到默认属性文件,则会抛出 IllegalStateException 异常。spring-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

如果 inheritLocations 注解中的 inheritProperties@TestPropertySource 属性被设置为 false,则该测试类的资源位置或内联属性将分别覆盖并有效替换其父类中定义的配置。spring-doc.cadn.net.cn

在下一个示例中,ApplicationContextBaseTest 仅使用 base.properties 文件作为测试属性源进行加载。相比之下,ApplicationContextExtendedTest 则使用 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() {
    // ...
}

在下一个示例中,ApplicationContextBaseTest 仅使用内联的 key1 属性加载。相比之下,ApplicationContextExtendedTest 则使用内联的 key1key2 属性进行加载。以下示例展示了如何在子类及其父类中使用内联属性来定义属性: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 注解提供了对动态属性的支持。该注解可用于集成测试中,当需要将具有动态值的属性添加到为集成测试所加载的 PropertySourcesEnvironment 中的 ApplicationContext 集合时。spring-doc.cadn.net.cn

@DynamicPropertySource 注解及其配套基础设施最初是为了让基于 Testcontainers 的测试能够轻松地将属性暴露给 Spring 集成测试而设计的。然而,该功能也可用于任何生命周期在测试的 ApplicationContext 之外进行管理的外部资源。

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

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

    @Container
    static RedisContainer redis = new RedisContainer();

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

    // tests ...

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

    companion object {

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

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

    // tests ...

}
优先级

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

正在加载一个WebApplicationContext

要指示 TestContext 框架加载一个 WebApplicationContext 而不是标准的 ApplicationContext,您可以在相应的测试类上添加 @WebAppConfiguration 注解。spring-doc.cadn.net.cn

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

请注意,Spring 对 WebApplicationContext 实现的测试支持与其对标准 ApplicationContext 实现的支持相当。在使用 WebApplicationContext 进行测试时,您可以通过 @ContextConfiguration 自由声明 XML 配置文件、Groovy 脚本或 @Configuration 类。您还可以自由使用任何其他测试注解,例如 @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 显式声明资源基础路径,并使用 @ContextConfiguration 声明 XML 资源位置:spring-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 Mock

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

一旦为您的测试加载了 WebApplicationContext,您可能会发现需要与 Web 模拟对象(mocks)进行交互——例如,用于设置测试夹具(test fixture),或在调用 Web 组件后执行断言。以下示例展示了哪些模拟对象可以自动注入到您的测试实例中。请注意,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 可以通过用于加载它的配置参数组合进行唯一标识。因此,该唯一的配置参数组合被用来生成一个键(key),在此键下缓存该上下文。TestContext 框架使用以下配置参数来构建上下文缓存键:spring-doc.cadn.net.cn

例如,如果 TestClassA@ContextConfigurationlocations(或 value)属性指定了 {"app-config.xml", "test-config.xml"},则 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 等构建框架执行测试时,务必确保构建框架不会在测试之间派生新进程。例如,如果 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 日志类别设置为 DEBUG 级别。spring-doc.cadn.net.cn

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

上下文层次结构

在编写依赖于已加载的 Spring ApplicationContext 的集成测试时,通常针对单个上下文进行测试就足够了。然而,有时针对 ApplicationContext 实例的层次结构进行测试会更有益,甚至必不可少。例如,如果您正在开发一个 Spring MVC Web 应用程序,通常会有一个由 Spring 的 WebApplicationContext 加载的根 ContextLoaderListener,以及一个由 Spring 的 WebApplicationContext 加载的子 DispatcherServlet。这形成了一种父子上下文层次结构:共享组件和基础设施配置声明在根上下文中,并由子上下文中的 Web 特定组件使用。另一个使用场景出现在 Spring Batch 应用程序中,通常会有一个父上下文提供共享批处理基础设施的配置,以及一个子上下文用于特定批处理作业的配置。spring-doc.cadn.net.cn

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

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

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

ControllerIntegrationTests 通过声明一个包含两个层级的上下文层次结构,代表了 Spring MVC Web 应用程序的一种典型集成测试场景: 一个用于根 WebApplicationContext(使用 TestAppConfig @Configuration 类加载), 另一个用于 Dispatcher 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 通过确保 @ContextConfigurationname 属性所声明的名称均为 child,来合并 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 中的 @ContextConfiguration 标志设置为 false,来覆盖上下文层次结构中指定命名层级的配置。因此,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 标志来控制如何清除上下文缓存。有关更多详细信息,请参阅 Spring 测试注解 中关于 @DirtiesContext 的讨论,以及 @DirtiesContext 的 Javadoc。

3.5.6. 测试夹具的依赖注入

当您使用DependencyInjectionTestExecutionListener(默认配置)时,测试实例的依赖项将从您通过@ContextConfiguration或相关注解配置的应用上下文中注入的 Bean 提供。根据您选择的注解以及将其放置在 setter 方法还是字段上,您可以使用 setter 注入、字段注入或两者兼用。如果您使用的是 JUnit Jupiter,还可以选择使用构造函数注入(请参阅使用SpringExtension进行依赖注入)。为了与 Spring 基于注解的注入支持保持一致,您也可以使用 Spring 的@Autowired注解或 JSR-330 中的@Inject注解来实现字段和 setter 注入。spring-doc.cadn.net.cn

对于 JUnit Jupiter 以外的测试框架,TestContext 框架不会参与测试类的实例化。因此,在测试类中对构造函数使用 @Autowired@Inject 不会产生任何效果。
尽管在生产代码中不推荐使用字段注入,但在测试代码中,字段注入实际上相当自然。这种差异的原因在于,你永远不会直接实例化你的测试类。因此,无需在测试类上调用public构造函数或setter方法。

由于 @Autowired 用于执行按类型自动装配,如果你有多个相同类型的 bean 定义,就无法对这些特定的 bean 依赖此方法。在这种情况下,你可以将 @Autowired@Qualifier 结合使用。你也可以选择将 @Inject@Named 结合使用。另外,如果你的测试类可以访问其 ApplicationContext,也可以通过显式查找来实现(例如,调用 applicationContext.getBean("titleRepository", TitleRepository.class))。spring-doc.cadn.net.cn

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

考虑在目标一节中所描述的测试#integration-testing-goals类的场景。接下来的两个代码清单展示了在字段和 setter 方法上使用@Autowired注解的方式。所有示例代码之后会给出应用程序上下文的配置。spring-doc.cadn.net.cn

以下代码清单中的依赖注入行为并非 JUnit Jupiter 所特有。相同的依赖注入技术可以与任何受支持的测试框架结合使用。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)
    }

// ...

指定的限定符(qualifier)值用于指示要注入的具体 DataSource bean,从而将类型匹配的范围缩小到特定的 bean。该值会与相应 <qualifier> 定义中的 <bean> 声明进行匹配。如果未找到匹配的限定符声明,则会退而使用 bean 的名称作为限定符值,因此你也可以通过 bean 的名称有效地指向特定的 bean(如前所示,假设 myDataSource 是该 bean 的 id)。spring-doc.cadn.net.cn

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

Spring 自早期版本起就支持请求作用域和会话作用域的 Bean,你可以按照以下步骤来测试你的请求作用域和会话作用域的 Bean:spring-doc.cadn.net.cn

下一个代码片段展示了登录用例的 XML 配置。请注意,userService bean 依赖于一个请求作用域(request-scoped)的 loginAction bean。此外,LoginAction 是通过使用SpEL 表达式进行实例化的,这些表达式从当前 HTTP 请求中获取用户名和密码。在我们的测试中,我们希望通过由 TestContext 框架管理的模拟对象来配置这些请求参数。以下清单展示了该用例的配置: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
    }
}

以下代码片段与我们之前看到的请求作用域(request-scoped)bean 的示例类似。但这一次,userService bean 依赖于一个会话作用域(session-scoped)的 userPreferences bean。请注意,UserPreferences bean 是通过使用 SpEL 表达式实例化的,该表达式从当前 HTTP 会话中获取主题(theme)。在我们的测试中,需要在 TestContext 框架所管理的模拟会话(mock session)中配置一个主题。以下示例展示了如何实现这一点: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.8. 事务管理

在 TestContext 框架中,事务由 TransactionalTestExecutionListener 管理,该监听器默认已配置,即使您未在测试类上显式声明 @TestExecutionListeners。然而,要启用对事务的支持,您必须在通过 PlatformTransactionManager 语义加载的 ApplicationContext 中配置一个 @ContextConfiguration bean(稍后会提供更详细的说明)。此外,您必须在测试类或测试方法级别上声明 Spring 的 @Transactional 注解。spring-doc.cadn.net.cn

测试管理的事务

测试管理的事务(test-managed transactions)是指通过使用 TransactionalTestExecutionListener 声明式地管理,或通过使用 TestTransaction(稍后描述)以编程方式管理的事务。你不应将此类事务与 Spring 管理的事务(即在为测试加载的 ApplicationContext 中由 Spring 直接管理的事务)或应用程序管理的事务(即在测试调用的应用程序代码中以编程方式管理的事务)混淆。Spring 管理的事务和应用程序管理的事务通常会参与到测试管理的事务中。然而,如果 Spring 管理或应用程序管理的事务配置了除 REQUIREDSUPPORTS 之外的任何传播行为类型,则应格外谨慎(详见关于事务传播行为的讨论)。spring-doc.cadn.net.cn

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

在使用测试框架中的任何形式的抢占式超时机制时,若同时结合 Spring 管理的测试事务,必须格外谨慎。spring-doc.cadn.net.cn

具体来说,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_SUPPORTED 的测试也不会在事务中运行。spring-doc.cadn.net.cn

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

valuetransactionManagerspring-doc.cadn.net.cn

是的spring-doc.cadn.net.cn

propagationspring-doc.cadn.net.cn

仅支持 Propagation.NOT_SUPPORTEDspring-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"))
    }
}

事务回滚与提交行为中所述,在createUser()方法执行后,无需清理数据库,因为对数据库所做的任何更改都会被TransactionalTestExecutionListener自动回滚。spring-doc.cadn.net.cn

事务回滚与提交行为

默认情况下,测试事务在测试完成后会自动回滚;但可以通过 @Commit@Rollback 注解以声明式方式配置事务的提交和回滚行为。更多详情请参见注解支持部分中的相应条目。spring-doc.cadn.net.cn

编程式事务管理

您可以使用 TestTransaction 中的静态方法以编程方式与测试管理的事务进行交互。例如,您可以在测试方法、前置方法(before methods)和后置方法(after methods)中使用 TestTransaction 来启动或结束当前测试管理的事务,或者将当前测试管理的事务配置为回滚或提交。TestTransaction 的支持在启用 TransactionalTestExecutionListener 时会自动可用。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") 来声明限定符,或者由 @Configuration 类实现 TransactionManagementConfigurer。请参阅 TestContextTransactionUtils.retrieveTransactionManager() 的 Javadoc,了解用于在测试的 ApplicationContext 中查找事务管理器的算法详情。spring-doc.cadn.net.cn

所有事务相关注解的演示

以下基于 JUnit Jupiter 的示例展示了一个虚构的集成测试场景,突出了所有与事务相关的注解。该示例并非旨在展示最佳实践,而是为了演示如何使用这些注解。请参阅 注解支持 部分以获取更多信息和配置示例。针对 @Sql 的事务管理 包含另一个示例,该示例使用 @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 持久化上下文状态的应用程序代码时,请确保在运行该代码的测试方法中刷新底层的工作单元(unit of work)。如果不刷新底层的工作单元,可能会产生误报(false positives):你的测试通过了,但相同的代码在真实的生产环境中却会抛出异常。请注意,这一点适用于任何维护内存中工作单元的 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()
}

// ...

3.5.9. 执行 SQL 脚本

在针对关系型数据库编写集成测试时,通常需要运行 SQL 脚本来修改数据库结构或向表中插入测试数据。 spring-jdbc 模块提供了对嵌入式数据库或现有数据库的初始化支持,即在 Spring ApplicationContext 加载时执行 SQL 脚本。详情请参见 嵌入式数据库支持使用嵌入式数据库测试数据访问逻辑spring-doc.cadn.net.cn

尽管在加载ApplicationContext一次性初始化数据库用于测试非常有用,但有时在集成测试期间能够修改数据库至关重要。以下章节将解释如何在集成测试期间以编程方式和声明方式运行 SQL 脚本。spring-doc.cadn.net.cn

以编程方式执行 SQL 脚本

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

ScriptUtils 提供了一组用于处理 SQL 脚本的静态工具方法,主要用于框架内部使用。然而,如果您需要完全控制 SQL 脚本的解析和执行方式,ScriptUtils 可能比后面描述的一些其他替代方案更适合您的需求。有关 https://docs.spring.io/spring-framework/docs/5.2.25.RELEASE/javadoc-api/org/springframework/jdbc/datasource/init/ScriptUtils.html 中各个方法的更多详细信息,请参阅javadocspring-doc.cadn.net.cn

ResourceDatabasePopulator 提供了一个基于对象的 API,用于通过使用定义在外部资源中的 SQL 脚本来以编程方式填充、初始化或清理数据库。ResourceDatabasePopulator 提供了配置字符编码、语句分隔符、注释分隔符以及解析和运行脚本时使用的错误处理标志的选项。每个配置选项都有一个合理的默认值。有关默认值的详细信息,请参阅 javadoc。要运行在 ResourceDatabasePopulator 中配置的脚本,您可以调用 populate(Connection) 方法针对 java.sql.Connection 运行填充器,或者调用 execute(DataSource) 方法针对 javax.sql.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

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

每个路径都被解释为一个 Spring Resource。一个普通路径(例如,"schema.sql")被视为相对于测试类所在包的 classpath 资源。以斜杠开头的路径被视为绝对 classpath 资源(例如,"/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 脚本。如果无法检测到默认脚本,则会抛出一个 IllegalStateException 异常。spring-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 实例。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 是可选的,但为了与其他 JVM 语言(例如 Kotlin)兼容,您可能需要使用 @SqlGroupspring-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 脚本的全局配置。当使用@2注解的@Sql属性直接声明时,@SqlConfig作为嵌套在@Sql注解内的SQL脚本的局部配置。每个在@SqlConfig中的属性都有一个隐含的默认值,该默认值在相应属性的javadoc中进行了文档说明。由于 Java 语言规范中为注解属性定义的规则,很不幸的是,无法将值null分配给注解属性。因此,为了支持对继承的全局配置进行覆盖,@SqlConfig 属性具有显式的默认值,分别为 ""(用于字符串)、{}(用于数组)或 DEFAULT(用于枚举)。这种方法允许在局部声明@SqlConfig中选择性地覆盖来自全局声明@SqlConfig的个别属性,通过提供不同于""{}DEFAULT的值。全局@SqlConfig属性会在局部@SqlConfig属性未提供除""{}DEFAULT以外的显式值时被继承。因此,显式的本地配置会覆盖全局配置。spring-doc.cadn.net.cn

@Sql@SqlConfig 提供的配置选项等同于 ScriptUtilsResourceDatabasePopulator 所支持的选项,但它们是 <jdbc:initialize-database/> XML 命名空间元素所提供选项的超集。有关详细信息,请参阅 @Sql@SqlConfig 中各个属性的 Javadoc。spring-doc.cadn.net.cn

@Sql 的事务管理spring-doc.cadn.net.cn

默认情况下,SqlScriptsTestExecutionListener 会推断通过 @Sql 配置的脚本所需的事务语义。具体而言,SQL 脚本会在无事务、现有 Spring 管理的事务(例如,由针对使用 @Transactional 注解的测试所管理的 TransactionalTestExecutionListener 事务)或隔离事务中运行,这取决于 @SqlConfigtransactionMode 属性的配置值以及测试的 ApplicationContext 中是否存在 PlatformTransactionManager。然而,最起码的要求是测试的 ApplicationContext 中必须存在 javax.sql.DataSourcespring-doc.cadn.net.cn

如果 SqlScriptsTestExecutionListener 用于检测 DataSourcePlatformTransactionManager 并推断事务语义的算法不符合您的需求, 您可以通过设置 @SqlConfigdataSourcetransactionManager 属性来指定显式名称。此外,您可以通过设置 @SqlConfigtransactionMode 属性来控制事务传播行为(例如,脚本是否应在隔离的事务中运行)。虽然对本参考手册而言,全面讨论使用 @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 注解文档部分spring-doc.cadn.net.cn

3.5.10. 并行测试执行

Spring Framework 5.0 在使用 Spring TestContext 框架时,引入了在单个 JVM 内并行执行测试的基本支持。通常来说,这意味着大多数测试类或测试方法无需对测试代码或配置进行任何修改即可并行运行。spring-doc.cadn.net.cn

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

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

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

  • 使用 Spring Framework 的 @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 的最大容量已被超出,您可以增大缓存的最大容量。详情请参见关于上下文缓存的讨论。spring-doc.cadn.net.cn

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

3.5.11. TestContext 框架支持类

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

Spring JUnit 4 运行器

Spring TestContext 框架通过自定义运行器(支持 JUnit 4.12 或更高版本)与 JUnit 4 实现了完全集成。通过在测试类上使用 @RunWith(SpringJUnit4ClassRunner.class) 注解,或者更简短的 @RunWith(SpringRunner.class) 变体,开发者可以编写基于标准 JUnit 4 的单元测试和集成测试,同时还能享受 TestContext 框架带来的诸多优势,例如加载应用上下文、对测试实例进行依赖注入、事务化的测试方法执行等。如果你希望在使用其他运行器(例如 JUnit 4 的 Parameterized 运行器)或第三方运行器(例如 MockitoJUnitRunner)的同时使用 Spring TestContext 框架,也可以选择使用 Spring 对 JUnit 规则(JUnit Rules)的支持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 被配置为一个空列表,以禁用默认监听器;否则,默认监听器将要求通过 ApplicationContext 配置一个 @ContextConfigurationspring-doc.cadn.net.cn

Spring JUnit 4 规则

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

SpringClassRule 是一个 JUnit TestRule,用于支持 Spring TestContext 框架的类级别功能;而 SpringMethodRule 是一个 JUnit MethodRule,用于支持 Spring TestContext 框架的实例级别和方法级别功能。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 支持类

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

AbstractJUnit4SpringContextTests 是一个抽象基测试类,它将 Spring TestContext 框架与显式的 ApplicationContext 测试支持集成到 JUnit 4 环境中。当你继承 AbstractJUnit4SpringContextTests 时,可以访问一个受保护(protected)的 applicationContext 实例变量,用于执行显式的 Bean 查找,或测试整个上下文的状态。spring-doc.cadn.net.cn

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

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

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

由于你也可以在 JUnit 5 中将注解用作元注解(meta-annotations),Spring 提供了组合注解 @SpringJUnitConfig@SpringJUnitWebConfig,以简化测试 ApplicationContext 和 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...
    }
}

同样,以下示例使用 @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...
    }
}

有关更多详细信息,请参阅Spring JUnit Jupiter 测试注解文档中关于@SpringJUnitWebConfig#integration-testing-annotations-junit-jupiter的部分。spring-doc.cadn.net.cn

使用依赖注入的SpringExtension

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

具体来说,SpringExtension 可以从测试的ApplicationContext中注入依赖到使用@BeforeAll@AfterAll@BeforeEach@AfterEach@Test@RepeatedTest@ParameterizedTest等注解修饰的测试构造函数和方法中。spring-doc.cadn.net.cn

构造函数注入

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

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

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

如果测试类的构造函数被视为可自动装配(autowirable),Spring 将负责解析该构造函数中所有参数的值。 因此,JUnit Jupiter 中注册的其他任何 ParameterResolver 都无法为此类构造函数解析参数。

如果使用 @TestInstance(PER_CLASS) 在测试方法之前或之后关闭测试的 @DirtiesContext,则测试类的构造函数注入不得与 JUnit Jupiter 的 ApplicationContext 功能结合使用。spring-doc.cadn.net.cn

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

要将 @DirtiesContext 与“测试方法前”或“测试方法后”模式结合使用,并同时使用 @TestInstance(PER_CLASS),必须将 Spring 的依赖项配置为通过字段注入或 setter 注入的方式提供,以便在测试方法调用之间能够重新注入这些依赖。spring-doc.cadn.net.cn

在以下示例中,Spring 将从通过 OrderService 加载的 ApplicationContext 中注入 TestConfig.class 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 将从 OrderService 加载的 ApplicationContext 中的 TestConfig.class 注入到 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 注解可以让测试方法访问 RepetitionInfospring-doc.cadn.net.cn

TestNG 支持类

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

AbstractTestNGSpringContextTests 是一个抽象基测试类,它将 Spring TestContext 框架与显式的 ApplicationContext 测试支持集成到 TestNG 环境中。当你继承 AbstractTestNGSpringContextTests 时,可以访问一个受保护(protected)的 applicationContext 实例变量,用于执行显式的 Bean 查找,或测试整个上下文的状态。spring-doc.cadn.net.cn

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

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

3.6. Spring MVC 测试框架

Spring MVC 测试框架为测试 Spring MVC 代码提供了头等支持,其流畅的 API 可与 JUnit、TestNG 或任何其他测试框架配合使用。该框架构建于 https://docs.spring.io/spring-framework/docs/5.2.25.RELEASE/javadoc-api/org/springframework/mock/web/package-summary.html 模块提供的Servlet API 模拟对象之上,因此无需运行 Servlet 容器。它使用 DispatcherServlet 来提供完整的 Spring MVC 运行时行为,并支持通过 TestContext 框架加载真实的 Spring 配置;此外,还提供了一种独立模式,允许你手动实例化控制器并逐个进行测试。spring-doc.cadn.net.cn

Spring MVC Test 还提供了客户端支持,用于测试使用 RestTemplate 的代码。客户端测试会模拟服务器响应,并且也不使用正在运行的服务器。spring-doc.cadn.net.cn

Spring Boot 提供了一种选项,用于编写包含运行中服务器的完整端到端集成测试。如果这是您的目标,请参阅Spring Boot 参考指南。 有关容器外测试与端到端集成测试之间区别的更多信息,请参见Spring MVC 测试与端到端测试

3.6.1. 服务端测试

你可以使用 JUnit 或 TestNG 为 Spring MVC 控制器编写普通的单元测试。为此,只需实例化控制器,注入模拟(mocked)或桩(stubbed)的依赖项,然后调用其方法(根据需要传入 MockHttpServletRequestMockHttpServletResponse 等对象)。然而,编写此类单元测试时,仍有大量内容未被覆盖:例如请求映射(request mappings)、数据绑定(data binding)、类型转换(type conversion)、验证(validation)等等。此外,在请求处理生命周期中,还可能调用其他控制器方法,如 @InitBinder@ModelAttribute@ExceptionHandlerspring-doc.cadn.net.cn

Spring MVC Test 的目标是通过实际的 DispatcherServlet 执行请求并生成响应,从而提供一种有效的方式来测试控制器。spring-doc.cadn.net.cn

Spring MVC Test 基于 #mock-objects-servlet 模块中提供的、大家熟悉的 Servlet API 的“模拟(mock)”实现。这使得无需在 Servlet 容器中运行即可执行请求并生成响应。在大多数情况下,一切行为都应与运行时一致,但也有少数例外情况,具体说明请参见Spring MVC Test 与端到端测试的对比。以下是一个基于 JUnit Jupiter 的示例,使用了 Spring MVC Test:spring-doc.cadn.net.cn

Java
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.;

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

    MockMvc mockMvc;

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

    @Test
    void getAccount() throws Exception {
        this.mockMvc.perform(get("/accounts/1")
                .accept(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andExpect(content().contentType("application/json"))
            .andExpect(jsonPath("$.name").value("Lee"));
    }
}
Kotlin
import org.springframework.test.web.servlet.get

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

    lateinit var mockMvc: MockMvc

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

    @Test
    fun getAccount() {
        mockMvc.get("/accounts/1") {
            accept = MediaType.APPLICATION_JSON
        }.andExpect {
            status { isOk }
            content { contentType(MediaType.APPLICATION_JSON) }
            jsonPath("$.name") { value("Lee") }
        }
    }
}
Kotlin 中提供了专用的 MockMvc DSL

上述测试依赖于 TestContext 框架的 WebApplicationContext 支持,从与测试类位于同一包中的 XML 配置文件加载 Spring 配置,但也支持基于 Java 和基于 Groovy 的配置。请参阅这些示例测试spring-doc.cadn.net.cn

MockMvc 实例用于向 GET 发起一个 /accounts/1 请求,并验证返回的响应状态码为 200,内容类型为 application/json,且响应体中包含一个名为 name 的 JSON 属性,其值为 LeejsonPath 语法由 Jayway JsonPath 项目 提供支持。本文档后续部分将讨论许多其他用于验证所执行请求结果的选项。spring-doc.cadn.net.cn

静态导入

前一节示例中使用的流式(fluent)API需要导入一些静态成员,例如 MockMvcRequestBuilders.*MockMvcResultMatchers.*MockMvcBuilders.*。查找这些类的一个简单方法是搜索匹配 MockMvc* 的类型。如果你使用 Eclipse 或 Spring Tools for Eclipse,请务必在 Eclipse 的偏好设置中(路径为:Java → Editor → Content Assist → Favorites)将它们添加为“收藏的静态成员”。这样,在你输入静态方法名的第一个字符后,即可使用内容辅助功能。其他 IDE(例如 IntelliJ)可能无需额外配置。请查阅你所用 IDE 对静态成员代码补全的支持情况。spring-doc.cadn.net.cn

设置选项

您有两种主要方式来创建 MockMvc 实例。第一种是通过 TestContext 框架加载 Spring MVC 配置,该框架会加载 Spring 配置并将一个 WebApplicationContext 注入到测试中,以用于构建 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(this.wac).build();
    }

    // ...

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

    lateinit var mockMvc: MockMvc

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

    // ...

}

您的第二个选项是手动创建一个控制器实例,而不加载 Spring 配置。系统会自动创建一个基本的默认配置,该配置大致相当于 MVC JavaConfig 或 MVC 命名空间所提供的配置。您可以在一定程度上对其进行自定义。以下示例展示了如何实现这一点: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()
    }

    // ...

}

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

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

    // ...

}

另一方面,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

查看 ConfigurableMockMvcBuilder 的 Javadoc,以获取所有 MockMvc 构建器功能的列表,或使用 IDE 探索可用选项。spring-doc.cadn.net.cn

执行请求

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

Java
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,因此不会实际解析 multipart 请求。相反,你需要按如下示例进行设置: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

在大多数情况下,最好从请求 URI 中省略上下文路径和 Servlet 路径。如果你必须使用完整的请求 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
mockMvc.perform(get("/accounts/1")).andExpect(status().isOk());
Kotlin
import org.springframework.test.web.servlet.get

mockMvc.get("/accounts/1").andExpect {
    status().isOk()
}

MockMvcResultMatchers.* 提供了多种断言,其中一些还嵌套了更详细的断言。spring-doc.cadn.net.cn

期望值通常分为两大类。第一类断言用于验证响应的属性(例如,响应状态、响应头和响应内容)。这些是最关键的断言结果。spring-doc.cadn.net.cn

第二类断言超出了响应本身的范畴。这些断言允许您检查 Spring MVC 特有的各个方面,例如哪个控制器方法处理了请求、是否抛出并处理了异常、模型的内容是什么、选择了哪个视图、添加了哪些 Flash 属性等等。它们还允许您检查 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

请注意,通用的期望(expectations)始终会被应用,除非创建一个单独的 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")
    }
}
异步请求

Servlet 3.0 异步请求,Spring MVC 中已支持,其工作原理是退出 Servlet 容器线程,允许应用程序异步计算响应,之后再通过一次异步分派(async dispatch)回到 Servlet 容器线程以完成处理。spring-doc.cadn.net.cn

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

Java
@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 手动执行一次异步分发(因为没有正在运行的容器)
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 手动执行一次异步分发(因为没有正在运行的容器)
5 验证最终响应
流式响应

Spring MVC Test 本身并未内置用于无容器环境下测试流式响应的选项。使用 Spring MVC 流式处理 功能的应用程序,可以利用 WebTestClient 对运行中的服务器执行端到端的集成测试。Spring Boot 也支持此功能,你可以在其中使用 https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-testing-spring-boot-applications-testing-with-running-server 测试正在运行的服务器。此外还有一个额外优势,即可以使用 Project Reactor 提供的 StepVerifier,它允许对数据流声明预期结果。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

Spring MVC 测试与端到端测试

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

理解这一点最简单的方式是从一个空白的 MockHttpServletRequest 开始。 你向其中添加的内容就构成了该请求。可能会让你感到意外的是:默认情况下没有上下文路径(context path); 没有 jsessionid cookie;没有转发(forwarding)、错误(error)或异步(async)分派; 因此,也不会实际渲染 JSP 页面。取而代之的是,“转发”和“重定向”的 URL 会被保存在 MockHttpServletResponse 中,你可以通过断言来验证它们。spring-doc.cadn.net.cn

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

或者,您可以考虑使用 Spring Boot 提供的完整端到端集成测试支持,即 @SpringBootTest。请参阅Spring Boot 参考指南spring-doc.cadn.net.cn

每种方法都有其优缺点。Spring MVC Test 提供的选项在经典单元测试到完整集成测试的尺度上处于不同的位置。可以肯定的是,Spring MVC Test 中的任何选项都不属于经典单元测试的范畴,但它们更接近于单元测试。例如,你可以通过向控制器注入模拟的服务来隔离 Web 层,此时你仅通过 DispatcherServlet 对 Web 层进行测试,但使用的是真实的 Spring 配置,就像你在隔离数据访问层时将其与上层解耦进行测试一样。此外,你还可以使用独立设置(stand-alone setup),一次专注于一个控制器,并手动提供使其正常工作所需的配置。spring-doc.cadn.net.cn

使用 Spring MVC Test 时另一个重要的区别在于,从概念上讲,这类测试属于服务器端测试,因此你可以检查使用了哪个处理器(handler)、异常是否通过 HandlerExceptionResolver 被处理、模型(model)的内容是什么、存在哪些绑定错误(binding errors)以及其他细节。这意味着编写断言会更加容易,因为在通过实际的 HTTP 客户端进行测试时,服务器就像一个黑盒,而在这里则不是。这通常是经典单元测试的一个优势:更容易编写、理解和调试,但它并不能取代完整的集成测试的必要性。同时,我们也不应忽视这样一个事实:响应(response)始终是最重要的验证内容。简而言之,即使在同一个项目中,也存在多种测试风格和策略的空间。spring-doc.cadn.net.cn

更多示例

框架自身的测试包括 许多 示例测试,旨在展示如何使用 Spring MVC Test。您可以浏览这些示例以获取更多灵感。此外, spring-mvc-showcase 项目基于 Spring MVC Test 实现了完整的测试覆盖。spring-doc.cadn.net.cn

3.6.2. HtmlUnit 集成

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

MockMvc 可以与不依赖 Servlet 容器的模板技术(例如 Thymeleaf、FreeMarker 等)配合使用,但不能用于 JSP,因为 JSP 依赖于 Servlet 容器。
为什么需要 HtmlUnit 集成?

人们脑海中首先浮现的最明显的问题是:“我为什么需要这个?”要找到最佳答案,最好通过一个非常基础的示例应用程序来探索。假设你有一个 Spring MVC Web 应用程序,它支持对 Message 对象执行 CRUD 操作,并且还支持对所有消息进行分页浏览。你会如何测试它呢?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

这些挑战并不意味着我们应该完全放弃端到端集成测试。相反,我们可以通过重构详细的测试,使其使用模拟服务(mock services)来减少端到端集成测试的数量。这些模拟服务运行速度更快、更可靠,并且没有副作用。然后,我们可以实施少量真正的端到端集成测试,用于验证简单的业务流程,以确保所有组件能够正确协同工作。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 的测试依赖。为了在 Apache HttpComponents 4.5+ 中使用 HtmlUnit,您需要使用 HtmlUnit 2.18 或更高版本。spring-doc.cadn.net.cn

我们可以使用 WebClient 轻松创建一个与 MockMvc 集成的 HtmlUnit MockMvcWebClientBuilder,如下所示: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 框架为我们加载的 WebClient 来构建一个 WebApplicationContext。以下示例重复使用了这种方法: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

这种方式更为冗长,但通过使用 WebClient 实例来构建 MockMvc,我们可以充分利用 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(Don't Repeat Yourself,不要重复自己)原则,因此我们最好将这段代码提取到一个独立的方法中,如下所示: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)
}

这样做可以确保在我们更改用户界面时,无需更新所有的测试。spring-doc.cadn.net.cn

我们甚至可以更进一步,将此逻辑放入一个代表当前所处 ObjectHtmlPage 中,如下例所示: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()
        }
    }
}

以前,这种模式被称为 页面对象模式(Page Object Pattern)。虽然我们当然可以使用 HtmlUnit 来实现这一点,但 WebDriver 提供了一些工具(我们将在以下章节中探讨),使该模式的实现变得更加容易。spring-doc.cadn.net.cn

MockMvc 和 WebDriver 设置

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

我们可以轻松地使用 MockMvcHtmlUnitDriverBuilder 创建一个与 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)

通过利用页面对象模式(Page Object Pattern),这改进了我们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 让我们能够从 CreateMessagePage 的 HtmlUnit 版本中移除大量代码,因为它会自动解析每个 WebElementPageFactory#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 让我们能够从 CreateMessagePage 的 HtmlUnit 版本中移除大量代码,因为它会自动解析每个 WebElementPageFactory#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 框架为我们加载的 WebDriver 来构建一个 WebApplicationContext。此方法在此再次展示如下: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

这种方式更为冗长,但通过使用 WebDriver 实例来构建 MockMvc,我们可以充分利用 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 一样,这里通过使用页面对象模式(Page Object Pattern)改进了我们的 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,通过该 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.6.3. 客户端 REST 测试

你可以使用客户端测试来测试内部使用 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 测试的核心类)通过一个自定义的 RestTemplateClientHttpRequestFactory 进行配置,该工厂会将实际请求与预期进行比对,并返回“模拟”(stub)响应。在此例中,我们期望收到一个发往 /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" 的请求,那么在任何对 "/somewhere" 的请求之前,必须先有一次对 "/something" 的请求;除此之外,后续的 "/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 ...
静态导入

与服务端测试类似,客户端测试的流式 API 需要导入一些静态方法。这些静态导入很容易通过搜索 MockRest* 找到。Eclipse 用户应在 Eclipse 的偏好设置中(路径为 Java → Editor → Content Assist → Favorites)将 MockRestRequestMatchers.*MockRestResponseCreators.* 添加为“收藏的静态成员”。这样,在输入静态方法名的第一个字符后,即可使用内容辅助功能。其他 IDE(例如 IntelliJ)可能无需任何额外配置。请查阅相关 IDE 对静态成员代码补全的支持情况。spring-doc.cadn.net.cn

客户端 REST 测试的更多示例

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

3.7. WebTestClient

WebTestClient 是围绕 WebClient 的一个轻量级封装, 利用它来执行请求,并提供专门的、流畅的 API 用于验证响应。 WebTestClient 可通过使用 模拟请求和响应 绑定到 WebFlux 应用程序, 也可以通过 HTTP 连接测试任意 Web 服务器。spring-doc.cadn.net.cn

Kotlin 用户:请参阅本节, 了解有关使用WebTestClient的内容。

3.7.1. 设置

要创建一个 WebTestClient,您必须从几种服务器设置选项中选择一种。 实际上,您可以选择将 WebFlux 应用程序配置为绑定到本地服务器,或者使用一个 URL 连接到正在运行的服务器。spring-doc.cadn.net.cn

绑定到控制器

以下示例展示了如何创建一个服务器设置,以一次测试一个 @Controllerspring-doc.cadn.net.cn

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

前面的示例加载了WebFlux Java 配置, 并注册了给定的控制器。所生成的 WebFlux 应用程序通过使用模拟的请求和响应对象进行测试, 而无需启动 HTTP 服务器。该构建器还提供了更多方法,用于自定义默认的 WebFlux Java 配置。spring-doc.cadn.net.cn

绑定到路由函数

以下示例展示了如何从 RouterFunction 设置服务器:spring-doc.cadn.net.cn

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

在内部,该配置会传递给 RouterFunctions.toWebHandler。 所生成的 WebFlux 应用程序通过使用模拟的请求和响应对象进行测试,而无需启动 HTTP 服务器。spring-doc.cadn.net.cn

绑定到ApplicationContext

以下示例展示了如何从应用程序的 Spring 配置(或其某个子集)中设置服务器: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

在内部,该配置会被传递给 WebHttpHandlerBuilder 以设置请求处理链。更多详细信息,请参阅WebHandler API。生成的 WebFlux 应用程序通过使用模拟的请求和响应对象,在不依赖 HTTP 服务器的情况下进行测试。spring-doc.cadn.net.cn

绑定到服务器

以下服务器设置选项允许您连接到正在运行的服务器: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.7.2. 编写测试

WebTestClient 提供的 API 与 WebClient 完全相同, 直到调用 exchange() 执行请求为止。在 exchange() 之后, 是一套用于验证响应的链式 API 工作流。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)

然后,您需要指定如何解码和消费响应体: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)...
        }

你也可以退出工作流并获取结果,如下所示:spring-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>
无内容

如果响应没有内容(或者你并不关心是否有内容),请使用 Void.class,这可以确保资源被释放。以下示例展示了如何这样做: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>()

或者,如果你想断言响应内容为空,可以使用类似于以下的代码: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()
JSON 内容

当你使用 expectBody() 时,响应内容会被作为 byte[] 消耗。这对于原始内容的断言非常有用。例如,你可以使用 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表达式,如下所示: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/stream+json"), 你需要在响应状态和头部断言之后立即退出链式 API(通过使用 returnResult),如下例所示:spring-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>()

现在你可以消费 Flux<T>,在对象被解码时对其进行断言,并在测试目标达成时的某个时刻取消操作。我们建议使用 StepVerifier 模块中的 reactor-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()
请求体

在构建请求时,WebTestClient 提供的 API 与 WebClient 完全相同,其实现基本上只是一个简单的透传。有关如何准备带请求体的请求(包括提交表单数据、多部分请求等)的示例,请参阅WebClient 文档spring-doc.cadn.net.cn

4. 更多资源

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