Spring中的集成测试与单元测试
本文涵盖了 Spring 对集成测试的支持以及单元测试的最佳实践。
Spring 团队提倡测试驱动开发(TDD)。Spring 团队发现,正确使用控制反转(IoC)确实能让单元测试和集成测试变得更容易(因为类上的 setter 方法和适当的构造函数能让它们在测试中更容易进行组装,而无需设置服务定位器注册中心和类似结构)。
# 一、单元测试
依赖注入应当使你的代码比传统 J2EE / Java EE 开发时更少依赖容器。构成你应用程序的 POJO 应该可以在 JUnit 或 TestNG 测试中进行测试,使用 new
操作符来实例化对象,而无需 Spring 或其他任何容器。你可以使用模拟对象(结合其他有价值的测试技术)来孤立地测试你的代码。如果你遵循 Spring 的架构建议,那么最终代码库的清晰分层和组件化将便于更轻松地进行单元测试。例如,你可以通过存根或模拟 DAO 或仓库接口来测试服务层对象,在运行单元测试时无需访问持久化数据。
真正的单元测试通常运行速度极快,因为无需设置运行时基础设施。在开发方法中强调真正的单元测试可以提高你的生产力。你可能不需要本章测试部分的内容来帮助你为基于 IoC 的应用程序编写有效的单元测试。但是,对于某些单元测试场景,Spring 框架提供了模拟对象和测试支持类,本章将对其进行介绍。
# 1、模拟对象
Spring 包含多个专门用于模拟的包:
- 环境
- Servlet API
- Spring Web Reactive
# 1.1、环境
org.springframework.mock.env
包包含 Environment
和 PropertySource
抽象的模拟实现。MockEnvironment
和 MockPropertySource
对于为依赖特定环境属性的代码编写容器外测试非常有用。
# 1.2、API
org.springframework.mock.web
包包含一套全面的 Servlet API 模拟对象,这些对象对于测试 Web 上下文、控制器和过滤器很有用。这些模拟对象主要用于 Spring 的 Web MVC 框架,通常比动态模拟对象(如 EasyMock (opens new window))或其他 Servlet API 模拟对象(如 MockObjects)更易于使用。
提示:自 Spring 框架 6.0 以来,org.springframework.mock.web
中的模拟对象基于 Servlet 6.0 API。
MockMvc 基于模拟的 Servlet API 对象,为 Spring MVC 提供了一个集成测试框架。
# 1.3、Web Reactive
org.springframework.mock.http.server.reactive
包包含用于 WebFlux 应用程序的 ServerHttpRequest
和 ServerHttpResponse
的模拟实现。org.springframework.mock.web.server
包包含一个依赖于这些模拟请求和响应对象的模拟 ServerWebExchange
。
MockServerHttpRequest
和 MockServerHttpResponse
都从与特定服务器实现相同的抽象基类扩展而来,并与它们共享行为。例如,模拟请求一旦创建就不可变,但你可以使用 ServerHttpRequest
中的 mutate()
方法创建一个修改后的实例。
为了让模拟响应正确实现写入契约并返回一个写入完成句柄(即 Mono<Void>
),默认情况下它使用带 cache().then()
的 Flux
,这会缓冲数据并使其可用于测试中的断言。应用程序可以设置自定义写入函数(例如,用于测试无限流)。
WebTestClient 基于模拟请求和响应,为无需 HTTP 服务器的 WebFlux 应用程序测试提供支持。该客户端还可用于对运行中的服务器进行端到端测试。
# 2、单元测试支持类
Spring 包含许多有助于单元测试的类。它们分为两类:
- 通用测试实用工具
- Spring MVC 测试实用工具
# 2.1、通用测试实用工具
org.springframework.test.util
包包含几个用于单元和集成测试的通用实用工具。
AopTestUtils
(opens new window) 是一组与 AOP 相关的实用方法。你可以使用这些方法获取隐藏在一个或多个 Spring 代理后面的底层目标对象的引用。例如,如果你使用 EasyMock 或 Mockito 等库将一个 bean 配置为动态模拟对象,并且该模拟对象被包装在 Spring 代理中,你可能需要直接访问底层模拟对象,以便对其配置期望并执行验证。有关 Spring 的核心 AOP 实用工具,请参阅 AopUtils
(opens new window) 和 AopProxyUtils
(opens new window)。
ReflectionTestUtils
(opens new window) 是一组基于反射的实用方法。你可以在以下测试场景中使用这些方法:你需要更改常量的值、设置非 public
字段、调用非 public
的 setter 方法,或调用非 public
的配置或生命周期回调方法,例如在以下用例中测试应用程序代码时:
- 允许在域实体中通过
private
或protected
字段访问属性,而不是通过public
的 setter 方法的 ORM 框架(如 JPA 和 Hibernate)。 - Spring 对注解(如
@Autowired
、@Inject
和@Resource
)的支持,这些注解为private
或protected
字段、setter 方法和配置方法提供依赖注入。 - 使用
@PostConstruct
和@PreDestroy
等注解定义生命周期回调方法。
TestSocketUtils
(opens new window) 是一个简单的实用工具,用于在 localhost
上查找可用的 TCP 端口,以便在集成测试场景中使用。
注意:TestSocketUtils
可用于在可用的随机端口上启动外部服务器的集成测试。但是,这些实用工具不能保证给定端口随后仍然可用,因此不可靠。建议你依靠服务器自身在其选择或由操作系统分配的随机临时端口上启动,而不是使用 TestSocketUtils
为服务器查找可用的本地端口。要与该服务器进行交互,你应该查询服务器当前使用的端口。
# 2.2、MVC 测试实用工具
org.springframework.test.web
包包含 ModelAndViewAssert
(opens new window),你可以结合 JUnit、TestNG 或其他任何测试框架,用于处理 Spring MVC ModelAndView
对象的单元测试。
提示:要将 Spring MVC 的 Controller
类作为 POJO 进行单元测试,可以将 ModelAndViewAssert
与 Spring 的 Servlet API 模拟对象 中的 MockHttpServletRequest
、MockHttpSession
等结合使用。要结合 Spring MVC 的 WebApplicationContext
配置对 Spring MVC 和 REST 的 Controller
类进行全面的集成测试,建议使用 MockMvc。
# 二、集成测试
能够在无需部署到应用服务器或连接其他企业级基础设施的情况下执行一些集成测试是很重要的。这样做可以让你测试以下内容:
- 确保 Spring IoC 容器上下文的正确配置。
- 验证使用 JDBC 或 ORM 工具进行数据访问的情况。这可能包括 SQL 语句的正确性、Hibernate 查询、JPA 实体映射等。
Spring 框架在 spring-test
模块中为集成测试提供了一流的支持。实际 JAR 文件的名称可能包含版本号,也可能采用完整的 org.springframework.test
形式,这取决于下载来源。这个库包含 org.springframework.test
包,其中包含一些对使用 Spring 容器进行集成测试很有价值的类。此类测试不依赖于应用服务器或其他部署环境。这种测试运行速度比单元测试慢,但比依赖于应用服务器部署的等效 Selenium 测试或远程测试要快得多。
单元和集成测试支持是以注解驱动的 Spring TestContext 框架的形式提供的。TestContext 框架与实际使用的测试框架无关,这使得它可以在各种环境中对测试进行检测,包括 JUnit、TestNG 等。
以下部分概述了 Spring 集成支持的高级目标,本章的其余部分则重点介绍特定主题:
- JDBC 测试支持
- Spring TestContext 框架
- WebTestClient
- MockMvc
- 测试客户端应用程序
- 注解
# 1、集成测试的目标
Spring 的集成测试支持主要有以下目标:
- 管理测试之间的 Spring IoC 容器缓存。
- 提供 测试夹具实例的依赖注入。
- 为集成测试提供合适的 事务管理。
- 提供 特定于 Spring 的基类,帮助开发人员编写集成测试。
接下来的几个部分将详细介绍每个目标,并提供相关实现和配置细节的链接。
# 1.1、上下文管理与缓存
Spring TestContext 框架提供了一致的方式来加载 Spring 的 ApplicationContext
实例和 WebApplicationContext
实例,同时还能对这些上下文进行缓存。对已加载上下文进行缓存的支持非常重要,因为启动时间可能会成为一个问题,这并非因为 Spring 本身的开销,而是因为 Spring 容器实例化的对象需要时间来初始化。例如,一个包含 50 到 100 个 Hibernate 映射文件的项目可能需要 10 到 20 秒来加载这些映射文件。如果在每个测试夹具中的每次测试运行之前都产生这样的成本,那么整体测试运行速度会变慢,从而降低开发人员的工作效率。
测试类通常会声明一个 XML 或 Groovy 配置元数据的资源位置数组(通常位于类路径下),或者一个用于配置应用程序的组件类数组。这些位置或类与生产环境部署时在 web.xml
或其他配置文件中指定的位置或类相同或相似。
默认情况下,一旦配置好的 ApplicationContext
被加载,就会被每个测试复用。因此,每个测试套件只需要承担一次设置成本,后续的测试执行速度会快得多。在这里,“测试套件” 指的是在同一个 JVM 中运行的所有测试,例如,从 Ant、Maven 或 Gradle 构建中为某个项目或模块运行的所有测试。在极少数情况下,如果某个测试破坏了应用程序上下文而需要重新加载(例如,通过修改 bean 定义或应用程序对象的状态),可以配置 TestContext 框架在执行下一个测试之前重新加载配置并重建应用程序上下文。
# 1.2、测试夹具的依赖注入
当 TestContext 框架加载应用程序上下文时,它可以选择使用依赖注入来配置测试类的实例。这提供了一种方便的机制,可以使用应用程序上下文中预先配置好的 bean 来设置测试夹具。这样做的一个显著好处是,你可以在各种测试场景中复用应用程序上下文(例如,用于配置 Spring 管理的对象图、事务代理、DataSource
实例等),从而避免为单个测试用例重复设置复杂的测试夹具。
例如,假设有一个类(HibernateTitleRepository
)实现了 Title
域实体的数据访问逻辑。我们想要编写集成测试来测试以下几个方面:
- Spring 配置:基本上,与
HibernateTitleRepository
bean 配置相关的所有内容是否正确且完整? - Hibernate 映射文件配置:所有映射是否正确?是否设置了正确的懒加载配置?
HibernateTitleRepository
的逻辑:这个类的配置实例是否按预期执行?
# 1.3、事务管理
在访问真实数据库的测试中,一个常见的问题是测试对持久化存储状态的影响。即使使用开发数据库,状态的更改也可能会影响未来的测试。此外,许多操作(例如插入或修改持久化数据)必须在事务中才能执行(或验证)。
TestContext 框架解决了这个问题。默认情况下,该框架会为每个测试创建一个事务并在测试结束时回滚。你可以编写假设存在事务的代码。如果在测试中调用了带有事务代理的对象,它们会根据配置的事务语义正常运行。此外,如果一个测试方法在测试管理的事务中删除了某些表的内容,默认情况下事务会回滚,数据库会恢复到测试执行之前的状态。测试的事务支持是通过测试应用程序上下文中定义的 PlatformTransactionManager
bean 提供的。
如果你希望某个事务提交(这种情况比较少见,但当你希望某个特定的测试来填充或修改数据库时偶尔会用到),可以使用 @Commit 注解告诉 TestContext 框架提交事务而不是回滚。
# 1.4、集成测试的支持类
Spring TestContext 框架提供了几个抽象的支持类,这些类简化了集成测试的编写。这些基础测试类为测试框架提供了明确定义的挂钩,以及方便的实例变量和方法,让你能够访问:
ApplicationContext
,用于执行显式的 bean 查找或测试整个上下文的状态。JdbcTemplate
,用于执行 SQL 语句来查询数据库。你可以使用这些查询在执行与数据库相关的应用程序代码之前和之后确认数据库状态,并且 Spring 会确保这些查询在与应用程序代码相同的事务范围内运行。当与 ORM 工具结合使用时,请务必避免误报。
此外,你可能还需要创建自己的、特定于项目的自定义应用程序范围的超类,其中包含特定于项目的实例变量和方法。
# 三、测试支持
# 1、JdbcTestUtils
org.springframework.test.jdbc
包中包含 JdbcTestUtils
,它是一组与 JDBC 相关的实用工具函数,旨在简化标准的数据库测试场景。具体而言,JdbcTestUtils
提供了以下静态实用方法:
countRowsInTable(..)
:统计给定表中的行数。countRowsInTableWhere(..)
:通过提供的WHERE
子句统计给定表中的行数。deleteFromTables(..)
:删除指定表中的所有行。deleteFromTableWhere(..)
:通过提供的WHERE
子句从给定表中删除行。dropTables(..)
:删除指定的表。
提示:AbstractTransactionalJUnit4SpringContextTests
和 AbstractTransactionalTestNGSpringContextTests
提供了便捷方法,这些方法委托调用了 JdbcTestUtils
中的上述方法。
# 2、嵌入式数据库
spring - jdbc
模块支持配置和启动嵌入式数据库,可在与数据库交互的集成测试中使用。
# 四、测试上下文框架
Spring 测试上下文框架(位于 org.springframework.test.context
包中)提供了通用的、基于注解驱动的单元和集成测试支持,并且该支持与所使用的测试框架无关。测试上下文框架非常重视约定优于配置的原则,它有合理的默认设置,你可以通过基于注解的配置来覆盖这些默认设置。
此外,除了通用的测试基础架构外,测试上下文框架还为 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
支持类。
以下部分概述了测试上下文框架的内部原理。如果你仅仅对使用该框架感兴趣,而不打算使用自己的自定义监听器或自定义加载器来扩展它,那么可以直接查看配置(上下文管理、依赖注入、事务管理)、支持类和注解支持部分。
# 1、章节总结
- 关键抽象
- 启动测试上下文框架
TestExecutionListener
配置- 应用程序事件
- 测试执行事件
- 上下文管理
- 测试夹具的依赖注入
- 测试中的 Bean 覆盖
- 测试请求和会话作用域的 Bean
- 事务管理
- 执行 SQL 脚本
- 并行测试执行
- 测试上下文框架支持类
- 测试的提前编译支持
# 2、关键抽象
框架的核心由 TestContextManager
类以及 TestContext
、TestExecutionListener
和 SmartContextLoader
接口组成。会为每个测试类创建一个 TestContextManager
(例如,在 JUnit Jupiter 中,用于执行单个测试类中的所有测试方法)。TestContextManager
反过来管理一个 TestContext
,该对象保存当前测试的上下文信息。TestContextManager
还会随着测试的进行更新 TestContext
的状态,并委托给 TestExecutionListener
的实现类。这些实现类通过提供依赖注入、管理事务等方式来监测实际的测试执行过程。SmartContextLoader
负责为给定的测试类加载一个 ApplicationContext
。有关更多信息和各种实现的示例,请参阅 JavaDoc (opens new window) 和 Spring 测试套件。
# 2.1、TestContext
TestContext
封装了测试运行的上下文(与实际使用的测试框架无关),并为其所负责的测试实例提供上下文管理和缓存支持。如果有请求,TestContext
还会委托给 SmartContextLoader
来加载一个 ApplicationContext
。
# 2.2、TestContextManager
TestContextManager
是 Spring 测试上下文框架的主要入口点,负责管理单个 TestContext
,并在明确定义的测试执行点向每个已注册的 TestExecutionListener
发送事件:
- 在特定测试框架的任何 “before class” 或 “before all” 方法之前。
- 测试实例后处理。
- 在特定测试框架的任何 “before” 或 “before each” 方法之前。
- 在测试方法执行前立即执行,但在测试设置之后。
- 在测试方法执行后立即执行,但在测试清理之前。
- 在特定测试框架的任何 “after” 或 “after each” 方法之后。
- 在特定测试框架的任何 “after class” 或 “after all” 方法之后。
# 2.3、TestExecutionListener
TestExecutionListener
定义了响应由其注册的 TestContextManager
发布的测试执行事件的 API。
# 2.4、上下文加载器
ContextLoader
是一个策略接口,用于为 Spring 测试上下文框架管理的集成测试加载 ApplicationContext
。你应该实现 SmartContextLoader
而不是这个接口,以支持组件类、活动的 bean 定义配置文件、测试属性源、上下文层级结构以及 WebApplicationContext
支持。
SmartContextLoader
是 ContextLoader
接口的扩展,取代了最初的最小 ContextLoader
SPI。具体来说,SmartContextLoader
可以选择处理资源位置、组件类或上下文初始化器。此外,SmartContextLoader
可以在其加载的上下文中设置活动的 bean 定义配置文件和测试属性源。
Spring 提供了以下实现:
DelegatingSmartContextLoader
:两个默认加载器之一,它会根据为测试类声明的配置或默认位置或默认配置类的存在情况,在内部委托给AnnotationConfigContextLoader
、GenericXmlContextLoader
或GenericGroovyXmlContextLoader
。只有当 Groovy 在类路径中时,才会启用 Groovy 支持。WebDelegatingSmartContextLoader
:两个默认加载器之一,它会根据为测试类声明的配置或默认位置或默认配置类的存在情况,在内部委托给AnnotationConfigWebContextLoader
、GenericXmlWebContextLoader
或GenericGroovyXmlWebContextLoader
。只有当测试类上存在@WebAppConfiguration
时,才会使用 WebContextLoader
。只有当 Groovy 在类路径中时,才会启用 Groovy 支持。AnnotationConfigContextLoader
:从组件类加载标准的ApplicationContext
。AnnotationConfigWebContextLoader
:从组件类加载WebApplicationContext
。GenericGroovyXmlContextLoader
:从 Groovy 脚本或 XML 配置文件的资源位置加载标准的ApplicationContext
。GenericGroovyXmlWebContextLoader
:从 Groovy 脚本或 XML 配置文件的资源位置加载WebApplicationContext
。GenericXmlContextLoader
:从 XML 资源位置加载标准的ApplicationContext
。GenericXmlWebContextLoader
:从 XML 资源位置加载WebApplicationContext
。
# 3、引导 TestContext 框架
Spring 测试上下文框架(TestContext Framework)内部的默认配置足以满足所有常见用例。不过,有时开发团队或第三方框架希望更改默认的 ContextLoader
,实现自定义的 TestContext
或 ContextCache
,扩充默认的 ContextCustomizerFactory
和 TestExecutionListener
实现集等等。为了对测试上下文框架的运行方式进行这种底层控制,Spring 提供了一种引导策略。
TestContextBootstrapper
定义了用于引导测试上下文框架的服务提供者接口(SPI)。TestContextManager
使用 TestContextBootstrapper
为当前测试加载 TestExecutionListener
实现,并构建它所管理的 TestContext
。你可以通过直接使用 @BootstrapWith
或把它作为元注解,为测试类(或测试类层次结构)配置自定义的引导策略。如果没有通过 @BootstrapWith
显式配置引导器,则根据是否存在 @WebAppConfiguration
,使用 DefaultTestContextBootstrapper
或 WebTestContextBootstrapper
。
由于 TestContextBootstrapper
服务提供者接口(SPI)未来可能会发生变化(以适应新需求),因此我们强烈建议实现者不要直接实现这个接口,而是扩展 AbstractTestContextBootstrapper
或其某个具体子类。
# 4、TestExecutionListener
配置
Spring 提供了以下默认注册的 TestExecutionListener
实现,顺序如下:
ServletTestExecutionListener
:为WebApplicationContext
配置 Servlet API 模拟对象。DirtiesContextBeforeModesTestExecutionListener
:处理 “before” 模式下的@DirtiesContext
注解。ApplicationEventsTestExecutionListener
:为ApplicationEvents
提供支持。BeanOverrideTestExecutionListener
:为 测试中的 Bean 覆盖 提供支持。DependencyInjectionTestExecutionListener
:为测试实例提供依赖注入。MicrometerObservationRegistryTestExecutionListener
:为 Micrometer 的ObservationRegistry
提供支持。DirtiesContextTestExecutionListener
:处理 “after” 模式下的@DirtiesContext
注解。CommonCachesTestExecutionListener
:必要时清除测试的ApplicationContext
中的资源缓存。TransactionalTestExecutionListener
:提供具有默认回滚语义的事务性测试执行。SqlScriptsTestExecutionListener
:运行通过@Sql
注解配置的 SQL 脚本。EventPublishingTestExecutionListener
:将测试执行事件发布到测试的ApplicationContext
。MockitoResetTestExecutionListener
:按照@MockitoBean
或@MockitoSpyBean
的配置重置模拟对象。
# 4.1、注册 TestExecutionListener
实现
你可以使用 @TestExecutionListeners
注解,为测试类、其子类和嵌套类显式注册 TestExecutionListener
实现。有关详细信息和示例,请参阅 注解支持 和 @TestExecutionListeners
的 Javadoc (opens new window)。
注意:切换到默认的 TestExecutionListener
实现
如果你继承了一个使用 @TestExecutionListeners
注解的类,并且需要切换到使用默认的监听器集,可以使用以下方式注解你的类:
// 切换到默认监听器
@TestExecutionListeners(
listeners = {},
inheritListeners = false,
mergeMode = MERGE_WITH_DEFAULTS
)
class MyTest extends BaseTest {
// 类主体...
}
# 4.2、自动发现默认的 TestExecutionListener
实现
使用 @TestExecutionListeners
注册 TestExecutionListener
实现,适用于在有限测试场景中使用的自定义监听器。但是,如果某个自定义监听器需要在整个测试套件中使用,这种方式就会变得繁琐。通过 SpringFactoriesLoader
机制对默认 TestExecutionListener
实现的自动发现支持,可以解决这个问题。
例如,spring - test
模块在其 META - INF/spring.factories
属性文件 (opens new window) 中,以 org.springframework.test.context.TestExecutionListener
为键声明了所有核心默认的 TestExecutionListener
实现。第三方框架和开发人员可以通过自己的 spring.factories
文件,以同样的方式将自定义的 TestExecutionListener
实现添加到默认监听器列表中。
# 4.3、排序 TestExecutionListener
实现
当 TestContext 框架通过 前面提到的 SpringFactoriesLoader
机制发现默认的 TestExecutionListener
实现时,实例化的监听器会使用 Spring 的 AnnotationAwareOrderComparator
进行排序,该比较器遵循 Spring 的 Ordered
接口和 @Order
注解来确定顺序。AbstractTestExecutionListener
和 Spring 提供的所有默认 TestExecutionListener
实现都实现了 Ordered
并设置了适当的值。因此,第三方框架和开发人员应该确保他们的默认 TestExecutionListener
实现通过实现 Ordered
或声明 @Order
以正确的顺序进行注册。有关每个核心监听器分配的值的详细信息,请参阅核心默认 TestExecutionListener
实现的 getOrder()
方法的 Javadoc。
# 4.4、合并 TestExecutionListener
实现
如果通过 @TestExecutionListeners
注册了自定义的 TestExecutionListener
,默认监听器将不会被注册。在大多数常见的测试场景中,这实际上迫使开发人员除了任何自定义监听器之外,还要手动声明所有默认监听器。以下示例展示了这种配置方式:
@ContextConfiguration
@TestExecutionListeners({
MyCustomTestExecutionListener.class,
ServletTestExecutionListener.class,
DirtiesContextBeforeModesTestExecutionListener.class,
DependencyInjectionTestExecutionListener.class,
DirtiesContextTestExecutionListener.class,
TransactionalTestExecutionListener.class,
SqlScriptsTestExecutionListener.class
})
class MyTest {
// 类主体...
}
这种方法的问题在于,开发人员需要确切知道哪些监听器是默认注册的。此外,默认监听器集可能会因版本不同而变化,例如,SqlScriptsTestExecutionListener
是在 Spring Framework 4.1 中引入的,DirtiesContextBeforeModesTestExecutionListener
是在 Spring Framework 4.2 中引入的。此外,像 Spring Boot 和 Spring Security 这样的第三方框架会使用上述 自动发现机制 注册自己的默认 TestExecutionListener
实现。
为了避免了解和重新声明所有默认监听器,你可以将 @TestExecutionListeners
的 mergeMode
属性设置为 MergeMode.MERGE_WITH_DEFAULTS
。MERGE_WITH_DEFAULTS
表示本地声明的监听器应与默认监听器合并。合并算法会确保从列表中移除重复项,并根据 AnnotationAwareOrderComparator
的语义对最终的合并监听器集进行排序,如 排序 TestExecutionListener
实现 中所述。如果某个监听器实现了 Ordered
或使用了 @Order
注解,它可以影响其在与默认监听器合并时的位置。否则,本地声明的监听器在合并时会被追加到默认监听器列表的末尾。
例如,如果上例中的 MyCustomTestExecutionListener
类将其 order
值(例如 500
)配置为小于 ServletTestExecutionListener
的顺序值(恰好为 1000
),那么 MyCustomTestExecutionListener
就可以自动合并到默认监听器列表中,并且位于 ServletTestExecutionListener
之前,上例可以替换为:
@ContextConfiguration
@TestExecutionListeners(
listeners = MyCustomTestExecutionListener.class,
mergeMode = MERGE_WITH_DEFAULTS
)
class MyTest {
// 类主体...
}
# 5、应用事件
TestContext 框架支持记录在 ApplicationContext
中发布的应用事件,这样就可以在测试中针对这些事件进行断言。在单个测试执行期间发布的所有事件都可以通过 ApplicationEvents
API 获取,该 API 允许你将这些事件作为 java.util.Stream
进行处理。
若要在测试中使用 ApplicationEvents
,请执行以下操作:
- 确保你的测试类使用 @RecordApplicationEvents 进行注解或元注解。
- 确保已注册
ApplicationEventsTestExecutionListener
。不过要注意,ApplicationEventsTestExecutionListener
默认是已注册的,只有当你通过@TestExecutionListeners
进行自定义配置且未包含默认监听器时,才需要手动注册它。 - 使用
@Autowired
注解ApplicationEvents
类型的字段,并在测试和生命周期方法(例如 JUnit Jupiter 中的@BeforeEach
和@AfterEach
方法)中使用该ApplicationEvents
实例。- 当使用 JUnit Jupiter 的 SpringExtension 时,你可以在测试或生命周期方法中声明一个
ApplicationEvents
类型的方法参数,以此替代测试类中使用@Autowired
注解的字段。
- 当使用 JUnit Jupiter 的 SpringExtension 时,你可以在测试或生命周期方法中声明一个
以下测试类使用 JUnit Jupiter 的 SpringExtension
和 AssertJ (opens new window) 来断言在调用 Spring 管理的组件中的方法时发布的应用事件的类型:
@SpringJUnitConfig(/* ... */)
@RecordApplicationEvents // (1)
class OrderServiceTests {
@Autowired
OrderService orderService;
@Autowired
ApplicationEvents events; // (2)
@Test
void submitOrder() {
// 调用 OrderService 中发布事件的方法
orderService.submitOrder(new Order(/* ... */));
// 验证是否发布了一个 OrderSubmitted 事件
long numEvents = events.stream(OrderSubmitted.class).count(); // (3)
assertThat(numEvents).isEqualTo(1);
}
}
序号 | 说明 |
---|---|
1 | 使用 @RecordApplicationEvents 注解测试类。 |
2 | 为当前测试注入 ApplicationEvents 实例。 |
3 | 使用 ApplicationEvents API 统计发布了多少个 OrderSubmitted 事件。 |
有关 ApplicationEvents
API 的更多详细信息,请参阅 ApplicationEvents javadoc (opens new window)。
# 6、测试执行事件
EventPublishingTestExecutionListener
提供了一种实现自定义 TestExecutionListener
的替代方法。测试的 ApplicationContext
中的组件可以监听由 EventPublishingTestExecutionListener
发布的以下事件,每个事件都对应 TestExecutionListener
API 中的一个方法。
BeforeTestClassEvent
PrepareTestInstanceEvent
BeforeTestMethodEvent
BeforeTestExecutionEvent
AfterTestExecutionEvent
AfterTestMethodEvent
AfterTestClassEvent
出于各种原因,可以使用这些事件,例如重置模拟 bean 或跟踪测试执行。使用测试执行事件而不是实现自定义 TestExecutionListener
的一个优点是,任何在测试 ApplicationContext
中注册的 Spring bean 都可以使用测试执行事件,并且这些 bean 可以直接受益于依赖注入和 ApplicationContext
的其他特性。相比之下,TestExecutionListener
不是 ApplicationContext
中的 bean。
注意:
EventPublishingTestExecutionListener
默认会被注册;但是,它仅在 ApplicationContext
已经“加载”后才会发布事件。这样可以防止不必要或过早地加载 ApplicationContext
。
因此,在 ApplicationContext
被另一个 TestExecutionListener
加载完成之前,BeforeTestClassEvent
不会被发布。例如,使用默认注册的 TestExecutionListener
实现集时,使用特定测试 ApplicationContext
的第一个测试类不会发布 BeforeTestClassEvent
,但是对于同一测试套件中使用相同测试 ApplicationContext
的任何后续测试类,BeforeTestClassEvent
将会被发布,因为后续测试类运行时上下文已经加载(只要上下文没有通过 @DirtiesContext
或者最大容量逐出策略从 ContextCache
中移除)。
如果你希望确保每个测试类都会发布 BeforeTestClassEvent
,则需要注册一个在 beforeTestClass
回调中加载 ApplicationContext
的 TestExecutionListener
,并且该 TestExecutionListener
必须在 EventPublishingTestExecutionListener
“之前”注册。
类似地,如果使用 @DirtiesContext
在给定测试类的最后一个测试方法之后将 ApplicationContext
从上下文缓存中移除,那么该测试类将不会发布 AfterTestClassEvent
。
为了监听测试执行事件,Spring bean 可以选择实现 org.springframework.context.ApplicationListener
接口。或者,可以使用 @EventListener
注解标注监听方法,并配置为监听上述特定事件类型之一。由于这种方法非常流行,Spring 提供了以下专用的 @EventListener
注解来简化测试执行事件监听器的注册。这些注解位于 org.springframework.test.context.event.annotation
包中。
@BeforeTestClass
@PrepareTestInstance
@BeforeTestMethod
@BeforeTestExecution
@AfterTestExecution
@AfterTestMethod
@AfterTestClass
# 6.1、异常处理
默认情况下,如果测试执行事件监听器在处理事件时抛出异常,则该异常将传播到正在使用的底层测试框架(如 JUnit 或 TestNG)。例如,如果处理 BeforeTestMethodEvent
时抛出异常,相应的测试方法将因该异常而失败。相比之下,如果异步测试执行事件监听器抛出异常,则异常不会传播到底层测试框架。有关异步异常处理的更多详细信息,请参阅 @EventListener
的类级 javadoc。
# 6.2、异步监听器
如果你希望某个测试执行事件监听器异步处理事件,可以使用 Spring 的常规 @Async
支持。有关更多详细信息,请参阅 @EventListener
的类级 javadoc。
# 7、上下文管理
每个 TestContext
都会为其负责的测试实例提供上下文管理和缓存支持。测试实例不会自动获得对已配置的 ApplicationContext
的访问权限。不过,如果测试类实现了 ApplicationContextAware
接口,那么 ApplicationContext
的引用会被提供给测试实例。请注意,AbstractJUnit4SpringContextTests
和 AbstractTestNGSpringContextTests
都实现了 ApplicationContextAware
接口,因此它们会自动提供对 ApplicationContext
的访问。
提示:@Autowired ApplicationContext
除了实现 ApplicationContextAware
接口之外,你还可以通过在字段或 setter 方法上使用 @Autowired
注解来为测试类注入应用上下文,如下例所示:
@SpringJUnitConfig
class MyTest {
@Autowired // (1) 注入 ApplicationContext
ApplicationContext applicationContext;
// 类的主体...
}
同样地,如果你的测试被配置为加载 WebApplicationContext
,你可以将 Web 应用上下文注入到测试中,如下所示:
@SpringJUnitWebConfig // (1) 配置 WebApplicationContext
class MyWebAppTest {
@Autowired // (2) 注入 WebApplicationContext
WebApplicationContext wac;
// 类的主体...
}
使用 @Autowired
进行依赖注入是由 DependencyInjectionTestExecutionListener
提供的,该监听器默认情况下就会被配置。
使用 TestContext 框架的测试类不需要扩展任何特定的类或实现特定的接口来配置其应用上下文。相反,可以通过在类级别声明 @ContextConfiguration
注解来实现配置。如果你的测试类没有显式声明应用上下文资源位置或组件类,那么配置的 ContextLoader
会确定如何从默认位置或默认配置类加载上下文。除了上下文资源位置和组件类之外,还可以通过应用上下文初始化器来配置应用上下文。
以下部分将介绍如何使用 Spring 的 @ContextConfiguration
注解,通过 XML 配置文件、Groovy 脚本、组件类(通常是 @Configuration
类)或上下文初始化器来配置测试用的 ApplicationContext
。或者,对于高级用例,你可以实现并配置自己的自定义 SmartContextLoader
。
- 使用 XML 资源进行上下文配置
- 使用 Groovy 脚本进行上下文配置
- 使用组件类进行上下文配置
- 混合使用 XML、Groovy 脚本和组件类
- 使用上下文定制器进行上下文配置
- 使用上下文初始化器进行上下文配置
- 上下文配置的继承
- 使用环境配置文件进行上下文配置
- 使用测试属性源进行上下文配置
- 使用动态属性源进行上下文配置
- 加载
WebApplicationContext
- 上下文缓存
- 上下文失败阈值
- 上下文层次结构
# 7.1、使用 XML 资源进行上下文配置
要通过使用 XML 配置文件为测试加载 ApplicationContext
,可以使用 @ContextConfiguration
注解测试类,并使用一个数组配置 locations
属性,该数组包含 XML 配置元数据的资源位置。普通路径或相对路径(例如 context.xml
)会被视为相对于测试类所在包的类路径资源。以斜杠开头的路径会被视为绝对类路径位置(例如 /org/example/config.xml
)。表示资源 URL 的路径(即以 classpath:
、file:
、http:
等为前缀的路径)将按原样使用。
以下是 Java 示例代码:
@ExtendWith(SpringExtension.class)
// ApplicationContext 将从类路径根目录下的 "/app-config.xml" 和 "/test-config.xml" 加载
@ContextConfiguration(locations = {"/app-config.xml", "/test-config.xml"}) // (1)
class MyTest {
// 类的主体...
}
- 将
locations
属性设置为 XML 文件列表。
@ContextConfiguration
通过标准的 Java value
属性为 locations
属性提供了别名。因此,如果您不需要在 @ContextConfiguration
中声明其他属性,可以省略 locations
属性名的声明,并使用以下示例中展示的简写格式来声明资源位置:
以下是 Java 示例代码:
@ExtendWith(SpringExtension.class)
@ContextConfiguration({"/app-config.xml", "/test-config.xml"}) // (1)
class MyTest {
// 类的主体...
}
- 不使用
locations
属性来指定 XML 文件。
如果您在 @ContextConfiguration
注解中同时省略 locations
和 value
属性,TestContext 框架将尝试检测默认的 XML 资源位置。具体来说,GenericXmlContextLoader
和 GenericXmlWebContextLoader
会根据测试类的名称来检测默认位置。如果您的类名为 com.example.MyTest
,GenericXmlContextLoader
将从 "classpath:com/example/MyTest-context.xml"
加载应用程序上下文。以下是示例代码:
以下是 Java 示例代码:
@ExtendWith(SpringExtension.class)
// ApplicationContext 将从 "classpath:com/example/MyTest-context.xml" 加载
@ContextConfiguration // (1)
class MyTest {
// 类的主体...
}
- 从默认位置加载配置。
# 7.2、使用 Groovy 脚本进行上下文配置
要通过使用 Groovy 脚本(这些脚本使用 Groovy Bean 定义 DSL)为测试加载 ApplicationContext
,你可以在测试类上使用 @ContextConfiguration
注解,并使用一个包含 Groovy 脚本资源位置的数组来配置 locations
或 value
属性。Groovy 脚本的资源查找语义与 XML 配置文件 中描述的相同。
提示:启用 Groovy 脚本支持
如果 Groovy 在类路径中,Spring 测试上下文框架会自动启用使用 Groovy 脚本来加载 ApplicationContext
的支持。
以下示例展示了如何指定 Groovy 配置文件:
@ExtendWith(SpringExtension.class)
// 应用上下文将从类路径根目录下的 "/AppConfig.groovy" 和 "/TestConfig.Groovy" 加载
@ContextConfiguration({"/AppConfig.groovy", "/TestConfig.Groovy"}) // (1)
class MyTest {
// 类体...
}
- 指定 Groovy 配置文件的位置。
如果你在 @ContextConfiguration
注解中同时省略 locations
和 value
属性,测试上下文框架会尝试检测默认的 Groovy 脚本。具体来说,GenericGroovyXmlContextLoader
和 GenericGroovyXmlWebContextLoader
会根据测试类的名称检测默认位置。如果你的类名为 com.example.MyTest
,Groovy 上下文加载器将从 "classpath:com/example/MyTestContext.groovy"
加载应用上下文。以下示例展示了如何使用默认配置:
@ExtendWith(SpringExtension.class)
// 应用上下文将从 "classpath:com/example/MyTestContext.groovy" 加载
@ContextConfiguration // (1)
class MyTest {
// 类体...
}
- 从默认位置加载配置。
提示:同时声明 XML 配置和 Groovy 脚本
你可以通过使用 @ContextConfiguration
的 locations
或 value
属性同时声明 XML 配置文件和 Groovy 脚本。如果配置的资源位置路径以 .xml
结尾,则使用 XmlBeanDefinitionReader
加载;否则,使用 GroovyBeanDefinitionReader
加载。
以下示例展示了如何在集成测试中同时使用两者:
@ExtendWith(SpringExtension.class)
// 应用上下文将从 "/app-config.xml" 和 "/TestConfig.groovy" 加载
@ContextConfiguration({ "/app-config.xml", "/TestConfig.groovy" })
class MyTest {
// 类体...
}
# 7.3、使用组件类进行上下文配置
要通过使用组件类为你的测试加载 ApplicationContext
,你可以在测试类上使用 @ContextConfiguration
注解,并使用包含组件类引用的数组来配置 classes
属性。以下示例展示了具体做法:
@ExtendWith(SpringExtension.class)
// ApplicationContext 将从 AppConfig 和 TestConfig 中加载
@ContextConfiguration(classes = {AppConfig.class, TestConfig.class}) // (1)
class MyTest {
// class body...
}
(1) 指定组件类。
# a、小贴士:组件类
术语“组件类”可以指以下任意一种:
- 使用
@Configuration
注解的类。 - 组件(即使用
@Component
、@Service
、@Repository
或其他构造型注解的类)。 - 符合 JSR - 330 标准并使用
jakarta.inject
注解的类。 - 任何包含
@Bean
方法的类。 - 任何打算注册为 Spring 组件(即在
ApplicationContext
中的 Spring Bean)的其他类,可能会利用单个构造函数的自动自动装配,而无需使用 Spring 注解。
有关组件类的配置和语义的更多信息,请参阅 @Configuration
(opens new window) 和 @Bean
(opens new window) 的 Javadoc,尤其要注意关于 @Bean
轻量级模式的讨论。
如果你在 @ContextConfiguration
注解中省略 classes
属性,TestContext 框架会尝试检测是否存在默认配置类。具体来说,AnnotationConfigContextLoader
和 AnnotationConfigWebContextLoader
会检测测试类中所有符合配置类实现要求的 static
嵌套类,如 @Configuration
(opens new window) 的 Javadoc 中所规定。请注意,配置类的名称是任意的。此外,如果需要,一个测试类可以包含多个 static
嵌套配置类。在以下示例中,OrderServiceTest
类声明了一个名为 Config
的 static
嵌套配置类,该类会自动用于为测试类加载 ApplicationContext
:
@SpringJUnitConfig // (1)
// ApplicationContext 将从静态嵌套的 Config 类中加载
class OrderServiceTest {
@Configuration
static class Config {
// 此 Bean 将被注入到 OrderServiceTest 类中
@Bean
OrderService orderService() {
OrderService orderService = new OrderServiceImpl();
// 设置属性等
return orderService;
}
}
@Autowired
OrderService orderService;
@Test
void testOrderService() {
// 测试 orderService
}
}
(1) 从嵌套的 Config
类中加载配置信息。
# 7.4、混合使用 XML、Groovy 脚本和组件类
有时,为测试配置 ApplicationContext
时,可能需要混合使用 XML 配置文件、Groovy 脚本和组件类(通常是 @Configuration
类)。例如,如果在生产环境中使用 XML 配置,你可能会决定在测试时使用 @Configuration
类来配置特定的 Spring 管理组件,反之亦然。
此外,一些第三方框架(如 Spring Boot)提供了从不同类型的资源(例如 XML 配置文件、Groovy 脚本和 @Configuration
类)同时加载 ApplicationContext
的一流支持。从历史上看,Spring 框架在标准部署中不支持这一点。因此,Spring 框架在 spring - test
模块中提供的大多数 SmartContextLoader
实现,对于每个测试上下文仅支持一种资源类型。但这并不意味着不能同时使用多种类型。一般规则的一个例外是,GenericGroovyXmlContextLoader
和 GenericGroovyXmlWebContextLoader
支持同时使用 XML 配置文件和 Groovy 脚本。此外,第三方框架可能会选择通过 @ContextConfiguration
支持同时声明 locations
和 classes
。借助 TestContext 框架中的标准测试支持,你有以下几种选择。
如果你想使用资源位置(例如 XML 或 Groovy)和 @Configuration
类来配置测试,必须选择其中一个作为入口点,并且该入口点必须包含或导入另一个。例如,在 XML 或 Groovy 脚本中,可以通过组件扫描或将其定义为普通的 Spring Bean 来包含 @Configuration
类;而在 @Configuration
类中,可以使用 @ImportResource
来导入 XML 配置文件或 Groovy 脚本。请注意,这种行为在语义上与在生产环境中配置应用程序的方式是等效的:在生产配置中,你可以定义一组 XML 或 Groovy 资源位置,也可以定义一组用于加载生产 ApplicationContext
的 @Configuration
类,但你仍然可以自由地包含或导入另一种类型的配置。
# 7.5、使用上下文定制器进行配置
ContextCustomizer
负责在 bean 定义已加载到上下文中,但在上下文刷新之前,对提供的 ConfigurableApplicationContext
进行定制。
ContextCustomizerFactory
负责创建一个 ContextCustomizer
,它基于一些定制逻辑来确定某个 ContextCustomizer
是否适用于给定的测试类,例如根据是否存在特定的注解。在 ContextLoaders
处理完测试类的上下文配置属性之后,但在创建 MergedContextConfiguration
之前,会调用这些工厂类。
例如,Spring 框架提供了以下默认注册的 ContextCustomizerFactory
实现:
MockServerContainerContextCustomizerFactory
:如果类路径中存在 WebSocket 支持,并且测试类或其某个封闭类使用@WebAppConfiguration
进行了注解或元注解,那么这个工厂会创建一个MockServerContainerContextCustomizer
。MockServerContainerContextCustomizer
会实例化一个新的MockServerContainer
,并将其存储在ServletContext
中,属性名为jakarta.websocket.server.ServerContainer
。
# a、注册 ContextCustomizerFactory
实现
你可以使用 @ContextCustomizerFactories
注解,为一个测试类、其子类和嵌套类显式注册 ContextCustomizerFactory
实现。有关详细信息和示例,请参阅注解支持和 @ContextCustomizerFactories
(opens new window) 的 Java 文档。
# b、自动发现默认的 ContextCustomizerFactory
实现
使用 @ContextCustomizerFactories
注册 ContextCustomizerFactory
实现适用于在有限测试场景中使用的自定义工厂。然而,如果一个自定义工厂需要在整个测试套件中使用,这种方式就会变得很麻烦。通过 SpringFactoriesLoader
机制支持自动发现默认的 ContextCustomizerFactory
实现,可以解决这个问题。
例如,组成 Spring 框架和 Spring Boot 测试支持的模块,会在它们的 META-INF/spring.factories
属性文件中,将所有核心默认的 ContextCustomizerFactory
实现声明在 org.springframework.test.context.ContextCustomizerFactory
键下。spring-test
模块的 spring.factories
文件可以在此查看 (opens new window)。第三方框架和开发者可以通过自己的 spring.factories
文件,以同样的方式将自己的 ContextCustomizerFactory
实现添加到默认工厂列表中。
# c、合并 ContextCustomizerFactory
实现
如果通过 @ContextCustomizerFactories
注册了一个自定义的 ContextCustomizerFactory
,它将与使用上述自动发现机制注册的默认工厂进行 合并。
合并算法会确保从列表中移除重复项,并且在合并时,将本地声明的工厂追加到默认工厂列表的末尾。
提示:若要替换测试类、其子类和嵌套类的默认工厂,可以将 @ContextCustomizerFactories
的 mergeMode
属性设置为 MergeMode.REPLACE_DEFAULTS
。
# 7.6、使用上下文初始化器进行上下文配置
若要通过使用上下文初始化器为测试配置 ApplicationContext
,可在测试类上使用 @ContextConfiguration
注解,并将 initializers
属性配置为一个数组,该数组包含对实现了 ApplicationContextInitializer
的类的引用。随后,所声明的上下文初始化器将用于初始化加载供测试使用的 ConfigurableApplicationContext
。请注意,每个声明的初始化器所支持的具体 ConfigurableApplicationContext
类型必须与当前使用的 SmartContextLoader
创建的 ApplicationContext
类型兼容(通常为 GenericApplicationContext
)。此外,初始化器的调用顺序取决于它们是否实现了 Spring 的 Ordered
接口,或者是否使用了 Spring 的 @Order
注解或标准的 @Priority
注解。以下示例展示了如何使用初始化器:
@ExtendWith(SpringExtension.class)
// ApplicationContext 将从 TestConfig 加载,并由 TestAppCtxInitializer 进行初始化
@ContextConfiguration(
classes = TestConfig.class,
initializers = TestAppCtxInitializer.class) // (1)
class MyTest {
// 类主体...
}
- 使用配置类和初始化器指定配置。
你也可以完全省略 @ContextConfiguration
中 XML 配置文件、Groovy 脚本或组件类的声明,而是仅声明 ApplicationContextInitializer
类,这些类将负责在上下文中注册 Bean,例如,通过编程方式从 XML 文件或配置类中加载 Bean 定义。以下示例展示了如何实现:
@ExtendWith(SpringExtension.class)
// ApplicationContext 将由 EntireAppInitializer 进行初始化,该类可能会在上下文中注册 Bean
@ContextConfiguration(initializers = EntireAppInitializer.class) // (1)
class MyTest {
// 类主体...
}
- 仅使用初始化器指定配置。
# 7.7、上下文配置继承
@ContextConfiguration
支持布尔类型的 inheritLocations
和 inheritInitializers
属性,这些属性用于表示是否应该继承由超类声明的资源位置、组件类和上下文初始化器。这两个标志的默认值均为 true
。这意味着测试类会继承任何超类声明的资源位置、组件类以及上下文初始化器。具体而言,测试类的资源位置或组件类会追加到超类声明的资源位置或带注解类的列表中。同样,给定测试类的初始化器会添加到测试超类定义的初始化器集合中。因此,子类可以选择扩展资源位置、组件类或上下文初始化器。
如果将 @ContextConfiguration
中的 inheritLocations
或 inheritInitializers
属性设置为 false
,那么测试类的资源位置、组件类以及上下文初始化器将分别覆盖并有效替换超类定义的配置。
注意:测试配置也可以从封闭类继承。详情请参阅 @Nested
测试类配置。
在下一个使用 XML 资源位置的示例中,ExtendedTest
的 ApplicationContext
会按顺序从 base-config.xml
和 extended-config.xml
加载。因此,extended-config.xml
中定义的 Bean 可以覆盖(即替换)base-config.xml
中定义的 Bean。以下示例展示了一个类如何扩展另一个类,并同时使用自身的配置文件和超类的配置文件:
@ExtendWith(SpringExtension.class)
// ApplicationContext 将从类路径根目录下的 "/base-config.xml" 加载
@ContextConfiguration("/base-config.xml") // (1)
class BaseTest {
// 类主体...
}
// ApplicationContext 将从类路径根目录下的 "/base-config.xml" 和 "/extended-config.xml" 加载
@ContextConfiguration("/extended-config.xml") // (2)
class ExtendedTest extends BaseTest {
// 类主体...
}
- 超类中定义的配置文件。
- 子类中定义的配置文件。
同样,在下一个使用组件类的示例中,ExtendedTest
的 ApplicationContext
会按顺序从 BaseConfig
和 ExtendedConfig
类加载。因此,ExtendedConfig
中定义的 Bean 可以覆盖(即替换)BaseConfig
中定义的 Bean。以下示例展示了一个类如何扩展另一个类,并同时使用自身的配置类和超类的配置类:
// ApplicationContext 将从 BaseConfig 加载
@SpringJUnitConfig(BaseConfig.class) // (1)
class BaseTest {
// 类主体...
}
// ApplicationContext 将从 BaseConfig 和 ExtendedConfig 加载
@SpringJUnitConfig(ExtendedConfig.class) // (2)
class ExtendedTest extends BaseTest {
// 类主体...
}
- 超类中定义的配置类。
- 子类中定义的配置类。
在下一个使用上下文初始化器的示例中,ExtendedTest
的 ApplicationContext
将使用 BaseInitializer
和 ExtendedInitializer
进行初始化。不过要注意,初始化器的调用顺序取决于它们是否实现了 Spring 的 Ordered
接口,或者是否使用了 Spring 的 @Order
注解或标准的 @Priority
注解。以下示例展示了一个类如何扩展另一个类,并同时使用自身的初始化器和超类的初始化器:
// ApplicationContext 将由 BaseInitializer 初始化
@SpringJUnitConfig(initializers = BaseInitializer.class) // (1)
class BaseTest {
// 类主体...
}
// ApplicationContext 将由 BaseInitializer 和 ExtendedInitializer 初始化
@SpringJUnitConfig(initializers = ExtendedInitializer.class) // (2)
class ExtendedTest extends BaseTest {
// 类主体...
}
- 超类中定义的初始化器。
- 子类中定义的初始化器。
# 7.8、基于环境配置文件的上下文配置
Spring 框架对环境和配置文件(也称为 “bean 定义配置文件”)的概念提供了一流的支持,并且集成测试可以配置为针对各种测试场景激活特定的 bean 定义配置文件。这可以通过使用 @ActiveProfiles
注解标记测试类,并提供在为测试加载 ApplicationContext
时应激活的配置文件列表来实现。
注意:@ActiveProfiles
可以与 SmartContextLoader
SPI 的任何实现一起使用,但不支持旧的 ContextLoader
SPI 的实现。
考虑两个使用 XML 配置和 @Configuration
类的示例:
<!-- app-config.xml -->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:jee="http://www.springframework.org/schema/jee"
xsi:schemaLocation="...">
<bean id="transferService"
class="com.bank.service.internal.DefaultTransferService">
<constructor-arg ref="accountRepository"/>
<constructor-arg ref="feePolicy"/>
</bean>
<bean id="accountRepository"
class="com.bank.repository.internal.JdbcAccountRepository">
<constructor-arg ref="dataSource"/>
</bean>
<bean id="feePolicy"
class="com.bank.service.internal.ZeroFeePolicy"/>
<beans profile="dev">
<jdbc:embedded-database id="dataSource">
<jdbc:script
location="classpath:com/bank/config/sql/schema.sql"/>
<jdbc:script
location="classpath:com/bank/config/sql/test-data.sql"/>
</jdbc:embedded-database>
</beans>
<beans profile="production">
<jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
</beans>
<beans profile="default">
<jdbc:embedded-database id="dataSource">
<jdbc:script
location="classpath:com/bank/config/sql/schema.sql"/>
</jdbc:embedded-database>
</beans>
</beans>
@ExtendWith(SpringExtension.class)
// ApplicationContext 将从 "classpath:/app-config.xml" 加载
@ContextConfiguration("/app-config.xml")
@ActiveProfiles("dev")
class TransferServiceTest {
@Autowired
TransferService transferService;
@Test
void testTransferService() {
// 测试 transferService
}
}
当运行 TransferServiceTest
时,它的 ApplicationContext
从类路径根目录下的 app-config.xml
配置文件加载。如果你查看 app-config.xml
,可以看到 accountRepository
bean 依赖于一个 dataSource
bean。但是,dataSource
不是作为顶级 bean 定义的。相反,dataSource
被定义了三次:在 production
配置文件中、dev
配置文件中以及 default
配置文件中。
通过使用 @ActiveProfiles("dev")
注解 TransferServiceTest
,我们指示 Spring 测试上下文框架以激活的配置文件为 {"dev"}
来加载 ApplicationContext
。结果是,会创建一个嵌入式数据库并使用测试数据进行填充,并且 accountRepository
bean 会与开发用的 DataSource
引用进行关联。这可能正是我们在集成测试中所需要的。
有时将 bean 分配到 default
配置文件中会很有用。仅当没有明确激活其他配置文件时,才会包含默认配置文件中的 bean。你可以使用此功能来定义在应用程序默认状态下使用的 “后备” bean。例如,你可以为 dev
和 production
配置文件显式提供数据源,但在这两个配置文件都未激活时,将内存中的数据源定义为默认数据源。
以下代码示例展示了如何使用 @Configuration
类而不是 XML 来实现相同的配置和集成测试:
@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();
}
}
@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");
}
}
@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();
}
}
@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();
}
}
@SpringJUnitConfig({
TransferServiceConfig.class,
StandaloneDataConfig.class,
JndiDataConfig.class,
DefaultDataConfig.class})
@ActiveProfiles("dev")
class TransferServiceTest {
@Autowired
TransferService transferService;
@Test
void testTransferService() {
// 测试 transferService
}
}
在这个变体中,我们将 XML 配置拆分为四个独立的 @Configuration
类:
TransferServiceConfig
:通过使用@Autowired
进行依赖注入来获取dataSource
。StandaloneDataConfig
:定义适合开发人员测试的嵌入式数据库的dataSource
。JndiDataConfig
:定义在生产环境中从 JNDI 获取的dataSource
。DefaultDataConfig
:定义默认嵌入式数据库的dataSource
,以防没有激活任何配置文件。
与基于 XML 的配置示例一样,我们仍然使用 @ActiveProfiles("dev")
注解 TransferServiceTest
,但这次我们使用 @ContextConfiguration
注解指定所有四个配置类。测试类的主体本身保持完全不变。
通常,在给定项目的多个测试类中会使用同一组配置文件。因此,为了避免重复声明 @ActiveProfiles
注解,你可以在基类上声明一次 @ActiveProfiles
,子类会自动从基类继承 @ActiveProfiles
配置。在以下示例中,@ActiveProfiles
(以及其他注解)的声明已移至抽象超类 AbstractIntegrationTest
:
注意:测试配置也可以从封闭类继承。有关详细信息,请参阅 @Nested
测试类配置。
@SpringJUnitConfig({
TransferServiceConfig.class,
StandaloneDataConfig.class,
JndiDataConfig.class,
DefaultDataConfig.class})
@ActiveProfiles("dev")
abstract class AbstractIntegrationTest {
}
// 从超类继承 "dev" 配置文件
class TransferServiceTest extends AbstractIntegrationTest {
@Autowired
TransferService transferService;
@Test
void testTransferService() {
// 测试 transferService
}
}
@ActiveProfiles
还支持一个 inheritProfiles
属性,可用于禁用活动配置文件的继承,如下例所示:
// 使用 "production" 覆盖 "dev" 配置文件
@ActiveProfiles(profiles = "production", inheritProfiles = false)
class ProductionTransferServiceTest extends AbstractIntegrationTest {
// 测试主体
}
此外,有时需要以编程方式而不是声明式方式解析测试的活动配置文件,例如基于以下情况:
- 当前操作系统。
- 测试是否在持续集成构建服务器上运行。
- 是否存在某些环境变量。
- 是否存在自定义类级注解。
- 其他因素。
要以编程方式解析活动的 bean 定义配置文件,你可以实现自定义的 ActiveProfilesResolver
,并使用 @ActiveProfiles
的 resolver
属性进行注册。有关详细信息,请参阅相应的 javadoc (opens new window)。以下示例展示了如何实现和注册自定义的 OperatingSystemActiveProfilesResolver
:
// 通过自定义解析器以编程方式覆盖 "dev" 配置文件
@ActiveProfiles(
resolver = OperatingSystemActiveProfilesResolver.class,
inheritProfiles = false)
class TransferServiceTest extends AbstractIntegrationTest {
// 测试主体
}
public class OperatingSystemActiveProfilesResolver implements ActiveProfilesResolver {
@Override
public String[] resolve(Class<?> testClass) {
String profile = ...;
// 根据操作系统确定 profile 的值
return new String[] {profile};
}
}
# 7.9、使用测试属性源进行上下文配置
Spring 框架对具有属性源层次结构的环境概念提供了一流的支持,你可以使用特定于测试的属性源来配置集成测试。与 @Configuration
类上使用的 @PropertySource
注解不同,你可以在测试类上声明 @TestPropertySource
注解,以声明测试属性文件的资源位置或内联属性。这些测试属性源会被添加到为带注解的集成测试加载的 ApplicationContext
的 Environment
中的 PropertySources
集合中。
注意:
你可以将 @TestPropertySource
与 SmartContextLoader
SPI 的任何实现一起使用,但旧的 ContextLoader
SPI 实现不支持 @TestPropertySource
。
SmartContextLoader
的实现可以通过 MergedContextConfiguration
中的 getPropertySourceDescriptors()
和 getPropertySourceProperties()
方法访问合并后的测试属性源值。
# a、声明测试属性源
你可以使用 @TestPropertySource
的 locations
或 value
属性来配置测试属性文件。
默认情况下,传统的和基于 XML 的 java.util.Properties
文件格式都被支持,例如 classpath:/com/example/test.properties
或 file:///path/to/file.xml
。从 Spring 框架 6.1 开始,你可以通过 @TestPropertySource
中的 factory
属性配置自定义的 PropertySourceFactory
,以支持 JSON、YAML 等不同的文件格式。
每个路径都会被解释为一个 Spring Resource
。简单路径(例如 "test.properties"
)会被视为相对于测试类所在包的类路径资源。以斜杠开头的路径会被视为绝对类路径资源(例如 "/org/example/test.xml"
)。引用 URL 的路径(例如,以 classpath:
、file:
或 http:
为前缀的路径)会使用指定的资源协议进行加载。
路径中的属性占位符(如 ${…}
)将根据 Environment
进行解析。
从 Spring 框架 6.1 开始,也支持资源位置模式,例如 "classpath*:/config/*.properties"
。
以下示例使用了一个测试属性文件:
@ContextConfiguration
@TestPropertySource("/test.properties") // 1. 用绝对路径指定一个属性文件
class MyIntegrationTests {
// class body...
}
你可以使用 @TestPropertySource
的 properties
属性以键值对的形式配置内联属性,如下例所示。所有键值对都会作为具有最高优先级的单个测试 PropertySource
添加到包围的 Environment
中。
键值对支持的语法与 Java 属性文件中条目的定义语法相同:
key=value
key:value
key value
提示:
虽然可以使用上述任何一种语法变体并在键和值之间使用任意数量的空格来定义属性,但建议在测试套件中使用一种语法变体并保持一致的间距。例如,始终使用 key = value
而不是 key= value
、key=value
等。同样,如果你使用文本块定义内联属性,应该在整个测试套件中始终如一地使用文本块来定义内联属性。
原因是你提供的确切字符串将用于确定上下文缓存的键。因此,为了从上下文缓存中受益,必须确保一致地定义内联属性。
以下示例设置了两个内联属性:
@ContextConfiguration
@TestPropertySource(properties = {"timezone = GMT", "port = 4242"}) // 1. 通过字符串数组设置两个属性
class MyIntegrationTests {
// class body...
}
从 Spring 框架 6.1 开始,你可以使用文本块在单个 String
中定义多个内联属性。以下示例使用文本块设置了两个内联属性:
@ContextConfiguration
@TestPropertySource(properties = """
timezone = GMT
port = 4242
""") // 1. 通过文本块设置两个属性
class MyIntegrationTests {
// class body...
}
注意:
@TestPropertySource
可以用作可重复注解。
这意味着你可以在单个测试类上有多个 @TestPropertySource
声明,后面的 @TestPropertySource
注解中的 locations
和 properties
会覆盖前面的 @TestPropertySource
注解中的那些。
此外,你可以在测试类上声明多个组合注解,每个组合注解都使用 @TestPropertySource
进行元注解,所有这些 @TestPropertySource
声明都将为测试属性源做出贡献。
直接存在的 @TestPropertySource
注解始终优先于元存在的 @TestPropertySource
注解。换句话说,直接存在的 @TestPropertySource
注解中的 locations
和 properties
将覆盖用作元注解的 @TestPropertySource
注解中的 locations
和 properties
。
# b、默认属性文件检测
如果 @TestPropertySource
被声明为空注解(即未为 locations
或 properties
属性指定显式值),系统将尝试检测相对于声明该注解的类的默认属性文件。例如,如果带注解的测试类是 com.example.MyTest
,则对应的默认属性文件是 classpath:com/example/MyTest.properties
。如果无法检测到默认文件,则会抛出 IllegalStateException
。
# c、优先级
测试属性的优先级高于操作系统环境、Java 系统属性或应用程序通过 @PropertySource
声明性或编程方式添加的属性源中定义的属性。因此,测试属性可用于有选择地覆盖从系统和应用程序属性源加载的属性。此外,内联属性的优先级高于从资源位置加载的属性。不过要注意,通过 @DynamicPropertySource
注册的属性的优先级高于通过 @TestPropertySource
加载的属性。
在下面的示例中,timezone
和 port
属性以及 /test.properties
文件中定义的任何属性都会覆盖系统和应用程序属性源中定义的同名属性。此外,如果 /test.properties
文件为 timezone
和 port
属性定义了条目,这些条目将被使用 properties
属性声明的内联属性覆盖。以下示例展示了如何在文件和内联方式中指定属性:
@ContextConfiguration
@TestPropertySource(
locations = "/test.properties",
properties = {"timezone = GMT", "port = 4242"}
)
class MyIntegrationTests {
// class body...
}
# d、继承和覆盖测试属性源
@TestPropertySource
支持布尔类型的 inheritLocations
和 inheritProperties
属性,它们表示是否应该继承超类声明的属性文件的资源位置和内联属性。这两个标志的默认值均为 true
。这意味着测试类会继承任何超类声明的位置和内联属性。具体来说,测试类的位置和内联属性会附加到超类声明的位置和内联属性之后。因此,子类可以选择扩展这些位置和内联属性。请注意,后出现的属性会覆盖(即隐藏)先出现的同名属性。此外,前面提到的优先级规则也适用于继承的测试属性源。
如果 @TestPropertySource
中的 inheritLocations
或 inheritProperties
属性设置为 false
,则测试类的位置或内联属性将隐藏并有效替换超类定义的配置。
注意:
测试配置也可以从封闭类继承。有关详细信息,请参阅 @Nested
测试类配置。
在下面的示例中,BaseTest
的 ApplicationContext
仅使用 base.properties
文件作为测试属性源进行加载。相比之下,ExtendedTest
的 ApplicationContext
使用 base.properties
和 extended.properties
文件作为测试属性源位置进行加载。以下示例展示了如何使用属性文件在子类和其超类中定义属性:
@TestPropertySource("base.properties")
@ContextConfiguration
class BaseTest {
// ...
}
@TestPropertySource("extended.properties")
@ContextConfiguration
class ExtendedTest extends BaseTest {
// ...
}
在下面的示例中,BaseTest
的 ApplicationContext
仅使用内联的 key1
属性进行加载。相比之下,ExtendedTest
的 ApplicationContext
使用内联的 key1
和 key2
属性进行加载。以下示例展示了如何使用内联属性在子类和其超类中定义属性:
@TestPropertySource(properties = "key1 = value1")
@ContextConfiguration
class BaseTest {
// ...
}
@TestPropertySource(properties = "key2 = value2")
@ContextConfiguration
class ExtendedTest extends BaseTest {
// ...
}
# 7.10、使用动态属性源的上下文配置
Spring 测试上下文框架通过 DynamicPropertyRegistry
、@DynamicPropertySource
注解和 DynamicPropertyRegistrar
API 提供对动态属性的支持。
注意:
动态属性源基础设施最初旨在让基于 Testcontainers (opens new window) 的测试中的属性可以轻松暴露给 Spring 集成测试。不过,这些特性可用于生命周期在测试的 ApplicationContext
外部管理的任何形式的外部资源,或者用于生命周期由测试的 ApplicationContext
管理的 bean。
# a、优先级
动态属性的优先级高于从 @TestPropertySource
、操作系统环境、Java 系统属性加载的属性,也高于应用通过 @PropertySource
声明式添加或通过编程方式添加的属性源中的属性。因此,动态属性可用于选择性地覆盖通过 @TestPropertySource
、系统属性源和应用属性源加载的属性。
# b、DynamicPropertyRegistry
DynamicPropertyRegistry
用于向 Environment
中添加名称 - 值对。值是动态的,通过 Supplier
提供,仅在解析属性时才会调用该 Supplier
。通常,会使用方法引用提供值。以下部分提供了如何使用 DynamicPropertyRegistry
的示例。
# c、@DynamicPropertySource
与应用于类级别的 @TestPropertySource
注解不同,@DynamicPropertySource
可以应用于集成测试类中的 static
方法,以便将具有动态值的属性添加到为集成测试加载的 ApplicationContext
的 Environment
中的 PropertySources
集合中。
集成测试类中使用 @DynamicPropertySource
注解的方法必须是 static
的,并且必须接受单个 DynamicPropertyRegistry
参数。有关详细信息,请参阅 DynamicPropertyRegistry
的类级别的 Javadoc。
提示:
如果你在基类中使用 @DynamicPropertySource
,并发现子类中的测试失败,原因是子类之间的动态属性发生了变化,你可能需要使用 @DirtiesContext
注解基类,以确保每个子类都能获得具有正确动态属性的 ApplicationContext
。
以下示例使用 Testcontainers 项目在 Spring ApplicationContext
外部管理 Redis 容器。被管理的 Redis 容器的 IP 地址和端口通过 redis.host
和 redis.port
属性提供给测试的 ApplicationContext
中的组件使用。这些属性可以通过 Spring 的 Environment
抽象访问,也可以直接注入到 Spring 管理的组件中,例如分别通过 @Value("${redis.host}")
和 @Value("${redis.port}")
。
@SpringJUnitConfig(/* ... */)
@Testcontainers
class ExampleIntegrationTests {
@Container
static GenericContainer redis =
new GenericContainer("redis:5.0.3-alpine").withExposedPorts(6379);
@DynamicPropertySource
static void redisProperties(DynamicPropertyRegistry registry) {
registry.add("redis.host", redis::getHost);
registry.add("redis.port", redis::getFirstMappedPort);
}
// tests ...
}
# d、DynamicPropertyRegistrar
除了在集成测试类中实现 @DynamicPropertySource
方法之外,你还可以将 DynamicPropertyRegistrar
API 的实现作为 bean 注册到测试的 ApplicationContext
中。这样做可以支持使用 @DynamicPropertySource
方法无法实现的其他用例。例如,由于 DynamicPropertyRegistrar
本身就是 ApplicationContext
中的一个 bean,它可以与上下文中的其他 bean 进行交互,并注册源自这些 bean 的动态属性。
测试的 ApplicationContext
中实现 DynamicPropertyRegistrar
接口的任何 bean 都会被自动检测到,并在单例预实例化阶段之前被急切地初始化,这些 bean 的 accept()
方法将使用 DynamicPropertyRegistry
调用,该 DynamicPropertyRegistry
代表注册器执行实际的动态属性注册操作。
警告: 与其他 bean 的任何交互都会导致这些其他 bean 及其依赖项被急切地初始化。
以下示例展示了如何将 DynamicPropertyRegistrar
实现为一个 lambda 表达式,该表达式为 ApiServer
bean 注册一个动态属性。api.url
属性可以通过 Spring 的 Environment
抽象访问,也可以直接注入到其他 Spring 管理的组件中,例如通过 @Value("${api.url}")
,api.url
属性的值将从 ApiServer
bean 动态获取。
@Configuration
class TestConfig {
@Bean
ApiServer apiServer() {
return new ApiServer();
}
@Bean
DynamicPropertyRegistrar apiPropertiesRegistrar(ApiServer apiServer) {
return registry -> registry.add("api.url", apiServer::getUrl);
}
}
# 7.11、加载 WebApplicationContext
若要指示测试上下文框架(TestContext framework)加载 WebApplicationContext
而非标准的 ApplicationContext
,可使用 @WebAppConfiguration
注解对应的测试类。
测试类上存在 @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 对 WebApplicationContext
实现的测试支持与对标准 ApplicationContext
实现的支持是相当的。在使用 WebApplicationContext
进行测试时,你可以使用 @ContextConfiguration
自由地声明 XML 配置文件、Groovy 脚本或 @Configuration
类。你还可以自由使用其他测试注解,如 @ActiveProfiles
、@TestExecutionListeners
、@Sql
、@Rollback
等。
本节中的其余示例展示了加载 WebApplicationContext
的一些不同配置选项。以下示例展示了测试上下文框架对约定优于配置的支持:
@ExtendWith(SpringExtension.class)
// 默认为 "file:src/main/webapp"
@WebAppConfiguration
// 检测同一包中的 "WacTests-context.xml"
// 或静态嵌套的 @Configuration 类
@ContextConfiguration
class WacTests {
//...
}
如果在不指定资源基础路径的情况下使用 @WebAppConfiguration
注解测试类,资源路径实际上默认是 file:src/main/webapp
。同样,如果在不指定资源 locations
、组件 classes
或上下文 initializers
的情况下声明 @ContextConfiguration
,Spring 会尝试根据约定检测配置的存在(即与 WacTests
类在同一包中的 WacTests-context.xml
或静态嵌套的 @Configuration
类)。
以下示例展示了如何使用 @WebAppConfiguration
显式声明资源基础路径,并使用 @ContextConfiguration
声明 XML 资源位置:
@ExtendWith(SpringExtension.class)
// 文件系统资源
@WebAppConfiguration("webapp")
// 类路径资源
@ContextConfiguration("/spring/test-servlet-config.xml")
class WacTests {
//...
}
这里需要注意的重要一点是,这两个注解的路径有着不同的语义。默认情况下,@WebAppConfiguration
资源路径基于文件系统,而 @ContextConfiguration
资源位置基于类路径。
以下示例展示了我们可以通过指定 Spring 资源前缀来覆盖这两个注解的默认资源语义:
@ExtendWith(SpringExtension.class)
// 类路径资源
@WebAppConfiguration("classpath:test-web-resources")
// 文件系统资源
@ContextConfiguration("file:src/main/webapp/WEB-INF/servlet-config.xml")
class WacTests {
//...
}
将此示例中的注释与上一个示例进行对比。
# 7.12、处理 Web 模拟对象
为了提供全面的 Web 测试支持,TestContext 框架默认启用了 ServletTestExecutionListener
。在针对 WebApplicationContext
进行测试时,此 TestExecutionListener
会在每个测试方法执行前使用 Spring Web 的 RequestContextHolder
设置默认的线程局部状态,并根据 @WebAppConfiguration
配置的基础资源路径创建一个 MockHttpServletRequest
、一个 MockHttpServletResponse
和一个 ServletWebRequest
。ServletTestExecutionListener
还能确保 MockHttpServletResponse
和 ServletWebRequest
可以被注入到测试实例中,并且在测试完成后清理线程局部状态。
当为测试加载了一个 WebApplicationContext
后,你可能会发现需要与 Web 模拟对象进行交互,例如,设置测试数据或在调用 Web 组件后进行断言。以下示例展示了哪些模拟对象可以自动注入到测试实例中。请注意,WebApplicationContext
和 MockServletContext
在整个测试套件中是被缓存的,而其他模拟对象由 ServletTestExecutionListener
针对每个测试方法进行管理。
@SpringJUnitWebConfig
class WacTests {
@Autowired
WebApplicationContext wac; // 已缓存
@Autowired
MockServletContext servletContext; // 已缓存
@Autowired
MockHttpSession session;
@Autowired
MockHttpServletRequest request;
@Autowired
MockHttpServletResponse response;
@Autowired
ServletWebRequest webRequest;
//...
}
# 7.13、上下文缓存
当测试上下文框架为测试加载 ApplicationContext
(或 WebApplicationContext
)后,该上下文会被缓存,并在同一测试套件中声明相同唯一上下文配置的所有后续测试中重复使用。要理解缓存的工作原理,重要的是要理解“唯一”和“测试套件”的含义。
一旦测试上下文框架为某个测试加载了一个 ApplicationContext
(或 WebApplicationContext
),该上下文就会被缓存,并在同一测试套件中声明相同唯一上下文配置的所有后续测试中重复使用。要理解缓存的工作原理,了解“唯一”和“测试套件”的含义很重要。
ApplicationContext
可以通过用于加载它的配置参数组合来唯一标识。因此,使用唯一的配置参数组合来生成一个键,上下文将以此键进行缓存。测试上下文框架使用以下配置参数来构建上下文缓存键:
locations
(来自@ContextConfiguration
)classes
(来自@ContextConfiguration
)contextInitializerClasses
(来自@ContextConfiguration
)contextCustomizers
(来自ContextCustomizerFactory
)—— 这包括@DynamicPropertySource
方法以及 Spring Boot 测试支持中的各种特性,如@MockBean
和@SpyBean
。contextLoader
(来自@ContextConfiguration
)parent
(来自@ContextHierarchy
)activeProfiles
(来自@ActiveProfiles
)propertySourceDescriptors
(来自@TestPropertySource
)propertySourceProperties
(来自@TestPropertySource
)resourceBasePath
(来自@WebAppConfiguration
)
例如,如果 TestClassA
在 @ContextConfiguration
的 locations
(或 value
)属性中指定了 {"app-config.xml", "test-config.xml"}
,测试上下文框架将加载相应的 ApplicationContext
,并将其存储在一个静态上下文缓存中,缓存键仅基于这些位置。所以,如果 TestClassB
也为其位置(无论是显式定义还是通过继承隐式定义)指定了 {"app-config.xml", "test-config.xml"}
,但没有定义 @WebAppConfiguration
、不同的 ContextLoader
、不同的活动配置文件、不同的上下文初始化器、不同的测试属性源或不同的父上下文,那么这两个测试类将共享同一个 ApplicationContext
。这意味着加载应用程序上下文的设置成本(每个测试套件)仅需承担一次,后续的测试执行将快得多。
注意:测试套件和分叉进程
Spring 测试上下文框架将应用程序上下文存储在一个静态缓存中。这意味着上下文实际上存储在一个 static
变量中。换句话说,如果测试在单独的进程中运行,每次测试执行之间静态缓存都会被清除,这实际上就禁用了缓存机制。
要从缓存机制中受益,所有测试必须在同一进程或测试套件中运行。这可以通过在 IDE 中将所有测试作为一个组执行来实现。同样,当使用构建框架(如 Ant、Maven 或 Gradle)执行测试时,确保构建框架在测试之间不会分叉很重要。例如,如果 Maven Surefire 插件的 forkMode
(opens new window) 设置为 always
或 pertest
,测试上下文框架就无法在测试类之间缓存应用程序上下文,结果构建过程的运行速度会明显变慢。
上下文缓存的大小是有限制的,默认最大大小为 32。每当达到最大大小时,将使用最近最少使用(LRU)清理策略来清理并关闭过时的上下文。你可以通过设置一个名为 spring.test.context.cache.maxSize
的 JVM 系统属性,从命令行或构建脚本中配置最大大小。或者,你也可以通过 SpringProperties
机制设置相同的属性。
由于在给定的测试套件中加载大量的应用程序上下文可能会导致套件运行时间过长,因此确切了解已加载和缓存的上下文数量通常是有益的。要查看底层上下文缓存的统计信息,你可以将 org.springframework.test.context.cache
日志类别的日志级别设置为 DEBUG
。
在极少数情况下,如果测试破坏了应用程序上下文并需要重新加载(例如,通过修改 bean 定义或应用程序对象的状态),你可以在测试类或测试方法上使用 @DirtiesContext
注解(请参阅 Spring 测试注解 中对 @DirtiesContext
的讨论)。这会指示 Spring 从缓存中移除上下文,并在运行下一个需要相同应用程序上下文的测试之前重新构建应用程序上下文。请注意,@DirtiesContext
注解的支持由 DirtiesContextBeforeModesTestExecutionListener
和 DirtiesContextTestExecutionListener
提供,它们默认是启用的。
注意:ApplicationContext 生命周期和控制台日志记录
当你需要调试使用 Spring 测试上下文框架执行的测试时,分析控制台输出(即输出到 SYSOUT
和 SYSERR
流)可能会很有用。一些构建工具和 IDE 能够将控制台输出与给定的测试关联起来;但是,有些控制台输出可能无法轻易与给定的测试关联起来。
关于由 Spring 框架本身或在 ApplicationContext
中注册的组件触发的控制台日志记录,理解 Spring 测试上下文框架在测试套件中加载的 ApplicationContext
的生命周期很重要。
测试的 ApplicationContext
通常在准备测试类的实例时加载 —— 例如,将依赖项注入到测试实例的 @Autowired
字段中。这意味着在 ApplicationContext
初始化期间触发的任何控制台日志记录通常无法与单个测试方法关联起来。但是,如果根据 @DirtiesContext
语义在执行测试方法之前立即关闭上下文,那么在执行测试方法之前将加载一个新的上下文实例。在后面这种情况下,IDE 或构建工具可能会将控制台日志记录与单个测试方法关联起来。
测试的 ApplicationContext
可以通过以下情况之一关闭:
- 上下文根据
@DirtiesContext
语义关闭。 - 上下文因根据 LRU 清理策略自动从缓存中清理而关闭。
- 当测试套件的 JVM 终止时,上下文通过 JVM 关闭钩子关闭。
如果在特定的测试方法执行后,上下文根据 @DirtiesContext
语义关闭,IDE 或构建工具可能会将控制台日志记录与单个测试方法关联起来。如果在测试类执行后,上下文根据 @DirtiesContext
语义关闭,在 ApplicationContext
关闭期间触发的任何控制台日志记录都无法与单个测试方法关联起来。同样,通过 JVM 关闭钩子在关闭阶段触发的任何控制台日志记录都无法与单个测试方法关联起来。
当 Spring ApplicationContext
通过 JVM 关闭钩子关闭时,在关闭阶段执行的回调将在一个名为 SpringContextShutdownHook
的线程上执行。因此,如果你希望禁用在通过 JVM 关闭钩子关闭 ApplicationContext
时触发的控制台日志记录,你可以向日志框架注册一个自定义过滤器,以忽略由该线程发起的任何日志记录。
# 7.14、上下文失败阈值
从 Spring 框架 6.1 版本开始,引入了上下文 失败阈值 策略,该策略有助于避免反复尝试加载失败的 ApplicationContext
。默认情况下,失败阈值设置为 1
,这意味着对于给定的上下文缓存键,只会尝试加载一次 ApplicationContext
。任何后续尝试为相同的上下文缓存键加载 ApplicationContext
时,都会立即抛出 IllegalStateException
,并包含一条错误消息说明该尝试已被预先跳过。这种行为可以让单个测试类和测试套件更快地失败,避免反复尝试加载一个永远无法成功加载的 ApplicationContext
,例如,由于配置错误或缺少外部资源,导致在当前环境中无法加载上下文。
你可以通过命令行或构建脚本,将名为 spring.test.context.failure.threshold
的 JVM 系统属性设置为一个正整数值,以此来配置上下文失败阈值。另外,你也可以通过 SpringProperties 机制设置相同的属性。
注意:如果你想有效地禁用上下文失败阈值,可以将该属性设置为一个非常大的值。例如,在命令行中,你可以通过 -Dspring.test.context.failure.threshold=1000000
来设置系统属性。
# 7.15、上下文层级结构
在编写依赖于已加载的 Spring ApplicationContext
的集成测试时,针对单个上下文进行测试通常就足够了。不过,有时候针对 ApplicationContext
实例的层级结构进行测试是有益的,甚至是必要的。例如,如果你正在开发一个 Spring MVC Web 应用程序,通常会有一个由 Spring 的 ContextLoaderListener
加载的根 WebApplicationContext
,以及一个由 Spring 的 DispatcherServlet
加载的子 WebApplicationContext
。这就形成了一个父子上下文层级结构,其中共享组件和基础架构配置在根上下文中声明,并在子上下文中由特定于 Web 的组件使用。另一个用例可以在 Spring Batch 应用程序中找到,通常会有一个父上下文为共享的批处理基础架构提供配置,还有一个子上下文用于配置特定的批处理作业。
你可以通过在单个测试类或测试类层级结构中使用 @ContextHierarchy
注解声明上下文配置,编写使用上下文层级结构的集成测试。如果在测试类层级结构中的多个类上声明了上下文层级结构,还可以合并或覆盖上下文层级结构中特定命名级别的上下文配置。合并层级结构中给定级别的配置时,配置资源类型(即 XML 配置文件或组件类)必须保持一致。不过,在上下文层级结构的不同级别使用不同的资源类型进行配置是完全可以接受的。
本节中其余基于 JUnit Jupiter 的示例展示了需要使用上下文层级结构的集成测试的常见配置场景。
# a、具有上下文层级结构的单个测试类
ControllerIntegrationTests
代表了 Spring MVC Web 应用程序的典型集成测试场景,它声明了一个由两个级别组成的上下文层级结构,一个用于根 WebApplicationContext
(通过使用 TestAppConfig
@Configuration
类加载),另一个用于调度器 servlet WebApplicationContext
(通过使用 WebConfig
@Configuration
类加载)。自动注入到测试实例中的 WebApplicationContext
是子上下文(即层级结构中最底层的上下文)的上下文。以下代码展示了这种配置场景:
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextHierarchy({
@ContextConfiguration(classes = TestAppConfig.class),
@ContextConfiguration(classes = WebConfig.class)
})
class ControllerIntegrationTests {
@Autowired
WebApplicationContext wac;
// ...
}
# b、具有隐式父上下文的类层级结构
此示例中的测试类在测试类层级结构中定义了一个上下文层级结构。AbstractWebTests
声明了 Spring 驱动的 Web 应用程序中根 WebApplicationContext
的配置。不过要注意,AbstractWebTests
并没有声明 @ContextHierarchy
。因此,AbstractWebTests
的子类可以选择参与上下文层级结构,也可以遵循 @ContextConfiguration
的标准语义。SoapWebServiceTests
和 RestWebServiceTests
都扩展了 AbstractWebTests
,并使用 @ContextHierarchy
定义了一个上下文层级结构。结果是加载了三个应用程序上下文(每个 @ContextConfiguration
声明对应一个),并且基于 AbstractWebTests
中的配置加载的应用程序上下文被设置为每个具体子类加载的上下文的父上下文。以下代码展示了这种配置场景:
@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 {}
# c、具有合并上下文层级结构配置的类层级结构
此示例中的类展示了如何使用命名层级来合并上下文层级结构中特定级别的配置。BaseTests
在层级结构中定义了两个级别,parent
和 child
。ExtendedTests
扩展了 BaseTests
,并指示 Spring 测试上下文框架合并 child
层级的上下文配置,方法是确保 @ContextConfiguration
中 name
属性声明的名称都是 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"}
加载的上下文的父上下文。以下代码展示了这种配置场景:
@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 {}
# d、具有覆盖上下文层级结构配置的类层级结构
与上一个示例不同,此示例展示了如何通过将 @ContextConfiguration
中的 inheritLocations
标志设置为 false
来覆盖上下文层级结构中给定命名级别的配置。因此,ExtendedTests
的应用程序上下文仅从 /test-user-config.xml
加载,并且其父上下文被设置为从 /app-config.xml
加载的上下文。以下代码展示了这种配置场景:
@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 {}
注意:在上下文层级结构中弄脏上下文
如果你在上下文配置为上下文层级结构一部分的测试中使用 @DirtiesContext
,可以使用 hierarchyMode
标志来控制如何清除上下文缓存。有关详细信息,请参阅《Spring 测试注解》中对 @DirtiesContext
的讨论以及 @DirtiesContext (opens new window) 的 Java 文档。
# 8、测试夹具的依赖注入
当你使用 DependencyInjectionTestExecutionListener
(默认已配置)时,测试实例的依赖会从通过 @ContextConfiguration
或相关注解所配置的应用上下文中的 bean 注入。你可以使用 setter 注入、字段注入,或同时使用这两种方式,具体取决于你选择的注解以及你将它们放在 setter 方法还是字段上。如果你使用的是 JUnit Jupiter,还可以选择使用构造函数注入(请参阅使用 SpringExtension
进行依赖注入)。为了与 Spring 基于注解的注入支持保持一致,你还可以使用 Spring 的 @Autowired
注解或 JSR - 330 中的 @Inject
注解来进行字段和 setter 注入。
提示:对于 JUnit Jupiter 以外的测试框架,TestContext 框架不会参与测试类的实例化过程。因此,在测试类的构造函数上使用 @Autowired
或 @Inject
不会产生任何效果。
注意:尽管在生产代码中不鼓励使用字段注入,但在测试代码中,字段注入其实非常自然。这种差异的原因在于,你永远不会直接实例化测试类。因此,无需在测试类上调用 public
构造函数或 setter 方法。
因为 @Autowired
用于按类型自动装配,如果你有多个相同类型的 bean 定义,就不能依赖这种方式来处理这些特定的 bean。在这种情况下,你可以将 @Autowired
与 @Qualifier
结合使用。你也可以选择将 @Inject
与 @Named
结合使用。或者,如果你的测试类可以访问其 ApplicationContext
,你可以通过显式查找(例如调用 applicationContext.getBean("titleRepository", TitleRepository.class)
)来获取 bean。
如果你不希望对测试实例应用依赖注入,请勿使用 @Autowired
或 @Inject
注解字段或 setter 方法。或者,你可以通过使用 @TestExecutionListeners
显式配置类,并从监听器列表中省略 DependencyInjectionTestExecutionListener.class
来完全禁用依赖注入。
考虑在目标部分中概述的测试 HibernateTitleRepository
类的场景。接下来的两段代码展示了在字段和 setter 方法上使用 @Autowired
的情况。所有示例代码清单之后将展示应用上下文配置。
注意:
- 以下代码清单中的依赖注入行为并非 JUnit Jupiter 所特有的。相同的依赖注入技术可以与任何受支持的测试框架结合使用。
- 以下示例调用了静态断言方法,如
assertNotNull()
,但并未在调用前添加Assertions
。在这种情况下,假设该方法是通过未在示例中显示的import static
声明正确导入的。
第一段代码展示了一个基于 JUnit Jupiter 的测试类实现,它使用 @Autowired
进行字段注入:
@ExtendWith(SpringExtension.class)
// 指定要为该测试夹具加载的 Spring 配置
@ContextConfiguration("repository-config.xml")
class HibernateTitleRepositoryTests {
// 这个实例将按类型进行依赖注入
@Autowired
HibernateTitleRepository titleRepository;
@Test
void findById() {
Title title = titleRepository.findById(new Long(10));
assertNotNull(title);
}
}
或者,你可以将类配置为使用 @Autowired
进行 setter 注入,如下所示:
@ExtendWith(SpringExtension.class)
// 指定要为该测试夹具加载的 Spring 配置
@ContextConfiguration("repository-config.xml")
class HibernateTitleRepositoryTests {
// 这个实例将按类型进行依赖注入
HibernateTitleRepository titleRepository;
@Autowired
void setTitleRepository(HibernateTitleRepository titleRepository) {
this.titleRepository = titleRepository;
}
@Test
void findById() {
Title title = titleRepository.findById(new Long(10));
assertNotNull(title);
}
}
前面的代码清单使用了 @ContextConfiguration
注解引用的同一个 XML 上下文文件(即 repository-config.xml
)。以下是该配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 这个 bean 将被注入到 HibernateTitleRepositoryTests 类中 -->
<bean id="titleRepository" class="com.foo.repository.hibernate.HibernateTitleRepository">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>
<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
<!-- 为简洁起见省略了配置 -->
</bean>
</beans>
注意:如果你继承自 Spring 提供的测试基类,而该基类碰巧在其某个 setter 方法上使用了 @Autowired
,那么你的应用上下文中可能定义了多个受影响类型的 bean(例如,多个 DataSource
bean)。在这种情况下,你可以重写该 setter 方法,并使用 @Qualifier
注解来指定特定的目标 bean,如下所示(但要确保也委托给超类中被重写的方法):
// ...
@Autowired
@Override
public void setDataSource(@Qualifier("myDataSource") DataSource dataSource) {
super.setDataSource(dataSource);
}
// ...
指定的限定符值表示要注入的特定 DataSource
bean,将类型匹配集缩小到特定的 bean。其值会与相应 <bean>
定义中的 <qualifier>
声明进行匹配。bean 名称会作为备用限定符值,因此你实际上也可以通过名称指向特定的 bean(如前所示,假设 myDataSource
是 bean 的 id
)。
# 9、测试中的 Bean 覆盖
测试中的 Bean 覆盖是指通过为测试类或测试类中的一个或多个非静态字段添加注解,在测试类的 ApplicationContext
中覆盖特定 Bean 的能力。
注意:此功能旨在作为一种风险较低的替代方案,用于替代通过 @Bean
注册 Bean 并将 DefaultListableBeanFactory
的 setAllowBeanDefinitionOverriding
标志设置为 true
的做法。
Spring 测试上下文(TestContext)框架提供了两组用于 Bean 覆盖的注解:
@TestBean
@MockitoBean
和@MockitoSpyBean
前者仅依赖于 Spring,而后者依赖于第三方库 Mockito (opens new window)。
# 9.1、自定义 Bean 覆盖支持
上述三个注解基于 @BeanOverride
元注解及相关基础组件构建,这使得用户能够定义自定义的 Bean 覆盖变体。
要实现自定义 Bean 覆盖支持,需要以下内容:
- 一个使用
@BeanOverride
进行元注解的注解,该注解需定义要使用的BeanOverrideProcessor
。 - 自定义的
BeanOverrideProcessor
实现。 - 由处理器创建的一个或多个具体的
BeanOverrideHandler
实现。
Spring 测试上下文框架包含以下支持 Bean 覆盖的 API 实现,这些实现负责设置其他的基础组件:
BeanFactoryPostProcessor
ContextCustomizerFactory
TestExecutionListener
spring-test
模块在其 META-INF/spring.factories 属性文件 (opens new window) 中注册了后两者的实现(BeanOverrideContextCustomizerFactory
和 BeanOverrideTestExecutionListener
)。
Bean 覆盖基础组件会在测试类以及测试类中被 @BeanOverride
元注解的非静态字段上搜索注解,并实例化相应的 BeanOverrideProcessor
,该处理器负责创建合适的 BeanOverrideHandler
。
内部的 BeanOverrideBeanFactoryPostProcessor
随后会使用 Bean 覆盖处理程序,通过根据相应的 BeanOverrideStrategy
创建、替换或包装 Bean 来修改测试的 ApplicationContext
:
REPLACE
:替换 Bean。如果相应的 Bean 不存在,则抛出异常。REPLACE_OR_CREATE
:如果 Bean 存在则替换它。如果相应的 Bean 不存在,则创建一个新的 Bean。WRAP
:获取原始 Bean 并对其进行包装。
提示:
- 只有单例 Bean 可以被覆盖。任何尝试覆盖非单例 Bean 的操作都会导致异常。
- 当替换由
FactoryBean
创建的 Bean 时,FactoryBean
本身将被一个与适用的BeanOverrideHandler
创建的 Bean 覆盖实例相对应的单例 Bean 替换。 - 当包装由
FactoryBean
创建的 Bean 时,将包装由FactoryBean
创建的对象,而不是FactoryBean
本身。
注意:与 Spring 的自动装配机制(例如解析 @Autowired
字段)不同,测试上下文框架中的 Bean 覆盖基础组件在定位 Bean 时能执行的启发式操作有限。要么 BeanOverrideProcessor
可以计算出要覆盖的 Bean 的名称,要么可以根据带注解字段的类型及其限定注解明确地选择它。
通常,BeanOverrideFactoryPostProcessor
会“按类型”选择 Bean。或者,用户可以在自定义注解中直接提供 Bean 的名称。BeanOverrideProcessor
实现也可以在内部根据约定或其他方法计算 Bean 的名称。
# 10、测试请求作用域和会话作用域的 Bean
Spring 自早期就开始支持请求作用域和会话作用域的 Bean,你可以按照以下步骤测试请求作用域和会话作用域的 Bean:
- 确保通过使用
@WebAppConfiguration
注解测试类,为测试加载WebApplicationContext
。 - 将模拟请求或会话注入到测试实例中,并适当地准备测试数据。
- 调用从已配置的
WebApplicationContext
(通过依赖注入)中获取的 Web 组件。 - 对模拟对象执行断言。
下一个代码片段展示了登录用例的 XML 配置。请注意,userService
Bean 依赖于一个请求作用域的 loginAction
Bean。此外,LoginAction
是通过使用SpEL 表达式来实例化的,该表达式从当前的 HTTP 请求中获取用户名和密码。在我们的测试中,我们希望通过 TestContext 框架管理的模拟对象来配置这些请求参数。以下是该用例的配置:
# 10.1、请求作用域的 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
。然后,我们可以根据已知的用户名和密码输入对结果进行断言。以下是具体实现:
# 10.2、请求作用域的 Bean 测试
@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();
// 断言结果
}
}
以下代码片段与我们之前看到的请求作用域的 Bean 示例类似。不过,这次 userService
Bean 依赖于一个会话作用域的 userPreferences
Bean。请注意,UserPreferences
Bean 是通过使用 SpEL 表达式来实例化的,该表达式从当前的 HTTP 会话中获取主题。在我们的测试中,我们需要在 TestContext 框架管理的模拟会话中配置一个主题。以下是具体实现:
# 10.3、会话作用域的 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
中,我们将 UserService
和 MockHttpSession
注入到测试实例中。在 sessionScope()
测试方法中,我们通过在提供的 MockHttpSession
中设置预期的 theme
属性来准备测试数据。当在 userService
上调用 processUserPreferences()
方法时,我们可以确保用户服务能够访问当前 MockHttpSession
的会话作用域 userPreferences
,并且我们可以根据配置的主题对结果进行断言。以下是具体实现:
# 10.4、会话作用域的 Bean 测试
@SpringJUnitWebConfig
class SessionScopedBeanTests {
@Autowired
UserService userService;
@Autowired
MockHttpSession session;
@Test
void sessionScope() throws Exception {
session.setAttribute("theme", "blue");
Results results = userService.processUserPreferences();
// 断言结果
}
}
# 11、事务管理
在 TestContext 框架中,事务由 TransactionalTestExecutionListener
管理,该监听器默认已配置,即使你没有在测试类中显式声明 @TestExecutionListeners
。然而,要启用对事务的支持,你必须在通过 @ContextConfiguration
语义加载的 ApplicationContext
中配置一个 PlatformTransactionManager
Bean(后续会提供更多细节)。此外,你必须在测试的类或方法级别声明 Spring 的 @Transactional
注解。
# 11.1、由测试管理的事务
由测试管理的事务是通过 TransactionalTestExecutionListener
以声明式方式管理的事务,或者是通过 TestTransaction
以编程方式管理的事务(后续会介绍)。不要将此类事务与 Spring 管理的事务(那些在为测试加载的 ApplicationContext
中由 Spring 直接管理的事务)或应用程序管理的事务(那些在测试调用的应用程序代码中以编程方式管理的事务)相混淆。Spring 管理的事务和应用程序管理的事务通常会参与到由测试管理的事务中。但是,如果 Spring 管理的事务或应用程序管理的事务配置为除 REQUIRED
或 SUPPORTS
之外的任何传播类型,则需要谨慎处理(有关详细信息,请参阅事务传播的讨论)。
警告:预定义超时和由测试管理的事务
在使用测试框架的任何形式的预定义超时时,必须与 Spring 的由测试管理的事务结合使用时要格外小心。
具体来说,Spring 的测试支持会在当前测试方法被调用之前,将事务状态绑定到当前线程(通过 java.lang.ThreadLocal
变量)。如果测试框架为了支持预定义超时,在一个新线程中调用当前测试方法,那么当前测试方法中执行的任何操作都不会在由测试管理的事务中执行。因此,任何此类操作的结果不会随着由测试管理的事务回滚。相反,即使 Spring 正确回滚了由测试管理的事务,这些操作仍会被提交到持久存储(例如关系型数据库)。
这种情况可能发生的场景包括但不限于以下几种:
- JUnit 4 的
@Test(timeout = …)
支持和TimeOut
规则。 - JUnit Jupiter 的
org.junit.jupiter.api.Assertions
类中的assertTimeoutPreemptively(…)
方法。 - TestNG 的
@Test(timeOut = …)
支持。
# 11.2、启用和禁用事务
用 @Transactional
注解测试方法会使测试在一个事务中运行,默认情况下,该事务在测试完成后会自动回滚。如果测试类使用 @Transactional
注解,则该类层级中的每个测试方法都会在一个事务中运行。未使用 @Transactional
注解(在类或方法级别)的测试方法不会在事务中运行。请注意,@Transactional
不支持在测试生命周期方法中使用,例如使用 JUnit Jupiter 的 @BeforeAll
、@BeforeEach
等注解的方法。此外,使用 @Transactional
注解但将 propagation
属性设置为 NOT_SUPPORTED
或 NEVER
的测试不会在事务中运行。
属性 | 对由测试管理的事务的支持情况 |
---|---|
value 和 transactionManager | 是 |
propagation | 仅支持 Propagation.NOT_SUPPORTED 和 Propagation.NEVER |
isolation | 否 |
timeout | 否 |
readOnly | 否 |
rollbackFor 和 rollbackForClassName | 否:请使用 TestTransaction.flagForRollback() 代替 |
noRollbackFor 和 noRollbackForClassName | 否:请使用 TestTransaction.flagForCommit() 代替 |
提示:
方法级别的生命周期方法(例如,使用 JUnit Jupiter 的 @BeforeEach
或 @AfterEach
注解的方法)会在由测试管理的事务中运行。另一方面,套件级和类级别的生命周期方法(例如,使用 JUnit Jupiter 的 @BeforeAll
或 @AfterAll
注解的方法,以及使用 TestNG 的 @BeforeSuite
、@AfterSuite
、@BeforeClass
或 @AfterClass
注解的方法)不会在由测试管理的事务中运行。
如果你需要在套件级或类级别的生命周期方法中的事务内运行代码,你可能希望将相应的 PlatformTransactionManager
注入到测试类中,然后将其与 TransactionTemplate
一起用于编程式事务管理。
请注意,AbstractTransactionalJUnit4SpringContextTests
和 AbstractTransactionalTestNGSpringContextTests
在类级别已经预配置了事务支持。
以下示例演示了为基于 Hibernate 的 UserRepository
编写集成测试的常见场景:
@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() {
// 跟踪测试数据库中的初始状态
final int count = countRowsInTable("user");
User user = new User(...);
repository.save(user);
// 需要手动刷新以避免测试中出现误判
sessionFactory.getCurrentSession().flush();
assertNumUsers(count + 1);
}
private int countRowsInTable(String tableName) {
return JdbcTestUtils.countRowsInTable(this.jdbcTemplate, tableName);
}
private void assertNumUsers(int expected) {
assertEquals("Number of rows in the [user] table.", expected, countRowsInTable("user"));
}
}
正如事务回滚和提交行为中所解释的,在 createUser()
方法运行后,无需清理数据库,因为对数据库所做的任何更改都会由 TransactionalTestExecutionListener
自动回滚。
# 11.3、事务回滚和提交行为
默认情况下,测试事务会在测试完成后自动回滚;但是,可以通过 @Commit
和 @Rollback
注解以声明式方式配置事务提交和回滚行为。有关详细信息,请参阅注解支持部分中的相应条目。
# 11.4、编程式事务管理
你可以通过使用 TestTransaction
中的静态方法以编程方式与由测试管理的事务进行交互。例如,你可以在测试方法、前置方法和后置方法中使用 TestTransaction
来启动或结束当前由测试管理的事务,或者将当前由测试管理的事务配置为回滚或提交。只要启用了 TransactionalTestExecutionListener
,就会自动支持 TestTransaction
。
以下示例演示了 TestTransaction
的一些功能。有关详细信息,请参阅 TestTransaction
(opens new window) 的 Javadoc。
@ContextConfiguration(classes = TestConfig.class)
public class ProgrammaticTransactionManagementTests extends
AbstractTransactionalJUnit4SpringContextTests {
@Test
public void transactionalTest() {
// 验证测试数据库中的初始状态
assertNumUsers(2);
deleteFromTables("user");
// 对数据库的更改将被提交
TestTransaction.flagForCommit();
TestTransaction.end();
assertFalse(TestTransaction.isActive());
assertNumUsers(0);
TestTransaction.start();
// 对数据库执行其他操作,这些操作将在测试完成后自动回滚...
}
protected void assertNumUsers(int expected) {
assertEquals("Number of rows in the [user] table.", expected, countRowsInTable("user"));
}
}
# 11.5、在事务外运行代码
有时,你可能需要在事务性测试方法之前或之后但在事务上下文之外运行某些代码,例如,在运行测试之前验证数据库的初始状态,或者在测试运行后验证预期的事务提交行为(如果测试配置为提交事务)。TransactionalTestExecutionListener
为此类场景提供了 @BeforeTransaction
和 @AfterTransaction
注解。你可以在测试类中的任何 void
方法或测试接口中的任何 void
默认方法上使用这些注解之一,TransactionalTestExecutionListener
会确保你的前置事务方法或后置事务方法在适当的时间运行。
注意:
一般来说,@BeforeTransaction
和 @AfterTransaction
方法不能接受任何参数。
但是,从 Spring Framework 6.1 开始,对于使用 SpringExtension
和 JUnit Jupiter 的测试,@BeforeTransaction
和 @AfterTransaction
方法可以选择性地接受参数,这些参数将由任何注册的 JUnit ParameterResolver
扩展(例如 SpringExtension
)解析。这意味着像 TestInfo
这样的特定于 JUnit 的参数或测试的 ApplicationContext
中的 Bean 可以提供给 @BeforeTransaction
和 @AfterTransaction
方法,如以下示例所示。
@BeforeTransaction
void verifyInitialDatabaseState(@Autowired DataSource dataSource) {
// 使用 DataSource 在事务开始前验证初始状态
}
提示:
任何前置方法(如使用 JUnit Jupiter 的 @BeforeEach
注解的方法)和任何后置方法(如使用 JUnit Jupiter 的 @AfterEach
注解的方法)都会在事务性测试方法的由测试管理的事务中运行。
同样,使用 @BeforeTransaction
或 @AfterTransaction
注解的方法仅会为事务性测试方法运行。
# 11.6、配置事务管理器
TransactionalTestExecutionListener
期望在测试的 Spring ApplicationContext
中定义一个 PlatformTransactionManager
Bean。如果测试的 ApplicationContext
中有多个 PlatformTransactionManager
实例,你可以使用 @Transactional("myTxMgr")
或 @Transactional(transactionManager = "myTxMgr")
声明一个限定符,或者由 @Configuration
类实现 TransactionManagementConfigurer
。有关在测试的 ApplicationContext
中查找事务管理器的算法的详细信息,请参阅 TestContextTransactionUtils.retrieveTransactionManager()
(opens new window) 的 Javadoc。
# 11.7、所有与事务相关的注解演示
以下基于 JUnit Jupiter 的示例展示了一个虚构的集成测试场景,突出了所有与事务相关的注解。该示例并非旨在展示最佳实践,而是演示如何使用这些注解。有关更多信息和配置示例,请参阅注解支持部分。对 @Sql
的事务管理包含一个额外的示例,该示例使用 @Sql
以声明式方式执行 SQL 脚本,并具有默认的事务回滚语义。以下示例展示了相关注解:
@SpringJUnitConfig
@Transactional(transactionManager = "txMgr")
@Commit
class FictitiousTransactionalTest {
@BeforeTransaction
void verifyInitialDatabaseState() {
// 在事务开始前验证初始状态的逻辑
}
@BeforeEach
void setUpTestDataWithinTransaction() {
// 在事务中设置测试数据
}
@Test
// 覆盖类级别的 @Commit 设置
@Rollback
void modifyDatabaseWithinTransaction() {
// 使用测试数据并修改数据库状态的逻辑
}
@AfterEach
void tearDownWithinTransaction() {
// 在事务中运行 "清理" 逻辑
}
@AfterTransaction
void verifyFinalDatabaseState() {
// 在事务回滚后验证最终状态的逻辑
}
}
注意:测试 ORM 代码时避免误判 当你测试操作 Hibernate 会话或 JPA 持久化上下文状态的应用程序代码时,确保在运行该代码的测试方法中刷新底层工作单元。不刷新底层工作单元可能会产生误判:你的测试通过了,但相同的代码在实际生产环境中会抛出异常。请注意,这适用于任何维护内存中工作单元的 ORM 框架。在以下基于 Hibernate 的示例测试用例中,一个方法演示了误判,另一个方法则正确展示了刷新会话的结果:
//...
@Autowired
SessionFactory sessionFactory;
@Transactional
@Test // 无预期异常
public void falsePositive() {
updateEntityInHibernateSession();
// 误判:Hibernate 会话最终刷新时(即生产代码中)将抛出异常
}
@Transactional
@Test(expected =...)
public void updateWithSessionFlush() {
updateEntityInHibernateSession();
// 需要手动刷新以避免测试中出现误判
sessionFactory.getCurrentSession().flush();
}
//...
以下示例展示了适用于 JPA 的匹配方法:
//...
@PersistenceContext
EntityManager entityManager;
@Transactional
@Test // 无预期异常
public void falsePositive() {
updateEntityInJpaPersistenceContext();
// 误判:JPA EntityManager 最终刷新时(即生产代码中)将抛出异常
}
@Transactional
@Test(expected =...)
public void updateWithEntityManagerFlush() {
updateEntityInJpaPersistenceContext();
// 需要手动刷新以避免测试中出现误判
entityManager.flush();
}
//...
注意:测试 ORM 实体生命周期回调 与测试 ORM 代码时避免误判的注意事项类似,如果你的应用程序使用了实体生命周期回调(也称为实体监听器),请确保在运行该代码的测试方法中刷新底层工作单元。不刷新或清除底层工作单元可能会导致某些生命周期回调无法被调用。
例如,在使用 JPA 时,除非在保存或更新实体后调用 entityManager.flush()
,否则 @PostPersist
、@PreUpdate
和 @PostUpdate
回调将不会被调用。同样,如果一个实体已经附着到当前工作单元(与当前持久化上下文关联),尝试重新加载该实体会不会触发 @PostLoad
回调,除非在重新加载实体的尝试之前调用 entityManager.clear()
。
以下示例展示了如何刷新 EntityManager
以确保在持久化实体时调用 @PostPersist
回调。在示例中使用的 Person
实体已经注册了一个带有 @PostPersist
回调方法的实体监听器。
//...
@Autowired
JpaPersonRepository repo;
@PersistenceContext
EntityManager entityManager;
@Transactional
@Test
void savePerson() {
// EntityManager#persist(...) 会触发 @PrePersist,但不会触发 @PostPersist
repo.save(new Person("Jane"));
// 需要手动刷新以调用 @PostPersist 回调
entityManager.flush();
// 依赖于 @PostPersist 回调已被调用的测试代码...
}
//...
有关使用所有 JPA 生命周期回调的工作示例,请参阅 Spring Framework 测试套件中的 JpaEntityListenerTests (opens new window)。
# 12、执行 SQL 脚本
在针对关系型数据库编写集成测试时,运行 SQL 脚本来修改数据库模式或向表中插入测试数据通常是很有益的。spring - jdbc
模块提供了在 Spring ApplicationContext
加载时通过执行 SQL 脚本来“初始化”嵌入式或现有数据库的支持。详情请参阅嵌入式数据库支持和使用嵌入式数据库测试数据访问逻辑。
虽然在 ApplicationContext
加载时“一次性”初始化数据库进行测试非常有用,但有时在集成测试“期间”修改数据库也是必不可少的。以下部分解释了如何在集成测试期间以编程和声明两种方式运行 SQL 脚本。
# 12.1、以编程方式执行 SQL 脚本
Spring 提供了以下在集成测试方法中以编程方式执行 SQL 脚本的选项:
org.springframework.jdbc.datasource.init.ScriptUtils
org.springframework.jdbc.datasource.init.ResourceDatabasePopulator
org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests
org.springframework.test.context.testng.AbstractTransactionalTestNGSpringContextTests
ScriptUtils
提供了一组用于处理 SQL 脚本的静态实用方法,主要供框架内部使用。但是,如果您需要完全控制 SQL 脚本的解析和运行方式,ScriptUtils
可能比后面描述的一些其他替代方案更适合您的需求。有关 ScriptUtils
中各个方法的更多详细信息,请参阅 Javadoc (opens new window)。
ResourceDatabasePopulator
提供了一个基于对象的 API,用于通过使用在外部资源中定义的 SQL 脚本来以编程方式填充、初始化或清理数据库。ResourceDatabasePopulator
提供了一些选项,用于配置解析和运行脚本时使用的字符编码、语句分隔符、注释分隔符和错误处理标志。每个配置选项都有一个合理的默认值。有关默认值的详细信息,请参阅 Javadoc (opens new window)。要运行在 ResourceDatabasePopulator
中配置的脚本,您可以调用 populate(Connection)
方法针对 java.sql.Connection
运行填充器,或者调用 execute(DataSource)
方法针对 javax.sql.DataSource
运行填充器。以下示例指定了用于测试模式和测试数据的 SQL 脚本,将语句分隔符设置为 @@
,并针对 DataSource
运行这些脚本:
@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);
// 运行使用测试模式和数据的代码
}
请注意,ResourceDatabasePopulator
在内部委托给 ScriptUtils
来解析和运行 SQL 脚本。类似地,AbstractTransactionalJUnit4SpringContextTests
和 AbstractTransactionalTestNGSpringContextTests
中的 executeSqlScript(..)
方法在内部使用 ResourceDatabasePopulator
来运行 SQL 脚本。有关 executeSqlScript(..)
各种方法的更多详细信息,请参阅 Javadoc。
# 12.2、使用 @Sql 声明式执行 SQL 脚本
除了上述以编程方式运行 SQL 脚本的机制外,您还可以在 Spring 测试上下文框架中声明式地配置 SQL 脚本。具体来说,您可以在测试类或测试方法上声明 @Sql
注解,以配置应在集成测试类或测试方法之前或之后针对给定数据库运行的单个 SQL 语句或 SQL 脚本的资源路径。@Sql
的支持由 SqlScriptsTestExecutionListener
提供,该监听器默认是启用的。
注意:默认情况下,方法级别的 @Sql
声明会覆盖类级别的声明,但可以通过 @SqlMergeMode
为每个测试类或每个测试方法配置此行为。有关更多详细信息,请参阅使用 @SqlMergeMode
合并和覆盖配置。
但是,这不适用于为 BEFORE_TEST_CLASS
或 AFTER_TEST_CLASS
执行阶段配置的类级声明。此类声明不能被覆盖,并且除了任何方法级脚本和语句外,相应的脚本和语句将针对每个类执行一次。
# a、路径资源语义
每个路径都被解释为 Spring Resource
。普通路径(例如,"schema.sql"
)被视为相对于测试类所在包的类路径资源。以斜杠开头的路径被视为绝对类路径资源(例如,"/org/example/schema.sql"
)。引用 URL 的路径(例如,以 classpath:
、file:
、http:
为前缀的路径)使用指定的资源协议加载。
从 Spring 框架 6.2 开始,路径可以包含属性占位符(${…}
),这些占位符将被测试的 ApplicationContext
的 Environment
中存储的属性替换。
以下示例展示了如何在基于 JUnit Jupiter 的集成测试类中在类级别和方法级别使用 @Sql
:
@SpringJUnitConfig
@Sql("/test-schema.sql")
class DatabaseTests {
@Test
void emptySchemaTest() {
// 运行使用测试模式但不使用任何测试数据的代码
}
@Test
@Sql({"/test-schema.sql", "/test-user-data.sql"})
void userTest() {
// 运行使用测试模式和测试数据的代码
}
}
# b、默认脚本检测
如果未指定任何 SQL 脚本或语句,将根据 @Sql
的声明位置尝试检测“默认”脚本。如果无法检测到默认脚本,将抛出 IllegalStateException
。
- 类级声明:如果带注解的测试类是
com.example.MyTest
,则相应的默认脚本是classpath:com/example/MyTest.sql
。 - 方法级声明:如果带注解的测试方法名为
testMethod()
且在com.example.MyTest
类中定义,则相应的默认脚本是classpath:com/example/MyTest.testMethod.sql
。
# c、记录 SQL 脚本和语句
如果您想查看正在执行哪些 SQL 脚本,请将 org.springframework.test.context.jdbc
日志记录类别设置为 DEBUG
。
如果您想查看正在执行哪些 SQL 语句,请将 org.springframework.jdbc.datasource.init
日志记录类别设置为 DEBUG
。
# d、声明多个 @Sql
集
如果您需要为给定的测试类或测试方法配置多组 SQL 脚本,但每组脚本的语法配置、错误处理规则或执行阶段不同,您可以声明多个 @Sql
实例。您可以将 @Sql
用作可重复注解,也可以使用 @SqlGroup
注解作为显式容器来声明多个 @Sql
实例。
以下示例展示了如何将 @Sql
用作可重复注解:
@Test
@Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`"))
@Sql("/test-user-data.sql")
void userTest() {
// 运行使用测试模式和测试数据的代码
}
在上述示例的场景中,test - schema.sql
脚本使用不同的语法来处理单行注释。
以下示例与上一个示例相同,只是 @Sql
声明在 @SqlGroup
中分组。@SqlGroup
的使用是可选的,但为了与其他 JVM 语言兼容,您可能需要使用 @SqlGroup
。
@Test
@SqlGroup({
@Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`")),
@Sql("/test-user-data.sql")
})
void userTest() {
// 运行使用测试模式和测试数据的代码
}
# e、脚本执行阶段
默认情况下,SQL 脚本在相应的测试方法之前运行。但是,如果您需要在测试方法之后运行特定的一组脚本(例如,清理数据库状态),您可以将 @Sql
中的 executionPhase
属性设置为 AFTER_TEST_METHOD
,如下例所示:
@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() {
// 运行需要将测试数据提交到数据库的代码,且该操作在测试事务之外
}
注意:ISOLATED
和 AFTER_TEST_METHOD
分别是从 Sql.TransactionMode
和 Sql.ExecutionPhase
静态导入的。
从 Spring 框架 6.1 开始,可以通过将类级 @Sql
声明中的 executionPhase
属性设置为 BEFORE_TEST_CLASS
或 AFTER_TEST_CLASS
,在测试类之前或之后运行特定的一组脚本,如下例所示:
@SpringJUnitConfig
@Sql(scripts = "/test-schema.sql", executionPhase = BEFORE_TEST_CLASS)
class DatabaseTests {
@Test
void emptySchemaTest() {
// 运行使用测试模式但不使用任何测试数据的代码
}
@Test
@Sql("/test-user-data.sql")
void userTest() {
// 运行使用测试模式和测试数据的代码
}
}
注意:BEFORE_TEST_CLASS
是从 Sql.ExecutionPhase
静态导入的。
# f、使用 @SqlConfig
进行脚本配置
您可以使用 @SqlConfig
注解来配置脚本解析和错误处理。当 @SqlConfig
作为类级注解在集成测试类中声明时,它充当测试类层次结构中所有 SQL 脚本的全局配置。当使用 @Sql
注解的 config
属性直接声明时,@SqlConfig
充当封闭 @Sql
注解中声明的 SQL 脚本的本地配置。@SqlConfig
中的每个属性都有一个隐式默认值,这些值在相应属性的 Javadoc 中有记录。由于 Java 语言规范中对注解属性定义的规则,不幸的是,不可能将 null
值赋给注解属性。因此,为了支持覆盖继承的全局配置,@SqlConfig
属性有一个显式的默认值,即 ""
(对于字符串)、{}
(对于数组)或 DEFAULT
(对于枚举)。这种方法允许 @SqlConfig
的本地声明通过提供除 ""
、{}
或 DEFAULT
之外的值来选择性地覆盖 @SqlConfig
全局声明中的个别属性。只要本地 @SqlConfig
属性没有提供除 ""
、{}
或 DEFAULT
之外的显式值,就会继承全局 @SqlConfig
属性。因此,显式的本地配置会覆盖全局配置。
@Sql
和 @SqlConfig
提供的配置选项与 ScriptUtils
和 ResourceDatabasePopulator
支持的选项相当,但比 <jdbc:initialize - database/>
XML 命名空间元素提供的选项更丰富。有关详细信息,请参阅@Sql
(opens new window) 和 @SqlConfig
(opens new window) 中各个属性的 Javadoc。
# @Sql
的事务管理
默认情况下,SqlScriptsTestExecutionListener
会推断使用 @Sql
配置的脚本所需的事务语义。具体来说,SQL 脚本将在无事务的情况下运行,或在现有的 Spring 管理的事务(例如,由 TransactionalTestExecutionListener
为使用 @Transactional
注解的测试管理的事务)中运行,或在隔离事务中运行,这取决于 @SqlConfig
中 transactionMode
属性的配置值以及测试的 ApplicationContext
中是否存在 PlatformTransactionManager
。不过,最低要求是测试的 ApplicationContext
中必须存在 javax.sql.DataSource
。
如果 SqlScriptsTestExecutionListener
用于检测 DataSource
和 PlatformTransactionManager
并推断事务语义的算法不满足您的需求,您可以通过设置 @SqlConfig
的 dataSource
和 transactionManager
属性来指定显式名称。此外,您可以通过设置 @SqlConfig
的 transactionMode
属性来控制事务传播行为(例如,脚本是否应在隔离事务中运行)。虽然全面讨论 @Sql
支持的所有事务管理选项超出了本参考手册的范围,但 @SqlConfig
(opens new window) 和 SqlScriptsTestExecutionListener
(opens new window)的 Javadoc 提供了详细信息,以下示例展示了一个使用 JUnit Jupiter 和 @Sql
进行事务测试的典型测试场景:
@SpringJUnitConfig(TestDatabaseConfig.class)
@Transactional
class TransactionalSqlScriptsTests {
final JdbcTemplate jdbcTemplate;
@Autowired
TransactionalSqlScriptsTests(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
@Test
@Sql("/test-data.sql")
void usersTest() {
// 验证测试数据库中的状态:
assertNumUsers(2);
// 运行使用测试数据的代码...
}
int countRowsInTable(String tableName) {
return JdbcTestUtils.countRowsInTable(this.jdbcTemplate, tableName);
}
void assertNumUsers(int expected) {
assertEquals(expected, countRowsInTable("user"),
"Number of rows in the [user] table.");
}
}
请注意,在 usersTest()
方法运行后无需清理数据库,因为对数据库所做的任何更改(无论是在测试方法中还是在 "/test-data.sql"
脚本中)都会被 TransactionalTestExecutionListener
自动回滚(有关详细信息,请参阅事务管理)。
# g、使用 @SqlMergeMode
合并和覆盖配置
可以将方法级别的 @Sql
声明与类级别的声明合并。例如,这允许您为每个测试类提供一次数据库模式或一些通用测试数据的配置,然后为每个测试方法提供额外的、特定用例的测试数据。要启用 @Sql
合并,请在测试类或测试方法上使用 @SqlMergeMode(MERGE)
注解。要为特定的测试方法(或特定的测试子类)禁用合并,您可以通过 @SqlMergeMode(OVERRIDE)
切换回默认模式。有关示例和更多详细信息,请参阅@SqlMergeMode
注解文档部分。
# 13、并行测试执行
Spring 测试上下文框架为在单个 JVM 内并行执行测试提供了基本支持。一般来说,这意味着大多数测试类或测试方法可以并行运行,而无需对测试代码或配置进行任何更改。
提示:关于如何设置并行测试执行的详细信息,请参阅测试框架、构建工具或集成开发环境(IDE)的相关文档。
请记住,在测试套件中引入并发可能会导致意外的副作用、奇怪的运行时行为,以及测试间歇性或看似随机地失败。因此,Spring 团队提供了以下关于何时不适合并行运行测试的一般准则。
如果测试存在以下情况,则不要并行运行:
- 使用 Spring 框架的
@DirtiesContext
支持。 - 使用 Spring 框架的
@MockitoBean
或@MockitoSpyBean
支持。 - 使用 Spring Boot 的
@MockBean
或@SpyBean
支持。 - 使用 JUnit 4 的
@FixMethodOrder
支持,或任何旨在确保测试方法按特定顺序运行的测试框架功能。不过,如果整个测试类并行运行,则此规则不适用。 - 更改共享服务或系统(如数据库、消息代理、文件系统等)的状态。这适用于嵌入式系统和外部系统。
提示:
如果并行测试执行失败并抛出异常,表明当前测试的 ApplicationContext
不再处于活动状态,这通常意味着 ApplicationContext
已在另一个线程中从 ContextCache
中被移除。
这可能是由于使用了 @DirtiesContext
,或者是由于 ContextCache
的自动逐出机制。如果 @DirtiesContext
是问题所在,你要么需要找到避免使用 @DirtiesContext
的方法,要么将此类测试排除在并行执行之外。如果 ContextCache
的最大容量已超过限制,你可以增大缓存的最大容量。有关详细信息,请参阅上下文缓存的相关讨论。
警告:
Spring 测试上下文框架中的并行测试执行只有在底层的 TestContext
实现提供拷贝构造函数时才可行,如 TestContext (opens new window) 的 Java 文档中所解释的那样。Spring 中使用的 DefaultTestContext
提供了这样一个构造函数。但是,如果你使用的第三方库提供了自定义的 TestContext
实现,你需要验证它是否适用于并行测试执行。
# 14、测试上下文框架支持类
本节介绍支持 Spring 测试上下文框架的各类。
# 14.1、JUnit 4 运行器
Spring 测试上下文框架通过自定义运行器(支持 JUnit 4.12 及更高版本)与 JUnit 4 实现了完全集成。开发人员可以在测试类上使用 @RunWith(SpringJUnit4ClassRunner.class)
注解,或者使用更简洁的 @RunWith(SpringRunner.class)
变体,这样就可以实现基于标准 JUnit 4 的单元测试和集成测试,同时还能享受到测试上下文框架的诸多好处,例如支持加载应用上下文、对测试实例进行依赖注入、执行事务性测试方法等。如果你想结合替代运行器(例如 JUnit 4 的 Parameterized
运行器)或第三方运行器(例如 MockitoJUnitRunner
)使用 Spring 测试上下文框架,可选择使用 Spring 对 JUnit 规则的支持。
以下代码清单展示了配置测试类以使用自定义 Spring Runner
运行的最低要求:
@RunWith(SpringRunner.class)
@TestExecutionListeners({})
public class SimpleTest {
@Test
public void testMethod() {
// 测试逻辑...
}
}
在上述示例中,@TestExecutionListeners
配置了一个空列表,以禁用默认监听器。否则,默认监听器会要求通过 @ContextConfiguration
配置一个 ApplicationContext
。
# 14.2、JUnit 4 规则
org.springframework.test.context.junit4.rules
包提供了以下 JUnit 4 规则(支持 JUnit 4.12 及更高版本):
SpringClassRule
SpringMethodRule
SpringClassRule
是一个 JUnit TestRule
,它支持 Spring 测试上下文框架的类级特性。而 SpringMethodRule
是一个 JUnit MethodRule
,支持 Spring 测试上下文框架的实例级和方法级特性。
与 SpringRunner
不同,Spring 基于规则的 JUnit 支持的优势在于它独立于任何 org.junit.runner.Runner
实现,因此可以与现有的替代运行器(例如 JUnit 4 的 Parameterized
)或第三方运行器(例如 MockitoJUnitRunner
)结合使用。
为了支持测试上下文框架的完整功能,你必须将 SpringClassRule
和 SpringMethodRule
结合使用。以下示例展示了在集成测试中正确声明这些规则的方法:
// 可选地通过 @RunWith(...) 指定非 Spring 运行器
@ContextConfiguration
public class IntegrationTest {
@ClassRule
public static final SpringClassRule springClassRule = new SpringClassRule();
@Rule
public final SpringMethodRule springMethodRule = new SpringMethodRule();
@Test
public void testMethod() {
// 测试逻辑...
}
}
# 14.3、4 支持类
org.springframework.test.context.junit4
包为基于 JUnit 4 的测试用例提供了以下支持类(支持 JUnit 4.12 及更高版本):
AbstractJUnit4SpringContextTests
AbstractTransactionalJUnit4SpringContextTests
AbstractJUnit4SpringContextTests
是一个抽象基测试类,它在 JUnit 4 环境中把 Spring 测试上下文框架与显式的 ApplicationContext
测试支持集成在一起。当你继承 AbstractJUnit4SpringContextTests
时,可以访问一个受保护的 applicationContext
实例变量,可使用该变量执行显式的 bean 查找或测试整个上下文的状态。
AbstractTransactionalJUnit4SpringContextTests
是 AbstractJUnit4SpringContextTests
的抽象事务性扩展类,它增加了一些方便进行 JDBC 访问的功能。此类期望在 ApplicationContext
中定义一个 javax.sql.DataSource
bean 和一个 PlatformTransactionManager
bean。当你继承 AbstractTransactionalJUnit4SpringContextTests
时,可以访问一个受保护的 jdbcTemplate
实例变量,使用它运行 SQL 语句来查询数据库。你可以在运行与数据库相关的应用程序代码之前和之后使用此类查询来确认数据库状态,并且 Spring 会确保这些查询在与应用程序代码相同的事务范围内运行。当与 ORM 工具结合使用时,务必避免误报。如 JDBC 测试支持中所述,AbstractTransactionalJUnit4SpringContextTests
还提供了一些便捷方法,这些方法通过上述 jdbcTemplate
委托给 JdbcTestUtils
中的方法。此外,AbstractTransactionalJUnit4SpringContextTests
提供了一个 executeSqlScript(..)
方法,用于针对配置的 DataSource
运行 SQL 脚本。
提示:这些类是为了方便扩展而提供的。如果你不想让测试类与特定于 Spring 的类层次结构绑定在一起,可以通过使用 @RunWith(SpringRunner.class)
或 Spring 的 JUnit 规则 来配置你自己的自定义测试类。
# 14.4、for JUnit Jupiter
Spring 测试上下文框架与 JUnit 5 引入的 JUnit Jupiter 测试框架实现了完全集成。通过在测试类上使用 @ExtendWith(SpringExtension.class)
注解,你可以实现基于标准 JUnit Jupiter 的单元测试和集成测试,同时还能享受到测试上下文框架的好处,例如支持加载应用上下文、对测试实例进行依赖注入、执行事务性测试方法等。
此外,得益于 JUnit Jupiter 中丰富的扩展 API,Spring 除了提供对 JUnit 4 和 TestNG 支持的功能集外,还提供了以下特性:
- 对测试构造函数、测试方法和测试生命周期回调方法的依赖注入。有关更多详细信息,请参阅使用
SpringExtension
进行依赖注入。 - 基于 SpEL 表达式、环境变量、系统属性等对条件测试执行 (opens new window)提供强大支持。有关更多详细信息和示例,请参阅 Spring JUnit Jupiter 测试注解 中关于
@EnabledIf
和@DisabledIf
的文档。 - 自定义组合注解,将 Spring 和 JUnit Jupiter 的注解结合在一起。有关更多详细信息,请参阅 测试的元注解支持 中的
@TransactionalDevTestConfig
和@TransactionalIntegrationTest
示例。
以下代码清单展示了如何结合 @ContextConfiguration
配置测试类以使用 SpringExtension
:
// 指示 JUnit Jupiter 用 Spring 支持扩展测试。
@ExtendWith(SpringExtension.class)
// 指示 Spring 从 TestConfig.class 加载一个 ApplicationContext
@ContextConfiguration(classes = TestConfig.class)
class SimpleTests {
@Test
void testMethod() {
// 测试逻辑...
}
}
由于在 JUnit 5 中也可以将注解用作元注解,Spring 提供了 @SpringJUnitConfig
和 @SpringJUnitWebConfig
组合注解,以简化测试 ApplicationContext
和 JUnit Jupiter 的配置。
以下示例使用 @SpringJUnitConfig
减少了上一个示例中的配置量:
// 指示 Spring 向 JUnit Jupiter 注册 SpringExtension
// 并从 TestConfig.class 加载一个 ApplicationContext
@SpringJUnitConfig(TestConfig.class)
class SimpleTests {
@Test
void testMethod() {
// 测试逻辑...
}
}
同样,以下示例使用 @SpringJUnitWebConfig
为 JUnit Jupiter 创建一个 WebApplicationContext
:
// 指示 Spring 向 JUnit Jupiter 注册 SpringExtension
// 并从 TestWebConfig.class 加载一个 WebApplicationContext
@SpringJUnitWebConfig(TestWebConfig.class)
class SimpleWebTests {
@Test
void testMethod() {
// 测试逻辑...
}
}
有关更多详细信息,请参阅 Spring JUnit Jupiter 测试注解 中关于 @SpringJUnitConfig
和 @SpringJUnitWebConfig
的文档。
# a、使用 SpringExtension
进行依赖注入
SpringExtension
实现了 JUnit Jupiter 中的 ParameterResolver
(opens new window) 扩展 API,借助这一 API,Spring 能够为测试构造函数、测试方法和测试生命周期回调方法提供依赖注入。
具体而言,SpringExtension
可以将测试的 ApplicationContext
中的依赖项注入到使用 Spring 的 @BeforeTransaction
和 @AfterTransaction
注解,或 JUnit 的 @BeforeAll
、@AfterAll
、@BeforeEach
、@AfterEach
、@Test
、@RepeatedTest
、@ParameterizedTest
等注解标注的测试构造函数和方法中。
# 构造函数注入
如果 JUnit Jupiter 测试类构造函数中的某个特定参数是 ApplicationContext
类型(或其子类型),或者使用了 @Autowired
、@Qualifier
或 @Value
注解或元注解,Spring 会将测试的 ApplicationContext
中对应的 bean 或值注入到该特定参数中。
如果构造函数被视为可自动装配的,Spring 也可以配置为自动装配测试类构造函数的所有参数。若满足以下条件之一(按优先级顺序),则认为构造函数是可自动装配的:
- 构造函数使用了
@Autowired
注解。 @TestConstructor
注解存在于测试类上,或作为元注解存在于测试类上,且autowireMode
属性设置为ALL
。- 默认的测试构造函数自动装配模式已更改为
ALL
。
有关 @TestConstructor
的使用以及如何更改全局测试构造函数自动装配模式的详细信息,请参阅 @TestConstructor
。
警告:如果测试类的构造函数被视为可自动装配的,Spring 将负责解析构造函数中所有参数的参数值。因此,向 JUnit Jupiter 注册的其他 ParameterResolver
都无法为这样的构造函数解析参数。
警告:如果使用 @DirtiesContext
在测试方法之前或之后关闭测试的 ApplicationContext
,则测试类的构造函数注入一定不要与 JUnit Jupiter 的 @TestInstance(PER_CLASS)
支持一起使用。
原因在于 @TestInstance(PER_CLASS)
会指示 JUnit Jupiter 在多次测试方法调用之间缓存测试实例。因此,测试实例将保留对最初从已关闭的 ApplicationContext
中注入的 bean 的引用。由于在这种情况下测试类的构造函数只会被调用一次,依赖注入将不会再次发生,后续测试将与已关闭的 ApplicationContext
中的 bean 进行交互,这可能会导致错误。
若要将 @DirtiesContext
与 “测试方法之前” 或 “测试方法之后” 模式结合 @TestInstance(PER_CLASS)
使用,必须配置通过字段或 setter 注入来提供 Spring 的依赖项,以便在多次测试方法调用之间重新注入这些依赖项。
在以下示例中,Spring 将从 TestConfig.class
加载的 ApplicationContext
中的 OrderService
bean 注入到 OrderServiceIntegrationTests
构造函数中:
@SpringJUnitConfig(TestConfig.class)
class OrderServiceIntegrationTests {
private final OrderService orderService;
@Autowired
OrderServiceIntegrationTests(OrderService orderService) {
this.orderService = orderService;
}
// 使用注入的 OrderService 的测试
}
请注意,此功能允许将测试依赖项设置为 final
,从而保证其不可变。
如果将 spring.test.constructor.autowire.mode
属性设置为 all
(请参阅 @TestConstructor
),我们可以省略上一个示例中构造函数上的 @Autowired
声明,结果如下:
@SpringJUnitConfig(TestConfig.class)
class OrderServiceIntegrationTests {
private final OrderService orderService;
OrderServiceIntegrationTests(OrderService orderService) {
this.orderService = orderService;
}
// 使用注入的 OrderService 的测试
}
# 方法注入
如果 JUnit Jupiter 测试方法或测试生命周期回调方法中的某个参数是 ApplicationContext
类型(或其子类型),或者使用了 @Autowired
、@Qualifier
或 @Value
注解或元注解,Spring 会将测试的 ApplicationContext
中对应的 bean 注入到该特定参数中。
在以下示例中,Spring 将从 TestConfig.class
加载的 ApplicationContext
中的 OrderService
注入到 deleteOrder()
测试方法中:
@SpringJUnitConfig(TestConfig.class)
class OrderServiceIntegrationTests {
@Test
void deleteOrder(@Autowired OrderService orderService) {
// 使用来自测试的 ApplicationContext 的 orderService
}
}
由于 JUnit Jupiter 中的 ParameterResolver
支持非常强大,你还可以将多个依赖项注入到单个方法中,这些依赖项不仅可以来自 Spring,还可以来自 JUnit Jupiter 本身或其他第三方扩展。
以下示例展示了如何让 Spring 和 JUnit Jupiter 同时将依赖项注入到 placeOrderRepeatedly()
测试方法中:
@SpringJUnitConfig(TestConfig.class)
class OrderServiceIntegrationTests {
@RepeatedTest(10)
void placeOrderRepeatedly(RepetitionInfo repetitionInfo,
@Autowired OrderService orderService) {
// 使用来自测试的 ApplicationContext 的 orderService
// 以及来自 JUnit Jupiter 的 repetitionInfo
}
}
请注意,使用 JUnit Jupiter 的 @RepeatedTest
注解可让测试方法访问 RepetitionInfo
。
# b、@Nested
测试类配置
Spring 测试上下文框架支持在 JUnit Jupiter 的 @Nested
测试类上使用与测试相关的注解,包括对从封闭类继承测试类配置的一流支持,并且此类配置默认会被继承。若要将默认的 INHERIT
模式更改为 OVERRIDE
模式,可以在单个 @Nested
测试类上使用 @NestedTestConfiguration(EnclosingConfiguration.OVERRIDE)
注解。显式的 @NestedTestConfiguration
声明将应用于被注解的测试类及其所有子类和嵌套类。因此,你可以在顶级测试类上使用 @NestedTestConfiguration
注解,这将递归地应用于其所有嵌套测试类。
提示:如果你正在开发一个与 Spring 测试上下文框架集成的组件,并且需要支持在封闭类层次结构中继承注解,则必须使用 TestContextAnnotationUtils
中提供的注解搜索实用工具,以遵循 @NestedTestConfiguration
的语义。
为了让开发团队将默认模式更改为 OVERRIDE
(例如,为了与 Spring Framework 5.0 到 5.2 版本兼容),可以通过 JVM 系统属性或类路径根目录中的 spring.properties
文件全局更改默认模式。有关详细信息,请参阅 “更改默认的封闭配置继承模式”。
尽管下面的 “Hello World” 示例非常简单,但它展示了如何在顶级类上声明通用配置,以供其 @Nested
测试类继承。在这个特定示例中,只有 TestConfig
配置类被继承。每个嵌套测试类都提供了自己的活动配置文件集,从而为每个嵌套测试类生成不同的 ApplicationContext
(有关详细信息,请参阅上下文缓存)。请查阅支持的注解列表,以了解哪些注解可以在 @Nested
测试类中被继承。
@SpringJUnitConfig(TestConfig.class)
class GreetingServiceTests {
@Nested
@ActiveProfiles("lang_en")
class EnglishGreetings {
@Test
void hello(@Autowired GreetingService service) {
assertThat(service.greetWorld()).isEqualTo("Hello World");
}
}
@Nested
@ActiveProfiles("lang_de")
class GermanGreetings {
@Test
void hello(@Autowired GreetingService service) {
assertThat(service.greetWorld()).isEqualTo("Hallo Welt");
}
}
}
# 14.5、支持类
org.springframework.test.context.testng
包为基于 TestNG 的测试用例提供了以下支持类:
AbstractTestNGSpringContextTests
AbstractTransactionalTestNGSpringContextTests
AbstractTestNGSpringContextTests
是一个抽象基测试类,它将 Spring 测试上下文框架与 TestNG 环境中的显式 ApplicationContext
测试支持集成在一起。当你继承 AbstractTestNGSpringContextTests
时,可以访问一个受保护的 applicationContext
实例变量,可使用该变量执行显式的 bean 查找或测试整个上下文的状态。
AbstractTransactionalTestNGSpringContextTests
是 AbstractTestNGSpringContextTests
的抽象事务性扩展类,它增加了一些方便进行 JDBC 访问的功能。此类期望在 ApplicationContext
中定义一个 javax.sql.DataSource
bean 和一个 PlatformTransactionManager
bean。当你继承 AbstractTransactionalTestNGSpringContextTests
时,可以访问一个受保护的 jdbcTemplate
实例变量,使用它运行 SQL 语句来查询数据库。你可以在运行与数据库相关的应用程序代码之前和之后使用此类查询来确认数据库状态,并且 Spring 会确保这些查询在与应用程序代码相同的事务范围内运行。当与 ORM 工具结合使用时,务必避免误报。如 JDBC 测试支持中所述,AbstractTransactionalTestNGSpringContextTests
还提供了一些便捷方法,这些方法通过上述 jdbcTemplate
委托给 JdbcTestUtils
中的方法。此外,AbstractTransactionalTestNGSpringContextTests
提供了一个 executeSqlScript(..)
方法,用于针对配置的 DataSource
运行 SQL 脚本。
提示:这些类是为了方便扩展而提供的。如果你不想让测试类与特定于 Spring 的类层次结构绑定在一起,可以通过使用 @ContextConfiguration
、@TestExecutionListeners
等注解,并手动使用 TestContextManager
来测试类。有关如何测试类的示例,请参阅 AbstractTestNGSpringContextTests
的源代码。
# 15、测试的提前编译(Ahead of Time)支持
本章介绍 Spring 使用 Spring 测试上下文框架(Spring TestContext Framework)为集成测试提供的提前编译(Ahead of Time,AOT)支持。
这种测试支持在 Spring 的核心 AOT 支持基础上扩展出了以下功能:
- 构建时检测当前项目中所有使用测试上下文框架来加载
ApplicationContext
的集成测试。- 为基于 JUnit Jupiter 和 JUnit 4 的测试类提供显式支持,并为使用 Spring 核心测试注解的 TestNG 及其他测试框架提供隐式支持。前提是这些测试要使用为当前项目注册的 JUnit 平台
TestEngine
来运行。
- 为基于 JUnit Jupiter 和 JUnit 4 的测试类提供显式支持,并为使用 Spring 核心测试注解的 TestNG 及其他测试框架提供隐式支持。前提是这些测试要使用为当前项目注册的 JUnit 平台
- 构建时进行 AOT 处理:当前项目中每个独特的测试
ApplicationContext
都将为 AOT 处理进行刷新。 - 运行时的 AOT 支持:在 AOT 运行时模式下执行时,Spring 集成测试将使用经过 AOT 优化的
ApplicationContext
,该上下文会与上下文缓存进行透明交互。
默认情况下,所有测试在 AOT 模式下都是启用的。不过,你可以通过使用 @DisabledInAotMode
注解,有选择地在 AOT 模式下禁用整个测试类或单个测试方法。在使用 JUnit Jupiter 时,你可以通过 Jupiter 的 @EnabledInNativeImage
和 @DisabledInNativeImage
注解,有选择地在 GraalVM 原生镜像中启用或禁用测试。请注意,@DisabledInAotMode
同样会在 GraalVM 原生镜像中运行时禁用带注解的测试类或测试方法,这与 JUnit Jupiter 的 @DisabledInNativeImage
注解类似。
# 15.1、提示
默认情况下,如果在构建时的 AOT 处理过程中遇到错误,将会抛出异常,并且整个进程会立即失败。
如果你希望在遇到错误后继续进行构建时的 AOT 处理,可以禁用 failOnError
模式,这样错误会以 WARN
级别记录,或者在 DEBUG
级别记录更多详细信息。
可以通过将名为 spring.test.aot.processing.failOnError
的 JVM 系统属性设置为 false
,从命令行或构建脚本中禁用 failOnError
模式。另外,你也可以通过 SpringProperties
机制设置相同的属性。
# 15.2、注意
在 AOT 模式下不支持 @ContextHierarchy
注解。
若要为 GraalVM 原生镜像内的使用提供特定于测试的运行时提示,你可以选择以下几种方式:
- 实现自定义的
TestRuntimeHintsRegistrar
(opens new window),并通过META-INF/spring/aot.factories
进行全局注册。 - 实现自定义的
RuntimeHintsRegistrar
(opens new window),通过META-INF/spring/aot.factories
进行全局注册,或者在测试类上通过@ImportRuntimeHints
(opens new window) 进行局部注册。 - 使用
@Reflective
(opens new window) 或@RegisterReflectionForBinding
(opens new window) 注解测试类。 - 有关 Spring 核心运行时提示和注解支持的详细信息,请参阅运行时提示。
# 15.3、提示
TestRuntimeHintsRegistrar
API 是核心 RuntimeHintsRegistrar
API 的补充。如果你需要为测试支持注册不特定于某个测试类的全局提示,建议实现 RuntimeHintsRegistrar
而不是使用特定于测试的 API。
如果你实现了自定义的 ContextLoader
,为了提供 AOT 构建时处理和 AOT 运行时执行支持,它必须实现 AotContextLoader
(opens new window)。不过要注意,Spring 框架和 Spring Boot 提供的所有上下文加载器实现都已经实现了 AotContextLoader
。
如果你实现了自定义的 TestExecutionListener
,为了参与 AOT 处理,它必须实现 AotTestExecutionListener
(opens new window)。可以参考 spring-test
模块中的 SqlScriptsTestExecutionListener
作为示例。
# 五、WebTestClient
WebTestClient
是一个专为测试服务器应用程序而设计的 HTTP 客户端。它包装了 Spring 的 WebClient,并使用其来执行请求,但会提供一个测试接口来验证响应。WebTestClient
可用于执行端到端的 HTTP 测试,也可以通过模拟服务器的请求和响应对象来测试 Spring MVC 和 Spring WebFlux 应用程序,而无需运行实际的服务器。
# 1、配置
要配置 WebTestClient
,需要选择要绑定的服务器设置。这可以是几种模拟服务器设置选项之一,也可以是连接到一个正在运行的实际服务器。
# 1.1、绑定到控制器
这种设置允许你通过模拟请求和响应对象来测试特定的控制器,而无需运行服务器。
对于 WebFlux 应用程序,可使用以下代码,该代码加载与 WebFlux Java 配置 等效的基础设施,注册给定的控制器,并创建一个 WebHandler 链 来处理请求:
WebTestClient client =
WebTestClient.bindToController(new TestController()).build();
对于 Spring MVC,可使用以下代码,该代码委托给 StandaloneMockMvcBuilder (opens new window) 来加载与 WebMvc Java 配置 等效的基础设施,注册给定的控制器,并创建一个 MockMvc 实例来处理请求:
WebTestClient client =
MockMvcWebTestClient.bindToController(new TestController()).build();
# 1.2、绑定到 ApplicationContext
这种设置允许你加载包含 Spring MVC 或 Spring WebFlux 基础设施以及控制器声明的 Spring 配置,并通过模拟请求和响应对象使用它来处理请求,而无需运行服务器。
对于 WebFlux,可使用以下代码,其中 Spring ApplicationContext
被传递给 WebHttpHandlerBuilder (opens new window) 以创建处理请求的 WebHandler 链:
@SpringJUnitConfig(WebConfig.class) // (1) 指定要加载的配置
class MyTests {
WebTestClient client;
@BeforeEach
void setUp(ApplicationContext context) { // (2) 注入配置
client = WebTestClient.bindToApplicationContext(context).build(); // (3) 创建 WebTestClient
}
}
对于 Spring MVC,可使用以下代码,其中 Spring ApplicationContext
被传递给 MockMvcBuilders.webAppContextSetup (opens new window) 以创建一个 MockMvc 实例来处理请求:
@ExtendWith(SpringExtension.class)
@WebAppConfiguration("classpath:META-INF/web-resources") // (1) 指定要加载的配置
@ContextHierarchy({
@ContextConfiguration(classes = RootConfig.class),
@ContextConfiguration(classes = WebConfig.class)
})
class MyTests {
@Autowired
WebApplicationContext wac; // (2) 注入配置
WebTestClient client;
@BeforeEach
void setUp() {
client = MockMvcWebTestClient.bindToApplicationContext(this.wac).build(); // (3) 创建 WebTestClient
}
}
# 1.3、绑定到路由函数
这种设置允许你通过模拟请求和响应对象来测试函数式端点,而无需运行服务器。
对于 WebFlux,可使用以下代码,该代码委托给 RouterFunctions.toWebHandler
来创建一个服务器设置以处理请求:
RouterFunction<?> route = ...
client = WebTestClient.bindToRouterFunction(route).build();
对于 Spring MVC,目前没有选项来测试WebMvc 函数式端点。
# 1.4、绑定到服务器
这种设置会连接到一个正在运行的服务器,以执行完整的端到端 HTTP 测试:
client = WebTestClient.bindToServer().baseUrl("http://localhost:8080").build();
# 1.5、客户端配置
除了前面描述的服务器设置选项外,你还可以配置客户端选项,包括基础 URL、默认标头、客户端过滤器等。在调用 bindToServer()
之后,这些选项就可以使用。对于所有其他配置选项,你需要使用 configureClient()
从服务器配置转换到客户端配置,如下所示:
client = WebTestClient.bindToController(new TestController())
.configureClient()
.baseUrl("/test")
.build();
# 2、编写测试
WebTestClient
提供的 API 与 WebClient 相同,直到使用 exchange()
执行请求。有关如何使用任何内容(包括表单数据、多部分数据等)准备请求的示例,请参阅 WebClient 文档。
在调用 exchange()
之后,WebTestClient
会与 WebClient
产生分歧,转而进入一个验证响应的工作流程。
要断言响应状态和标头,可使用以下代码:
client.get().uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON);
如果你希望即使其中一个期望断言失败,也能执行所有的期望断言,你可以使用 expectAll(..)
而不是多个链式的 expect*(..)
调用。这个功能类似于 AssertJ 中的“软断言”支持和 JUnit Jupiter 中的 assertAll()
支持。
client.get().uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectAll(
spec -> spec.expectStatus().isOk(),
spec -> spec.expectHeader().contentType(MediaType.APPLICATION_JSON)
);
然后你可以选择通过以下方式之一对响应主体进行解码:
expectBody(Class<T>)
:解码为单个对象。expectBodyList(Class<T>)
:解码并将对象收集到List<T>
中。expectBody()
:对于 JSON 内容 或空主体,解码为byte[]
。
并对生成的高层级对象进行断言:
client.get().uri("/persons")
.exchange()
.expectStatus().isOk()
.expectBodyList(Person.class).hasSize(3).contains(person);
如果内置的断言不够用,你可以使用对象并执行其他任何断言:
import org.springframework.test.web.reactive.server.expectBody
client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody(Person.class)
.consumeWith(result -> {
// 自定义断言(例如,使用 AssertJ)...
});
或者你可以退出工作流程并获取一个 EntityExchangeResult
:
EntityExchangeResult<Person> result = client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody(Person.class)
.returnResult();
提示:当你需要解码为带有泛型的目标类型时,请寻找接受 ParameterizedTypeReference (opens new window) 而不是 Class<T>
的重载方法。
# 2.1、无内容
如果预期响应没有内容,你可以这样进行断言:
client.post().uri("/persons")
.body(personMono, Person.class)
.exchange()
.expectStatus().isCreated()
.expectBody().isEmpty();
如果你想忽略响应内容,以下代码会释放内容而不进行任何断言:
client.get().uri("/persons/123")
.exchange()
.expectStatus().isNotFound()
.expectBody(Void.class);
# 2.2、内容
你可以使用 expectBody()
而不指定目标类型,以对原始内容进行断言,而不是通过高层级的对象进行断言。
要使用 JSONAssert (opens new window) 验证完整的 JSON 内容:
client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody()
.json("{\"name\":\"Jane\"}")
要使用 JSONPath (opens new window) 验证 JSON 内容:
client.get().uri("/persons")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$[0].name").isEqualTo("Jane")
.jsonPath("$[1].name").isEqualTo("Jason");
# 2.3、流式响应
要测试可能的无限流,如 "text/event-stream"
或 "application/x-ndjson"
,首先验证响应状态和标头,然后获取一个 FluxExchangeResult
:
FluxExchangeResult<MyEvent> result = client.get().uri("/events")
.accept(TEXT_EVENT_STREAM)
.exchange()
.expectStatus().isOk()
.returnResult(MyEvent.class);
现在你可以使用 reactor-test
中的 StepVerifier
来消费响应流:
Flux<Event> eventFlux = result.getResponseBody();
StepVerifier.create(eventFlux)
.expectNext(person)
.expectNextCount(4)
.consumeNextWith(p -> ...)
.thenCancel()
.verify();
# 2.4、断言
WebTestClient
是一个 HTTP 客户端,因此它只能验证客户端响应中的内容,包括状态、标头和主体。
当使用 MockMvc 服务器设置测试 Spring MVC 应用程序时,你可以额外选择对服务器响应执行进一步的断言。为此,首先在断言主体后获取一个 ExchangeResult
:
// 对于有主体的响应
EntityExchangeResult<Person> result = client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody(Person.class)
.returnResult();
// 对于没有主体的响应
EntityExchangeResult<Void> result = client.get().uri("/path")
.exchange()
.expectBody().isEmpty();
然后切换到 MockMvc 服务器响应断言:
MockMvcWebTestClient.resultActionsFor(result)
.andExpect(model().attribute("integer", 3))
.andExpect(model().attribute("string", "a string value"));
# 六、MockMvc
MockMvc 为测试 Spring MVC 应用程序提供支持。它能完整处理 Spring MVC 请求,但使用的是模拟的请求和响应对象,而不是运行中的服务器。
MockMvc 可以单独使用,借助 Hamcrest 来执行请求并验证响应,也可以通过 MockMvcTester
来使用,MockMvcTester
提供了基于 AssertJ 的流式 API。它还能通过 WebTestClient 使用,此时 MockMvc 作为处理请求的服务器。使用 WebTestClient
的优势在于,它让你可以使用更高级别的对象而非原始数据进行操作,并且能够切换到针对实时服务器进行完整的端到端 HTTP 测试,同时使用相同的测试 API。
# 1、章节总结
- 概述
- 设置选项
- Hamcrest 集成
- AssertJ 集成
- HtmlUnit 集成
- MockMvc 与端到端测试的对比
- 更多示例
# 2、概述
你可以通过实例化一个控制器,为其注入依赖并调用其方法来编写普通的 Spring MVC 单元测试。然而,这样的测试并不能验证请求映射、数据绑定、消息转换、类型转换或验证,也不会涉及任何支持性的 @InitBinder
、@ModelAttribute
或 @ExceptionHandler
方法。
MockMvc
旨在为 Spring MVC 控制器提供更全面的测试支持,且无需运行服务器。它通过调用 DispatcherServlet
来实现这一点,并传递来自 spring-test
模块的 Servlet API 的“模拟”实现,从而在不运行服务器的情况下模仿完整的 Spring MVC 请求处理流程。
MockMvc 是一个服务端测试框架,它允许你使用轻量级且有针对性的测试来验证 Spring MVC 应用程序的大部分功能。你可以单独使用它来执行请求,并使用 Hamcrest 验证响应,也可以通过 MockMvcTester
(使用 AssertJ 提供流式 API)来使用它。你还可以通过 WebTestClient API 使用它,并将 MockMvc 作为处理请求的服务器。
# 3、设置选项
MockMvc 有两种设置方式。
WebApplicationContext
:指向包含 Spring MVC 和控制器基础组件的 Spring 配置。- 独立模式:直接指向你想要测试的控制器,并以编程方式配置 Spring MVC 基础组件。
你应该选择哪种设置选项呢?
基于 WebApplicationContext
的测试会加载实际的 Spring MVC 配置,从而进行更全面的集成测试。由于 TestContext 框架会缓存已加载的 Spring 配置,因此即使你在测试套件中使用相同配置引入更多测试,也有助于保持测试的快速运行。此外,你可以使用 @MockitoBean
或 @TestBean
覆盖控制器使用的服务,从而专注于测试 Web 层。
另一方面,独立测试更像是单元测试。它一次测试一个控制器。你可以手动将模拟依赖项注入控制器,而且无需加载 Spring 配置。这种测试在风格上更具针对性,能更轻松地查看正在测试的是哪个控制器,以及是否需要特定的 Spring MVC 配置才能正常工作,等等。独立设置也是编写临时测试以验证特定行为或调试问题的便捷方式。
与大多数“集成测试与单元测试”的争论一样,没有绝对正确或错误的答案。但是,使用独立测试意味着需要额外的集成测试来验证 Spring MVC 配置。或者,你可以使用 WebApplicationContext
编写所有测试,这样它们就始终针对实际的 Spring MVC 配置进行测试。
# 4、Hamcrest集成
普通的 MockMvc
提供了一种以构建器风格构建请求的 API,可以通过静态导入来初始化。Hamcrest 用于定义预期,并且它为常见需求提供了许多现成的选项。
# 4.1、章节概要
- 静态导入
- 配置 MockMvc
- 设置功能
- 执行请求
- 定义预期
- 异步请求
- 流式响应
- 过滤器注册
# 4.2、静态导入
当直接使用 MockMvc 来执行请求时,你需要对以下内容进行静态导入:
MockMvcBuilders.*
MockMvcRequestBuilders.*
MockMvcResultMatchers.*
MockMvcResultHandlers.*
一个简单的记忆方法是搜索 MockMvc*
。如果你使用的是 Eclipse,一定要在 Eclipse 偏好设置中将上述内容添加为“收藏的静态成员”。
当通过 WebTestClient 使用 MockMvc 时,你不需要进行静态导入。WebTestClient
提供了一个无需静态导入的流式 API。
# 4.3、配置 MockMvc
MockMvc 可以通过以下两种方式之一进行设置。一种是直接指定要测试的控制器,并以编程方式配置 Spring MVC 基础设施。第二种是指向包含 Spring MVC 和控制器基础设施的 Spring 配置。
提示:如需对比这两种模式,请查看设置选项。
若要设置 MockMvc 来测试特定的控制器,可使用以下代码:
class MyWebTests {
MockMvc mockMvc;
@BeforeEach
void setup() {
this.mockMvc = MockMvcBuilders.standaloneSetup(new AccountController()).build();
}
// ...
}
或者在通过 WebTestClient 进行测试时也可以使用此设置,它会委托给上述相同的构建器。
若要通过 Spring 配置来设置 MockMvc,可使用以下代码:
@SpringJUnitWebConfig(locations = "my-servlet-context.xml")
class MyWebTests {
MockMvc mockMvc;
@BeforeEach
void setup(WebApplicationContext wac) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}
// ...
}
或者在通过 WebTestClient 进行测试时也可以使用此设置,它会委托给上述相同的构建器。
# 4.4、设置特性
无论你使用哪种 MockMvc 构建器,所有 MockMvcBuilder
实现都提供了一些常见且非常有用的特性。例如,你可以为所有请求声明一个 Accept
请求头,并期望所有响应的状态码为 200,同时包含 Content-Type
响应头,如下所示:
// 静态导入 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();
此外,第三方框架(以及应用程序)可以预先封装设置指令,例如 MockMvcConfigurer
中的指令。Spring 框架有一个这样的内置实现,它有助于在多个请求之间保存和重用 HTTP 会话。你可以按如下方式使用它:
// 静态导入 SharedHttpSessionConfigurer.sharedHttpSession
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new TestController())
.apply(sharedHttpSession())
.build();
// 使用 mockMvc 执行请求...
有关所有 MockMvc 构建器特性的列表,请参阅 ConfigurableMockMvcBuilder (opens new window) 的 Java 文档,或者使用集成开发环境 (IDE) 来探索可用选项。
# 4.5、执行请求
本节将展示如何单独使用 MockMvc 来执行请求并验证响应。如果要通过 WebTestClient
使用 MockMvc,请参考编写测试中对应的章节。
若要执行使用任何 HTTP 方法的请求,可参考以下示例:
// 静态导入 MockMvcRequestBuilders.*
mockMvc.perform(post("/hotels/{id}", 42).accept(MediaType.APPLICATION_JSON));
你还可以执行文件上传请求,这些请求在内部使用 MockMultipartHttpServletRequest
,因此不会实际解析多部分请求。相反,你必须按照如下示例进行设置:
mockMvc.perform(multipart("/doc").file("a1", "ABC".getBytes("UTF-8")));
你可以使用 URI 模板样式指定查询参数,示例如下:
mockMvc.perform(get("/hotels?thing={thing}", "somewhere"));
你还可以添加代表查询参数或表单参数的 Servlet 请求参数,示例如下:
mockMvc.perform(get("/hotels").param("thing", "somewhere"));
如果应用程序代码依赖于 Servlet 请求参数,且不显式检查查询字符串(大多数情况下都是如此),那么使用哪种方式都无关紧要。不过请记住,通过 URI 模板提供的查询参数会被解码,而通过 param(…)
方法提供的请求参数则应已被解码。
在大多数情况下,最好在请求 URI 中不包含上下文路径和 Servlet 路径。如果必须使用完整的请求 URI 进行测试,请务必相应地设置 contextPath
和 servletPath
,以确保请求映射正常工作,示例如下:
mockMvc.perform(get("/app/main/hotels/{id}").contextPath("/app").servletPath("/main"))
在上述示例中,为每个执行的请求都设置 contextPath
和 servletPath
会很繁琐。相反,你可以设置默认请求属性,示例如下:
class MyWebTests {
MockMvc mockMvc;
@BeforeEach
void setup() {
mockMvc = standaloneSetup(new AccountController())
.defaultRequest(get("/")
.contextPath("/app").servletPath("/main")
.accept(MediaType.APPLICATION_JSON)).build();
}
}
上述属性会影响通过 MockMvc
实例执行的每个请求。如果在某个给定请求中也指定了相同的属性,那么该属性将覆盖默认值。这就是为什么默认请求中的 HTTP 方法和 URI 无关紧要,因为每个请求都必须指定它们。
# 4.6、定义预期结果
你可以在执行请求后,通过追加一个或多个 andExpect(..)
调用来定义预期结果,如下例所示。一旦有一个预期结果未满足,就不会再对其他预期结果进行断言。
// 静态导入 MockMvcRequestBuilders.* 和 MockMvcResultMatchers.*
mockMvc.perform(get("/accounts/1")).andExpect(status().isOk());
你可以在执行请求后追加 andExpectAll(..)
来定义多个预期结果,如下例所示。与 andExpect(..)
不同,andExpectAll(..)
可以保证对所有提供的预期结果进行断言,并跟踪和报告所有未满足的预期结果。
// 静态导入 MockMvcRequestBuilders.* 和 MockMvcResultMatchers.*
mockMvc.perform(get("/accounts/1")).andExpectAll(
status().isOk(),
content().contentType("application/json;charset=UTF-8")
);
MockMvcResultMatchers.*
提供了许多预期结果,其中一些还进一步嵌套了更详细的预期结果。
预期结果大致分为两类。第一类断言用于验证响应的属性(例如,响应状态、头部信息和内容)。这些是最重要的断言结果。
第二类断言的范围超出了响应本身。这些断言可以让你检查 Spring MVC 的特定方面,例如处理请求的控制器方法、是否抛出并处理了异常、模型的内容是什么、选择了哪个视图、添加了哪些闪存属性等等。它们还可以让你检查 Servlet 的特定方面,例如请求和会话属性。
下面的测试用于断言绑定或验证失败:
mockMvc.perform(post("/persons"))
.andExpect(status().isOk())
.andExpect(model().attributeHasErrors("person"));
在编写测试时,很多时候打印执行请求的结果是很有用的。你可以按如下方式操作,其中 print()
是从 MockMvcResultHandlers
进行的静态导入:
mockMvc.perform(post("/persons"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(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
日志类别下。
在某些情况下,你可能希望直接访问结果,并验证一些用其他方法无法验证的内容。这可以通过在所有其他预期结果之后追加 .andReturn()
来实现,如下例所示:
MvcResult mvcResult = mockMvc.perform(post("/persons")).andExpect(status().isOk()).andReturn();
// ...
如果所有测试都重复相同的预期结果,你可以在构建 MockMvc
实例时一次性设置通用预期结果,如下例所示:
standaloneSetup(new SimpleController())
.alwaysExpect(status().isOk())
.alwaysExpect(content().contentType("application/json;charset=UTF-8"))
.build()
请注意,通用预期结果会始终应用,并且在不创建单独的 MockMvc
实例的情况下无法覆盖。
当 JSON 响应内容包含使用 Spring HATEOAS (opens new window) 创建的超媒体链接时,你可以使用 JsonPath 表达式来验证生成的链接,如下例所示:
mockMvc.perform(get("/people").accept(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.links[?(@.rel == 'self')].href").value("http://localhost:8080/people"));
当 XML 响应内容包含使用 Spring HATEOAS (opens new window) 创建的超媒体链接时,你可以使用 XPath 表达式来验证生成的链接:
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"));
# 4.7、异步请求
本节展示了如何单独使用 MockMvc 来测试异步请求处理。如果通过WebTestClient使用 MockMvc,则无需进行特殊操作来让异步请求正常工作,因为WebTestClient
会自动完成本节中描述的操作。
Servlet 异步请求在 Spring MVC 中得到支持,其工作方式是退出 Servlet 容器线程,允许应用程序异步计算响应,之后进行异步调度,在 Servlet 容器线程上完成处理。
在 Spring MVC 测试中,可以通过先断言生成的异步值,然后手动执行异步调度,最后验证响应来测试异步请求。以下是一个针对返回DeferredResult
、Callable
或响应式类型(如 Reactor Mono
)的控制器方法的测试示例:
// 静态导入 MockMvcRequestBuilders.* 和 MockMvcResultMatchers.*
@Test
void test() throws Exception {
MvcResult mvcResult = this.mockMvc.perform(get("/path"))
.andExpect(status().isOk()) // 1. 检查响应状态仍未改变
.andExpect(request().asyncStarted()) // 2. 异步处理必须已启动
.andExpect(request().asyncResult("body")) // 3. 等待并断言异步结果
.andReturn();
this.mockMvc.perform(asyncDispatch(mvcResult)) // 4. 手动执行异步调度(因为没有运行的容器)
.andExpect(status().isOk()) // 5. 验证最终响应
.andExpect(content().string("body"));
}
- 检查响应状态仍未改变
- 异步处理必须已启动
- 等待并断言异步结果
- 手动执行异步调度(因为没有运行的容器)
- 验证最终响应
# 4.8、流式响应
你可以使用 WebTestClient
来测试流式响应,例如服务器发送事件(Server - Sent Events)。不过,MockMvcWebTestClient
不支持无限流,因为无法从客户端取消服务器端的流。若要测试无限流,你需要绑定到一个正在运行的服务器,或者在使用 Spring Boot 时,使用运行中的服务器进行测试 (opens new window)。
MockMvcWebTestClient
确实支持异步响应,甚至流式响应。但它的局限性在于无法让服务器停止,所以服务器必须自行完成响应的写入。
# 4.9、过滤器注册
在设置 MockMvc
实例时,您可以注册一个或多个 Servlet Filter
实例,如下例所示:
mockMvc = standaloneSetup(new PersonController()).addFilters(new CharacterEncodingFilter()).build();
注册的过滤器会通过 spring-test
中的 MockFilterChain
进行调用,最后一个过滤器会委托给 DispatcherServlet
。
# 5、集成
AssertJ 集成基于普通的 MockMvc
构建,有以下几点不同:
- 无需使用静态导入,因为请求和断言都可以使用流式 API 来编写。
- 未处理的异常会得到统一处理,这样你的测试代码就无需抛出(或捕获)
Exception
。 - 默认情况下,无论处理过程是否为异步,要进行断言的结果都是完整的。换句话说,无需对异步请求进行特殊处理。
MockMvcTester
是 AssertJ 支持的入口点。它允许构造请求,并返回一个与 AssertJ 兼容的结果,这样就可以将其包装在标准的 assertThat()
方法中。
# 5.1、章节总结
- 配置 MockMvcTester
- 执行请求
- 定义预期
- MockMvc 集成
# 5.2、配置 MockMvcTester
MockMvcTester
可以通过以下两种方式之一进行设置。一种是直接指向你想要测试的控制器,并以编程方式配置 Spring MVC 基础设施。另一种是指向包含 Spring MVC 和控制器基础设施的 Spring 配置。
提示:若要比较这两种模式,请查看设置选项。
若要设置 MockMvcTester
来测试特定的控制器,请使用以下代码:
public class AccountControllerStandaloneTests {
private final MockMvcTester mockMvc = MockMvcTester.of(new AccountController());
// ...
}
若要通过 Spring 配置来设置 MockMvcTester
,请使用以下代码:
@SpringJUnitWebConfig(ApplicationWebConfiguration.class)
class AccountControllerIntegrationTests {
private final MockMvcTester mockMvc;
AccountControllerIntegrationTests(@Autowired WebApplicationContext wac) {
this.mockMvc = MockMvcTester.from(wac);
}
// ...
}
只要注册了相关的 HttpMessageConverter
,MockMvcTester
就可以将 JSON 响应体或 JSONPath 表达式的结果转换为你的某个领域对象。
如果你使用 Jackson 将内容序列化为 JSON,以下示例展示了如何注册转换器:
@SpringJUnitWebConfig(ApplicationWebConfiguration.class)
class AccountControllerIntegrationTests {
private final MockMvcTester mockMvc;
AccountControllerIntegrationTests(@Autowired WebApplicationContext wac) {
this.mockMvc = MockMvcTester.from(wac).withHttpMessageConverters(
List.of(wac.getBean(AbstractJackson2HttpMessageConverter.class)));
}
// ...
}
注意:上述代码假设转换器已作为 Bean 进行了注册。
最后,如果你手头有一个 MockMvc
实例,可以通过使用 create
工厂方法提供 MockMvc
实例来创建一个 MockMvcTester
。
# 5.3、执行请求
本节将展示如何使用 MockMvcTester
来执行请求,以及它如何与 AssertJ 集成来验证响应。
MockMvcTester
提供了一个流畅的 API 来构建请求,该 API 复用了与 Hamcrest 支持相同的 MockHttpServletRequestBuilder
,只不过无需导入静态方法。返回的构建器支持 AssertJ,因此将其包装在常规的 assertThat()
工厂方法中会触发请求交换,并提供对 MvcTestResult
专用断言对象的访问权限。
下面是一个简单的示例,它在 /hotels/42
上执行一个 POST
请求,并配置请求以指定一个 Accept
标头:
assertThat(mockMvc.post().uri("/hotels/{id}", 42).accept(MediaType.APPLICATION_JSON))
// ...
AssertJ 通常包含多个 assertThat()
语句,用于验证请求交换的不同部分。你可以使用 .exchange()
方法返回一个 MvcTestResult
,而不是像上面那样只使用一个语句,这样就可以在多个 assertThat
语句中使用该结果:
MvcTestResult result = mockMvc.post().uri("/hotels/{id}", 42)
.accept(MediaType.APPLICATION_JSON).exchange();
assertThat(result). // ...
你可以按 URI 模板风格指定查询参数,如下示例所示:
assertThat(mockMvc.get().uri("/hotels?thing={thing}", "somewhere"))
// ...
你还可以添加表示查询参数或表单参数的 Servlet 请求参数,如下示例所示:
assertThat(mockMvc.get().uri("/hotels").param("thing", "somewhere"))
// ...
如果应用程序代码依赖于 Servlet 请求参数,而不明确检查查询字符串(大多数情况下都是如此),那么使用哪种方式并不重要。不过请注意,通过 URI 模板提供的查询参数会被解码,而通过 param(…)
方法提供的请求参数应该已经是解码后的。
# a、异步请求
如果请求的处理是异步进行的,exchange()
方法会等待请求完成,以便要断言的结果是有效不变的。默认超时时间是 10 秒,但可以根据每个请求进行控制,如下示例所示:
assertThat(mockMvc.get().uri("/compute").exchange(Duration.ofSeconds(5)))
// ...
如果你更喜欢获取原始结果并自己管理异步请求的生命周期,可以使用 asyncExchange
而不是 exchange
。
# b、多部分请求
你可以执行文件上传请求,这些请求在内部使用 MockMultipartHttpServletRequest
,因此无需实际解析多部分请求。相反,你需要像下面的示例一样进行设置:
assertThat(mockMvc.post().uri("/upload").multipart()
.file("file1.txt", "Hello".getBytes(StandardCharsets.UTF_8))
.file("file2.txt", "World".getBytes(StandardCharsets.UTF_8)))
// ...
# c、使用 Servlet 和上下文路径
在大多数情况下,最好将上下文路径和 Servlet 路径从请求 URI 中排除。如果你必须使用完整的请求 URI 进行测试,请务必相应地设置 contextPath
和 servletPath
,以便请求映射能够正常工作,如下示例所示:
assertThat(mockMvc.get().uri("/app/main/hotels/{id}", 42)
.contextPath("/app").servletPath("/main"))
// ...
在上述示例中,为每个执行的请求都设置 contextPath
和 servletPath
会很麻烦。相反,你可以设置默认请求属性,如下示例所示:
MockMvcTester mockMvc = MockMvcTester.of(List.of(new HotelController()),
builder -> builder.defaultRequest(get("/")
.contextPath("/app").servletPath("/main")
.accept(MediaType.APPLICATION_JSON)).build());
上述属性会影响通过 mockMvc
实例执行的每个请求。如果在某个请求中也指定了相同的属性,则该属性会覆盖默认值。这就是为什么默认请求中的 HTTP 方法和 URI 并不重要,因为每个请求都必须指定这些信息。
# 5.4、定义预期
断言的工作方式与任何 AssertJ 断言相同。该支持为 MvcTestResult
的各个部分提供了专门的断言对象,如以下示例所示:
assertThat(mockMvc.get().uri("/hotels/{id}", 42))
.hasStatusOk()
.hasContentTypeCompatibleWith(MediaType.APPLICATION_JSON)
.bodyJson().isLenientlyEqualTo("sample/hotel-42.json");
如果请求失败,该请求不会抛出异常。相反,你可以断言该请求的结果是失败的:
assertThat(mockMvc.get().uri("/hotels/{id}", -1))
.hasFailed()
.hasStatus(HttpStatus.BAD_REQUEST)
.failure().hasMessageContaining("Identifier should be positive");
请求也可能意外失败,也就是说,处理程序抛出的异常没有被处理,而是原样抛出。你仍然可以使用 .hasFailed()
和 .failure()
,但任何尝试访问结果部分的操作都会抛出异常,因为请求尚未完成。
# a、支持
MvcTestResult
的 AssertJ 支持通过 bodyJson()
提供 JSON 支持。
如果 JSONPath (opens new window) 可用,你可以在 JSON 文档上应用表达式。返回的值提供了方便的方法,为各种支持的 JSON 数据类型返回专用的断言对象:
assertThat(mockMvc.get().uri("/family")).bodyJson()
.extractingPath("$.members[0]")
.asMap()
.contains(entry("name", "Homer"));
只要消息转换器配置正确,你还可以将原始内容转换为任何数据类型:
assertThat(mockMvc.get().uri("/family")).bodyJson()
.extractingPath("$.members[0]")
.convertTo(Member.class)
.satisfies(member -> assertThat(member.name).isEqualTo("Homer"));
将内容转换为目标 Class
会提供一个通用的断言对象。对于更复杂的类型,如果可能的话,你可能希望使用 AssertFactory
来返回一个专用的断言类型:
assertThat(mockMvc.get().uri("/family")).bodyJson()
.extractingPath("$.members")
.convertTo(InstanceOfAssertFactories.list(Member.class))
.hasSize(5)
.element(0).satisfies(member -> assertThat(member.name).isEqualTo("Homer"));
JSONAssert (opens new window) 也受到支持。响应体可以与 Resource
或内容进行匹配。如果内容以 .json
结尾,我们会在类路径中查找匹配该名称的文件:
assertThat(mockMvc.get().uri("/family")).bodyJson()
.isStrictlyEqualTo("sample/simpsons.json");
如果你更喜欢使用其他库,可以提供一个 JsonComparator (opens new window) 的实现。
# 5.5、MockMvc集成
如果你想使用 AssertJ 支持,但已经使用了原生的 MockMvc
API,MockMvcTester
提供了几种与之集成的方法。
如果你有自己的 RequestBuilder
实现,你可以使用 perform
触发请求处理。下面的示例展示了如何使用原生 API 来构建查询:
// 静态导入 MockMvcRequestBuilders.get
assertThat(mockMvc.perform(get("/hotels/{id}", 42)))
.hasStatusOk();
同样地,如果你创建了自定义匹配器并与 MockMvc
的 .andExpect
功能一起使用,你可以通过 .matches
来使用它们。在下面的示例中,我们重写了前面的示例,使用 MockMvc
提供的 ResultMatcher
实现来断言状态:
// 静态导入 MockMvcResultMatchers.status
assertThat(mockMvc.get().uri("/hotels/{id}", 42))
.matches(status().isOk());
MockMvc
还定义了一个 ResultHandler
契约,允许你对 MvcResult
执行任意操作。如果你已经实现了这个契约,你可以使用 .apply
来调用它。
# 6、集成
Spring 提供了 MockMvc 和 HtmlUnit (opens new window) 之间的集成。在使用基于 HTML 的视图时,这简化了端到端测试的执行。此集成让你能够:
- 无需部署到 Servlet 容器,即可使用 HtmlUnit (opens new window)、WebDriver (opens new window) 和 Geb (opens new window) 等工具轻松测试 HTML 页面。
- 测试页面内的 JavaScript。
- 可选择使用模拟服务进行测试,以加快测试速度。
- 在容器内的端到端测试和容器外的集成测试之间共享逻辑。
注意:MockMvc 适用于不依赖 Servlet 容器的模板技术(例如 Thymeleaf、FreeMarker 等),但不适用于 JSP,因为 JSP 依赖于 Servlet 容器。
# 6.1、章节总结
- 为何需要 HtmlUnit 集成?
- MockMvc 和 HtmlUnit
- MockMvc 和 WebDriver
- MockMvc 和 Geb
# 6.2、为何要集成 HtmlUnit?
脑海中首先浮现的问题就是“我为什么需要这个?”要找到答案,最好的办法是探究一个非常简单的示例应用程序。假设你有一个 Spring MVC 网络应用程序,它支持对 Message
对象进行 CRUD 操作,该应用程序还支持对所有消息进行分页。那么你要如何对其进行测试呢?
借助 Spring MVC 测试,我们可以轻松测试是否能够创建一个 Message
,如下所示:
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"));
要是我们想测试用于创建消息的表单视图,该怎么办呢?例如,假设我们的表单如下所示:
<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>
我们怎样才能确保表单能生成创建新消息的正确请求呢?一种简单的尝试可能如下:
mockMvc.perform(get("/messages/form"))
.andExpect(xpath("//input[@name='summary']").exists())
.andExpect(xpath("//textarea[@name='text']").exists());
这个测试存在一些明显的缺陷。如果我们更新控制器,用参数 message
代替 text
,那么即便 HTML 表单与控制器不同步,我们的表单测试仍然能够通过。为了解决这个问题,我们可以将两个测试结合起来,如下所示:
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"));
这会降低测试误通过的风险,但仍然存在一些问题:
- 如果页面上有多个表单怎么办?诚然,我们可以更新 XPath 表达式,但考虑的因素越多,表达式就会越复杂,例如:这些字段的类型是否正确?这些字段是否可用?等等。
- 另一个问题是,我们做的工作比预期的多了一倍。我们必须先验证视图,然后再用刚刚验证过的相同参数提交视图。理想情况下,这些操作应该可以一次性完成。
- 最后,有些情况我们仍然无法处理。例如,如果表单有 JavaScript 验证,而我们也想对其进行测试,该怎么办?
总的来说,测试网页并非单一的交互过程,而是用户与网页以及网页与其他资源交互的综合过程。例如,表单视图的结果会作为用户创建消息的输入。此外,我们的表单视图可能会使用一些额外的资源,这些资源会影响页面的行为,比如 JavaScript 验证。
# 6.3、集成测试能否解决问题?
为了解决上述问题,我们可以进行端到端的集成测试,但这也有一些弊端。以测试消息分页视图为例,我们可能需要进行以下测试:
- 当消息为空时,页面是否会向用户显示“没有可用结果”的提示?
- 页面是否能正确显示单条消息?
- 页面是否能正确支持分页?
要设置这些测试,我们需要确保数据库中包含合适的消息。这会带来一系列额外的挑战:
- 确保数据库中存在合适的消息可能会很繁琐(要考虑外键约束)。
- 测试可能会变慢,因为每个测试都需要确保数据库处于正确的状态。
- 由于数据库需要处于特定的状态,我们无法并行运行测试。
- 对自动生成的 ID、时间戳等内容进行断言可能会很困难。
这些挑战并不意味着我们要完全放弃端到端的集成测试。相反,我们可以重构详细的测试,使用运行速度更快、更可靠且无副作用的模拟服务,从而减少端到端集成测试的数量。然后,我们可以实现少量真正的端到端集成测试,验证简单的工作流程,以确保所有部分都能正确协同工作。
# 6.4、引入 HtmlUnit 集成
那么,我们如何在测试页面交互的同时,仍能保证测试套件具有良好的性能呢?答案是:“将 MockMvc 与 HtmlUnit 集成”。
# 6.5、集成选项
当你想将 MockMvc 与 HtmlUnit 集成时,有以下几种选项:
- MockMvc 和 HtmlUnit:如果你想使用原始的 HtmlUnit 库,可以选择此选项。
- MockMvc 和 WebDriver:选择此选项可简化开发过程,并在集成测试和端到端测试之间复用代码。
- MockMvc 和 Geb:如果你想用 Groovy 进行测试、简化开发过程,并在集成测试和端到端测试之间复用代码,可以选择此选项。
# 6.6、MockMvc和HtmlUnit
本节介绍如何集成MockMvc和HtmlUnit。如果你想使用原生的HtmlUnit库,可以选择这个方案。
# a、MockMvc和HtmlUnit的设置
首先,确保你已添加对 org.htmlunit:htmlunit
的测试依赖。
我们可以使用 MockMvcWebClientBuilder
轻松创建一个与MockMvc集成的HtmlUnit WebClient
,示例如下:
WebClient webClient;
@BeforeEach
void setup(WebApplicationContext context) {
webClient = MockMvcWebClientBuilder
.webAppContextSetup(context)
.build();
}
这样可以确保所有将 localhost
作为服务器的URL请求都被定向到我们的 MockMvc
实例,而无需建立真实的HTTP连接。其他URL则会像正常情况一样使用网络连接进行请求。这使我们可以轻松测试CDN的使用情况。
# b、MockMvc和HtmlUnit的使用
现在,我们可以像平常一样使用HtmlUnit,而无需将应用程序部署到Servlet容器中。例如,我们可以使用以下代码请求创建消息的视图:
HtmlPage createMsgFormPage = webClient.getPage("http://localhost/messages/form");
注意:默认的上下文路径是 ""
。或者,我们可以像 高级 MockMvcWebClientBuilder
中所描述的那样指定上下文路径。
一旦我们获取到 HtmlPage
引用,就可以填写表单并提交以创建消息,示例如下:
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();
最后,我们可以验证是否成功创建了新消息。以下断言使用了 AssertJ (opens new window) 库:
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!");
上述代码在多个方面改进了我们的 MockMvc测试。首先,我们不再需要显式地验证表单,然后创建一个类似表单的请求。相反,我们请求表单、填写并提交,从而显著减少了工作量。
另一个重要因素是,HtmlUnit使用Mozilla Rhino引擎 (opens new window) 来执行JavaScript代码。这意味着我们还可以测试页面中JavaScript的行为。
有关使用HtmlUnit的更多信息,请参阅 HtmlUnit文档 (opens new window)。
# c、高级 MockMvcWebClientBuilder
在前面的示例中,我们以最简单的方式使用了 MockMvcWebClientBuilder
,即基于Spring TestContext框架为我们加载的 WebApplicationContext
构建一个 WebClient
。以下示例重复了这种方法:
WebClient webClient;
@BeforeEach
void setup(WebApplicationContext context) {
webClient = MockMvcWebClientBuilder
.webAppContextSetup(context)
.build();
}
我们也可以指定额外的配置选项,示例如下:
WebClient webClient;
@BeforeEach
void setup() {
webClient = MockMvcWebClientBuilder
// 展示如何应用MockMvc配置器(Spring Security)
.webAppContextSetup(context, springSecurity())
// 仅作示例,默认为 ""
.contextPath("")
// 默认情况下MockMvc仅用于localhost;
// 以下配置将使MockMvc也处理example.com和example.org
.useMockMvcForHosts("example.com","example.org")
.build();
}
另外,我们也可以先单独配置 MockMvc
实例,再将其提供给 MockMvcWebClientBuilder
,以实现完全相同的设置,示例如下:
MockMvc mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
webClient = MockMvcWebClientBuilder
.mockMvcSetup(mockMvc)
// 仅作示例,默认为 ""
.contextPath("")
// 默认情况下MockMvc仅用于localhost;
// 以下配置将使MockMvc也处理example.com和example.org
.useMockMvcForHosts("example.com","example.org")
.build();
这种方式更详细,但通过使用 MockMvc
实例构建 WebClient
,我们可以充分发挥MockMvc的强大功能。
# 6.7、和 WebDriver
在前面几节中,我们已经了解了如何将 MockMvc 与原始的 HtmlUnit API 结合使用。在本节中,我们将使用 Selenium WebDriver (opens new window) 中的附加抽象功能,让操作变得更加简单。
# a、为什么要使用 WebDriver 和 MockMvc?
我们已经能够使用 HtmlUnit 和 MockMvc 了,那为什么还要使用 WebDriver 呢?Selenium WebDriver 提供了非常优雅的 API,让我们能够轻松地组织代码。为了更好地展示其工作原理,我们将在本节中探讨一个示例。
注意:尽管 WebDriver 是 Selenium (opens new window) 的一部分,但运行测试时并不需要 Selenium 服务器。
假设我们需要确保消息能够被正确创建。这些测试涉及查找 HTML 表单输入元素、填写表单并进行各种断言。
这种方法会产生大量单独的测试,因为我们还需要测试错误情况。例如,我们要确保在只填写部分表单时会出现错误提示。如果我们填写了整个表单,之后应该会显示新创建的消息。
如果其中一个字段名为 “summary”,那么我们的测试中可能会在多个地方重复出现类似下面的代码:
HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary");
summaryInput.setValueAttribute(summary);
那么,如果我们将 id
改成 smmry
会怎样呢?这就迫使我们更新所有测试来适应这个变化。这违反了 “Don't Repeat Yourself”(DRY)原则,所以理想情况下,我们应该将这段代码提取到一个独立的方法中,如下所示:
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);
}
这样做可以确保在我们修改用户界面时,不必更新所有测试。
我们甚至可以更进一步,将这个逻辑放在一个代表当前 HtmlPage
的 Object
中,如下例所示:
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());
}
}
以前,这种模式被称为页面对象模式 (opens new window)。虽然我们当然可以用 HtmlUnit 来实现这种模式,但 WebDriver 提供了一些工具,我们将在以下几节中进行探索,这些工具能让这种模式的实现变得更加容易。
# b、和 WebDriver 的设置
要将 Selenium WebDriver 与 MockMvc
一起使用,确保你的项目中包含对 org.seleniumhq.selenium:selenium-htmlunit3-driver
的测试依赖。
我们可以使用 MockMvcHtmlUnitDriverBuilder
轻松创建一个与 MockMvc 集成的 Selenium WebDriver,如下例所示:
WebDriver driver;
@BeforeEach
void setup(WebApplicationContext context) {
driver = MockMvcHtmlUnitDriverBuilder
.webAppContextSetup(context)
.build();
}
上述示例确保了任何以 localhost
作为服务器的 URL 都会被定向到我们的 MockMvc
实例,而无需实际的 HTTP 连接。任何其他 URL 则会像平常一样通过网络连接进行请求。这让我们可以轻松测试内容分发网络(CDN)的使用情况。
# c、和 WebDriver 的使用
现在,我们可以像平常一样使用 WebDriver,而无需将应用程序部署到 Servlet 容器中。例如,我们可以通过以下代码请求创建消息的视图:
CreateMessagePage page = CreateMessagePage.to(driver);
然后,我们可以填写表单并提交以创建消息,如下所示:
ViewMessagePage viewMessagePage =
page.createMessage(ViewMessagePage.class, expectedSummary, expectedText);
这通过利用页面对象模式改进了我们的 HtmlUnit 测试的设计。正如我们在为什么要使用 WebDriver 和 MockMvc? 中提到的,我们可以用 HtmlUnit 实现页面对象模式,但使用 WebDriver 会容易得多。以下是 CreateMessagePage
的实现示例:
public class CreateMessagePage extends AbstractPage { // (1)
// (2)
private WebElement summary;
private WebElement text;
@FindBy(css = "input[type=submit]") // (3)
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);
}
}
CreateMessagePage
继承自AbstractPage
。我们在此不详细介绍AbstractPage
,但简而言之,它包含了所有页面的通用功能。例如,如果我们的应用程序有导航栏、全局错误消息等功能,我们可以将这些逻辑放在一个共享的位置。- 我们为 HTML 页面中我们感兴趣的每个部分都设置了一个成员变量。这些变量的类型为
WebElement
。WebDriver 的 PageFactory (opens new window) 可以自动解析每个WebElement
,从而帮我们去掉CreateMessagePage
的 HtmlUnit 版本中的大量代码。PageFactory#initElements(WebDriver,Class<T>)
(opens new window) 方法会使用字段名,通过 HTML 页面中元素的id
或name
来自动解析每个WebElement
。 - 我们可以使用 @FindBy 注解 (opens new window) 来覆盖默认的查找行为。我们的示例展示了如何使用
@FindBy
注解通过 CSS 选择器 (input[type=submit]
) 来查找提交按钮。
最后,我们可以验证新消息是否已成功创建。以下断言使用了 AssertJ (opens new window) 断言库:
assertThat(viewMessagePage.getMessage()).isEqualTo(expectedMessage);
assertThat(viewMessagePage.getSuccess()).isEqualTo("Successfully created a new message");
我们可以看到,我们的 ViewMessagePage
允许我们与自定义的领域模型进行交互。例如,它提供了一个返回 Message
对象的方法:
public Message getMessage() throws ParseException {
Message message = new Message();
message.setId(getId());
message.setCreated(getCreated());
message.setSummary(getSummary());
message.setText(getText());
return message;
}
然后,我们就可以在断言中使用丰富的领域对象。
最后,不要忘记在测试完成时关闭 WebDriver
实例,如下所示:
@AfterEach
void destroy() {
if (driver != null) {
driver.close();
}
}
有关使用 WebDriver 的更多信息,请参阅 Selenium 的 WebDriver 文档 (opens new window)。
# d、高级 MockMvcHtmlUnitDriverBuilder
在目前的示例中,我们以最简单的方式使用了 MockMvcHtmlUnitDriverBuilder
,即基于 Spring 测试上下文框架为我们加载的 WebApplicationContext
来构建 WebDriver
。这种方法在此重复如下:
WebDriver driver;
@BeforeEach
void setup(WebApplicationContext context) {
driver = MockMvcHtmlUnitDriverBuilder
.webAppContextSetup(context)
.build();
}
我们还可以指定其他配置选项,如下所示:
WebDriver driver;
@BeforeEach
void setup() {
driver = MockMvcHtmlUnitDriverBuilder
// 演示应用 MockMvcConfigurer(Spring Security)
.webAppContextSetup(context, springSecurity())
// 仅用于演示 - 默认值为 ""
.contextPath("")
// 默认为仅对 localhost 使用 MockMvc;
// 以下设置也会对 example.com 和 example.org 使用 MockMvc
.useMockMvcForHosts("example.com","example.org")
.build();
}
作为一种替代方法,我们可以通过分别配置 MockMvc
实例并将其提供给 MockMvcHtmlUnitDriverBuilder
来完成完全相同的设置,如下所示:
MockMvc mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
driver = MockMvcHtmlUnitDriverBuilder
.mockMvcSetup(mockMvc)
// 仅用于演示 - 默认值为 ""
.contextPath("")
// 默认为仅对 localhost 使用 MockMvc;
// 以下设置也会对 example.com 和 example.org 使用 MockMvc
.useMockMvcForHosts("example.com","example.org")
.build();
这种方法更冗长一些,但通过使用 MockMvc
实例来构建 WebDriver
,我们可以充分利用 MockMvc 的功能。
提示:有关创建 MockMvc
实例的更多信息,请参阅配置 MockMvc。
# 6.8、和 Geb
在上一节中,我们了解了如何将 MockMvc 与 WebDriver 结合使用。在本节中,我们将使用 Geb (opens new window) 让我们的测试更加 “Groovy”。
# a、为什么选择 Geb 和 MockMvc?
Geb 基于 WebDriver,因此它提供了许多与 WebDriver 相同的优势。不过,Geb 会帮我们处理一些样板代码,从而让事情变得更加简单。
# b、和 Geb 的设置
我们可以使用一个基于 MockMvc 的 Selenium WebDriver 轻松初始化 Geb 的 Browser
,如下所示:
def setup() {
browser.driver = MockMvcHtmlUnitDriverBuilder
.webAppContextSetup(context)
.build()
}
这样可以确保所有将 localhost
作为服务器的 URL 请求都会被定向到我们的 MockMvc
实例,而无需进行真正的 HTTP 连接。对于其他 URL,则会像平常一样使用网络连接进行请求。这让我们可以轻松测试 CDN 的使用情况。
# c、和 Geb 的使用
现在我们可以像平常一样使用 Geb,而无需将应用程序部署到 Servlet 容器中。例如,我们可以使用以下代码请求创建消息的视图:
to CreateMessagePage
然后我们可以填写表单并提交以创建消息,如下所示:
when:
form.summary = expectedSummary
form.text = expectedMessage
submit.click(ViewMessagePage)
任何未被识别的方法调用、属性访问或引用都会被转发到当前的页面对象。这就消除了我们直接使用 WebDriver 时所需的大量样板代码。
与直接使用 WebDriver 一样,通过使用页面对象模式,这种方式改进了我们的 HtmlUnit 测试的设计。如前所述,我们可以在 HtmlUnit 和 WebDriver 中使用页面对象模式,但在 Geb 中使用会更加容易。来看一下基于 Groovy 的新 CreateMessagePage
实现:
class CreateMessagePage extends Page {
static url = 'messages/form'
static at = { assert title == 'Messages : Create'; true }
static content = {
submit { $('input[type=submit]') }
form { $('form') }
errors(required:false) { $('label.error, .alert-error')?.text() }
}
}
CreateMessagePage
继承自 Page
。这里我们不会详细介绍 Page
,简单来说,它包含了所有页面的通用功能。我们定义了页面的 URL,这样就可以导航到该页面,如下所示:
to CreateMessagePage
我们还有一个 at
闭包,用于判断我们是否位于指定的页面。如果我们处于正确的页面,它应该返回 true
。这就是我们能够如下断言我们处于正确页面的原因:
then:
at CreateMessagePage
errors.contains('This field is required.')
注意:我们在闭包中使用断言,这样在页面错误时就能确定问题所在。
接下来,我们创建一个 content
闭包,用于指定页面中所有我们感兴趣的区域。我们可以使用 类似 jQuery 的导航器 API (opens new window) 来选择我们感兴趣的内容。
最后,我们可以验证新消息是否成功创建,如下所示:
then:
at ViewMessagePage
success == 'Successfully created a new message'
id
date
summary == expectedSummary
message == expectedMessage
如需了解如何充分利用 Geb 的更多详细信息,请参阅 Geb 用户手册 (opens new window)。
# 7、与端到端测试
MockMvc 基于 spring - test
模块中的 Servlet API 模拟实现构建,并且不依赖于正在运行的容器。因此,与使用实际客户端和运行中的实时服务器进行的完整端到端集成测试相比,存在一些差异。
思考这个问题的最简单方法是从一个空白的 MockHttpServletRequest
开始。你添加到其中的任何内容都会成为请求的一部分。可能会让你感到意外的是,默认情况下没有上下文路径;没有 jsessionid
cookie;没有转发、错误或异步调度;因此,也没有实际的 JSP 渲染。相反,“转发” 和 “重定向” 的 URL 会保存在 MockHttpServletResponse
中,并且可以根据预期进行断言。
这意味着,如果你使用 JSP,你可以验证请求被转发到的 JSP 页面,但不会渲染 HTML。换句话说,JSP 不会被调用。不过要注意,所有其他不依赖于转发的渲染技术,如 Thymeleaf 和 Freemarker,会按预期将 HTML 渲染到响应体中。通过 @ResponseBody
方法渲染 JSON、XML 和其他格式的情况也是如此。
或者,你可以考虑使用 Spring Boot 中带有 @SpringBootTest
的完整端到端集成测试支持。请参阅 Spring Boot 参考指南 (opens new window)。
每种方法都有其优缺点。Spring MVC 测试中提供的选项是从经典单元测试到完整集成测试范围内的不同阶段。可以确定的是,Spring MVC 测试中的任何选项都不属于经典单元测试的范畴,但它们更接近单元测试。例如,你可以通过将模拟服务注入到控制器中来隔离 Web 层,在这种情况下,你仅通过 DispatcherServlet
测试 Web 层,但使用实际的 Spring 配置,就像你可以将数据访问层与上层隔离进行测试一样。此外,你可以使用独立设置,一次专注于一个控制器,并手动提供使其工作所需的配置。
使用 Spring MVC 测试时的另一个重要区别是,从概念上讲,此类测试是服务器端测试,因此你可以检查使用了哪个处理程序、是否使用 HandlerExceptionResolver
处理了异常、模型的内容是什么、存在哪些绑定错误以及其他细节。这意味着编写预期更容易,因为服务器不像通过实际的 HTTP 客户端进行测试时那样是一个不透明的盒子。这通常是经典单元测试的一个优势:编写、推理和调试都更容易,但它不能替代完整集成测试的需求。同时,重要的是不要忽视这样一个事实:响应是最需要检查的内容。简而言之,即使在同一个项目中,也有空间采用多种测试风格和策略。
# 8、更多示例
框架自身的测试套件包含许多示例测试 (opens new window),这些示例旨在展示如何单独使用 MockMvc 或通过WebTestClient (opens new window) 使用 MockMvc。浏览这些示例以获取更多思路。
# 七、测试客户端应用程序
你可以使用客户端测试来测试内部使用 RestTemplate
的代码。其思路是声明预期的请求并提供 “存根” 响应,这样你就可以专注于独立测试代码(即无需运行服务器)。以下示例展示了具体做法:
RestTemplate restTemplate = new RestTemplate();
MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build();
mockServer.expect(requestTo("/greeting")).andRespond(withSuccess());
// 测试使用上述 RestTemplate 的代码 ...
mockServer.verify();
在上述示例中,MockRestServiceServer
(客户端 REST 测试的核心类)使用自定义的 ClientHttpRequestFactory
配置 RestTemplate
,该工厂会根据预期断言实际请求并返回 “存根” 响应。在这个例子中,我们预期会有一个对 /greeting
的请求,并希望返回一个内容类型为 text/plain
的 200 响应。我们可以根据需要定义更多预期请求和存根响应。当我们定义好预期请求和存根响应后,RestTemplate
就可以像平常一样在客户端代码中使用。在测试结束时,可以使用 mockServer.verify()
来验证所有预期是否都已满足。
默认情况下,请求预期按照声明的顺序进行。你可以在构建服务器时设置 ignoreExpectOrder
选项,在这种情况下,所有预期都会按顺序检查,以找到与给定请求匹配的内容。这意味着请求可以以任意顺序到来。以下示例展示了如何使用 ignoreExpectOrder
:
server = MockRestServiceServer.bindTo(restTemplate).ignoreExpectOrder(true).build();
即使默认允许无序请求,每个请求默认也只能运行一次。expect
方法提供了一个重载变体,它接受一个 ExpectedCount
参数,该参数指定了请求次数的范围(例如,once
、manyTimes
、max
、min
、between
等等)。以下示例展示了如何使用 times
:
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();
请注意,如果未设置 ignoreExpectOrder
(默认情况),因此请求预期按照声明的顺序进行,那么该顺序仅适用于任何预期请求中的第一个。例如,如果预期先有两次对 /something
的请求,然后再有三次对 /somewhere
的请求,那么在请求 /somewhere
之前应该先有对 /something
的请求,但除此之外,后续的 /something
和 /somewhere
请求可以在任何时候到来。
作为上述所有方法的替代方案,客户端测试支持还提供了一个 ClientHttpRequestFactory
实现,你可以将其配置到 RestTemplate
中,以将其绑定到 MockMvc
实例。这样就可以使用实际的服务器端逻辑来处理请求,而无需运行服务器。以下示例展示了具体做法:
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
this.restTemplate = new RestTemplate(new MockMvcClientHttpRequestFactory(mockMvc));
// 测试使用上述 RestTemplate 的代码 ...
在某些情况下,可能需要实际调用远程服务,而不是模拟响应。以下示例展示了如何通过 ExecutingResponseCreator
来实现这一点:
RestTemplate restTemplate = new RestTemplate();
// 使用原始请求工厂创建 ExecutingResponseCreator
ExecutingResponseCreator withActualResponse = new ExecutingResponseCreator(restTemplate.getRequestFactory());
MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build();
mockServer.expect(requestTo("/profile")).andRespond(withSuccess());
mockServer.expect(requestTo("/quoteOfTheDay")).andRespond(withActualResponse);
// 测试使用上述 RestTemplate 的代码 ...
mockServer.verify();
在上述示例中,我们在 MockRestServiceServer
用一个模拟响应的不同工厂替换 RestTemplate
的 ClientHttpRequestFactory
之前,使用该原始工厂创建了 ExecutingResponseCreator
。然后我们定义了带有两种响应的预期:
- 为
/profile
端点返回一个存根 200 响应(不会执行实际请求)。 - 通过调用
/quoteOfTheDay
端点获得的响应。
在第二种情况下,请求通过之前捕获的 ClientHttpRequestFactory
执行。这会生成一个响应,例如,该响应可能来自实际的远程服务器,具体取决于 RestTemplate
最初是如何配置的。
# 1、静态导入
与服务器端测试一样,客户端测试的流畅 API 需要一些静态导入。通过搜索 MockRest*
可以轻松找到这些导入。Eclipse 用户应在 Eclipse 首选项的 Java → Editor → Content Assist → Favorites 中,将 MockRestRequestMatchers.*
和 MockRestResponseCreators.*
添加为 “常用静态成员”。这样在输入静态方法名称的第一个字符后,就可以使用内容辅助功能。其他 IDE(如 IntelliJ)可能不需要任何额外的配置。请检查其对静态成员代码补全功能的支持情况。
# 2、客户端 REST 测试的更多示例
Spring MVC Test 自身的测试中包含了客户端 REST 测试的示例 (opens new window)。
# 八、附录
# 1、章节总结
- 注解
- 更多参考资源
# 2、注解
本节将介绍在测试 Spring 应用程序时可以使用的注解。
# 2.1、章节总结
- 标准注解支持
- Spring 测试注解
- Spring JUnit 4 测试注解
- Spring JUnit Jupiter 测试注解
- 测试的元注解支持
# 2.2、标准注解支持
对于 Spring 测试上下文框架的所有配置,以下注解均支持标准语义。请注意,这些注解并非特定于测试,也可在 Spring 框架的任何地方使用。
@Autowired
@Qualifier
@Value
- 如果存在 JSR - 250,则支持
@Resource
(jakarta.annotation) - 如果存在 JSR - 250,则支持
@ManagedBean
(jakarta.annotation) - 如果存在 JSR - 330,则支持
@Inject
(jakarta.inject) - 如果存在 JSR - 330,则支持
@Named
(jakarta.inject) - 如果存在 JPA,则支持
@PersistenceContext
(jakarta.persistence) - 如果存在 JPA,则支持
@PersistenceUnit
(jakarta.persistence) @Transactional
(org.springframework.transaction.annotation)仅支持有限的属性 (opens new window)
# a、注意:JSR - 250 生命周期注解
在 Spring 测试上下文框架中,你可以在 ApplicationContext
配置的任何应用程序组件上,以标准语义使用 @PostConstruct
和 @PreDestroy
。不过,这些生命周期注解在实际测试类中的使用较为有限。
如果测试类中的某个方法使用 @PostConstruct
注解,该方法会在底层测试框架的任何 before
方法(例如,使用 JUnit Jupiter 的 @BeforeEach
注解的方法)之前运行,并且测试类中的每个测试方法都会如此。另一方面,如果测试类中的某个方法使用 @PreDestroy
注解,该方法永远不会运行。因此,在测试类中,我们建议使用底层测试框架的测试生命周期回调,而不是 @PostConstruct
和 @PreDestroy
。
# 2.3、测试注解
Spring 框架提供了一组特定于 Spring 的注解,你可以在单元测试和集成测试中结合 TestContext 框架使用这些注解。
Spring 的测试注解包括以下内容:
@BootstrapWith
@ContextConfiguration
@WebAppConfiguration
@ContextHierarchy
@ContextCustomizerFactories
@ActiveProfiles
@TestPropertySource
@DynamicPropertySource
@TestBean
@MockitoBean
和@MockitoSpyBean
@DirtiesContext
@TestExecutionListeners
@RecordApplicationEvents
@Commit
@Rollback
@BeforeTransaction
@AfterTransaction
@Sql
@SqlConfig
@SqlMergeMode
@SqlGroup
@DisabledInAotMode
# a、@BootstrapWith
@BootstrapWith
是一个可以应用于测试类的注解,用于配置 Spring 测试上下文框架(Spring TestContext Framework)的启动方式。具体而言,你可以使用 @BootstrapWith
来指定自定义的 TestContextBootstrapper
。
# b、@ContextConfiguration
@ContextConfiguration
是一个可以应用于测试类的注解,用于配置元数据,这些元数据用于确定如何为集成测试加载和配置 ApplicationContext
。具体来说,@ContextConfiguration
声明了用于加载上下文的应用上下文资源 locations
或组件 classes
。
资源位置通常是位于类路径中的 XML 配置文件或 Groovy 脚本,而组件类通常是 @Configuration
类。不过,资源位置也可以引用文件系统中的文件和脚本,组件类可以是 @Component
类、@Service
类等等。
以下示例展示了一个引用 XML 文件的 @ContextConfiguration
注解:
@ContextConfiguration("/test-config.xml") // (1) 引用一个 XML 文件
class XmlApplicationContextTests {
// 类的主体...
}
以下示例展示了一个引用类的 @ContextConfiguration
注解:
@ContextConfiguration(classes = TestConfig.class) // (1) 引用一个类
class ConfigClassApplicationContextTests {
// 类的主体...
}
除了声明资源位置或组件类,或者作为替代方案,你可以使用 @ContextConfiguration
声明 ApplicationContextInitializer
类。以下示例展示了这种情况:
@ContextConfiguration(initializers = CustomContextInitializer.class) // (1) 声明一个初始化器类
class ContextInitializerTests {
// 类的主体...
}
你还可以选择使用 @ContextConfiguration
声明 ContextLoader
策略。不过需要注意,通常你不需要显式配置加载器,因为默认加载器支持 initializers
以及资源 locations
或组件 classes
。
以下示例同时使用了位置和加载器:
@ContextConfiguration(locations = "/test-context.xml", loader = CustomContextLoader.class) // (1) 同时配置一个位置和一个自定义加载器
class CustomLoaderXmlApplicationContextTests {
// 类的主体...
}
注意:@ContextConfiguration
还支持继承超类或封闭类中声明的资源位置、配置类以及上下文初始化器。
# c、@WebAppConfiguration
@WebAppConfiguration
是一个可以应用于测试类的注解,用于声明为集成测试加载的 ApplicationContext
应该是一个 WebApplicationContext
。只要在测试类中出现 @WebAppConfiguration
,就可以确保为测试加载一个 WebApplicationContext
,并使用默认值 "file:src/main/webapp"
作为 Web 应用程序根目录的路径(即资源基础路径)。资源基础路径在后台用于创建一个 MockServletContext
,它将作为测试的 WebApplicationContext
的 ServletContext
。
下面的示例展示了如何使用 @WebAppConfiguration
注解:
@ContextConfiguration
@WebAppConfiguration // (1)
class WebAppTests {
// 类主体...
}
@WebAppConfiguration
注解。
如果要覆盖默认设置,可以使用隐式的 value
属性指定不同的基础资源路径。classpath:
和 file:
资源前缀均受支持。如果没有提供资源前缀,那么该路径将被视为文件系统资源。下面的示例展示了如何指定类路径资源:
@ContextConfiguration
@WebAppConfiguration("classpath:test-web-resources") // (1)
class WebAppTests {
// 类主体...
}
- 指定类路径资源。
请注意,@WebAppConfiguration
必须与 @ContextConfiguration
一起使用,可以在单个测试类中使用,也可以在测试类层次结构中使用。有关更多详细信息,请参阅 @WebAppConfiguration (opens new window) 的 Javadoc 文档。
# d、@ContextHierarchy
@ContextHierarchy
是一个可以应用于测试类的注解,用于为集成测试定义 ApplicationContext
实例的层次结构。@ContextHierarchy
应该与一个或多个 @ContextConfiguration
实例列表一起声明,其中每个 @ContextConfiguration
实例都定义了上下文层次结构中的一个级别。以下示例演示了在单个测试类中使用 @ContextHierarchy
(@ContextHierarchy
也可以在测试类层次结构中使用):
@ContextHierarchy({
@ContextConfiguration("/parent-config.xml"),
@ContextConfiguration("/child-config.xml")
})
class ContextHierarchyTests {
// 类的主体...
}
@WebAppConfiguration
@ContextHierarchy({
@ContextConfiguration(classes = AppConfig.class),
@ContextConfiguration(classes = WebConfig.class)
})
class WebIntegrationTests {
// 类的主体...
}
如果你需要在测试类层次结构中合并或覆盖上下文层次结构中某个级别配置,必须通过在类层次结构中相应的每个级别上为 @ContextConfiguration
中的 name
属性提供相同的值来显式命名该级别。有关更多示例,请参阅上下文层次结构和 @ContextHierarchy
(opens new window) 的 JavaDoc。
# e、@ContextCustomizerFactories
@ContextCustomizerFactories
是一个可以应用于测试类的注解,用于为特定的测试类、其子类及其嵌套类注册 ContextCustomizerFactory
实现。如果你希望全局注册工厂,则应该通过《ContextCustomizerFactory
配置》中描述的自动发现机制进行注册。
以下示例展示了如何注册两个 ContextCustomizerFactory
实现:
@ContextConfiguration
@ContextCustomizerFactories({CustomContextCustomizerFactory.class, AnotherContextCustomizerFactory.class}) // (1) 注册两个 `ContextCustomizerFactory` 实现。
class CustomContextCustomizerFactoryTests {
// 类主体...
}
默认情况下,@ContextCustomizerFactories
支持从超类或外部类继承工厂。有关示例和详细信息,请参阅《@Nested
测试类配置》和《@ContextCustomizerFactories
的 Javadoc (opens new window)》。
# f、@ActiveProfiles
@ActiveProfiles
是一个可以应用于测试类的注解,用于声明在为集成测试加载 ApplicationContext
时,哪些 Bean 定义配置文件应该处于激活状态。
以下示例表明应该激活 dev
配置文件:
@ContextConfiguration
@ActiveProfiles("dev") // (1) 表明应该激活 `dev` 配置文件
class DeveloperTests {
// 类主体...
}
以下示例表明 dev
和 integration
配置文件都应该处于激活状态:
@ContextConfiguration
@ActiveProfiles({"dev", "integration"}) // (1) 表明 `dev` 和 `integration` 配置文件都应该激活
class DeveloperIntegrationTests {
// 类主体...
}
注意:@ActiveProfiles
默认支持继承由超类和外部类声明的活动 Bean 定义配置文件。你还可以通过实现自定义的 ActiveProfilesResolver
并使用 @ActiveProfiles
的 resolver
属性进行注册,以编程方式解析活动的 Bean 定义配置文件。
有关示例和更多详细信息,请参阅使用环境配置文件的上下文配置、@Nested
测试类配置以及 @ActiveProfiles (opens new window) 的 Java 文档。
# g、@TestPropertySource
@TestPropertySource
是一个可以应用于测试类的注解,用于配置属性文件的位置,以及设置内联属性。这些属性将被添加到为集成测试加载的 ApplicationContext
的 Environment
中的 PropertySources
集合中。
下面的示例展示了如何声明类路径中的属性文件:
@ContextConfiguration
@TestPropertySource("/test.properties") // (1)
class MyIntegrationTests {
// 类主体...
}
- 从类路径根目录下的
test.properties
文件中获取属性。
下面的示例展示了如何声明内联属性:
@ContextConfiguration
@TestPropertySource(properties = { "timezone = GMT", "port: 4242" }) // (1)
class MyIntegrationTests {
// 类主体...
}
- 声明
timezone
和port
属性。
# h、@DynamicPropertySource
@DynamicPropertySource
是一种注解,可应用于集成测试类中的方法。这些方法需要注册“动态”属性,以便将其添加到为集成测试加载的 ApplicationContext
的 Environment
中的 PropertySources
集合里。当你事先不知道属性的值时,动态属性就非常有用,例如,如果这些属性由外部资源管理,就像由 Testcontainers (opens new window) 项目管理的容器那样。
以下示例展示了如何注册动态属性:
@ContextConfiguration
class MyIntegrationTests {
static MyExternalServer server = // ...
@DynamicPropertySource // (1)
static void dynamicProperties(DynamicPropertyRegistry registry) { // (2)
registry.add("server.port", server::getPort); // (3)
}
// 测试方法...
}
- 使用
@DynamicPropertySource
注解一个static
方法。 - 接受一个
DynamicPropertyRegistry
作为参数。 - 注册一个动态的
server.port
属性,该属性将从服务器惰性获取。
# i、@TestBean
@TestBean
用于测试类中的非静态字段,以便使用工厂方法提供的实例来覆盖测试的 ApplicationContext
中的特定 bean。
@TestBean
所关联的工厂方法名称由被注解字段的名称推导得出,如果指定了 bean 名称,则使用该 bean 名称。工厂方法必须是静态的,不接受任何参数,并且其返回类型必须与要覆盖的 bean 类型兼容。若想让表达更明确,或者想使用不同的名称,该注解允许指定特定的方法名称。
默认情况下,被注解字段的类型用于查找要覆盖的候选 bean。如果有多个候选 bean 匹配,可以使用 @Qualifier
来缩小要覆盖的候选范围。另外,如果某个候选 bean 的名称与字段名称匹配,它也会被选中。
如果对应的 bean 不存在,将会创建一个新的 bean。不过,如果你希望在对应 bean 不存在时测试失败,可以将 enforceOverride
属性设置为 true
,例如 @TestBean(enforceOverride = true)
。
若要使用按名称覆盖而非按类型覆盖,则需指定该注解的 name
属性。
警告:限定符(包括字段名称)用于确定是否需要创建单独的 ApplicationContext
。如果你在多个测试中使用此功能来覆盖同一个 bean,请确保统一字段名称,以避免创建不必要的上下文。
注意:对于 @TestBean
字段或工厂方法的可见性没有限制。因此,这些字段和方法可以是 public
、protected
、包私有(默认可见性)或 private
,这取决于项目的需求或编码规范。
以下示例展示了如何使用 @TestBean
注解的默认行为:
class OverrideBeanTests {
@TestBean // (1)
CustomService customService;
// 测试用例主体...
static CustomService customService() { // (2)
return new MyFakeCustomService();
}
}
- 标记一个字段,用于覆盖类型为
CustomService
的 bean。 - 此静态方法的返回结果将作为实例并注入到该字段中。
在上述示例中,我们正在覆盖类型为 CustomService
的 bean。如果存在多个该类型的 bean,则会考虑名为 customService
的 bean。否则,测试将会失败,此时你需要提供某种限定符,以确定要覆盖哪个 CustomService
bean。
以下示例使用按名称查找,而非按类型查找:
class OverrideBeanTests {
@TestBean(name = "service", methodName = "createCustomService") // (1)
CustomService customService;
// 测试用例主体...
static CustomService createCustomService() { // (2)
return new MyFakeCustomService();
}
}
- 标记一个字段,用于覆盖名称为
service
的 bean,并指定工厂方法名为createCustomService
。 - 此静态方法的返回结果将作为实例并注入到该字段中。
小贴士:为了定位要调用的工厂方法,Spring 会在声明 @TestBean
字段的类、其某个超类或任何实现的接口中进行搜索。如果 @TestBean
字段在 @Nested
测试类中声明,还会搜索封闭类的层次结构。
另外,外部类中的工厂方法可以通过其全限定方法名来引用,语法为 <全限定类名>#<方法名>
,例如 methodName = "org.example.TestUtils#createCustomService"
。
小贴士:只有单例 bean 可以被覆盖。任何尝试覆盖非单例 bean 的操作都会导致异常。
当覆盖由 FactoryBean
创建的 bean 时,FactoryBean
将被一个单例 bean 取代,该单例 bean 对应于 @TestBean
工厂方法返回的值。
# j、@MockitoBean
和 @MockitoSpyBean
@MockitoBean
和 @MockitoSpyBean
可在测试类中使用,分别用 Mockito 的 模拟对象(mock) 或 间谍对象(spy) 覆盖测试的 ApplicationContext
中的 Bean。对于后者,会捕获原始 Bean 的早期实例并将其包装在间谍对象中。
这些注解可以通过以下方式应用:
- 应用于测试类或其任何超类的非静态字段上。
- 应用于
@Nested
测试类的封闭类或@Nested
测试类的类型层次结构或封闭类层次结构中任何类的非静态字段上。 - 应用于测试类或测试类类型层次结构中任何超类或已实现接口的类型级别上。
- 应用于
@Nested
测试类的封闭类或@Nested
测试类的类型层次结构或封闭类层次结构中任何类或接口的类型级别上。
当在字段上声明 @MockitoBean
或 @MockitoSpyBean
时,要模拟或监视的 Bean 会从带注解字段的类型推断得出。如果 ApplicationContext
中存在多个候选 Bean,则可以在字段上声明 @Qualifier
注解以帮助消除歧义。如果没有 @Qualifier
注解,则带注解字段的名称将用作 后备限定符。或者,你可以通过设置注解中的 value
或 name
属性来显式指定要模拟或监视的 Bean 名称。
当在类型级别声明 @MockitoBean
或 @MockitoSpyBean
时,必须通过注解中的 types
属性指定要模拟或监视的 Bean 类型,例如 @MockitoBean(types = {OrderService.class, UserService.class})
。如果 ApplicationContext
中存在多个候选 Bean,则可以通过设置 name
属性来显式指定要模拟或监视的 Bean 名称。但是请注意,如果配置了显式的 Bean name
,则 types
属性必须只包含一个类型,例如 @MockitoBean(name = "ps1", types = PrintingService.class)
。
为了支持重复使用模拟配置,@MockitoBean
和 @MockitoSpyBean
可以用作元注解来创建自定义 组合注解,例如在单个注解中定义常见的模拟或间谍配置,以便在整个测试套件中重复使用。@MockitoBean
和 @MockitoSpyBean
还可以在类型级别用作可重复注解,例如按名称模拟或监视多个 Bean。
警告:限定符(包括字段名称)用于确定是否需要创建单独的 ApplicationContext
。如果你使用此功能在多个测试类中模拟或监视同一个 Bean,请确保一致地命名这些字段,以避免创建不必要的上下文。
每个注解还定义了特定于 Mockito 的属性,用于微调模拟行为。
@MockitoBean
注解使用 REPLACE_OR_CREATE
Bean 覆盖策略。如果对应的 Bean 不存在,将创建一个新的 Bean。但是,你可以通过将 enforceOverride
属性设置为 true
切换到 REPLACE
策略,例如 @MockitoBean(enforceOverride = true)
。
@MockitoSpyBean
注解使用 WRAP
策略,并且原始实例会被包装在 Mockito 间谍对象中。此策略要求恰好存在一个候选 Bean。
提示:
- 只有 单例 Bean 可以被覆盖。任何尝试覆盖非单例 Bean 的操作都将导致异常。
- 当使用
@MockitoBean
模拟由FactoryBean
创建的 Bean 时,FactoryBean
将被替换为FactoryBean
创建的对象类型的单例模拟对象。 - 当使用
@MockitoSpyBean
为FactoryBean
创建间谍对象时,将为FactoryBean
创建的对象创建间谍对象,而不是为FactoryBean
本身创建。
注意:
- 对
@MockitoBean
和@MockitoSpyBean
字段的可见性没有限制。 - 因此,这些字段可以是
public
、protected
、包私有(默认可见性)或private
,具体取决于项目的需求或编码习惯。
# @MockitoBean
示例
以下示例展示了如何使用 @MockitoBean
注解的默认行为:
@SpringJUnitConfig(TestConfig.class)
class BeanOverrideTests {
@MockitoBean // 1. 用 Mockito 模拟对象替换类型为 CustomService 的 Bean
CustomService customService;
// 测试...
}
在上述示例中,我们为 CustomService
创建一个模拟对象。如果存在多个该类型的 Bean,则会考虑名为 customService
的 Bean。否则,测试将失败,你需要提供某种限定符来确定要覆盖的 CustomService
Bean。如果不存在这样的 Bean,将使用自动生成的 Bean 名称创建一个 Bean。
以下示例使用按名称查找,而不是按类型查找。如果不存在名为 service
的 Bean,则会创建一个:
@SpringJUnitConfig(TestConfig.class)
class BeanOverrideTests {
@MockitoBean("service") // 1. 用 Mockito 模拟对象替换名为 service 的 Bean
CustomService customService;
// 测试...
}
以下 @SharedMocks
注解按类型注册两个模拟对象,按名称注册一个模拟对象:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@MockitoBean(types = {OrderService.class, UserService.class}) // 1. 按类型注册 OrderService 和 UserService 模拟对象
@MockitoBean(name = "ps1", types = PrintingService.class) // 2. 按名称注册 PrintingService 模拟对象
public @interface SharedMocks {
}
以下展示了如何在测试类中使用 @SharedMocks
:
@SpringJUnitConfig(TestConfig.class)
@SharedMocks // 1. 通过自定义 @SharedMocks 注解注册常见的模拟对象
class BeanOverrideTests {
@Autowired OrderService orderService; // 2. 可选地注入模拟对象以进行存根或验证
@Autowired UserService userService; // 2. 可选地注入模拟对象以进行存根或验证
@Autowired PrintingService ps1; // 2. 可选地注入模拟对象以进行存根或验证
// 注入依赖于这些模拟对象的其他组件
@Test
void testThatDependsOnMocks() {
// ...
}
}
提示:模拟对象还可以注入到 @Configuration
类或 ApplicationContext
中其他与测试相关的组件中,以便使用 Mockito 的存根 API 对其进行配置。
# @MockitoSpyBean
示例
以下示例展示了如何使用 @MockitoSpyBean
注解的默认行为:
@SpringJUnitConfig(TestConfig.class)
class BeanOverrideTests {
@MockitoSpyBean // 1. 用 Mockito 间谍对象包装类型为 CustomService 的 Bean
CustomService customService;
// 测试...
}
在上述示例中,我们包装类型为 CustomService
的 Bean。如果存在多个该类型的 Bean,则会考虑名为 customService
的 Bean。否则,测试将失败,你需要提供某种限定符来确定要监视的 CustomService
Bean。
以下示例使用按名称查找,而不是按类型查找:
@SpringJUnitConfig(TestConfig.class)
class BeanOverrideTests {
@MockitoSpyBean("service") // 1. 用 Mockito 间谍对象包装名为 service 的 Bean
CustomService customService;
// 测试...
}
以下 @SharedSpies
注解按类型注册两个间谍对象,按名称注册一个间谍对象:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@MockitoSpyBean(types = {OrderService.class, UserService.class}) // 1. 按类型注册 OrderService 和 UserService 间谍对象
@MockitoSpyBean(name = "ps1", types = PrintingService.class) // 2. 按名称注册 PrintingService 间谍对象
public @interface SharedSpies {
}
以下展示了如何在测试类中使用 @SharedSpies
:
@SpringJUnitConfig(TestConfig.class)
@SharedSpies // 1. 通过自定义 @SharedSpies 注解注册常见的间谍对象
class BeanOverrideTests {
@Autowired OrderService orderService; // 2. 可选地注入间谍对象以进行存根或验证
@Autowired UserService userService; // 2. 可选地注入间谍对象以进行存根或验证
@Autowired PrintingService ps1; // 2. 可选地注入间谍对象以进行存根或验证
// 注入依赖于这些间谍对象的其他组件
@Test
void testThatDependsOnMocks() {
// ...
}
}
提示:间谍对象还可以注入到 @Configuration
类或 ApplicationContext
中其他与测试相关的组件中,以便使用 Mockito 的存根 API 对其进行配置。
# k、@DirtiesContext
@DirtiesContext
表明在测试执行期间,底层的 Spring ApplicationContext
已被弄脏(也就是说,测试以某种方式修改或破坏了它,例如,更改了单例 bean 的状态),并且应该关闭该上下文。当一个应用程序上下文被标记为已弄脏时,它会从测试框架的缓存中移除并关闭。因此,对于后续任何需要具有相同配置元数据的上下文的测试,都会重新构建底层的 Spring 容器。
你可以在同一个测试类或测试类层次结构中,将 @DirtiesContext
同时用作类级和方法级注解。在这类场景中,根据配置的 methodMode
和 classMode
,ApplicationContext
会在任何有此注解的方法之前或之后,以及当前测试类之前或之后被标记为已弄脏。当 @DirtiesContext
同时在类级别和方法级别声明时,两个注解配置的模式都会被遵循。例如,如果类模式设置为 BEFORE_EACH_TEST_METHOD
,而方法模式设置为 AFTER_METHOD
,那么在给定的测试方法之前和之后,上下文都会被标记为已弄脏。
以下示例解释了在各种配置场景下,上下文何时会被标记为已弄脏:
在当前测试类之前:当在类上声明
@DirtiesContext
并且类模式设置为BEFORE_CLASS
时。@DirtiesContext(classMode = BEFORE_CLASS) // (1) class FreshContextTests { // 一些需要新的 Spring 容器的测试 }
(1) 在当前测试类之前弄脏上下文。
在当前测试类之后:当在类上声明
@DirtiesContext
并且类模式设置为AFTER_CLASS
(即默认的类模式)时。@DirtiesContext // (1) class ContextDirtyingTests { // 一些会使 Spring 容器被弄脏的测试 }
(1) 在当前测试类之后弄脏上下文。
在当前测试类的每个测试方法之前:当在类上声明
@DirtiesContext
并且类模式设置为BEFORE_EACH_TEST_METHOD
时。@DirtiesContext(classMode = BEFORE_EACH_TEST_METHOD) // (1) class FreshContextTests { // 一些需要新的 Spring 容器的测试 }
(1) 在每个测试方法之前弄脏上下文。
在当前测试类的每个测试方法之后:当在类上声明
@DirtiesContext
并且类模式设置为AFTER_EACH_TEST_METHOD
时。@DirtiesContext(classMode = AFTER_EACH_TEST_METHOD) // (1) class ContextDirtyingTests { // 一些会使 Spring 容器被弄脏的测试 }
(1) 在每个测试方法之后弄脏上下文。
在当前测试之前:当在方法上声明
@DirtiesContext
并且方法模式设置为BEFORE_METHOD
时。@DirtiesContext(methodMode = BEFORE_METHOD) // (1) @Test void testProcessWhichRequiresFreshAppCtx() { // 一些需要新的 Spring 容器的逻辑 }
(1) 在当前测试方法之前弄脏上下文。
在当前测试之后:当在方法上声明
@DirtiesContext
并且方法模式设置为AFTER_METHOD
(即默认的方法模式)时。@DirtiesContext // (1) @Test void testProcessWhichDirtiesAppCtx() { // 一些会使 Spring 容器被弄脏的逻辑 }
(1) 在当前测试方法之后弄脏上下文。
如果你在一个使用 @ContextHierarchy
将上下文配置为上下文层次结构一部分的测试中使用 @DirtiesContext
,可以使用 hierarchyMode
标志来控制如何清空上下文缓存。默认情况下,会使用一种详尽的算法来清空上下文缓存,不仅包括当前级别,还包括与当前测试共享共同祖先上下文的所有其他上下文层次结构。所有驻留在共同祖先上下文的子层次结构中的 ApplicationContext
实例都会从上下文缓存中移除并关闭。如果这种详尽的算法对于特定用例来说过于复杂,你可以指定更简单的当前级别算法,如下例所示:
@ContextHierarchy({
@ContextConfiguration("/parent-config.xml"),
@ContextConfiguration("/child-config.xml")
})
class BaseTests {
// 类体...
}
class ExtendedTests extends BaseTests {
@Test
@DirtiesContext(hierarchyMode = CURRENT_LEVEL) // (1)
void test() {
// 一些会使子上下文被弄脏的逻辑
}
}
(1) 使用当前级别算法。
有关 EXHAUSTIVE
和 CURRENT_LEVEL
算法的更多详细信息,请参阅 DirtiesContext.HierarchyMode (opens new window) 的 Javadoc 文档。
# l、@TestExecutionListeners
@TestExecutionListeners
用于为带注释的测试类、其子类及其嵌套类注册监听器。如果你希望全局注册监听器,应该通过《TestExecutionListener
配置》中描述的自动发现机制进行注册。
以下示例展示了如何注册两个 TestExecutionListener
实现:
@ContextConfiguration
@TestExecutionListeners({CustomTestExecutionListener.class, AnotherTestExecutionListener.class}) // (1) 注册两个 TestExecutionListener 实现
class CustomTestExecutionListenerTests {
// 类的主体...
}
默认情况下,@TestExecutionListeners
支持从超类或外部类继承监听器。有关示例和更多详细信息,请参阅《@Nested
测试类配置》和 《@TestExecutionListeners
Java 文档》。如果你发现需要切换回使用默认的 TestExecutionListener
实现,请参阅《注册 TestExecutionListener
实现》中的说明。
# m、@RecordApplicationEvents
@RecordApplicationEvents
是一个可以应用于测试类的注解,它会指示“Spring 测试上下文框架”记录在单个测试执行期间在 ApplicationContext
中发布的所有应用事件。
可以在测试中通过 ApplicationEvents
API 访问已记录的事件。
有关示例和更多详细信息,请参阅应用事件和 @RecordApplicationEvents
Java文档 (opens new window)。
# n、@Commit
@Commit
表明在测试方法执行完毕后,事务性测试方法所涉及的事务应该被提交。你可以用 @Commit
直接替换 @Rollback(false)
,这样能更明确地表达代码意图。与 @Rollback
类似,@Commit
也可以被声明为类级或方法级注释。
以下示例展示了如何使用 @Commit
注释:
@Commit // (1) 将测试结果提交到数据库
@Test
void testProcessWithoutRollback() {
// ...
}
# o、@Rollback
@Rollback
用于指示事务性测试方法完成后,其事务是否应回滚。如果值为 true
,则事务会回滚;否则,事务会提交(另见 @Commit
)。即便没有显式声明 @Rollback
,Spring 测试上下文框架中集成测试的回滚默认值也是 true
。
当 @Rollback
作为类级注解声明时,它为测试类层次结构中的所有测试方法定义了默认的回滚语义。当作为方法级注解声明时,@Rollback
为特定的测试方法定义回滚语义,有可能会覆盖类级别的 @Rollback
或 @Commit
语义。
以下示例会使测试方法的结果不会回滚(即结果会提交到数据库):
@Rollback(false) // 1. 不回滚结果
@Test
void testProcessWithoutRollback() {
// ...
}
- 不回滚结果。
# p、@BeforeTransaction
@BeforeTransaction
表明,对于那些通过使用 Spring 的 @Transactional
注解配置为在事务中运行的测试方法,被该注解标注的 void
方法应当在事务开始之前执行。@BeforeTransaction
方法不要求必须是 public
类型的,并且可以在基于 Java 8 的接口默认方法中声明。
以下示例展示了如何使用 @BeforeTransaction
注解:
@BeforeTransaction // (1)
void beforeTransaction() {
// 在事务开始之前要执行的逻辑
}
- 在事务开始前执行此方法。
# q、@AfterTransaction
@AfterTransaction
注解表明,对于那些使用 Spring 的 @Transactional
注解配置为在事务中运行的测试方法,被该注解标注的 void
方法应该在事务结束后执行。@AfterTransaction
方法不必是 public
方法,并且可以在基于 Java 8 的接口默认方法中声明。
以下是 Java 示例:
@AfterTransaction // (1) 在事务结束后运行此方法
void afterTransaction() {
// 事务结束后要执行的逻辑
}
- 在事务结束后运行此方法。
# r、@Sql
@Sql
用于标注测试类或测试方法,以便在集成测试期间配置要对指定数据库运行的 SQL 脚本。以下示例展示了如何使用它:
@Test
@Sql({"/test-schema.sql", "/test-user-data.sql"}) // 1. 为该测试运行两个脚本
void userTest() {
// 运行依赖于测试模式和测试数据的代码
}
有关更多详细信息,请参阅使用 @Sql 声明式地执行 SQL 脚本。
# s、@SqlConfig
@SqlConfig
定义了元数据,用于确定如何解析和运行使用 @Sql
注解配置的 SQL 脚本。以下示例展示了如何使用它:
@Test
@Sql(
scripts = "/test-user-data.sql",
config = @SqlConfig(commentPrefix = "`", separator = "@@") // (1)
)
void userTest() {
// 运行依赖测试数据的代码
}
- 设置 SQL 脚本中的注释前缀和分隔符。
# t、@SqlMergeMode
@SqlMergeMode
用于注解测试类或测试方法,以此配置方法级别的 @Sql
声明是否与类级别的 @Sql
声明合并。如果在测试类或测试方法上未声明 @SqlMergeMode
,则默认将使用 OVERRIDE
合并模式。在 OVERRIDE
模式下,方法级别的 @Sql
声明将有效覆盖类级别的 @Sql
声明。
请注意,方法级别的 @SqlMergeMode
声明会覆盖类级别的声明。
以下示例展示了如何在类级别使用 @SqlMergeMode
:
@SpringJUnitConfig(TestConfig.class)
@Sql("/test-schema.sql")
@SqlMergeMode(MERGE) // 1. 为类中的所有测试方法将 @Sql 合并模式设置为 MERGE
class UserTests {
@Test
@Sql("/user-test-data-001.sql")
void standardUserProfile() {
// 运行依赖测试数据集 001 的代码
}
}
以下示例展示了如何在方法级别使用 @SqlMergeMode
:
@SpringJUnitConfig(TestConfig.class)
@Sql("/test-schema.sql")
class UserTests {
@Test
@Sql("/user-test-data-001.sql")
@SqlMergeMode(MERGE) // 1. 为特定的测试方法将 @Sql 合并模式设置为 MERGE
void standardUserProfile() {
// 运行依赖测试数据集 001 的代码
}
}
# u、@SqlGroup
@SqlGroup
是一个容器注解,它可以聚合多个 @Sql
注解。你既可以直接使用 @SqlGroup
来声明多个嵌套的 @Sql
注解,也可以结合 Java 8 对可重复注解的支持来使用它。在 Java 8 中,@Sql
可以在同一个类或方法上多次声明,这样会隐式生成这个容器注解。以下示例展示了如何声明一个 SQL 组:
@Test
@SqlGroup({ // (1)
@Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`")),
@Sql("/test-user-data.sql")
})
void userTest() {
// 运行使用测试模式和测试数据的代码
}
- 声明一组 SQL 脚本。
# v、@DisabledInAotMode
@DisabledInAotMode
表明被注解的测试类在 Spring AOT(提前编译,ahead - of - time)模式下是禁用的,这意味着该测试类的 ApplicationContext
在构建时不会进行 AOT 优化处理。
如果一个测试类被 @DisabledInAotMode
注解,那么所有指定要加载相同 ApplicationContext
配置的其他测试类也必须用 @DisabledInAotMode
进行注解。如果未能对所有这类测试类进行注解,那么在构建时或运行时会抛出异常。
在基于 JUnit Jupiter 的测试中使用时,@DisabledInAotMode
还表明,当在 Spring AOT 模式下运行测试套件时,被注解的测试类或测试方法会被禁用。如果该注解应用在类级别,那么该类中的所有测试方法都将被禁用。从这个意义上说,@DisabledInAotMode
的语义与 JUnit Jupiter 的 @DisabledInNativeImage
注解类似。
有关特定于集成测试的 AOT 支持的详细信息,请参阅《测试的提前编译支持》(Ahead of Time Support for Tests)。
# 2.4、JUnit 4 测试注解
以下注解仅在与 SpringRunner、Spring 的 JUnit 4 规则 或 Spring 的 JUnit 4 支持类 结合使用时才受支持:
@IfProfileValue
@ProfileValueSourceConfiguration
@Timed
@Repeat
# a、@IfProfileValue
@IfProfileValue
表明带注解的测试类或测试方法在特定的测试环境中启用。如果配置的 ProfileValueSource
为提供的 name
返回匹配的 value
,则该测试将启用;否则,该测试将被禁用,实际上会被忽略。
你可以在类级别、方法级别或同时在这两个级别应用 @IfProfileValue
。对于该类或其子类中的任何方法,类级别的 @IfProfileValue
使用优先级高于方法级别的使用。具体来说,如果一个测试在类级别和方法级别都被启用,那么它才会被启用。@IfProfileValue
不存在意味着该测试隐式启用。这类似于 JUnit 4 的 @Ignore
注解的语义,不同的是 @Ignore
的存在总是会禁用一个测试。
下面的示例展示了一个带有 @IfProfileValue
注解的测试:
@IfProfileValue(name="java.vendor", value="Oracle Corporation") // (1) 仅当 Java 供应商为 "Oracle Corporation" 时运行此测试
@Test
public void testProcessWhichRunsOnlyOnOracleJvm() {
// 仅在 Oracle 公司的 Java 虚拟机上运行的一些逻辑
}
或者,你可以使用 values
列表(具有 “或” 语义)来配置 @IfProfileValue
,以在 JUnit 4 环境中实现类似 TestNG 的测试组支持。考虑以下示例:
@IfProfileValue(name="test-groups", values={"unit-tests", "integration-tests"}) // (1) 针对单元测试和集成测试运行此测试
@Test
public void testProcessWhichRunsForUnitOrIntegrationTestGroups() {
// 仅在单元测试组和集成测试组中运行的一些逻辑
}
# b、@ProfileValueSourceConfiguration
@ProfileValueSourceConfiguration
是一个可以应用于测试类的注解,用于指定在检索通过 @IfProfileValue
注解配置的配置文件值时使用的 ProfileValueSource
类型。如果测试中未声明 @ProfileValueSourceConfiguration
,则默认使用 SystemProfileValueSource
。以下示例展示了如何使用 @ProfileValueSourceConfiguration
:
@ProfileValueSourceConfiguration(CustomProfileValueSource.class) // (1) 使用自定义的配置文件值源
public class CustomProfileValueSourceTests {
// 类的主体...
}
# c、@Timed
@Timed
表明带注解的测试方法必须在指定的时间段(以毫秒为单位)内完成执行。如果测试执行时间超过指定的时间段,则测试失败。
该时间段包括运行测试方法本身、测试的任何重复(请参阅 @Repeat
)以及测试装置的任何设置或拆卸。以下示例展示了如何使用它:
@Timed(millis = 1000) // (1) 将测试的时间段设置为一秒
public void testProcessWithOneSecondTimeout() {
// 运行时间不应超过一秒的一些逻辑
}
Spring 的 @Timed
注解与 JUnit 4 的 @Test(timeout=…)
支持具有不同的语义。具体而言,由于 JUnit 4 处理测试执行超时的方式(即在单独的 Thread
中执行测试方法),如果测试花费的时间过长,@Test(timeout=…)
会抢先使测试失败。而 Spring 的 @Timed
不会抢先使测试失败,而是在测试完成后才标记为失败。
# d、@Repeat
@Repeat
表明带注解的测试方法必须重复运行。测试方法运行的次数在注解中指定。
要重复执行的范围包括测试方法本身的执行以及测试装置的任何设置或拆卸。当与 SpringMethodRule 一起使用时,该范围还包括 TestExecutionListener
实现对测试实例的准备。以下示例展示了如何使用 @Repeat
注解:
@Repeat(10) // (1) 重复此测试十次
@Test
public void testProcessRepeatedly() {
// ...
}
# 2.5、JUnit Jupiter测试注解
当与 SpringExtension
和 JUnit Jupiter(即 JUnit 5 中的编程模型)结合使用时,支持以下注解:
@SpringJUnitConfig
@SpringJUnitWebConfig
@TestConstructor
@NestedTestConfiguration
@EnabledIf
@DisabledIf
@DisabledInAotMode
# a、@SpringJUnitConfig
@SpringJUnitConfig
是一个组合注解,它将 JUnit Jupiter 中的 @ExtendWith(SpringExtension.class)
与 Spring 测试上下文框架中的 @ContextConfiguration
结合在一起。它可以在类级别使用,作为 @ContextConfiguration
的直接替代品。关于配置选项,@ContextConfiguration
和 @SpringJUnitConfig
之间的唯一区别是,可以在 @SpringJUnitConfig
中使用 value
属性声明组件类。
以下示例展示了如何使用 @SpringJUnitConfig
注解指定配置类:
@SpringJUnitConfig(TestConfig.class) // (1)
class ConfigurationClassJUnitJupiterSpringTests {
// 类主体...
}
- (1) 指定配置类。
以下示例展示了如何使用 @SpringJUnitConfig
注解指定配置文件的位置:
@SpringJUnitConfig(locations = "/test-config.xml") // (1)
class XmlJUnitJupiterSpringTests {
// 类主体...
}
- (1) 指定配置文件的位置。
有关更多详细信息,请参阅上下文管理 (opens new window)以及 @SpringJUnitConfig (opens new window) 和 @ContextConfiguration
的 Javadoc。
# b、@SpringJUnitWebConfig
@SpringJUnitWebConfig
是一个组合注解,它将 JUnit Jupiter 中的 @ExtendWith(SpringExtension.class)
与 Spring 测试上下文框架中的 @ContextConfiguration
和 @WebAppConfiguration
结合在一起。你可以在类级别使用它,作为 @ContextConfiguration
和 @WebAppConfiguration
的直接替代品。关于配置选项,@ContextConfiguration
和 @SpringJUnitWebConfig
之间的唯一区别是,你可以在 @SpringJUnitWebConfig
中使用 value
属性声明组件类。此外,你只能在 @SpringJUnitWebConfig
中使用 resourcePath
属性来覆盖 @WebAppConfiguration
中的 value
属性。
以下示例展示了如何使用 @SpringJUnitWebConfig
注解指定配置类:
@SpringJUnitWebConfig(TestConfig.class) // (1)
class ConfigurationClassJUnitJupiterSpringWebTests {
// 类主体...
}
- (1) 指定配置类。
以下示例展示了如何使用 @SpringJUnitWebConfig
注解指定配置文件的位置:
@SpringJUnitWebConfig(locations = "/test-config.xml") // (1)
class XmlJUnitJupiterSpringWebTests {
// 类主体...
}
- (1) 指定配置文件的位置。
有关更多详细信息,请参阅上下文管理 (opens new window)以及 @SpringJUnitWebConfig (opens new window)、@ContextConfiguration (opens new window) 和 @WebAppConfiguration (opens new window) 的 Javadoc。
# c、@TestConstructor
@TestConstructor
是一个可以应用于测试类的注解,用于配置如何从测试的 ApplicationContext
中的组件自动注入测试类构造函数的参数。
如果 @TestConstructor
不存在或不是元存在于测试类中,则将使用默认的测试构造函数自动注入模式。有关如何更改默认模式的详细信息,请参阅下面的提示。但是请注意,在构造函数上本地声明的 @Autowired
、@jakarta.inject.Inject
或 @javax.inject.Inject
优先于 @TestConstructor
和默认模式。
提示:更改默认的测试构造函数自动注入模式
可以通过将 spring.test.constructor.autowire.mode
JVM 系统属性设置为 all
来更改默认的测试构造函数自动注入模式。或者,可以通过 SpringProperties (opens new window) 机制设置默认模式。
默认模式也可以配置为 JUnit 平台配置参数 (opens new window)。
如果未设置 spring.test.constructor.autowire.mode
属性,则测试类构造函数不会自动进行自动注入。
注意: @TestConstructor
仅在与用于 JUnit Jupiter 的 SpringExtension
结合使用时受支持。请注意,SpringExtension
通常会自动为你注册,例如,当使用 @SpringJUnitConfig
和 @SpringJUnitWebConfig
等注解或 Spring Boot 测试中各种与测试相关的注解时。
# d、@NestedTestConfiguration
@NestedTestConfiguration
是一个可以应用于测试类的注解,用于配置在内部测试类的封闭类层次结构中如何处理 Spring 测试配置注解。
如果 @NestedTestConfiguration
不存在或不是元存在于测试类中、其超类型层次结构中或其封闭类层次结构中,则将使用默认的封闭配置继承模式。有关如何更改默认模式的详细信息,请参阅下面的提示。
提示:更改默认的封闭配置继承模式
默认的封闭配置继承模式是 INHERIT
,但可以通过将 spring.test.enclosing.configuration
JVM 系统属性设置为 OVERRIDE
来更改。或者,可以通过 SpringProperties (opens new window) 机制设置默认模式。
Spring 测试上下文框架会遵循 @NestedTestConfiguration
语义来处理以下注解:
@BootstrapWith
@ContextConfiguration
@WebAppConfiguration
@ContextHierarchy
@ContextCustomizerFactories
@ActiveProfiles
@TestPropertySource
@DynamicPropertySource
@DirtiesContext
@TestExecutionListeners
@RecordApplicationEvents
@Transactional
@Commit
@Rollback
@Sql
@SqlConfig
@SqlMergeMode
@TestConstructor
注意: @NestedTestConfiguration
的使用通常仅在与 JUnit Jupiter 中的 @Nested
测试类结合使用时才有意义;但是,可能有其他支持 Spring 和嵌套测试类的测试框架会使用此注解。
有关示例和更多详细信息,请参阅 @Nested 测试类配置 (opens new window)。
# e、@EnabledIf
@EnabledIf
用于表明,如果提供的 expression
计算结果为 true
,则带注释的 JUnit Jupiter 测试类或测试方法将被启用并应运行。具体来说,如果表达式计算结果为 Boolean.TRUE
或等于 true
的 String
(忽略大小写),则测试将被启用。当应用于类级别时,该类中的所有测试方法默认也会自动启用。
表达式可以是以下任何一种:
- Spring 表达式语言 (opens new window) (SpEL) 表达式。例如:
@EnabledIf("#{systemProperties['os.name'].toLowerCase().contains('mac')}")
- Spring Environment (opens new window) 中可用属性的占位符。例如:
@EnabledIf("${smoke.tests.enabled}")
- 文本字面量。例如:
@EnabledIf("true")
但是请注意,不是属性占位符动态解析结果的文本字面量没有实际价值,因为 @EnabledIf("false")
等同于 @Disabled
,而 @EnabledIf("true")
在逻辑上没有意义。
你可以将 @EnabledIf
用作元注解来创建自定义组合注解。例如,你可以按如下方式创建自定义 @EnabledOnMac
注解:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@EnabledIf(
expression = "#{systemProperties['os.name'].toLowerCase().contains('mac')}",
reason = "Enabled on Mac OS"
)
public @interface EnabledOnMac {}
注意: @EnabledOnMac
仅作为一个可能的示例。如果你有确切的用例,请使用 JUnit Jupiter 中内置的 @EnabledOnOs(MAC)
支持。
警告: 从 JUnit 5.7 开始,JUnit Jupiter 也有一个名为 @EnabledIf
的条件注解。因此,如果你想使用 Spring 的 @EnabledIf
支持,请确保从正确的包中导入注解类型。
# f、@DisabledIf
@DisabledIf
用于表明,如果提供的 expression
计算结果为 true
,则带注释的 JUnit Jupiter 测试类或测试方法将被禁用且不应运行。具体来说,如果表达式计算结果为 Boolean.TRUE
或等于 true
的 String
(忽略大小写),则测试将被禁用。当应用于类级别时,该类中的所有测试方法也会自动被禁用。
表达式可以是以下任何一种:
- Spring 表达式语言 (opens new window) (SpEL) 表达式。例如:
@DisabledIf("#{systemProperties['os.name'].toLowerCase().contains('mac')}")
- Spring Environment (opens new window) 中可用属性的占位符。例如:
@DisabledIf("${smoke.tests.disabled}")
- 文本字面量。例如:
@DisabledIf("true")
但是请注意,不是属性占位符动态解析结果的文本字面量没有实际价值,因为 @DisabledIf("true")
等同于 @Disabled
,而 @DisabledIf("false")
在逻辑上没有意义。
你可以将 @DisabledIf
用作元注解来创建自定义组合注解。例如,你可以按如下方式创建自定义 @DisabledOnMac
注解:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@DisabledIf(
expression = "#{systemProperties['os.name'].toLowerCase().contains('mac')}",
reason = "Disabled on Mac OS"
)
public @interface DisabledOnMac {}
注意: @DisabledOnMac
仅作为一个可能的示例。如果你有确切的用例,请使用 JUnit Jupiter 中内置的 @DisabledOnOs(MAC)
支持。
警告: 从 JUnit 5.7 开始,JUnit Jupiter 也有一个名为 @DisabledIf
的条件注解。因此,如果你想使用 Spring 的 @DisabledIf
支持,请确保从正确的包中导入注解类型。
# 2.6、测试的元注解支持
你可以将大多数与测试相关的注解用作 元注解,以创建自定义组合注解,并减少整个测试套件中的配置重复。
你可以结合 测试上下文框架,将以下每个注解用作元注解:
@BootstrapWith
@ContextConfiguration
@ContextHierarchy
@ContextCustomizerFactories
@ActiveProfiles
@TestPropertySource
@DirtiesContext
@WebAppConfiguration
@TestExecutionListeners
@Transactional
@BeforeTransaction
@AfterTransaction
@Commit
@Rollback
@Sql
@SqlConfig
@SqlMergeMode
@SqlGroup
@Repeat
(仅在 JUnit 4 中支持)@Timed
(仅在 JUnit 4 中支持)@IfProfileValue
(仅在 JUnit 4 中支持)@ProfileValueSourceConfiguration
(仅在 JUnit 4 中支持)@SpringJUnitConfig
(仅在 JUnit Jupiter 中支持)@SpringJUnitWebConfig
(仅在 JUnit Jupiter 中支持)@TestConstructor
(仅在 JUnit Jupiter 中支持)@NestedTestConfiguration
(仅在 JUnit Jupiter 中支持)@EnabledIf
(仅在 JUnit Jupiter 中支持)@DisabledIf
(仅在 JUnit Jupiter 中支持)
考虑以下示例:
@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 { }
如果我们发现自己在基于 JUnit 4 的测试套件中重复上述配置,我们可以通过引入一个自定义组合注解来减少重复,该注解集中了 Spring 的通用测试配置,如下所示:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
public @interface TransactionalDevTestConfig { }
然后,我们可以使用自定义的 @TransactionalDevTestConfig
注解来简化基于 JUnit 4 的各个测试类的配置,如下所示:
@RunWith(SpringRunner.class)
@TransactionalDevTestConfig
public class OrderRepositoryTests { }
@RunWith(SpringRunner.class)
@TransactionalDevTestConfig
public class UserRepositoryTests { }
如果我们编写使用 JUnit Jupiter 的测试,我们甚至可以进一步减少代码重复,因为 JUnit 5 中的注解也可以用作元注解。考虑以下示例:
@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 的通用测试配置,如下所示:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(SpringExtension.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
public @interface TransactionalDevTestConfig { }
然后,我们可以使用自定义的 @TransactionalDevTestConfig
注解来简化基于 JUnit Jupiter 的各个测试类的配置,如下所示:
@TransactionalDevTestConfig
class OrderRepositoryTests { }
@TransactionalDevTestConfig
class UserRepositoryTests { }
由于 JUnit Jupiter 支持将 @Test
、@RepeatedTest
、ParameterizedTest
等用作元注解,你还可以在测试方法级别创建自定义组合注解。例如,如果我们希望创建一个组合注解,将 JUnit Jupiter 中的 @Test
和 @Tag
注解与 Spring 中的 @Transactional
注解结合起来,我们可以创建一个 @TransactionalIntegrationTest
注解,如下所示:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Transactional
@Tag("integration-test") // org.junit.jupiter.api.Tag
@Test // org.junit.jupiter.api.Test
public @interface TransactionalIntegrationTest { }
然后,我们可以使用自定义的 @TransactionalIntegrationTest
注解来简化基于 JUnit Jupiter 的各个测试方法的配置,如下所示:
@TransactionalIntegrationTest
void saveOrder() { }
@TransactionalIntegrationTest
void deleteOrder() { }
有关更多详细信息,请参阅 Spring 注解编程模型 (opens new window) 的维基页面。
# 3、更多资源
JUnit (opens new window) “一款面向 Java 和 JVM 的程序员友好型测试框架”。Spring 框架在其测试套件中会使用该框架,并且在Spring TestContext 框架中也支持使用它。
TestNG (opens new window) 这是一款受 JUnit 启发的测试框架,它额外支持测试分组、数据驱动测试、分布式测试等多种功能。在Spring TestContext 框架中也支持使用该框架。
AssertJ (opens new window) “用于 Java 的流式断言”,支持 Java 8 中的 Lambda 表达式、流以及许多其他功能。在 Spring 的MockMvc 测试支持中支持使用该框架。
Mock Objects (opens new window) 维基百科上的相关文章。
Mockito (opens new window) 基于测试间谍模式的 Java 模拟库。Spring 框架在其测试套件中会使用该库。
EasyMock (opens new window) Java 库,“通过使用 Java 的代理机制动态生成模拟对象,从而为接口(以及通过类扩展为对象)提供模拟对象”。
Testcontainers (opens new window) 支持 JUnit 测试的 Java 库,可提供轻量级、一次性的常见数据库实例、Selenium 网络浏览器,或任何可以在 Docker 容器中运行的实例。
SpringMockK (opens new window) 支持使用 MockK (opens new window) 而非 Mockito 编写用 Kotlin 语言实现的 Spring Boot 集成测试。