本站(springdoc.cn)中的内容来源于 spring.io ,原始版权归属于 spring.io。由 springdoc.cn 进行翻译,整理。可供个人学习、研究,未经许可,不得进行任何转载、商用或与之相关的行为。 商标声明:Spring 是 Pivotal Software, Inc. 在美国以及其他国家的商标。

本章介绍了Spring对集成测试的支持和单元测试的最佳实践。Spring团队提倡测试驱动的开发(TDD)。Spring团队发现,正确使用反转控制(IoC)肯定会使单元测试和集成测试更容易(因为类上存在setter方法和适当的构造函数,使它们更容易在测试中连接在一起,而不需要设置服务定位器注册表(service locator register)和类似结构)。

1. Spring 测试简介

测试是企业软件开发的一个组成部分。本章重点介绍IoC原则对 单元测试的增值作用,以及Spring框架对 集成测试 支持的好处。(对企业测试的彻底处理超出了本参考手册的范围)。

2. 单元测试

与传统的J2EE/Java EE开发相比,依赖注入应该使你的代码对容器的依赖性降低。组成你的应用程序的POJOs应该可以在JUnit或TestNG测试中进行测试,通过使用 new 操作符来实例化对象,而不需要Spring或任何其他容器。你可以使用 mock 对象(结合其他有价值的测试技术)来孤立地测试你的代码。如果你遵循Spring的架构建议,由此产生的干净的分层和代码库的组件化会使单元测试更加容易。例如,你可以通过 stub 或 mock DAO 或 repositoy 接口来测试服务(service)层对象,在运行单元测试时不需要访问持久化数据。

真正的单元测试通常运行得非常快,因为没有运行时的基础设施需要设置。强调真正的单元测试是你开发方法的一部分,可以提高你的生产力。你可能不需要测试章节的这一部分来帮助你为基于IoC的应用程序编写有效的单元测试。然而,对于某些单元测试场景,Spring框架提供了模拟(mock)对象和测试支持类,本章对此进行了描述。

2.1. Mock 对象(Object)

Spring包括一些专门用于mock的包。

2.1.1. Environment

org.springframework.mock.env 包包含 EnvironmentPropertySource 抽象的mock实现(参见 Bean Definition ProfilePropertySource 抽象)。MockEnvironmentMockPropertySource 对于开发依赖环境特定属性的代码的容器外测试非常有用。

2.1.2. JNDI

org.springframework.mock.jndi 包包含JNDI SPI的部分实现,你可以用它来为测试套件或独立的应用程序建立一个简单的JNDI环境。例如,如果JDBC DataSource 实例在测试代码中与Jakarta EE容器中的JNDI名称相同,你就可以在测试场景中重复使用应用程序代码和配置,而无需修改。

org.springframework.mock.jndi 包中的 mock JNDI支持从Spring Framework 5.2开始正式废弃,转而采用第三方的完整解决方案,如 Simple-JNDI

2.1.3. Servlet API

org.springframework.mock.web 包包含了一套全面的Servlet APImock对象,对于测试Web上下文、controller 和 filter 非常有用。这些mock对象是针对Spring的Web MVC框架使用的,通常比动态mock对象(如 EasyMock)或其他Servlet API mock对象(如 MockObjects)使用起来更方便。

从Spring Framework 6.0开始,org.springframework.mock.web 中的mock对象是基于Servlet 6.0的API。

Spring MVC测试框架建立在 mock Servlet API对象的基础上,为Spring MVC提供一个集成测试框架。参见 MockMvc

2.1.4. Spring Web Reactive

org.springframework.mock.http.server.reactive 包包含了 ServerHttpRequestServerHttpResponse 的 mock 实现,可以在WebFlux应用程序中使用。org.springframework.mock.web.server 包包含一个模拟的 ServerWebExchange,它依赖于这些mock的request和response对象。

MockServerHttpRequestMockServerHttpResponse 都是从相同的抽象基类中延伸出来的server专用实现,并与它们共享行为。例如,一个mock request一旦被创建就是不可改变的,但是你可以使用 ServerHttpRequestmutate() 方法来创建一个修改的实例。

为了让 mock response 正确地实现写契约(contract)并返回一个写完成的句柄(即 Mono<Void>),它默认使用一个带有 cache().then()Flux,它缓冲了数据并使其可用于测试中的断言。应用程序可以设置一个自定义的写函数(例如,测试一个无限的流)。

WebTestClient 建立在 mock request 和 response 的基础上,为测试没有HTTP服务器的WebFlux应用提供支持。该客户端也可用于与运行中的服务器进行端到端测试。

2.2. 单元测试的支持类

Spring包括一些可以帮助单元测试的类。它们分为两类。

2.2.1. 一般的测试工具

org.springframework.test.util 包包含几个通用的工具,用于单元和集成测试。

AopTestUtils 是AOP相关实用方法的集合。你可以使用这些方法来获取隐藏在一个或多个Spring代理背后的底层目标对象的引用。例如,如果你通过使用 EasyMock 或Mockito等库将一个bean配置为动态mock,并且该mock被包装在Spring代理中,你可能需要直接访问底层mock,以便对其配置期望并执行验证。关于Spring的核心AOP工具,见 AopUtilsAopProxyUtils

ReflectionTestUtils 是一个基于反射的实用方法集合。你可以在测试场景中使用这些方法,在测试应用程序代码时,你需要改变一个常量的值,设置一个非 public 的字段,调用一个非 public 的setter方法,或者调用一个非 public 的配置或生命周期回调方法,其用例如下。

  • ORM框架(如JPA和Hibernate)容忍 privateprotected 的字段访问,而不是domain实体中属性的 public setter 方法。

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

  • 对生命周期回调方法使用注解,如 @PostConstruct@PreDestroy

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

TestSocketUtils 可以在集成测试中使用,在一个可用的随机端口上启动一个外部服务器。然而,这些工具不保证给定端口的后续可用性,因此是不可靠的。与其使用 TestSocketUtils 为服务器寻找一个可用的本地端口,我们建议你依靠服务器的能力,在它选择的或由操作系统分配的一个随机的短暂端口上启动。要与该服务器交互,你应该查询该服务器当前使用的端口。

2.2.2. Spring MVC 测试工具

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

Spring MVC Controller 单元测试
要将Spring MVC Controller 类作为POJO进行单元测试,可以使用 ModelAndViewAssert 与Spring的 Servlet API mock 中的 MockHttpServletRequestMockHttpSession 等相结合。对于Spring MVC和REST Controller 类与Spring MVC的 WebApplicationContext 配置的彻底集成测试,请使用 Spring MVC测试框架

3. 集成测试

能够执行一些集成测试而不需要部署到你的应用服务器或连接到其他企业基础设施是很重要的。这样做可以让你测试一些东西,例如。

  • 对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集成支持的高层次目标,本章的其余部分随后将集中讨论专门的主题。

3.1. 集成测试的目标

Spring的集成测试支持有以下主要目标。

接下来的几节描述了每个目标,并提供了实施和配置细节的链接。

3.1.1. 上下文(Context)管理和缓存

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

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

默认情况下,一旦加载,配置的 ApplicationContext 将在每个测试中重复使用。因此,每个测试套件只产生一次设置成本,随后的测试执行会快很多。在这里,术语 "测试套件" 是指在同一JVM中运行的所有测试—​例如,从Ant、Maven或Gradle构建中为特定项目或模块运行的所有测试。在不太可能的情况下,一个测试破坏了 application context,需要重新加载(例如,通过修改Bean定义或应用程序对象的状态),TestContext框架可以被配置为在执行下一个测试之前重新加载配置并重建 application context。

Context 管理上下文(Context)缓存 与 TestContext 框架。

3.1.2. Test Fixture 的依赖注入

当TestContext框架加载你的应用程序上下文时,它可以通过使用依赖性注入来配置你的测试类的实例。这提供了一个方便的机制,通过使用你的应用上下文中的预配置的Bean来设置测试fixture。这里有一个很大的好处是,你可以在不同的测试场景中重复使用应用上下文(例如,配置Spring管理的对象图、事务代理、DataSource 实例等),从而避免了为单个测试案例重复设置复杂的测试fixture。

举个例子,考虑一个场景,我们有一个类(HibernateTitleRepository),实现了 Title domain 实体的数据访问逻辑。我们想写集成测试来测试以下几个方面。

  • Spring的配置。基本上,与 HibernateTitleRepository Bean的配置有关的一切都正确且存在吗?

  • Hibernate的映射文件配置。所有的映射都是正确的,正确的懒加载设置也到位了吗?

  • HibernateTitleRepository 的逻辑。这个类的配置实例是否像预期的那样执行?

请看使用 TestContext 框架 的测试 fixture 的依赖注入。

3.1.3. 事务管理

在访问真实数据库的测试中,一个常见的问题是它们对持久性存储的状态的影响。即使你使用一个开发数据库,对状态的改变也可能影响到未来的测试。另外,许多操作—​如插入或修改持久化数据—​不能在事务之外执行(或验证)。

TestContext 框架解决了这个问题。默认情况下,该框架为每个测试创建和回滚一个事务。你可以编写代码,可以假设事务的存在。如果你在测试中调用事务代理的对象,它们的行为是正确的,根据它们配置的事务语义。此外,如果一个测试方法在为测试管理的事务中运行时删除了所选表的内容,那么该事务会默认回滚,数据库会返回到执行测试前的状态。事务支持是通过使用测试应用上下文中定义的 PlatformTransactionManager Bean提供给测试的。

如果你想让一个事务提交(不常见,但当你想让一个特定的测试填充或修改数据库时,偶尔会很有用),你可以通过使用 @Commit 注解来告诉 TestContext 框架使事务提交而不是回滚。

参见 TestContext框架 的事务管理。

3.1.4. 集成测试的支持类

Spring TestContext框架提供了几个 abstract 的支持类,简化了集成测试的编写。这些基础测试类为测试框架提供了定义明确的钩子(hook),以及方便的实例变量和方法,让你访问。

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

  • JdbcTemplate,用于执行查询数据库的SQL语句。你可以在执行数据库相关的应用程序代码之前和之后使用这种查询来确认数据库状态,Spring确保这种查询在与应用程序代码相同的事务范围内运行。当与ORM工具一起使用时,要注意避免 误报

此外,你可能想创建你自己的、适用于整个应用程序的超类(superclass),并为你的项目指定实例变量和方法。

参见 TestContext框架 的支持类。

4. JDBC测试的支持

4.1. JdbcTestUtils

org.springframework.test.jdbc 包包含 JdbcTestUtils,它是一个JDBC相关实用函数的集合,旨在简化标准数据库测试场景。具体来说,JdbcTestUtils 提供了以下静态实用方法。

  • countRowsInTable(..): 计算给定表中的行数。

  • countRowsInTableWhere(..): 通过使用提供的 WHERE 子句,计算给定表中的行数。

  • deleteFromTables(..): 删除指定表中的所有行。

  • deleteFromTableWhere(..): 通过使用提供的 WHERE 子句,从给定的表中删除记录。

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

AbstractTransactionalJUnit4SpringContextTestsAbstractTransactionalTestNGSpringContextTests 提供了方便的方法,委托给 JdbcTestUtils 中的上述方法。

4.2. 嵌入式数据库

spring-jdbc 模块提供对配置和启动嵌入式数据库的支持,你可以在与数据库交互的集成测试中使用它。详情请见 嵌入式数据库支持使用嵌入式数据库测试数据访问逻辑

5. Spring TestContext 框架

Spring TestContext 框架(位于 org.springframework.test.context 包中)提供通用的、注解驱动的单元和集成测试支持,与使用的测试框架无关。TestContext框架也非常重视惯例而不是配置,有合理的默认值,你可以通过基于注解的配置来覆盖。

除了通用测试基础设施,TestContext框架还为JUnit 4、JUnit Jupiter(又称JUnit 5)和TestNG提供明确的支持。对于JUnit 4和TestNG,Spring提供了 abstract 的支持类。此外,Spring为JUnit 4提供了自定义的JUnit Runner 和自定义的JUnit Rules,为JUnit Jupiter提供了自定义的 Extension,让你编写所谓的POJO测试类。POJO测试类不需要扩展特定的类层次结构,比如 abstract 支持类。

下面一节概述了TestContext框架的内部结构。如果你只对使用该框架感兴趣,而对用你自己的自定义监听器或自定义加载器来扩展它不感兴趣,可以直接去看配置(上下文管理依赖注入事务管理)、支持类注解支持 部分。

5.1. 关键摘要

该框架的核心由 TestContextManager 类和 TestContextTestExecutionListenerSmartContextLoader 接口组成。为每个测试类创建一个 TestContextManager(例如,为执行JUnit Jupiter中一个测试类中的所有测试方法)。 TestContextManager 反过来管理一个 TestContext,它持有当前测试的上下文。 TestContextManager 还随着测试的进展更新 TestContext 的状态,并委托给 TestExecutionListener 实现,该实现通过提供依赖注入、管理事务等来记录实际的测试执行。SmartContextLoader 负责为一个给定的测试类加载一个 ApplicationContext。参见 javadoc 和Spring测试套件,以获得进一步的信息和各种实现的例子。

5.1.1. TestContext

TestContext 封装了运行测试的上下文(与实际使用的测试框架无关),并为它所负责的测试实例提供上下文管理和缓存支持。TestContext 还委托给 SmartContextLoader,以便在请求时加载 ApplicationContext

5.1.2. TestContextManager

TestContextManager 是进入Spring TestContext 框架的主要入口,它负责管理单个 TestContext,并在明确定义的测试执行点向每个注册的 TestExecutionListener 发出事件信号。

  • 在某一测试框架的任何 “before class” 或 “before all” 方法之前。

  • 测试实例的后处理。

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

  • 在执行测试方法之前,但在测试设置之后立即进行。

  • 在执行测试方法之后,但在测试拆除之前,立即进行。

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

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

5.1.3. TestExecutionListener

TestExecutionListener 定义了对由监听器注册的 TestContextManager 发布的测试执行事件做出反应的API。参见 TestExecutionListener 配置

5.1.4. Context(上下文)加载器

ContextLoader 是一个策略接口,用于为Spring TestContext 框架管理的集成测试加载一个 ApplicationContext。你应该实现 SmartContextLoader 而不是这个接口,以提供对组件类、活动bean定义配置文件(active bean definition profile)、测试属性源、上下文层次结构和 WebApplicationContext 的支持。

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

Spring提供了以下实现。

  • DelegatingSmartContextLoader: 两个默认加载器之一,它在内部委托给 AnnotationConfigContextLoaderGenericXmlContextLoaderGenericGroovyXmlContextLoader,这取决于为测试类声明的配置或默认位置或默认配置类的存在。只有当Groovy在classpath上时,才会启用Groovy支持。

  • WebDelegatingSmartContextLoader: 两个默认加载器之一,它在内部委托给 AnnotationConfigWebContextLoaderGenericXmlWebContextLoaderGenericGroovyXmlWebContextLoader,取决于为测试类声明的配置或默认位置或默认配置类的存在。只有在测试类上存在 @WebAppConfiguration 的情况下才会使用Web ContextLoader。只有当Groovy在classpath上时,才会启用Groovy支持。

  • AnnotationConfigContextLoader: 从组件类加载一个标准的 ApplicationContext

  • AnnotationConfigWebContextLoader: 从组件类加载一个 WebApplicationContext

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

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

  • GenericXmlContextLoader: 从XML资源位置加载一个标准的 ApplicationContext

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

5.2. 启动 TestContext 框架

Spring TestContext 框架内部的默认配置足以满足所有常见的使用情况。然而,有时开发团队或第三方框架希望改变默认的 ContextLoader,实现自定义的 TestContextContextCache,增强 ContextCustomizerFactoryTestExecutionListener 实现的默认集,等等。对于这种对 TestContext 框架运行方式的底层控制,Spring提供了一个引导策略。

TestContextBootstrapper 定义了用于引导 TestContext 框架的SPI。 TestContextBootstrapperTestContextManager 用来加载当前测试的 TestExecutionListener 实现并构建它所管理的 TestContext。你可以通过使用 @BootstrapWith,直接或作为元注解,为一个测试类(或测试类层次结构)配置一个自定义的引导策略。如果没有通过使用 @BootstrapWith 明确地配置一个引导器(bootstrapper),那么就会使用 DefaultTestContextBootstrapper 或者 WebTestContextBootstrapper,这取决于 @WebAppConfiguration 的存在。

由于 TestContextBootstrapper 的SPI在未来可能会发生变化(以适应新的需求),我们强烈建议实现者不要直接实现这个接口,而是扩展 AbstractTestContextBootstrapper 或其具体子类之一。

5.3. TestExecutionListener 配置

Spring提供了以下 TestExecutionListener 的实现,这些实现在默认情况下被注册,完全按照以下顺序。

  • ServletTestExecutionListener: 为 WebApplicationContext 配置Servlet API mocks。

  • DirtiesContextBeforeModesTestExecutionListener: 处理 “before” 模式的 @DirtiesContext 注解。

  • ApplicationEventsTestExecutionListener: 提供对 ApplicationEvents 的支持。

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

  • DirtiesContextTestExecutionListener: 处理 “after” 模式的 @DirtiesContext 注解。

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

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

  • EventPublishingTestExecutionListener: 向测试的 ApplicationContext 发布测试执行事件(见 测试执行事件)。

5.3.1. 注册 TestExecutionListener 实现

你可以通过使用 @TestExecutionListeners 注解为一个测试类、它的子类和它的嵌套类明确地注册 TestExecutionListener 实现。请参阅 注解支持@TestExecutionListeners 的javadoc以了解细节和示例。

切换到默认的 TestExecutionListener 实现

如果你扩展了一个用 @TestExecutionListeners 注解的类,并且你需要切换到使用默认的监听器集合,你可以用下面的方法注解你的类。

Java
// Switch to default listeners
@TestExecutionListeners(
    listeners = {},
    inheritListeners = false,
    mergeMode = MERGE_WITH_DEFAULTS)
class MyTest extends BaseTest {
    // class body...
}
Kotlin
// Switch to default listeners
@TestExecutionListeners(
    listeners = [],
    inheritListeners = false,
    mergeMode = MERGE_WITH_DEFAULTS)
class MyTest : BaseTest {
    // class body...
}

5.3.2. 自动发现默认的 TestExecutionListener 实现

通过使用 @TestExecutionListeners 注册 TestExecutionListener 实现,适合于在有限的测试场景中使用的自定义监听器。然而,如果一个自定义监听器需要在整个测试套件中使用,它可能变得很麻烦。这个问题通过支持通过 SpringFactoriesLoader 机制自动发现默认 TestExecutionListener 实现来解决。

具体来说,spring-test 模块在其 META-INF/spring.factories properties文件的 org.springframework.test.context.TestExecutionListener key下声明了所有核心默认 TestExecutionListener 实现。第三方框架和开发者可以通过他们自己的 META-INF/spring.factories properties 文件,以同样的方式向默认监听器列表贡献他们自己的 TestExecutionListener 实现。

5.3.3. 对 TestExecutionListener 实现进行排序

当 TestContext 框架通过 上述 SpringFactoriesLoader 机制发现默认的 TestExecutionListener 实现时,通过使用Spring的 AnnotationAwareOrderComparator 对实例化的监听器进行排序,它尊重Spring的 Ordered 接口和 @Order 注解的排序。AbstractTestExecutionListener 和Spring提供的所有默认 TestExecutionListener 实现都使用适当的值实现了 Ordered。因此,第三方框架和开发人员应该确保他们的默认 TestExecutionListener 实现通过实现 Ordered 或声明 @Order 而以适当的顺序注册。参见核心默认 TestExecutionListener 实现的 getOrder() 方法的javadoc,以了解分配给每个核心监听器的值的细节。

5.3.4. 合并 TestExecutionListener 实现

如果通过 @TestExecutionListeners 注册了一个自定义的 TestExecutionListener,默认监听器就不会被注册。在大多数常见的测试场景中,这有效地迫使开发者在任何自定义监听器之外手动声明所有默认监听器。下面的列表演示了这种配置风格。

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

这种方法的挑战在于,它要求开发者确切地知道哪些监听器是默认注册的。此外,默认监听器的集合会随着版本的变化而变化—​例如,SqlScriptsTestExecutionListener 是在Spring Framework 4.1中引入的,而 DirtiesContextBeforeModesTestExecutionListener 是在Spring Framework 4.2中引入的。此外,Spring Boot和Spring Security等第三方框架通过使用上述的 自动发现机制 注册了自己的默认 TestExecutionListener 实现。

为了避免必须意识到并重新声明所有的默认监听器,你可以将 @TestExecutionListenersmergeMode 属性设置为 MergeMode.MERGE_WITH_DEFAULTSMERGE_WITH_DEFAULTS 表示本地声明的监听器应该与默认监听器合并。合并算法确保从列表中删除重复的内容,并确保合并后的监听器集合根据 AnnotationAwareOrderComparator 的语义进行排序,如 TestExecutionListener 实现进行排序 中所述。如果一个监听器实现了 Ordered 或者被 @Order 注解了,它可以影响它与默认值合并的位置。否则,本地声明的监听器在合并时将被追加到默认监听器的列表中。

例如,如果前面例子中的 MyCustomTestExecutionListener 类将其 order 值(例如 500)配置为小于 ServletTestExecutionListener 的顺序(刚好是 1000),那么 MyCustomTestExecutionListener 就可以自动与 ServletTestExecutionListener 前面的默认列表合并,前面的例子可以替换为以下内容。

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

5.4. Application Events

自Spring Framework 5.3.3以来,TestContext 框架提供了对记录 ApplicationContext 中发布的 application event 的支持,因此可以在测试中针对这些事件执行断言。在单个测试的执行过程中发布的所有事件都可以通过 ApplicationEvents API获得,该API允许你将事件作为 java.util.Stream 进行处理。

要在你的测试中使用 ApplicationEvents,请做以下工作。

  • 确保你的测试类有 @RecordApplicationEvents 的注解或元注解。

  • 确保 ApplicationEventsTestExecutionListener 被注册。但是请注意, ApplicationEventsTestExecutionListener 是默认注册的,只有当你通过 @TestExecutionListeners 的自定义配置不包括默认监听器时,才需要手动注册。

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

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

下面的测试类使用JUnit Jupiter的 SpringExtensionAssertJ 来断言在调用Spring管理的组件中的方法时发布的应用程序事件的类型。

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

    @Autowired
    OrderService orderService;

    @Autowired
    ApplicationEvents events; (2)

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

    @Autowired
    lateinit var orderService: OrderService

    @Autowired
    lateinit var events: ApplicationEvents (2)

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

请参阅 ApplicationEvents javadoc 以了解有关 ApplicationEvents API 的进一步细节。

5.5. 测试执行事件

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

  • BeforeTestClassEvent

  • PrepareTestInstanceEvent

  • BeforeTestMethodEvent

  • BeforeTestExecutionEvent

  • AfterTestExecutionEvent

  • AfterTestMethodEvent

  • AfterTestClassEvent

这些事件可以出于各种原因被消费,例如重置模拟Bean或追踪测试执行。消费测试执行事件而不是实现一个自定义的 TestExecutionListener 的一个优点是,测试执行事件可以被任何在测试 ApplicationContext 中注册的Spring Bean消费,这些Bean可以直接受益于依赖注入和 ApplicationContext 的其他功能。相反,TestExecutionListener 不是 ApplicationContext 中的一个Bean。

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

因此,BeforeTestClassEvent 将不会被发布,直到 ApplicationContext 被另一个 TestExecutionListener 加载之后。例如,在注册了一组默认的 TestExecutionListener 实现后,BeforeTestClassEvent 将不会被发布给使用特定测试 ApplicationContext 的第一个测试类,但 BeforeTestClassEvent 将被发布给同一测试套件中使用相同测试 ApplicationContext 的任何后续测试类,因为当后续测试类运行时,该上下文已经被加载(只要该上下文没有通过 @DirtiesContext 或最大尺寸驱逐策略从 ContextCache 移除)。

如果你希望确保 BeforeTestClassEvent 总是为每个测试类发布,你需要注册一个 TestExecutionListener,在 beforeTestClass 回调中加载 ApplicationContext,并且该 TestExecutionListener 必须在 EventPublishingTestExecutionListener 之前注册。

同样地,如果 @DirtiesContext 被用来在某个测试类的最后一个测试方法之后从上下文缓存中移除 ApplicationContextAfterTestClassEvent 将不会为该测试类发布。

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

  • @BeforeTestClass

  • @PrepareTestInstance

  • @BeforeTestMethod

  • @BeforeTestExecution

  • @AfterTestExecution

  • @AfterTestMethod

  • @AfterTestClass

5.5.1. 异常(Exception)处理

默认情况下,如果一个测试执行事件监听器在消费一个事件时抛出一个异常,这个异常将传播到正在使用的底层测试框架(如JUnit或TestNG)。例如,如果 BeforeTestMethodEvent 的消耗导致了一个异常,相应的测试方法就会因为这个异常而失败。相反,如果一个异步测试执行事件监听器抛出一个异常,该异常将不会传播到底层测试框架。关于异步异常处理的进一步细节,请参考 @EventListener 的类级javadoc。

5.5.2. 异步监听器

如果你想让一个特定的测试执行事件监听器异步处理事件,你可以使用 Spring 的 常规 @Async 支持。关于进一步的细节,请参考 @EventListener 的类级 javadoc。

5.6. Context 管理

每个 TestContext 为它所负责的测试实例提供上下文管理和缓存支持。测试实例不会自动收到对配置的 ApplicationContext 的访问。然而,如果一个测试类实现了 ApplicationContextAware 接口,对 ApplicationContext 的引用将提供给测试实例。请注意,AbstractJUnit4SpringContextTestsAbstractTestNGSpringContextTests 实现了 ApplicationContextAware,因此,自动提供对 ApplicationContext 的访问。

@Autowired ApplicationContext

作为实现 ApplicationContextAware 接口的替代方案,你可以通过 @Autowired 注解在字段或setter方法上为你的测试类注入应用上下文(application context),如下例所示。

Java
@SpringJUnitConfig
class MyTest {

    @Autowired (1)
    ApplicationContext applicationContext;

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

    @Autowired (1)
    lateinit var applicationContext: ApplicationContext

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

同样,如果你的测试被配置为加载一个 WebApplicationContext,你可以将 web application context 注入你的测试,如下所示。

Java
@SpringJUnitWebConfig (1)
class MyWebAppTest {

    @Autowired (2)
    WebApplicationContext wac;

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

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

通过使用 @Autowired 的依赖注入是由 DependencyInjectionTestExecutionListener 提供的,它是默认配置的(见 Test Fixture 的依赖注入)。

使用TestContext框架的测试类不需要扩展任何特定的类或实现一个特定的接口来配置他们的应用上下文。相反,配置是通过在类级别声明 @ContextConfiguration 注解来实现的。如果你的测试类没有明确声明应用上下文资源位置或组件类,那么配置的 ContextLoader 会决定如何从默认位置或默认配置类中加载上下文。除了上下文资源位置和组件类之外,应用上下文(application context)还可以通过应用上下文初始化器进行配置。

下面几节解释了如何使用Spring的 @ContextConfiguration 注解,通过使用XML配置文件、Groovy脚本、组件类(通常是 @Configuration 类)或上下文初始化器来配置测试 ApplicationContext。另外,你也可以为高级用例实现和配置自己的自定义 SmartContextLoader

5.6.1. 使用XML资源的上下文配置

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

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

@ContextConfiguration 通过标准的 Java value 属性支持 locations 属性的别名。因此,如果你不需要在 @ContextConfiguration 中声明额外的属性,你可以省略对 locations 属性名称的声明,并通过使用下面例子中演示的速记格式来声明资源位置。

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

如果你从 @ContextConfiguration 注解中省略了 locationsvalue 属性,TestContext 框架会尝试检测一个默认的 XML 资源位置。具体来说, GenericXmlContextLoaderGenericXmlWebContextLoader 会根据测试类的名称检测一个默认位置。如果你的类被命名为 com.example.MyTestGenericXmlContextLoader 会从 "classpath:com/example/MyTest-context.xml" 加载你的应用程序上下文。下面的例子显示了如何做到这一点。

Java
@ExtendWith(SpringExtension.class)
// ApplicationContext will be loaded from
// "classpath:com/example/MyTest-context.xml"
@ContextConfiguration (1)
class MyTest {
    // class body...
}
1 从默认位置加载配置。
Kotlin
@ExtendWith(SpringExtension::class)
// ApplicationContext will be loaded from
// "classpath:com/example/MyTest-context.xml"
@ContextConfiguration (1)
class MyTest {
    // class body...
}
1 从默认位置加载配置。

5.6.2. 用Groovy脚本进行 Context 配置

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

启用Groovy脚本支持
如果Groovy在classpath上,就会自动启用对使用Groovy脚本在Spring TestContext框架中加载 ApplicationContext 的支持。

下面的例子显示了如何指定Groovy配置文件。

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

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

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

你可以通过使用 @ContextConfigurationlocationsvalue 属性同时声明 XML 配置文件和 Groovy 脚本。如果配置的资源位置的路径以 .xml 结尾,它将通过使用 XmlBeanDefinitionReader 加载。否则,它将通过使用 GroovyBeanDefinitionReader 来加载。

下面的列表显示了如何在集成测试中结合两者。

Java
@ExtendWith(SpringExtension.class)
// ApplicationContext will be loaded from
// "/app-config.xml" and "/TestConfig.groovy"
@ContextConfiguration({ "/app-config.xml", "/TestConfig.groovy" })
class MyTest {
    // class body...
}
Kotlin
@ExtendWith(SpringExtension::class)
// ApplicationContext will be loaded from
// "/app-config.xml" and "/TestConfig.groovy"
@ContextConfiguration("/app-config.xml", "/TestConfig.groovy")
class MyTest {
    // class body...
}

5.6.3. 使用组件类的 Context 配置

要通过使用组件类为你的测试加载 ApplicationContext(见 基于Java的容器配置),你可以用 @ContextConfiguration 注解你的测试类,并用一个包含对组件类引用的数组配置 classes 属性。下面的例子显示了如何做到这一点。

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

术语 "组件类" 可以指以下任何一种。

  • 一个用 @Configuration 注解的类。

  • 一个组件(即一个用 @Component@Service@Repository 或其他 stereotype 注解的类)。

  • 一个符合JSR-330标准的类,它被 jakarta.inject 注解所注解。

  • 任何包含 @Bean 方法的类。

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

请参阅 @Configuration@Bean 的 javadoc,了解有关组件类的配置和语义的进一步信息,特别注意 @Bean Lite 模式的讨论。

如果你省略了 @ContextConfiguration 注解中的 classes 属性,TestContext 框架会尝试检测默认配置类的存在。具体来说,AnnotationConfigContextLoaderAnnotationConfigWebContextLoader 会检测测试类的所有 static 嵌套类,这些类符合配置类实现的要求,如 @Configuration javadoc 中所规定的。请注意,配置类的名称是任意的。此外,如果需要,一个测试类可以包含一个以上的 static 嵌套配置类。在下面的例子中,OrderServiceTest 类声明了一个名为 Configstatic 嵌套配置类,它被自动用于加载测试类的 ApplicationContext

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

    @Configuration
    static class Config {

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

    @Autowired
    OrderService orderService;

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

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

    @Autowired
    lateinit var orderService: OrderService

    @Configuration
    class Config {

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

    @Test
    fun testOrderService() {
        // test the orderService
    }
}
1 从嵌套的 Config 类中加载配置信息。

5.6.4. 混合使用 XML、Groovy 脚本和组件类

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

此外,一些第三方框架(如Spring Boot)为同时从不同类型的资源(例如,XML配置文件、Groovy脚本和 @Configuration 类)加载 ApplicationContext 提供了一流的支持。从历史上看,Spring框架并不支持标准部署。因此,Spring框架在 spring-test 模块中提供的大多数 SmartContextLoader 实现,对每个测试上下文只支持一种资源类型。然而,这并不意味着你不能同时使用两种资源。一般规则的一个例外是, GenericGroovyXmlContextLoaderGenericGroovyXmlWebContextLoader 同时支持XML配置文件和Groovy脚本。此外,第三方框架可以选择通过 @ContextConfiguration 来支持 locationsclasses 的声明,而且,在 TestContext 框架的标准测试支持下,你有以下选择。

如果你想使用资源位置(例如,XML或Groovy)和 @Configuration 类来配置你的测试,你必须选择一个作为入口点,而且这个入口点必须包括或导入另一个。例如,在XML或Groovy脚本中,你可以通过使用组件扫描或将它们定义为正常的Spring Bean来包含 @Configuration 类,而在 @Configuration 类中,你可以使用 @ImportResource 来导入XML配置文件或Groovy脚本。请注意,这种行为在语义上等同于你在生产中配置你的应用程序的方式。在生产配置中,你定义了一组XML或Groovy资源位置或一组 @Configuration 类,你的生产 ApplicationContext 就是从这里加载的,但你仍然可以自由地包含或导入其他类型的配置。

5.6.5. 带有 Context 初始化器(Initializers) 的 Context 配置

要通过使用上下文初始化器为你的测试配置 ApplicationContext,用 @ContextConfiguration 注解你的测试类,并用一个数组配置 initializers 属性,其中包含对实现 ApplicationContextInitializer 的类的引用。然后声明的上下文初始化器被用来初始化为你的测试加载的 ConfigurableApplicationContext。注意,每个声明的初始化器所支持的具体的 ConfigurableApplicationContext 类型必须与使用中的 SmartContextLoader 所创建的 ApplicationContext 类型兼容(通常是 GenericApplicationContext)。此外,初始化器被调用的顺序取决于它们是否实现了Spring的 Ordered 接口或被Spring的 @Order 注解或标准的 @Priority 注解所注解。下面的例子展示了如何使用初始化器。

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

你也可以完全省略 @ContextConfiguration 中对XML配置文件、Groovy脚本或组件类的声明,而只声明 ApplicationContextInitializer 类,然后由这些类负责在上下文中注册Bean—​例如,通过编程方式从XML文件或配置类加载Bean定义。下面的例子展示了如何做到这一点。

Java
@ExtendWith(SpringExtension.class)
// ApplicationContext will be initialized by EntireAppInitializer
// which presumably registers beans in the context
@ContextConfiguration(initializers = EntireAppInitializer.class) (1)
class MyTest {
    // class body...
}
1 只使用一个初始化器(initializer)来指定配置。
Kotlin
@ExtendWith(SpringExtension::class)
// ApplicationContext will be initialized by EntireAppInitializer
// which presumably registers beans in the context
@ContextConfiguration(initializers = [EntireAppInitializer::class]) (1)
class MyTest {
    // class body...
}
1 只使用一个初始化器(initializer)来指定配置。

5.6.6. Context Configuration 的继承性

@ContextConfiguration 支持 boolean inheritLocationsinheritInitializers 属性,表示是否应该继承超类所声明的资源位置或组件类和上下文初始化器(initializers)。这两个标志的默认值是 true。这意味着测试类会继承任何超类所声明的资源位置或组件类,以及上下文初始化器。具体来说,一个测试类的资源位置或组件类被附加到超类所声明的资源位置或注解类的列表中。同样地,一个给定的测试类的初始化器被添加到测试超类定义的初始化器集合中。因此,子类可以选择扩展资源位置、组件类或上下文初始化器。

如果 @ContextConfiguration 中的 inheritLocationsinheritInitializers 属性被设置为 false,那么测试类的资源位置或组件类和上下文初始化器就会分别产生阴影,并有效地取代由超类定义的配置。

从Spring Framework 5.3开始,测试配置也可以从包围的类中继承下来。详见 @Nested 测试类的配置

在下一个使用 XML 资源位置的例子中,ExtendedTestApplicationContext 依次从 base-config.xmlextended-config.xml 中加载。因此,在 extended-config.xml 中定义的 Bean 可以覆盖(也就是替换)base-config.xml 中定义的 Bean。下面的例子显示了一个类如何扩展另一个类并同时使用它自己的配置文件和超类的配置文件。

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

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

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

同样,在下一个使用组件类的例子中,ExtendedTestApplicationContext 依次从 BaseConfigExtendedConfig 类加载。因此,在 ExtendedConfig 中定义的 Bean 可以覆盖(也就是替换)BaseConfig 中定义的 Bean。下面的例子显示了一个类如何扩展另一个类并同时使用它自己的配置类和超类的配置类。

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

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

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

在下一个使用上下文初始化器(context initializer)的例子中,ExtendedTestApplicationContext 是通过使用 BaseInitializerExtendedInitializer 来初始化的。但是请注意,初始化器被调用的顺序取决于它们是否实现了Spring 的 Ordered 接口,或被Spring的 @Order 注解或标准的 @Priority 注解所注解。下面的例子显示了一个类如何扩展另一个类并同时使用自己的初始化器和超类的初始化器(initializer)。

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

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

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

5.6.7. 使用 Environment Profiles 的上下文(Context )配置

Spring框架对环境和配置文件(AKA "bean definition profiles")的概念有一流的支持,集成测试可以被配置为激活各种测试场景的特定bean definition profiles。这可以通过用 @ActiveProfiles 注解来实现,并提供一个在加载测试的 ApplicationContext 时应该被激活的配置文件(profiles)列表。

你可以在 SmartContextLoader SPI 的任何实现中使用 @ActiveProfiles,但 @ActiveProfiles 不支持旧的 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>
Java
@ExtendWith(SpringExtension.class)
// ApplicationContext will be loaded from "classpath:/app-config.xml"
@ContextConfiguration("/app-config.xml")
@ActiveProfiles("dev")
class TransferServiceTest {

    @Autowired
    TransferService transferService;

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

    @Autowired
    lateinit var transferService: TransferService

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

TransferServiceTest 运行时,它的 ApplicationContext 是从classpath根目录下的 app-config.xml 配置文件中加载的。如果你检查 app-config.xml,你可以看到 accountRepository bean对 dataSource bean 有依赖性。然而, dataSource 并没有被定义为顶级Bean。相反,dataSource 被定义了三次:在 production 配置文件、dev 配置文件和 default 配置文件中。

通过用 @ActiveProfiles("dev") 注解 TransferServiceTest,我们指示Spring TestContext 框架加载 ApplicationContext,并将活动配置文件设置为 {"dev"}。结果是,一个嵌入式数据库被创建并填充了测试数据,而且 accountRepository Bean被装配到了开发 DataSource 的引用。这可能就是我们在集成测试中想要的。

有时,将bean分配到一个 default 的配置文件(profile)是很有用的。只有在没有特别激活其他配置文件的情况下,默认配置文件中的Bean才会被包含。你可以用它来定义 "后备" Bean,以便在应用程序的默认状态下使用。例如,你可以明确地为 devproduction 配置文件提供一个数据源,但当这两个配置文件都没有激活时,你可以定义一个内存数据源作为默认。

下面的代码列表演示了如何用 @Configuration 类而不是XML实现相同的配置和集成测试。

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

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

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

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

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

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

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

    @Autowired DataSource dataSource;

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

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

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

    @Autowired
    lateinit var dataSource: DataSource

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

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

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

    @Autowired
    TransferService transferService;

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

    @Autowired
    lateinit var transferService: TransferService

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

下面的代码列表演示了如何用 @Configuration 类而不是XML实现相同的配置和集成测试。

  • TransferServiceConfig: 通过使用 @Autowired,通过依赖注入获得一个 dataSource

  • StandaloneDataConfig: 定义了一个适合开发者测试的嵌入式数据库的 dataSource

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

  • DefaultDataConfig: 为默认的嵌入式数据库定义一个 dataSource,以备没有配置文件(profile)时使用。

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

通常的情况是,在一个给定的项目中,一组配置文件被用于多个测试类。因此,为了避免重复声明 @ActiveProfiles 注解,你可以在基类上声明一次 @ActiveProfiles,子类自动从基类继承 @ActiveProfiles 配置。在下面的例子中,@ActiveProfiles 的声明(以及其他注解)已经被移到了 abstract 的超类 AbstractIntegrationTest

从 Spring Framework 5.3 开始,测试配置也可以从包围的类中继承下来。详见 @Nested 测试类的配置
Java
@SpringJUnitConfig({
        TransferServiceConfig.class,
        StandaloneDataConfig.class,
        JndiDataConfig.class,
        DefaultDataConfig.class})
@ActiveProfiles("dev")
abstract class AbstractIntegrationTest {
}
Kotlin
@SpringJUnitConfig(
        TransferServiceConfig::class,
        StandaloneDataConfig::class,
        JndiDataConfig::class,
        DefaultDataConfig::class)
@ActiveProfiles("dev")
abstract class AbstractIntegrationTest {
}
Java
// "dev" profile inherited from superclass
class TransferServiceTest extends AbstractIntegrationTest {

    @Autowired
    TransferService transferService;

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

    @Autowired
    lateinit var transferService: TransferService

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

@ActiveProfiles 还支持一个 inheritProfiles 属性,可以用来禁用活动配置文件(active profile)的继承,如下例所示。

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

此外,有时需要以编程方式而不是声明方式解决测试的活动配置文件(active profile)--例如,基于:

  • 当前的操作系统。

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

  • 某些环境变量的存在。

  • 自定义类级注解的存在。

  • 其他关注的问题。

为了以编程方式解析活动bean定义配置文件(profile),你可以实现一个自定义的 ActiveProfilesResolver 并通过使用 @ActiveProfilesresolver 属性来注册它。有关进一步的信息,请参阅相应的 javadoc。下面的示例演示了如何实现和注册一个自定义 OperatingSystemActiveProfilesResolver

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

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

    override fun resolve(testClass: Class<*>): Array<String> {
        val profile: String = ...
        // determine the value of profile based on the operating system
        return arrayOf(profile)
    }
}

5.6.8. 带有测试属性源(Property Sources)的 Context 配置

Spring框架对具有属性源层次结构的环境概念有一流的支持,你可以用测试特定的属性源配置集成测试。与 @Configuration 类上使用的 @PropertySource 注解不同,你可以在测试类上声明 @TestPropertySource 注解,以声明测试属性文件或内联属性的资源位置。这些测试属性源被添加到 Environment 中的 PropertySources 集合中,用于加载注解的集成测试的 ApplicationContext

你可以在 SmartContextLoader SPI的任何实现中使用 @TestPropertySource,但 @TestPropertySource 不支持旧的 ContextLoader SPI的实现。

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

声明测试属性源(Property Sources)

你可以通过使用 @TestPropertySourcelocationsvalue 属性来配置测试属性(properties)文件。

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

每个路径都被解释为一个Spring Resource。一个普通的路径(例如,"test.properties")被视为一个classpath resource,它与定义测试类的包相对。以斜线开头的路径被视为绝对的classpath resource(例如:"/org/example/test.xml")。引用URL的路径(例如,以 classpath:file:http: 为前缀的路径)通过使用指定的 resource 协议加载。 resource位置通配符(如 */.properties)是不允许的:每个位置必须精确地评估为一个 .properties.xml resource。

下面的例子使用了一个测试属性文件(test properties file):

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

你可以通过使用 @TestPropertySourceproperties 属性,以键值对的形式配置内联属性,如下例所示。所有的键值对被添加到包围的 Environment 中,作为一个具有最高优先级的单一测试 PropertySource

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

  • key=value

  • key:value

  • key value

下面的例子设置了两个内联的属性:

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

从Spring Framework 5.2开始,@TestPropertySource 可以作为可重复注解使用。这意味着你可以在一个测试类上有多个 @TestPropertySource 的声明,后面的 @TestPropertySource 注解的 locationsproperties 会覆盖之前的 @TestPropertySource 注解的内容。

此外,你可以在一个测试类上声明多个组成注解,每个注解都用 @TestPropertySource 进行元注解,所有这些 @TestPropertySource 声明都会对你的测试属性源做出贡献。

直接存在的 @TestPropertySource 注解总是优先于元存在的 @TestPropertySource 注解。换句话说,来自直接存在的 @TestPropertySource 注解的 locationsproperties 将覆盖来自作为元注解的 @TestPropertySource 注解的 locationsproperties

默认属性(Properties)文件检测

如果 @TestPropertySource 被声明为一个空注解(也就是说,没有明确的 locationsproperties 值),就会尝试检测一个相对于声明该注解的类的默认属性文件。例如,如果被注解的测试类是 com.example.MyTest,相应的默认属性文件是 classpath:com/example/MyTest.properties。如果不能检测到默认,就会抛出一个 IllegalStateException

优先级

测试属性(properties)比在操作系统环境中定义的属性、Java系统属性或应用程序通过使用 @PropertySource 声明性地或以编程方式添加的属性源有更高的优先权。因此,测试属性可以被用来选择性地覆盖从系统和应用程序属性源加载的属性。此外,内联的属性比从资源位置加载的属性有更高的优先权。然而,请注意,通过 @DynamicPropertySource 注册的属性比通过 @TestPropertySource 加载的属性有更高的优先权。

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

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

@TestPropertySource 支持布尔值 inheritLocationsinheritProperties 属性,表示是否应该继承由超类声明的属性文件和内联属性的资源 locations。这两个标志的默认值是 true。这意味着测试类会继承任何超类所声明的 locations 和内联属性。具体来说,测试类的 locations 和内联属性被附加到超类所声明的 locations 和内联属性上。因此,子类可以选择扩展 locations 和内联属性。注意,后面出现的属性会影射(也就是覆盖)前面出现的同名属性。此外,前面提到的优先规则也适用于继承的测试属性源。

如果 @TestPropertySource 中的 inheritLocationsinheritProperties 属性被设置为 false,那么测试类的位置或内联属性就会分别产生阴影(shadow),并有效地取代由超类定义的配置。

从Spring Framework 5.3开始,测试配置也可以从包围的类中继承下来。详见 @Nested 测试类的配置

在下一个例子中,BaseTestApplicationContext 是通过仅使用 base.properties 文件作为测试属性源加载的。与此相反,ExtendedTestApplicationContext 是通过使用 base.propertiesextended.properties 文件作为测试属性源 locations 来加载的。下面的例子显示了如何通过使用 properties 文件在子类和其超类中定义属性:

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

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

@TestPropertySource("extended.properties")
@ContextConfiguration
class ExtendedTest : BaseTest() {
    // ...
}

在下一个例子中,BaseTestApplicationContext 只通过使用内联的 key1 属性被加载。与此相反,ExtendedTestApplicationContext 是通过使用内联的 key1key2 属性加载的。下面的例子显示了如何通过使用内联属性在子类和它的超类中定义属性:

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

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

@TestPropertySource(properties = ["key2 = value2"])
@ContextConfiguration
class ExtendedTest : BaseTest() {
    // ...
}

5.6.9. 使用动态属性源的 Context 配置

从Spring Framework 5.2.5开始,TestContext框架通过 @DynamicPropertySource 注解提供了对动态属性的支持。这个注解可用于集成测试,它需要将具有动态值的属性添加到集成测试加载的 ApplicationContextEnvironment 中的 PropertySources 集合。

@DynamicPropertySource 注解及其支持的基础结构最初是为了让基于 Testcontainers 的测试的属性能够轻松地暴露给Spring集成测试。然而,该功能也可用于任何形式的外部资源,其生命周期在测试的 ApplicationContext 之外被维护。

与应用于类级别的 @TestPropertySource 注解不同,@DynamicPropertySource 必须应用于一个 static 方法,该方法接受一个 DynamicPropertyRegistry 参数,用于向 Environment 添加name-value对。值是动态的,并通过一个 Supplier 提供,该 Supplier 只有在属性被解析时才会被调用。通常情况下,方法引用被用来提供值,正如在下面的例子中所看到的,它使用 Testcontainers 项目来管理Spring ApplicationContext 之外的Redis容器。管理的Redis容器的IP地址和端口通过 redis.hostredis.port 属性提供给测试的 ApplicationContext 中的组件。这些属性可以通过Spring的 Environment 抽象访问,也可以直接注入Spring管理的组件中—​例如,分别通过 @Value("${redis.host}")@Value("${redis.port}")

如果你在基类中使用了 @DynamicPropertySource,并且发现子类中的测试因为动态属性在子类之间改变而失败,你可能需要用 @DirtiesContext 来注解你的基类,以确保每个子类得到它自己的具有正确动态属性的 ApplicationContext

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

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

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

    // tests ...

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

    companion object {

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

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

    // tests ...

}
优先级

动态属性比那些从 @TestPropertySource、操作系统环境、Java 系统属性、或由应用程序通过使用 @PropertySource 声明性地或以编程方式添加的属性源加载的属性具有更高的优先权。因此,动态属性可以用来选择性地覆盖通过 @TestPropertySource、系统属性源和应用程序属性源加载的属性。

5.6.10. 加载 WebApplicationContext

为了指示 TestContext 框架加载一个 WebApplicationContext 而不是标准的 ApplicationContext,你可以用 @WebAppConfiguration 来注解各自的测试类。

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

请注意,Spring对 WebApplicationContext 实现的测试支持与它对标准 ApplicationContext 实现的支持是一样的。当使用 WebApplicationContext 进行测试时,你可以通过使用 @ContextConfiguration 来自由声明XML配置文件、Groovy脚本或 @Configuration 类。你也可以自由地使用任何其他测试注解,如 @ActiveProfiles@TestExecutionListeners@Sql@Rollback 等。

本节剩下的例子展示了一些加载 WebApplicationContext 的各种配置选项。下面的例子展示了TestContext框架对惯例的支持,而不是配置:

Java
@ExtendWith(SpringExtension.class)

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

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

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

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

如果你用 @WebAppConfiguration 来注解一个测试类,而没有指定资源基础路径,那么资源路径实际上默认为 file:src/main/webapp。同样地,如果你声明了 @ContextConfiguration 而没有指定资源 locations、组件 classes 或 context initializers,Spring会尝试通过惯例(即 WacTests-context.xmlWacTests 类或静态嵌套的 @Configuration 类在同一个包里)来检测你的配置的存在。

下面的例子显示了如何用 @WebAppConfiguration 显式地声明资源基本路径和用 @ContextConfiguration 声明XML资源 location:

Java
@ExtendWith(SpringExtension.class)

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

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

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

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

这里需要注意的是这两个注解的路径的不同语义。默认情况下,@WebAppConfiguration 资源路径是基于文件系统的,而 @ContextConfiguration 资源位置是基于classpath的。

下面的例子表明,我们可以通过指定 Spring resource prefix 来覆盖这两个注解的默认资源语义:

Java
@ExtendWith(SpringExtension.class)

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

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

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

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

将这个例子中的注释与上一个例子进行对比。

5.6.11. 使用Web Mock

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

一旦你为你的测试加载了 WebApplicationContext,你可能会发现你需要与Web mocks交互—​例如,设置你的测试夹具或在调用你的Web组件后执行断言。下面的例子显示了哪些模拟可以被自动装配到你的测试实例。请注意,WebApplicationContextMockServletContext 都在整个测试套件中被缓存,而其他 mock 是由 ServletTestExecutionListener 管理每个测试方法。

Java
@SpringJUnitWebConfig
class WacTests {

    @Autowired
    WebApplicationContext wac; // cached

    @Autowired
    MockServletContext servletContext; // cached

    @Autowired
    MockHttpSession session;

    @Autowired
    MockHttpServletRequest request;

    @Autowired
    MockHttpServletResponse response;

    @Autowired
    ServletWebRequest webRequest;

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

    @Autowired
    lateinit var wac: WebApplicationContext // cached

    @Autowired
    lateinit var servletContext: MockServletContext // cached

    @Autowired
    lateinit var session: MockHttpSession

    @Autowired
    lateinit var request: MockHttpServletRequest

    @Autowired
    lateinit var response: MockHttpServletResponse

    @Autowired
    lateinit var webRequest: ServletWebRequest

    //...
}

5.6.12. 上下文(Context)缓存

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

ApplicationContext 可以由用于加载它的配置参数的组合来唯一地识别。因此,配置参数的唯一组合被用来生成一个key,在这个key下,上下文被缓存。TestContext框架使用以下配置参数来建立上下文缓存key:

  • locations (来自 @ContextConfiguration)

  • classes (来自 @ContextConfiguration)

  • contextInitializerClasses (来自 @ContextConfiguration)

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

  • contextLoader (来自 @ContextConfiguration)

  • parent (来自 @ContextHierarchy)

  • activeProfiles (来自 @ActiveProfiles)

  • propertySourceLocations (来自 @TestPropertySource)

  • propertySourceProperties (来自 @TestPropertySource)

  • resourceBasePath (来自 @WebAppConfiguration)

例如,如果 TestClassA@ContextConfigurationlocation(或 value)属性指定了 {"app-config.xml", "test-config.xml"},TestContext框架就会加载相应的 ApplicationContext 并将其存储在 static context缓存中,该缓存的key只基于这些 locations。因此,如果 TestClassB 也定义了 {"app-config.xml", "test-config.xml"} 作为它的locations(无论是显式还是隐式通过继承),但没有定义 @WebAppConfiguration、不同的 ContextLoader、不同的活动配置文件(active profile)、不同的context初始化器(initializers)、不同的测试属性源或不同的父context,那么两个测试类就共享同一个 ApplicationContext。这意味着加载 application context 的设置成本只发生一次(每个测试套件),随后的测试执行会快很多。

测试套件和fork进程

Spring TestContext框架在静态缓存中存储应用上下文。这意味着上下文实际上是存储在一个 static 变量中。换句话说,如果测试在不同的进程中运行,静态缓存在每次测试执行之间被清除,这有效地禁用了缓存机制。

为了从缓存机制中获益,所有的测试必须在同一进程或测试套件中运行。这可以通过在IDE中作为一个组执行所有测试来实现。同样,当使用 Ant、Maven 或 Gradle 等构建框架执行测试时,必须确保构建框架在测试之间不fork。例如,如果 Maven Surefire 插件的 forkMode 设置为 alwayspertest,TestContext 框架就不能在测试类之间缓存应用上下文,构建过程的运行速度会因此而大大降低。

上下文缓存的大小是有限制的,默认最大大小为32。每当达到最大容量时,就会使用最近使用最少的驱逐策略(LRU)来驱逐和关闭陈旧的上下文。你可以通过设置名为 spring.test.context.cache.maxSize 的JVM系统属性,从命令行或构建脚本中配置最大尺寸。作为替代方法,你可以通过 SpringProperties 机制设置相同的属性。

由于在一个给定的测试套件中加载大量的应用程序上下文会导致套件运行时间过长,因此准确了解有多少上下文被加载和缓存往往是有益的。要查看底层上下文缓存的统计数据,你可以将 org.springframework.test.context.cache 日志类别的日志级别设为 DEBUG

在不太可能发生的情况下,测试破坏了应用程序上下文并需要重新加载(例如,通过修改Bean定义或应用程序对象的状态),你可以用 @DirtiesContext 来注解你的测试类或测试方法(见 Spring测试注解 中关于 @DirtiesContext 的讨论)。这将指示Spring在运行需要相同应用上下文的下一个测试之前,从缓存中删除上下文并重建应用上下文。请注意,对 @DirtiesContext 注解的支持是由 DirtiesContextBeforeModesTestExecutionListenerDirtiesContextTestExecutionListener 提供的,它们默认是启用的。

ApplicationContext生命周期和控制台日志

当你需要调试一个用Spring TestContext框架执行的测试时,分析控制台输出(即输出到 SYSOUTSYSERR 流)是很有用的。一些构建工具和IDE能够将控制台输出与给定的测试相关联;然而,一些控制台输出不能轻易与给定的测试相关联。

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

测试的 ApplicationContext 通常在准备测试类的实例时被加载—​例如,对测试实例的 @Autowired 字段进行依赖注入。这意味着在 ApplicationContext 的初始化过程中触发的任何控制台日志通常不能与单个测试方法相关。然而,如果根据 @DirtiesContext 的语义,在执行测试方法之前立即关闭上下文,那么在执行测试方法之前将加载一个新的上下文实例。在后一种情况下,IDE 或构建工具可能会将控制台 logging 与单个测试方法联系起来。

一个测试的 ApplicationContext 可以通过以下情况之一关闭。

  • 根据 @DirtiesContext 的语义,该上下文被关闭。

  • 上下文被关闭是因为它已经根据LRU驱逐策略被自动从缓存中驱逐了。

  • 当测试套件的JVM终止时,该上下文通过 JVM shutdown hook 关闭。

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

当Spring ApplicationContext 通过 JVM shutdown hook 关闭时,在关机阶段执行的回调会在一个名为 SpringContextShutdownHook 的线程上执行。因此,如果你希望禁用 ApplicationContext 通过 JVM shutdown hook 关闭时触发的控制台日志,你可以在日志框架中注册一个自定义过滤器,允许你忽略由该线程发起的任何日志。

5.6.13. 上下文(Context)层次结构

当编写依赖于加载的Spring ApplicationContext 的集成测试时,针对单个上下文进行测试通常是足够的。然而,有些时候,针对 ApplicationContext 实例的层次结构进行测试是有益的,甚至是必要的。例如,如果你正在开发一个Spring MVC Web应用程序,你通常有一个由Spring的 ContextLoaderListener 加载的root WebApplicationContext 和一个由Spring的 DispatcherServlet 加载的子 WebApplicationContext。这导致了一个父子上下文层次结构,其中共享组件和基础设施配置在根上下文中被声明,并在子上下文中被特定的Web组件消费。另一个用例可以在Spring批处理(Batch)应用程序中找到,在那里你通常有一个为共享批处理基础设施提供配置的父上下文和一个为特定批处理作业配置的子上下文。

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

本节中剩余的基于JUnit Jupiter的例子展示了集成测试的常见配置场景,需要使用上下文层次。

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

ControllerIntegrationTests 代表了一个典型的Spring MVC Web应用程序的集成测试场景,它声明了一个由两层组成的上下文层次,一个是root WebApplicationContext(通过使用 TestAppConfig @Configuration 类加载),一个是调度器Servlet WebApplicationContext(通过使用 WebConfig @Configuration 类加载)。被自动装配到测试实例的 WebApplicationContext 是子上下文(即层次结构中最低的上下文)。下面的列表显示了这种配置情况:

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

    @Autowired
    WebApplicationContext wac;

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

    @Autowired
    lateinit var wac: WebApplicationContext

    // ...
}

具有隐性父级上下文的类层次结构

本例中的测试类在测试类层次结构中定义了一个上下文层次结构。AbstractWebTests 声明了Spring驱动的Web应用中root WebApplicationContext 的配置。但是请注意, AbstractWebTests 并没有声明 @ContextHierarchy。因此,AbstractWebTests 的子类可以选择性地参与上下文层次结构,或遵循 @ContextConfiguration 的标准语义。 SoapWebServiceTestsRestWebServiceTests 都扩展了 AbstractWebTests,并通过使用 @ContextHierarchy 定义了一个上下文层次结构。其结果是,三个应用程序上下文被加载(@ContextConfiguration 的每个声明都有一个),基于 AbstractWebTests 中的配置加载的应用程序上下文被设置为具体子类加载的每个上下文的父级上下文。下面的列表显示了这种配置情况:

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

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

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

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

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

具有合并上下文层次结构配置的类层次结构

这个例子中的类显示了命名的层次结构级别的使用,以便合并上下文层次结构中特定级别的配置。 BaseTests 在层次结构中定义了两个级别,即 parentchildExtendedTests 扩展了 BaseTests,并指示 Spring TestContext Framework 合并 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"} 加载的上下文的父上下文。下面的列表显示了这种配置情况:

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

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

@ContextHierarchy(
    ContextConfiguration(name = "child", locations = ["/order-config.xml"])
)
class ExtendedTests : BaseTests() {}

类的层次结构与覆盖的上下文层次结构配置

与前面的例子相反,这个例子演示了如何通过将 @ContextConfiguration 中的 inheritLocations 标志设置为 false 来覆盖上下文层次结构中某个指定级别的配置。因此,ExtendedTests 的应用上下文只从 /test-user-config.xml 中加载,并且其父级设置为从 /app-config.xml 中加载的上下文。下面的列表显示了这种配置情况:

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

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

@ContextHierarchy(
        ContextConfiguration(
                name = "child",
                locations = ["/test-user-config.xml"],
                inheritLocations = false
        ))
class ExtendedTests : BaseTests() {}
Dirtying a context within a context hierarchy
如果你在一个测试中使用 @DirtiesContext,其上下文被配置为上下文层次结构的一部分,你可以使用 hierarchyMode 标志来控制如何清除上下文缓存。关于进一步的细节,请参见 Spring Testing Annotations 中关于 @DirtiesContext 的讨论以及 @DirtiesContext javadoc。

5.7. Test Fixture 的依赖注入

当你使用 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) 来进行显式查找。

如果你不希望依赖注入应用于你的测试实例,就不要用 @Autowired@Inject 来注解字段或 setter 方法。另外,你可以通过明确地用 @TestExecutionListeners 来配置你的类,并从监听器列表中省略 DependencyInjectionTestExecutionListener.class 来完全禁用依赖注入。

考虑测试 HibernateTitleRepository 类的情景,如 目标 部分所述。接下来的两个代码列表演示了 @Autowired 在字段和 setter 方法上的使用。application context 配置在所有示例代码列表之后呈现。

以下代码列表中的依赖注入行为并不是针对JUnit Jupiter的。同样的DI技术可以与任何支持的测试框架一起使用。

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

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

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

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

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

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

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

另外,你也可以配置类,使其使用 @Autowired 进行 setter 注入,如下所示:

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

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

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

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

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

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

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

前面的代码列表使用了由 @ContextConfiguration 注解引用的同一个 XML 上下文文件(也就是 repository-config.xml)。下面显示了这个配置:

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

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

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

</beans>

如果你是从一个Spring提供的测试基类中扩展出来的,而这个基类恰好在它的一个setter方法上使用了 @Autowired,你可能在你的 application context 中定义了多个受影响类型的Bean(例如,多个 DataSource Bean)。在这种情况下,你可以覆盖setter方法,并使用 @Qualifier 注解来表示特定的目标Bean,如下所示(但要确保在超类中也委托给被覆盖的方法):

Java
// ...

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

// ...
Kotlin
// ...

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

// ...

指定的qualifier值表示要注入的特定 DataSource bean,将类型匹配的范围缩小到特定的bean。其值与相应的 <bean> 定义中的 <qualifier> 声明相匹配。Bean名称被用作后备qualifier值,所以你也可以有效地通过名称指向特定的Bean(如前面所示,假设 myDataSource 是Bean的 id)。

5.8. 测试 Request 和 Session scope 的 Bean

Spring早年就支持 Request 和 Session scope 的 Bean,你可以通过以下步骤测试你的 Request 和 Session scope 的 Bean:

  • 通过用 @WebAppConfiguration 注解你的测试类,确保为你的测试加载一个 WebApplicationContext

  • 将 mock request 或 session 注入到你的测试实例中,并适当地准备你的测试 fixture。

  • 调用你从配置的 WebApplicationContext 中获取的Web组件(使用依赖注入)。

  • 对mock进行断言。

下一个代码片断显示了登录用例的XML配置。请注意,userService Bean对 request scope 的 loginAction Bean有依赖性。另外,LoginAction 是通过使用 SpEL表达式 来实例化的,它从当前的HTTP请求中获取用户名和密码。在我们的测试中,我们想通过TestContext框架管理的mock来配置这些请求参数。下面的列表显示了这个用例的配置:

Request scope 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 中设置请求参数来设置我们的测试fixture。当 loginUser() 方法在我们的 userService 上被调用时,我们确信用户服务可以访问当前 MockHttpServletRequest 的 request scope loginAction(也就是我们刚刚设置参数的那个)。然后我们可以根据已知的用户名和密码的输入,对结果进行断言。下面的列表显示了如何做到这一点:

Java
@SpringJUnitWebConfig
class RequestScopedBeanTests {

    @Autowired UserService userService;
    @Autowired MockHttpServletRequest request;

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

        LoginResults results = userService.loginUser();
        // assert results
    }
}
Kotlin
@SpringJUnitWebConfig
class RequestScopedBeanTests {

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

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

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

下面的代码片段与我们之前看到的request scope Bean的代码片段相似。然而,这一次, userService Bean对 session scope 的 userPreferences Bean有依赖性。请注意, UserPreferences Bean是通过使用一个SpEL表达式来实例化的,该表达式从当前的HTTP会话中检索出theme。在我们的测试中,我们需要在 TestContext 框架所管理的 mock session 中配置一个theme。下面的例子展示了如何做到这一点:

Session scope bean 配置
<beans>

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

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

</beans>

SessionScopedBeanTests 中,我们将 UserServiceMockHttpSession 注入到我们的测试实例。在我们的 sessionScope() 测试方法中,我们通过在提供的 MockHttpSession 中设置预期的 theme 属性来设置我们的测试fixture。当 processUserPreferences() 方法在我们的 userService 上被调用时,我们确信 user service 可以访问当前 MockHttpSession 的session scope userPreferences,并且我们可以根据配置的 theme 对结果进行断言。下面的例子展示了如何做到这一点:

Java
@SpringJUnitWebConfig
class SessionScopedBeanTests {

    @Autowired UserService userService;
    @Autowired MockHttpSession session;

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

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

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

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

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

5.9. 事务管理

在TestContext框架中,事务是由 TransactionalTestExecutionListener 管理的,即使你没有在你的测试类上明确声明 @TestExecutionListeners,它也是默认配置的。然而,要启用对事务的支持,你必须在 ApplicationContext 中配置一个 PlatformTransactionManager Bean,该bean以 @ContextConfiguration 语义加载(后面会提供进一步细节)。此外,你必须在测试的类或方法级别声明Spring的 @Transactional 注解。

5.9.1. 测试管理的事务

测试管理的事务是通过使用 TransactionalTestExecutionListener 声明性地管理的事务,或通过使用 TestTransaction(后面描述)程序性地管理的事务。你不应该把这种事务与Spring管理的事务(那些直接由Spring在测试加载的 ApplicationContext 中管理的事务)或应用程序管理的事务(那些在被测试调用的应用程序代码中以编程方式管理的事务)混淆。Spring管理的和应用管理的事务通常参与测试管理的事务。然而,如果Spring管理的或应用管理的事务被配置为除 REQUIREDSUPPORTS 之外的任何传播类型,你应该谨慎行事(详见关于 事务传播 的讨论)。

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

当从测试框架中使用任何形式的抢占式超时与Spring的测试管理事务相结合时,必须谨慎行事。

具体来说,Spring的测试支持在当前测试方法被调用之前将事务状态绑定到当前线程(通过 java.lang.ThreadLocal 变量)。如果测试框架在一个新的线程中调用当前的测试方法,以支持抢占式超时,在当前测试方法中执行的任何操作都不会在测试管理的事务中被调用。因此,任何此类操作的结果都不会随测试管理的事务回滚。相反,这些操作将被提交到持久性存储—​例如,关系数据库—​即使测试管理的事务被Spring正确地回滚。

可能发生这种情况的情况包括但不限于以下情况。

  • JUnit 4 的 @Test(timeout = …​) 支持和 TimeOut 规则。

  • org.junit.jupiter.api.Assertions 类中 JUnit Jupiter 的 assertTimeoutPreemptively(…​) 方法

  • TestNG 的 @Test(timeOut = …​) 支持

5.9.2. 启用和停用事务

@Transactional 来注解一个测试方法会使测试在一个事务中运行,默认情况下,在测试完成后会自动回滚。如果一个测试类被 @Transactional 注解,该类层次结构中的每个测试方法都会在事务中运行。没有用 @Transactional 注解的测试方法(在类或方法级别)不会在事务中运行。注意,@Transactional 不支持测试生命周期方法—​例如,用 JUnit Jupiter 的 @BeforeAll@BeforeEach 等注解的方法。此外,被 @Transactional 注解但 propagation 属性被设置为 NOT_SUPPORTEDNEVER 的测试也不会在事务中运行。

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

valuetransactionManager

yes

propagation

只支持 Propagation.NOT_SUPPORTEDPropagation.NEVER

isolation

no

timeout

no

readOnly

no

rollbackForrollbackForClassName

no: 使用 TestTransaction.flagForRollback() 代替

noRollbackFornoRollbackForClassName

no: 使用 TestTransaction.flagForCommit() 代替

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

如果你需要在事务中运行套装级(suite-level)或类级(class-level)生命周期方法中的代码,你可能希望将相应的 PlatformTransactionManager 注入到你的测试类中,然后将其与 TransactionTemplate 一起用于编程式事务管理。

请注意,AbstractTransactionalJUnit4SpringContextTestsAbstractTransactionalTestNGSpringContextTests 是在类的层面上预设了事务支持。

下面的例子演示了为基于Hibernate的 UserRepository 编写集成测试的一个常见场景:

Java
@SpringJUnitConfig(TestConfig.class)
@Transactional
class HibernateUserRepositoryTests {

    @Autowired
    HibernateUserRepository repository;

    @Autowired
    SessionFactory sessionFactory;

    JdbcTemplate jdbcTemplate;

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

    @Test
    void createUser() {
        // track initial state in test database:
        final int count = countRowsInTable("user");

        User user = new User(...);
        repository.save(user);

        // Manual flush is required to avoid false positive in test
        sessionFactory.getCurrentSession().flush();
        assertNumUsers(count + 1);
    }

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

    private void assertNumUsers(int expected) {
        assertEquals("Number of rows in the [user] table.", expected, countRowsInTable("user"));
    }
}
Kotlin
@SpringJUnitConfig(TestConfig::class)
@Transactional
class HibernateUserRepositoryTests {

    @Autowired
    lateinit var repository: HibernateUserRepository

    @Autowired
    lateinit var sessionFactory: SessionFactory

    lateinit var jdbcTemplate: JdbcTemplate

    @Autowired
    fun setDataSource(dataSource: DataSource) {
        this.jdbcTemplate = JdbcTemplate(dataSource)
    }

    @Test
    fun createUser() {
        // track initial state in test database:
        val count = countRowsInTable("user")

        val user = User()
        repository.save(user)

        // Manual flush is required to avoid false positive in test
        sessionFactory.getCurrentSession().flush()
        assertNumUsers(count + 1)
    }

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

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

正如在 事务的回滚和提交行为 中所解释的,在 createUser() 方法运行后,没有必要清理数据库,因为对数据库所做的任何改变都会被 TransactionalTestExecutionListener 自动回滚。

5.9.3. 事务的回滚和提交行为

默认情况下,测试事务将在测试完成后自动回滚;然而,事务提交和回滚行为可以通过 @Commit@Rollback 注解来声明性地配置。更多细节见 注解支持 部分的相应条目。

5.9.4. 编程式事务管理

你可以通过使用 TestTransaction 的静态方法与测试管理的事务进行编程式的交互。例如,你可以在测试方法、before方法和after方法中使用 TestTransaction 来开始或结束当前测试管理的事务,或者配置当前测试管理的事务进行回滚或提交。只要启用 TransactionalTestExecutionListener,对 TestTransaction 的支持就自动可用。

下面的例子演示了 TestTransaction 的一些功能。更多细节请参见 TestTransaction 的javadoc。

Java
@ContextConfiguration(classes = TestConfig.class)
public class ProgrammaticTransactionManagementTests extends
        AbstractTransactionalJUnit4SpringContextTests {

    @Test
    public void transactionalTest() {
        // assert initial state in test database:
        assertNumUsers(2);

        deleteFromTables("user");

        // changes to the database will be committed!
        TestTransaction.flagForCommit();
        TestTransaction.end();
        assertFalse(TestTransaction.isActive());
        assertNumUsers(0);

        TestTransaction.start();
        // perform other actions against the database that will
        // be automatically rolled back after the test completes...
    }

    protected void assertNumUsers(int expected) {
        assertEquals("Number of rows in the [user] table.", expected, countRowsInTable("user"));
    }
}
Kotlin
@ContextConfiguration(classes = [TestConfig::class])
class ProgrammaticTransactionManagementTests : AbstractTransactionalJUnit4SpringContextTests() {

    @Test
    fun transactionalTest() {
        // assert initial state in test database:
        assertNumUsers(2)

        deleteFromTables("user")

        // changes to the database will be committed!
        TestTransaction.flagForCommit()
        TestTransaction.end()
        assertFalse(TestTransaction.isActive())
        assertNumUsers(0)

        TestTransaction.start()
        // perform other actions against the database that will
        // be automatically rolled back after the test completes...
    }

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

5.9.5. 在事务之外运行代码

偶尔,你可能需要在事务性测试方法之前或之后运行某些代码,但在事务性上下文之外—​例如,在运行测试之前验证初始数据库状态,或在测试运行后验证预期的事务性提交行为(如果测试被配置为提交事务)。TransactionalTestExecutionListener 支持 @BeforeTransaction@AfterTransaction 注解,正是为了这种情况。你可以用这些注解之一来注解测试类中的任何 void 方法或测试接口中的任何 void 默认方法,TransactionalTestExecutionListener 确保你的事务前方法或事务后方法在适当的时间运行。

任何之前的方法(比如用JUnit Jupiter的 @BeforeEach 注解的方法)和任何之后的方法(比如用JUnit Jupiter的 @AfterEach 注解的方法)都在事务中运行。此外,对于没有被配置为在事务中运行的测试方法,用 @BeforeTransaction@AfterTransaction 注解的方法不会被运行。

5.9.6. 配置 Transaction Manager

TransactionalTestExecutionListener 期望在测试的Spring ApplicationContext 中定义一个 PlatformTransactionManager Bean。如果在测试的 ApplicationContext 中存在多个 PlatformTransactionManager 的实例,你可以通过使用 @Transactional("myTxMgr")@Transactional(transactionManager = "myTxMgr") 声明一个qualifier,或者 TransactionManagementConfigurer 可以通过 @Configuration 类来实现。咨询 TestContextTransactionUtils.retrieveTransactionManager() 的 javadoc 以了解用于在测试的 ApplicationContext 中查找事务管理器的算法细节。

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

下面这个基于JUnit Jupiter的例子显示了一个虚构的集成测试场景,突出了所有与事务相关的注解。这个例子不是为了展示最佳实践,而是为了展示如何使用这些注解。更多信息和配置实例请参见 注解支持 部分。用于 @Sql 的事务管理 包含一个额外的例子,该例子将 @Sql 用于具有默认事务回滚语义的声明性SQL脚本执行。下面的例子显示了相关的注解:

Java
@SpringJUnitConfig
@Transactional(transactionManager = "txMgr")
@Commit
class FictitiousTransactionalTest {

    @BeforeTransaction
    void verifyInitialDatabaseState() {
        // logic to verify the initial state before a transaction is started
    }

    @BeforeEach
    void setUpTestDataWithinTransaction() {
        // set up test data within the transaction
    }

    @Test
    // overrides the class-level @Commit setting
    @Rollback
    void modifyDatabaseWithinTransaction() {
        // logic which uses the test data and modifies database state
    }

    @AfterEach
    void tearDownWithinTransaction() {
        // run "tear down" logic within the transaction
    }

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

}
Kotlin
@SpringJUnitConfig
@Transactional(transactionManager = "txMgr")
@Commit
class FictitiousTransactionalTest {

    @BeforeTransaction
    fun verifyInitialDatabaseState() {
        // logic to verify the initial state before a transaction is started
    }

    @BeforeEach
    fun setUpTestDataWithinTransaction() {
        // set up test data within the transaction
    }

    @Test
    // overrides the class-level @Commit setting
    @Rollback
    fun modifyDatabaseWithinTransaction() {
        // logic which uses the test data and modifies database state
    }

    @AfterEach
    fun tearDownWithinTransaction() {
        // run "tear down" logic within the transaction
    }

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

}
在测试ORM代码时避免误报

当你测试操纵Hibernate会话或JPA持久化上下文状态的应用程序代码时,确保在运行该代码的测试方法中flush底层工作单元。不flush底层工作单元会产生误报结果: 你的测试通过了,但同样的代码在实际的生产环境中却抛出了异常。注意,这适用于任何维护内存工作单元的ORM框架。在下面这个基于Hibernate的测试案例中,一个方法显示了一个误报,而另一个方法正确地暴露了 flush session 的结果:

Java
// ...

@Autowired
SessionFactory sessionFactory;

@Transactional
@Test // no expected exception!
public void falsePositive() {
    updateEntityInHibernateSession();
    // False positive: an exception will be thrown once the Hibernate
    // Session is finally flushed (i.e., in production code)
}

@Transactional
@Test(expected = ...)
public void updateWithSessionFlush() {
    updateEntityInHibernateSession();
    // Manual flush is required to avoid false positive in test
    sessionFactory.getCurrentSession().flush();
}

// ...
Kotlin
// ...

@Autowired
lateinit var sessionFactory: SessionFactory

@Transactional
@Test // no expected exception!
fun falsePositive() {
    updateEntityInHibernateSession()
    // False positive: an exception will be thrown once the Hibernate
    // Session is finally flushed (i.e., in production code)
}

@Transactional
@Test(expected = ...)
fun updateWithSessionFlush() {
    updateEntityInHibernateSession()
    // Manual flush is required to avoid false positive in test
    sessionFactory.getCurrentSession().flush()
}

// ...

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

Java
// ...

@PersistenceContext
EntityManager entityManager;

@Transactional
@Test // no expected exception!
public void falsePositive() {
    updateEntityInJpaPersistenceContext();
    // False positive: an exception will be thrown once the JPA
    // EntityManager is finally flushed (i.e., in production code)
}

@Transactional
@Test(expected = ...)
public void updateWithEntityManagerFlush() {
    updateEntityInJpaPersistenceContext();
    // Manual flush is required to avoid false positive in test
    entityManager.flush();
}

// ...
Kotlin
// ...

@PersistenceContext
lateinit var entityManager:EntityManager

@Transactional
@Test // no expected exception!
fun falsePositive() {
    updateEntityInJpaPersistenceContext()
    // False positive: an exception will be thrown once the JPA
    // EntityManager is finally flushed (i.e., in production code)
}

@Transactional
@Test(expected = ...)
void updateWithEntityManagerFlush() {
    updateEntityInJpaPersistenceContext()
    // Manual flush is required to avoid false positive in test
    entityManager.flush()
}

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

与关于在测试ORM代码时避免 误报 的说明类似,如果你的应用程序使用了实体生命周期回调(也称为实体监听器),请确保在运行该代码的测试方法中flush底层工作单元。如果没有flush或clear底层工作单元,会导致某些生命周期回调不被调用。

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

下面的例子展示了如何刷新 EntityManager 以确保在实体被持久化时调用 @PostPersist 回调。一个带有 @PostPersist 回调方法的实体监听器已经为本例中使用的 Person 实体注册。

Java
// ...

@Autowired
JpaPersonRepository repo;

@PersistenceContext
EntityManager entityManager;

@Transactional
@Test
void savePerson() {
    // EntityManager#persist(...) results in @PrePersist but not @PostPersist
    repo.save(new Person("Jane"));

    // Manual flush is required for @PostPersist callback to be invoked
    entityManager.flush();

    // Test code that relies on the @PostPersist callback
    // having been invoked...
}

// ...
Kotlin
// ...

@Autowired
lateinit var repo: JpaPersonRepository

@PersistenceContext
lateinit var entityManager: EntityManager

@Transactional
@Test
fun savePerson() {
    // EntityManager#persist(...) results in @PrePersist but not @PostPersist
    repo.save(Person("Jane"))

    // Manual flush is required for @PostPersist callback to be invoked
    entityManager.flush()

    // Test code that relies on the @PostPersist callback
    // having been invoked...
}

// ...

参见Spring框架测试套件中的 JpaEntityListenerTests,了解使用所有JPA生命周期回调的工作实例。

5.10. 执行SQL脚本

当针对关系型数据库编写集成测试时,运行SQL脚本来修改数据库模式或向表中插入测试数据往往是有益的。spring-jdbc 模块通过在加载 Spring ApplicationContext 时执行SQL脚本,为初始化嵌入式或现有数据库提供了支持。详情参见 嵌入式数据库支持使用嵌入式数据库测试数据访问逻辑

尽管在加载 ApplicationContext 时初始化一次用于测试的数据库是非常有用的,但有时在集成测试期间能够修改数据库是非常必要的。下面的章节解释了如何在集成测试期间以编程方式和声明方式运行SQL脚本。

5.10.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 可能比后面描述的其他一些替代方法更适合你的需要。更多细节请参见 javadoc 中关于 ScriptUtils 的各个方法。

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

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

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

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

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

方法级的 @Sql 声明默认会覆盖类级的声明。然而,从Spring Framework 5.2开始,这种行为可以通过 @SqlMergeMode 对每个测试类或每个测试方法进行配置。更多细节请参见 @SqlMergeMode 合并和覆盖配置
路径资源(Path Resource)语义

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

下面的例子显示了如何在一个基于JUnit Jupiter的集成测试类中,在类级和方法级使用 @Sql

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

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

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

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

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

如果没有指定SQL脚本或语句,将尝试检测一个 default 脚本,这取决于 @Sql 的声明位置。如果不能检测到默认脚本,就会抛出一个 IllegalStateException

  • 类级声明: 如果被注解的测试类是 com.example.MyTest,相应的默认脚本是 classpath:com/example/MyTest.sql

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

声明多个 @Sql 集合

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

下面的例子显示了如何使用 @Sql 作为Java 8的可重复注解:

Java
@Test
@Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`"))
@Sql("/test-user-data.sql")
void userTest() {
    // run code that uses the test schema and test data
}
Kotlin
// Repeatable annotations with non-SOURCE retention are not yet supported by Kotlin

在前面的例子中介绍的情况下,test-schema.sql 脚本对单行注释使用了不同的语法。

下面的例子与前面的例子相同,只是 @Sql 的声明被分组在 @SqlGroup 中。在Java 8及以上版本中,@SqlGroup 的使用是可选的,但你可能需要使用 @SqlGroup 来与其他JVM语言(如Kotlin)兼容。

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

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

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

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

@SqlConfig 进行脚本配置

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

@Sql@SqlConfig 提供的配置选项与 ScriptUtilsResourceDatabasePopulator 支持的配置选项相当,但它们是 <jdbc:initialize-database/> XML命名空间元素提供的配置选项的超集。详情请参见 @Sql@SqlConfig 中单个属性的javadoc。

用于 @Sql 的事务管理

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

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

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

    final JdbcTemplate jdbcTemplate;

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

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

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

    void assertNumUsers(int expected) {
        assertEquals(expected, countRowsInTable("user"),
            "Number of rows in the [user] table.");
    }
}
Kotlin
@SpringJUnitConfig(TestDatabaseConfig::class)
@Transactional
class TransactionalSqlScriptsTests @Autowired constructor(dataSource: DataSource) {

    val jdbcTemplate: JdbcTemplate = JdbcTemplate(dataSource)

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

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

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

注意,在运行 usersTest() 方法后,不需要清理数据库,因为对数据库所做的任何改变(无论是在测试方法中还是在 /test-data.sql 脚本中)都会被 TransactionalTestExecutionListener 自动回滚(详见 transaction management)。

@SqlMergeMode 合并和覆盖配置

从Spring Framework 5.2 开始,可以将方法级 @Sql 声明与类级声明合并。例如,这允许你在每个测试类中提供数据库schema的配置或一些常见的测试数据,然后在每个测试方法中提供额外的、特定于用例的测试数据。要启用 @Sql 合并,用 @SqlMergeMode(MERGE) 注解你的测试类或测试方法。要禁用特定测试方法(或特定测试子类)的合并,你可以通过 @SqlMergeMode(OVERRIDE) 切换回默认模式。请查阅 @SqlMergeMode 注解文档部分,以了解示例和更多细节。

5.11. 并行测试执行

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

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

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

如果是测试,请不要并行运行测试:

  • 使用Spring框架的 @DirtiesContext 支持。

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

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

  • 改变共享服务或系统的状态,如数据库、消息代理、文件系统和其他。这同时适用于嵌入式和外部系统。

如果并行测试执行失败,出现异常,说明当前测试的 ApplicationContext 不再活动,这通常意味着 ApplicationContext 在不同的线程中被从 ContextCache 中删除。

这可能是由于使用了 @DirtiesContext 或由于从 ContextCache 中自动驱逐。如果 @DirtiesContext 是罪魁祸首,你需要找到一种方法来避免使用 @DirtiesContext,或者将这类测试排除在并行执行之外。如果已经超过了 ContextCache 的最大尺寸,你可以增加缓存的最大尺寸。详见关于 上下文缓存 的讨论。

在Spring TestContext框架中,只有当底层的 TestContext 实现提供一个拷贝构造函数时,平行测试执行才有可能,正如 TestContext 的javadoc所解释的那样。Spring中使用的 DefaultTestContext 提供了这样一个构造函数。然而,如果你使用一个提供自定义 TestContext 实现的第三方库,你需要验证它是否适合于并行测试执行。

5.12. TestContext 框架支持类

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

5.12.1. Spring JUnit 4 Runner

Spring TestContext框架通过本节描述了支持Spring TestContext框架的各种类.自定义runner(在JUnit 4.12 或更高版本上支持),提供了与JUnit 4的完全集成。通过用 @RunWith(SpringJUnit4ClassRunner.class) 或更短的 @RunWith(SpringRunner.class) 变体来注解测试类,开发者可以实现基于JUnit 4的标准单元和集成测试,同时获得TestContext框架的好处,如支持加载应用上下文、测试实例的依赖注入、事务性测试方法执行等。如果你想将Spring TestContext框架与其他runner(如JUnit 4的 Parameterized runner)或第三方runner(如 MockitoJUnitRunner)一起使用,你可以选择性地使用 Spring 对 JUnit rule 的支持

下面的代码列表显示了配置一个测试类与自定义Spring Runner 一起运行的最低要求:

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

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

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

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

5.12.2. Spring JUnit 4 Rule

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

  • SpringClassRule

  • SpringMethodRule

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

SpringRunner 相比,Spring基于rule的JUnit支持具有独立于任何 org.junit.runner.Runner 实现的优势,因此可以与现有的 alternative runner(如JUnit 4的 Parameterized)或第三方 runner(如 MockitoJUnitRunner)相结合。

为了支持TestContext框架的全部功能,你必须把 SpringClassRuleSpringMethodRule 结合起来。下面的例子显示了在集成测试中声明这些规则的正确方式:

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

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

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

    @Test
    public void testMethod() {
        // test logic...
    }
}
Kotlin
// Optionally specify a non-Spring Runner via @RunWith(...)
@ContextConfiguration
class IntegrationTest {

    @Rule
    val springMethodRule = SpringMethodRule()

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

    companion object {
        @ClassRule
        val springClassRule = SpringClassRule()
    }
}

5.12.3. JUnit 4 支持类

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

  • AbstractJUnit4SpringContextTests

  • AbstractTransactionalJUnit4SpringContextTests

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

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

这些类是扩展的一种便利。如果你不希望你的测试类被束缚在Spring特定的类层次中,你可以通过使用 @RunWith(SpringRunner.class)Spring 的 JUnit rule 配置你自己的自定义测试类。

5.12.4. JUnit Jupiter 的 SpringExtension

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

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

  • 测试构造函数、测试方法和测试生命周期回调方法的依赖注入。更多细节请参见 SpringExtension 依赖注入

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

  • 自定义组成的注解,结合Spring和JUnit Jupiter的注解。更多细节请参见 对测试的元注解支持 中的 @TransactionalDevTestConfig@TransactionalIntegrationTest 示例。

下面的代码列表显示了如何配置一个测试类,使其与 @ContextConfiguration 一起使用 SpringExtension

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

    @Test
    void testMethod() {
        // test logic...
    }
}
Kotlin
// Instructs JUnit Jupiter to extend the test with Spring support.
@ExtendWith(SpringExtension::class)
// Instructs Spring to load an ApplicationContext from TestConfig::class
@ContextConfiguration(classes = [TestConfig::class])
class SimpleTests {

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

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

下面的例子使用 @SpringJUnitConfig 来减少前面例子中的配置量:

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

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

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

类似地,下面的例子使用 @SpringJUnitWebConfig 来创建一个 WebApplicationContext 以用于 JUnit Jupiter:

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

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

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

更多细节请参见 Spring JUnit Jupiter 测试注解@SpringJUnitConfig@SpringJUnitWebConfig 的文档。

SpringExtension 依赖注入

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

具体来说,SpringExtension 可以将测试的 ApplicationContext 中的依赖注入到用 @BeforeAll@AfterAll@BeforeEach@AfterEach@Test@RepeatedTest@ParameterizedTest 等注解的测试构造器和方法中。

构造函数注入

如果JUnit Jupiter测试类的构造函数中的特定参数是 ApplicationContext 类型(或其子类型),或被 @Autowired@Qualifier@Value 注解或元注解,Spring会用测试的 ApplicationContext 中的相应Bean或值来注入该特定参数的值。

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

  • 该构造函数用 @Autowired 注解。

  • @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交互,这可能导致错误。

要在 "测试方法前 "或 "测试方法后" 模式下与 @TestInstance(PER_CLASS) 一起使用 @DirtiesContext,必须配置来自Spring的依赖关系,通过字段或setter注入来提供,这样它们就可以在测试方法调用之间被重新注入。

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

Java
@SpringJUnitConfig(TestConfig.class)
class OrderServiceIntegrationTests {

    private final OrderService orderService;

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

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

请注意,这个功能让测试的依赖关系是 final 的,因此是不可改变的。

如果 spring.test.constructor.autowire.mode 属性为 all(见 @TestConstructor),我们可以省略前面例子中对构造函数的 @Autowired 声明,结果如下。

Java
@SpringJUnitConfig(TestConfig.class)
class OrderServiceIntegrationTests {

    private final OrderService orderService;

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

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

如果JUnit Jupiter 测试方法或测试生命周期回调方法中的参数是 ApplicationContext 类型(或其子类型),或被 @Autowired@Qualifier@Value 注解或元注解,Spring 会从测试的 ApplicationContext 中为该特定参数注入相应的bean值。

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

Java
@SpringJUnitConfig(TestConfig.class)
class OrderServiceIntegrationTests {

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

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

由于JUnit Jupiter中 ParameterResolver 支持的健壮性,你也可以将多个依赖注入到一个方法中,不仅来自Spring,也来自JUnit Jupiter本身或其他第三方扩展。

下面的例子展示了如何让Spring和JUnit Jupiter同时向 placeOrderRepeatedly() 测试方法注入依赖。

Java
@SpringJUnitConfig(TestConfig.class)
class OrderServiceIntegrationTests {

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

        // use orderService from the test's ApplicationContext
        // and repetitionInfo from JUnit Jupiter
    }
}
Kotlin
@SpringJUnitConfig(TestConfig::class)
class OrderServiceIntegrationTests {

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

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

注意,使用JUnit Jupiter中的 @RepeatedTest 可以让测试方法获得对 RepetitionInfo 的访问。

@Nested 测试类的配置

从Spring Framework 5.0 开始,Spring TestContext 框架就支持在JUnit Jupiter的 @Nested 测试类上使用测试相关的注解;然而,直到Spring Framework 5.3,类级的测试配置注解并没有像从超类中继承那样从包围类中继承。

Spring Framework 5.3 引入了对从包围类中继承测试类配置的一流支持,并且这种配置将被默认继承。要从默认的 INHERIT 模式变为 OVERRIDE 模式,你可以用 @NestedTestConfiguration(EnclosingConfiguration.OVERRIDE) 来注解一个单独的 @Nested 测试类。一个明确的 @NestedTestConfiguration 声明将适用于被注解的测试类以及它的任何子类和嵌套类。因此,你可以用 @NestedTestConfiguration 注解一个顶层测试类,它将递归地适用于所有的嵌套测试类。

为了让开发团队将默认值改为 OVERRIDE --例如,为了与Spring Framework 5.0 到5.2 兼容—​可以通过JVM系统属性或 classpath root 的 spring.properties 文件全局地改变默认模式。详情见 "改变默认的包围配置继承模式" 说明。

尽管下面的 "Hello World" 例子非常简单,但它显示了如何在一个顶层类上声明普通配置,并由其 @Nested 测试类继承。在这个特殊的例子中,只有 TestConfig 配置类被继承了。每个嵌套的测试类提供它自己的活动配置文件集,导致每个嵌套的测试类有一个不同的 ApplicationContext(详见 上下文(Context)缓存)。查阅 支持的注解列表,看看哪些注释可以在 @Nested 测试类中继承。

Java
@SpringJUnitConfig(TestConfig.class)
class GreetingServiceTests {

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

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

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

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

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

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

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

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

5.12.5. TestNG 支持类

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

  • AbstractTestNGSpringContextTests

  • AbstractTransactionalTestNGSpringContextTests

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

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

这些类是扩展的一种便利。如果你不希望你的测试类被束缚在Spring特定的类层次中,你可以通过使用 @ContextConfiguration@TestExecutionListeners 等配置你自己的自定义测试类,并通过 TestContextManager 手动对你的测试类进行工具化。请参阅 AbstractTestNGSpringContextTests 的源代码,了解如何为你的测试类提供工具的例子。

5.13. AOT 对测试的支持

本章介绍了Spring的AOT(Ahead of Time)对使用Spring TestContext框架的集成测试的支持。

测试支持通过以下功能扩展了 Spring的核心AOT支持

  • 对当前项目中使用 TestContext 框架加载 ApplicationContext 的所有集成测试进行构建时检测。

    • 为基于JUnit Jupiter和JUnit 4的测试类提供显式支持,也为TestNG和其他使用Spring核心测试注解的测试框架提供隐式支持—​只要测试是使用为当前项目注册的JUnit Platform TestEngine 运行。

  • 构建时的AOT处理:当前项目中每个独特的测试 ApplicationContext 将被 刷新以进行AOT处理

  • 运行时AOT支持:当在AOT运行时模式下执行时,Spring集成测试将使用AOT优化的 ApplicationContext,它可以透明地参与 上下文(context)缓存

目前在AOT模式下不支持 @ContextHierarchy 注解。

为了提供特定于测试的运行时提示,以便在 GraalVM 原生镜像中使用,你有以下选择。

TestRuntimeHintsRegistrar API是核心 RuntimeHintsRegistrar API的伙伴。如果你需要为测试支持注册全局 hint,而不是特定的测试类,那么请选择实现 RuntimeHintsRegistrar 而不是特定的测试API。

如果你实现了一个自定义的 ContextLoader,它必须实现 AotContextLoader,以便提供AOT构建时处理和AOT运行时执行支持。然而,请注意,Spring 框架和 Spring Boot 提供的所有 context loader 实现都已经实现了 AotContextLoader

如果你实现了一个自定义的 TestExecutionListener,它必须实现 AotTestExecutionListener 才能参与AOT处理。请看 spring-test 模块中的 SqlScriptsTestExecutionListener 的例子。

6. WebTestClient

WebTestClient 是一个为测试服务器应用程序而设计的HTTP客户端。它包装了Spring的WebClient ,并使用它来执行请求,但暴露了一个测试界面(facade)来验证响应。WebTestClient 可以用来执行端到端的HTTP测试。它还可以用来测试Spring MVC和Spring WebFlux应用程序,而不需要通过mock服务器请求和响应对象来运行服务器。

Kotlin用户:请参阅与使用 WebTestClient 有关的 这一节

6.1. 设置

要设置一个 WebTestClient,你需要选择一个服务器设置来与之绑定。这可以是几个mock服务器设置中的一个,也可以是与一个实时服务器的连接。

6.1.1. 绑定到 Controller

这种设置允许你通过 mock 请求和响应对象来测试特定的 controller,而无需运行服务器。

对于WebFlux应用程序,使用下面的方法,加载相当于 WebFlux Java 配置 的基础设施,注册给定的控制器,并创建一个 WebHandler chain 来处理请求:

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

对于Spring MVC,使用下面的方法,它委托给 StandaloneMockMvcBuilder 来加载相当于 WebMvc Java 配置 的基础设施,注册指定的controller,并创建一个 MockMvc 实例来处理请求:

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

6.1.2. 绑定到 ApplicationContext

这种设置允许你用Spring MVC或Spring WebFlux基础设施和 controller 声明加载Spring配置,并通过 mock request 和 response 对象使用它来处理请求,而无需运行服务器。

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

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

    WebTestClient client;

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

    lateinit var client: WebTestClient

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

对于Spring MVC,使用下面的方法,其中Spring ApplicationContext 被传递给 MockMvcBuilders.webAppContextSetup 以创建一个 MockMvc 实例来处理请求:

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

    @Autowired
    WebApplicationContext wac; (2)

    WebTestClient client;

    @BeforeEach
    void setUp() {
        client = MockMvcWebTestClient.bindToApplicationContext(this.wac).build(); (3)
    }
}
1 指定要加载的配置
2 注入配置
3 创建 WebTestClient
Kotlin
@ExtendWith(SpringExtension.class)
@WebAppConfiguration("classpath:META-INF/web-resources") (1)
@ContextHierarchy({
    @ContextConfiguration(classes = RootConfig.class),
    @ContextConfiguration(classes = WebConfig.class)
})
class MyTests {

    @Autowired
    lateinit var wac: WebApplicationContext; (2)

    lateinit var client: WebTestClient

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

6.1.3. 绑定到路由(Router) Function

这种设置允许你通过 mock request 和 response 对象来测试 功能端点,而不需要运行服务器。

对于 WebFlux,使用以下内容,它委托给 RouterFunctions.toWebHandler 来创建一个服务器设置(server setup)来处理请求:

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

对于Spring MVC,目前还没有测试 WebMvc 功能端点 的选项。

6.1.4. 绑定到服务器(Server)

这个设置连接到一个正在运行的服务器,进行完整的、端到端的HTTP测试:

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

6.1.5. 客户端(Client)配置

除了前面描述的服务器设置选项外,你还可以配置客户端选项,包括基本URL、默认header、客户端过滤器等。这些选项在 bindToServer() 之后可以随时使用。对于所有其他配置选项,你需要使用 configureClient() 来从服务器配置过渡到客户端配置,如下所示:

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

6.2. 编写测试

WebTestClient 提供了一个与 WebClient 相同的API,直至使用 exchange() 来执行请求。关于如何准备一个带有任何内容(包括form data、multipart data等)的请求的例子,请参阅 WebClient 文档。

在调用 exchange() 之后,WebTestClientWebClient 相背离,而是继续用工作流程来验证响应。

要断定响应状态和header,请使用以下方法:

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

如果你想让所有的期望(expectation)被断言,即使其中一个失败了,你可以使用 expectAll(..) 而不是多个链式的 expect*(..) 调用。这个功能类似于 AssertJ 的软断言支持和JUnit Jupiter 的 assertAll() 支持。

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

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

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

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

  • expectBody(): 对于 JSON 内容 或空 body,解码为 byte[]

并对产生的更高层次的对象执行断言:

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

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

如果内置的断言是不够的,你可以消消费对象,并执行任何其他断言:

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

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

或者你可以退出工作流并获得一个 EntityExchangeResult

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

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

6.2.1. 无内容

如果不期望响应有内容,你可以按以下方式断言:

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

如果你想忽略响应内容,下面的内容是在没有任何断言的情况下释放的:

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

6.2.2. JSON 内容

你可以在没有目标类型的情况下使用 expectBody() 来对原始内容进行断言,而不是通过更高级别的 Object。

要用 JSONAssert 验证完整的JSON内容:

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

要用 JSONPath 验证JSON内容:

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

6.2.3. 流式(Stream)响应

要测试潜在的无限流,如 "text/event-stream""application/x-ndjson",首先要验证响应状态和header,然后获得 FluxExchangeResult

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

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

现在你已经准备好用 reactor-testStepVerifier 来消费响应流了:

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

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

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

6.2.4. MockMvc 断言

WebTestClient 是一个HTTP客户端,因此它只能验证客户端响应中的内容,包括状态、header信息和body。

当用MockMvc服务器设置来测试Spring MVC应用程序时,你有额外的选择来对服务器响应进行进一步断言。要做到这一点,首先要在断言 body 后获得一个 ExchangeResult

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

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

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

然后切换到 MockMvc server 响应(response)断言:

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

7. MockMvc

Spring MVC测试框架,也被称为MockMvc,为测试Spring MVC应用程序提供支持。它执行完整的Spring MVC请求处理,但通过 mock request 和 response 对象而不是运行中的服务器。

MockMvc 可以单独使用来执行请求(request)和验证响应(response)。它也可以通过 WebTestClient 来使用,其中MockMvc被插入作为服务器来处理请求。 WebTestClient 的优点是可以选择使用更高级别的对象而不是原始数据,以及能够切换到针对实时服务器的完整的、端到端的HTTP测试并使用相同的测试API。

7.1. 概览

你可以通过实例化一个controller,为其注入依赖,并调用其方法,为Spring MVC编写普通的单元测试。然而,这样的测试并不能验证请求映射、数据绑定、消息转换、类型转换、验证,也不涉及任何支持 @InitBinder@ModelAttribute@ExceptionHandler 方法。

Spring MVC测试框架,也被称为 MockMvc,旨在为Spring MVC controller 提供更完整的测试,而无需运行服务器。它通过调用 DispatcherServlet 和传递来自 spring-test 模块的 Servlet API的 “mock” 实现 来实现这一目标,该模块在没有运行服务器的情况下复制了完整的Spring MVC请求处理。

MockMvc是一个服务器端的测试框架,它可以让你使用轻量级和有针对性的测试来验证Spring MVC应用程序的大部分功能。你可以单独使用它来执行请求和验证响应,或者你也可以通过 WebTestClient API使用它,并将MockMvc插入作为服务器来处理请求。

7.2. 静态导入

当直接使用MockMvc来执行请求时,你将需要静态导入:

  • MockMvcBuilders.*

  • MockMvcRequestBuilders.*

  • MockMvcResultMatchers.*

  • MockMvcResultHandlers.*

记住的一个简单方法是搜索 MockMvc*。如果使用Eclipse,请确保在 Eclipse preferences 中将上述内容添加为 “favorite static members”。

当通过 WebTestClient 使用MockMvc时,你不需要静态导入。WebTestClient 提供了一个 fluent API,无需静态导入。

7.3. 设置的选择

MockMvc可以通过两种方式之一进行设置。一种是直接指向你想测试的 controller,并以编程方式配置Spring MVC基础设施。第二种是指向带有Spring MVC和 controller基础设施的Spring配置。

要设置MockMvc来测试一个特定的 controller,请使用以下方法:

Java
class MyWebTests {

    MockMvc mockMvc;

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

    // ...

}
Kotlin
class MyWebTests {

    lateinit var mockMvc : MockMvc

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

    // ...

}

或者你也可以在通过 WebTestClient 进行测试时使用这个设置,它委托给上面所示的同一个 builder。

要通过Spring配置来设置 MockMvc,请使用以下内容:

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

    MockMvc mockMvc;

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

    // ...

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

    lateinit var mockMvc: MockMvc

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

    // ...

}

或者你也可以在通过 WebTestClient 进行测试时使用这个设置,它委托给上面所示的同一个 builder。

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

webAppContextSetup 加载你的实际Spring MVC配置,导致一个更完整的集成测试。由于TestContext框架缓存了加载的Spring配置,它有助于保持测试快速运行,即使你在测试套件中引入更多的测试。此外,你可以通过Spring配置将mock service注入到 controller中,以保持对Web层的测试专注。下面的例子用Mockito声明了一个mock service:

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

然后,你可以将 mock service 注入测试中,以设置和验证你的期望,如下例所示:

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

    @Autowired
    AccountService accountService;

    MockMvc mockMvc;

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

    // ...

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

    @Autowired
    lateinit var accountService: AccountService

    lateinit mockMvc: MockMvc

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

    // ...

}

另一方面,StandaloneSetup 更接近于一个单元测试。它一次测试一个 controller。你可以手动注入 controller的mock依赖,它不涉及加载Spring配置。这样的测试更注重风格,更容易看到哪个 controller正在被测试,是否需要任何特定的Spring MVC配置来工作,等等。StandaloneSetup 也是一种非常方便的方式,可以编写临时的测试来验证特定的行为或调试一个问题。

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

7.4. 设置功能

无论你使用哪种MockMvc builder,所有的 MockMvcBuilder 实现都提供了一些共同的、非常有用的功能。例如,你可以为所有的请求声明一个 Accept 头,并期望在所有的响应中获得200的状态以及 Content-Type 头,如下所示:

Java
// static import of MockMvcBuilders.standaloneSetup

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

此外,第三方框架(和应用程序)可以预先打包设置指令,例如 MockMvcConfigurer 中的指令。Spring框架有一个这样的内置实现,它可以帮助保存和重用跨请求的HTTP Session。你可以按以下方式使用它:

Java
// static import of SharedHttpSessionConfigurer.sharedHttpSession

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

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

参见 ConfigurableMockMvcBuilder 的javadoc,以获得所有MockMvc builder 功能的列表,或者使用IDE来探索可用选项。

7.5. 执行请求

本节展示了如何单独使用MockMvc来执行请求和验证响应。如果通过 WebTestClient 使用MockMvc,请参见 编写测试 上的相应章节。

要执行使用任何HTTP方法的请求,如以下例子所示:

Java
// static import of MockMvcRequestBuilders.*

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

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

你也可以执行文件上传请求,在内部使用 MockMultipartHttpServletRequest,这样就没有实际解析 multipart request。相反,你必须把它设置成类似于下面的例子:

Java
mockMvc.perform(multipart("/doc").file("a1", "ABC".getBytes("UTF-8")));
Kotlin
import org.springframework.test.web.servlet.multipart

mockMvc.multipart("/doc") {
    file("a1", "ABC".toByteArray(charset("UTF8")))
}

你可以在URI模板风格中指定查询参数,如下例所示:

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

你也可以添加代表查询或表单参数的Servlet请求参数,如下例所示:

Java
mockMvc.perform(get("/hotels").param("thing", "somewhere"));
Kotlin
import org.springframework.test.web.servlet.get

mockMvc.get("/hotels") {
    param("thing", "somewhere")
}

如果应用程序代码依赖于Servlet请求参数,并且不明确检查查询字符串(这是最常见的情况),那么你使用哪个选项并不重要。然而,请记住,与URI模板一起提供的查询参数是被解码的,而通过 param(…​) 方法提供的请求参数预计已经被解码了。

在大多数情况下,最好不要把上下文路径(context path)和Servlet路径放在请求URI中。如果你必须用完整的请求URI进行测试,请确保相应地设置 contextPathservletPath,以便请求映射(request mapping)工作,如下面的例子所示:

Java
mockMvc.perform(get("/app/main/hotels/{id}").contextPath("/app").servletPath("/main"))
Kotlin
import org.springframework.test.web.servlet.get

mockMvc.get("/app/main/hotels/{id}") {
    contextPath = "/app"
    servletPath = "/main"
}

在前面的例子中,每次执行请求都要设置 contextPathservletPath 是很麻烦的。相反,你可以设置默认的请求属性,如下面的例子所示:

Java
class MyWebTests {

    MockMvc mockMvc;

    @BeforeEach
    void setup() {
        mockMvc = standaloneSetup(new AccountController())
            .defaultRequest(get("/")
            .contextPath("/app").servletPath("/main")
            .accept(MediaType.APPLICATION_JSON)).build();
    }
}
Kotlin
// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed

前面的属性会影响通过 MockMvc 实例执行的每个请求。如果同样的属性也在一个给定的请求中被指定,它将覆盖默认值。这就是为什么默认请求中的HTTP方法和URI并不重要,因为它们必须在每个请求中被指定。

7.6. 定义期望值

你可以通过在执行请求后附加一个或多个 andExpect(..) 调用来定义期望,如下例所示。一旦一个期望失败,其他的期望将不会被断言。

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

mockMvc.perform(get("/accounts/1")).andExpect(status().isOk());
Kotlin
import org.springframework.test.web.servlet.get

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

你可以通过在执行一个请求后附加 andExpectAll(..) 来定义多个期望,正如下面的例子所示。与 andExpect(..) 相反,andExpectAll(..) 保证所有提供的期望将被断言,所有失败将被跟踪和报告。

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

mockMvc.perform(get("/accounts/1")).andExpectAll(
    status().isOk(),
    content().contentType("application/json;charset=UTF-8"));
Kotlin
import org.springframework.test.web.servlet.get

mockMvc.get("/accounts/1").andExpectAll {
    status { isOk() }
    content { contentType(APPLICATION_JSON) }
}

MockMvcResultMatchers.* 提供了一些预期(expectation),其中一些预期又进一步嵌套了更详细的预期。

断言一般分为两类。第一类断言验证了响应的属性(例如,响应状态、header和body)。这些是要断言的最重要的结果。

第二类断言超越了响应。这些断言让你检查Spring MVC的具体方面,例如哪个 controller方法处理了请求,是否引发和处理了异常,Model 的内容是什么,选择了什么视图(View),添加了什么 Flash 属性,等等。它们还可以让你检查 Servlet 的具体方面,如 request 和 Session attributes。

下面的测试断言,绑定或验证失败:

Java
mockMvc.perform(post("/persons"))
    .andExpect(status().isOk())
    .andExpect(model().attributeHasErrors("person"));
Kotlin
import org.springframework.test.web.servlet.post

mockMvc.post("/persons").andExpect {
    status { isOk() }
    model {
        attributeHasErrors("person")
    }
}

很多时候,在编写测试时,转储(dump)执行请求的结果是很有用的。你可以这样做,print()MockMvcResultHandlers 的一个静态导入:

Java
mockMvc.perform(post("/persons"))
    .andDo(print())
    .andExpect(status().isOk())
    .andExpect(model().attributeHasErrors("person"));
Kotlin
import org.springframework.test.web.servlet.post

mockMvc.post("/persons").andDo {
        print()
    }.andExpect {
        status { isOk() }
        model {
            attributeHasErrors("person")
        }
    }

只要请求处理没有引起未处理的异常,print() 方法就会将所有可用的结果数据打印到 System.out。还有一个 log() 方法和两个额外的 print() 方法的变体,一个接受 OutputStream,一个接受 Writer。例如,调用 print(System.err) 将结果数据打印到 System.err,而调用 print(myWriter) 将结果数据打印到一个自定义的Writer。如果你想把结果数据记录下来,而不是打印出来,你可以调用 log() 方法,它把结果数据记录为 org.springframework.test.web.servlet.result logging类别下的一条 DEBUG 消息。

在某些情况下,你可能想直接访问结果,并验证一些以其他方式无法验证的东西。这可以通过在所有其他期望之后附加 .andReturn() 来实现,如下例所示:

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

如果所有的测试都重复相同的期望,你可以在构建 MockMvc 实例时设置一次共同的期望,正如下面的例子所示:

Java
standaloneSetup(new SimpleController())
    .alwaysExpect(status().isOk())
    .alwaysExpect(content().contentType("application/json;charset=UTF-8"))
    .build()
Kotlin
// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed

请注意,共同的期望总是被应用,如果不创建一个单独的 MockMvc 实例,就不能被重写。

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

Java
mockMvc.perform(get("/people").accept(MediaType.APPLICATION_JSON))
    .andExpect(jsonPath("$.links[?(@.rel == 'self')].href").value("http://localhost:8080/people"));
Kotlin
mockMvc.get("/people") {
    accept(MediaType.APPLICATION_JSON)
}.andExpect {
    jsonPath("$.links[?(@.rel == 'self')].href") {
        value("http://localhost:8080/people")
    }
}

当XML响应内容包含用 Spring HATEOAS 创建的超媒体链接时,你可以通过使用XPath表达式来验证产生的链接:

Java
Map<String, String> ns = Collections.singletonMap("ns", "http://www.w3.org/2005/Atom");
mockMvc.perform(get("/handle").accept(MediaType.APPLICATION_XML))
    .andExpect(xpath("/person/ns:link[@rel='self']/@href", ns).string("http://localhost:8080/people"));
Kotlin
val ns = mapOf("ns" to "http://www.w3.org/2005/Atom")
mockMvc.get("/handle") {
    accept(MediaType.APPLICATION_XML)
}.andExpect {
    xpath("/person/ns:link[@rel='self']/@href", ns) {
        string("http://localhost:8080/people")
    }
}

7.7. 异步请求

本节展示了如何单独使用MockMvc来测试异步请求处理。如果通过 WebTestClient 使用MockMvc,没有什么特别的事情要做,以使异步请求工作,因为 WebTestClient 会自动做本节所描述的事情。

Spring MVC中支持的 Servlet异步请求,其工作方式是退出Servlet容器线程,允许应用程序异步计算响应,之后进行异步调度,在Servlet容器线程上完成处理。

在 Spring MVC 测试中,可以通过首先断言产生的异步值来测试异步请求,然后手动执行异步调度,最后验证响应。下面是一个测试 controller方法的例子,这些方法返回 DeferredResultCallable 或响应式类型,如 Reactor Mono

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

@Test
void test() throws Exception {
    MvcResult mvcResult = this.mockMvc.perform(get("/path"))
            .andExpect(status().isOk()) (1)
            .andExpect(request().asyncStarted()) (2)
            .andExpect(request().asyncResult("body")) (3)
            .andReturn();

    this.mockMvc.perform(asyncDispatch(mvcResult)) (4)
            .andExpect(status().isOk()) (5)
            .andExpect(content().string("body"));
}
1 检查响应状态仍未改变。
2 异步处理必须已经开始。
3 等待并断言异步的结果。
4 手动执行一个ASYNC调度(因为没有运行中的容器)。
5 验证最终的响应。
Kotlin
@Test
fun test() {
    var mvcResult = mockMvc.get("/path").andExpect {
        status { isOk() } (1)
        request { asyncStarted() } (2)
        // TODO Remove unused generic parameter
        request { asyncResult<Nothing>("body") } (3)
    }.andReturn()


    mockMvc.perform(asyncDispatch(mvcResult)) (4)
            .andExpect {
                status { isOk() } (5)
                content().string("body")
            }
}
1 检查响应状态仍未改变。
2 异步处理必须已经开始。
3 等待并断言异步的结果。
4 手动执行一个ASYNC调度(因为没有运行中的容器)。
5 验证最终的响应。

7.8. 流式(Stream)响应

测试流式响应(如 Server-Sent)的最好方法是通过 WebTestClient,它可以作为测试客户端连接到 MockMvc 实例,在没有运行服务器的情况下对Spring MVC controller进行测试。比如说:

Java
WebTestClient client = MockMvcWebTestClient.bindToController(new SseController()).build();

FluxExchangeResult<Person> exchangeResult = client.get()
        .uri("/persons")
        .exchange()
        .expectStatus().isOk()
        .expectHeader().contentType("text/event-stream")
        .returnResult(Person.class);

// Use StepVerifier from Project Reactor to test the streaming response

StepVerifier.create(exchangeResult.getResponseBody())
        .expectNext(new Person("N0"), new Person("N1"), new Person("N2"))
        .expectNextCount(4)
        .consumeNextWith(person -> assertThat(person.getName()).endsWith("7"))
        .thenCancel()
        .verify();

WebTestClient 还可以连接到一个实时服务器,并执行完整的端到端集成测试。这在Spring Boot中也得到了支持,你可以 测试一个正在运行的服务器

7.9. Filter 注册

在设置 MockMvc 实例时,你可以注册一个或多个Servlet Filter 实例,如下例所示:

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

注册的过滤器通过 spring-testMockFilterChain 被调用,最后一个 filter 委托给 DispatcherServlet

7.10. MockMvc与端到端的测试

MockMVc 是建立在 spring-test 模块的Servlet API mock实现上的,不依赖于运行中的容器。因此,与有实际客户端和运行中的服务器的完全端到端集成测试相比,有一些区别。

思考这个问题的最简单的方法是,从一个空白的 MockHttpServletRequest 开始。无论你向它添加什么,都会成为这个请求。可能会让你吃惊的是,默认情况下没有上下文路径;没有 jsessionid cookie;没有转发(forwarding)、错误或异步调度;因此,没有实际的JSP渲染。相反,“forwarded” 和 “redirected” 的URL被保存在 MockHttpServletResponse 中,并且可以用期望来断言。

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

另外,你也可以考虑用 @SpringBootTest 从Spring Boot获得完整的端到端集成测试支持。见 Spring Boot 参考指南

每种方法都有优点和缺点。在Spring MVC测试中提供的选项在从经典单元测试到完全集成测试的范围内是不同的。可以肯定的是,Spring MVC测试中的所有选项都不属于经典单元测试的范畴,但它们离经典单元测试更近一些。例如,你可以通过将 mock service 注入 controller来隔离Web层,在这种情况下,你只通过 DispatcherServlet 来测试Web层,但要使用实际的Spring配置,就像你可能在隔离上面的层来测试数据访问层一样。另外,你可以使用独立的设置,一次只关注一个 controller,并手动提供使其工作所需的配置。

在使用Spring MVC Test时,另一个重要的区别是,从概念上讲,这种测试是服务器端的,所以你可以检查使用了什么处理程序,是否用 HandlerExceptionResolver 处理了一个异常,Model 的内容是什么,有什么binding error,以及其他细节。这意味着写预期比较容易,因为服务器不是一个不透明的盒子,就像通过实际的HTTP客户端测试时那样。这通常是经典单元测试的一个优势: 它更容易编写、推理和调试,但不能取代对完整集成测试的需要。同时,重要的是不要忽略了响应是最重要的检查内容这一事实。简而言之,即使在同一个项目中,这里也有多种风格和策略的测试空间。

7.11. 更多实例

该框架自己的测试包括 许多样本测试,旨在展示如何单独使用 MockMvc 或通过 WebTestClient。浏览这些例子以获得进一步的想法。

7.12. HtmlUnit 整合

Spring提供了 MockMvcHtmlUnit 的集成。这简化了在使用基于HTML的视图时执行端到端的测试。这种集成让你:

  • 通过使用 HtmlUnitWebDriverGeb 等工具轻松地测试HTML页面,而不需要部署到Servlet容器。

  • 测试页面内的JavaScript。

  • 可以选择使用 mock service 进行测试,以加快测试速度。

  • 在容器内的端到端测试和容器外的集成测试之间共享逻辑。

MockMvc可以与不依赖Servlet容器的模板技术一起使用(例如,Thymeleaf、FreeMarker和其他),但它不能与JSP一起使用,因为它们依赖Servlet容器。

7.12.1. 为什么要整合 HtmlUnit?

人们想到的最明显的问题是 "为什么我需要这个?" 答案最好是通过探索一个非常基本的示例应用程序来找到。假设你有一个Spring MVC Web 应用,支持对 Message 对象的CRUD操作。该应用还支持对所有 message 进行分页。你会如何去测试它呢?

通过Spring MVC测试,我们可以很容易地测试我们是否能够创建一个 Message,如下所示:

Java
MockHttpServletRequestBuilder createMessage = post("/messages/")
        .param("summary", "Spring Rocks")
        .param("text", "In case you didn't know, Spring Rocks!");

mockMvc.perform(createMessage)
        .andExpect(status().is3xxRedirection())
        .andExpect(redirectedUrl("/messages/123"));
Kotlin
@Test
fun test() {
    mockMvc.post("/messages/") {
        param("summary", "Spring Rocks")
        param("text", "In case you didn't know, Spring Rocks!")
    }.andExpect {
        status().is3xxRedirection()
        redirectedUrl("/messages/123")
    }
}

如果我们想测试让我们创建 message 的表单视图呢?例如,假设我们的表单看起来像下面的片段:

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

我们如何确保我们的表单产生正确的请求来创建一个新的 message?一个天真的尝试可能类似于以下:

Java
mockMvc.perform(get("/messages/form"))
        .andExpect(xpath("//input[@name='summary']").exists())
        .andExpect(xpath("//textarea[@name='text']").exists());
Kotlin
mockMvc.get("/messages/form").andExpect {
    xpath("//input[@name='summary']") { exists() }
    xpath("//textarea[@name='text']") { exists() }
}

这个测试有一些明显的缺点。如果我们更新 controller,使用参数 message 而不是 text,我们的表单测试就会继续通过,尽管HTML表单与 controller不同步。为了解决这个问题,我们可以将我们的两个测试结合起来,如下所示:

Java
String summaryParamName = "summary";
String textParamName = "text";
mockMvc.perform(get("/messages/form"))
        .andExpect(xpath("//input[@name='" + summaryParamName + "']").exists())
        .andExpect(xpath("//textarea[@name='" + textParamName + "']").exists());

MockHttpServletRequestBuilder createMessage = post("/messages/")
        .param(summaryParamName, "Spring Rocks")
        .param(textParamName, "In case you didn't know, Spring Rocks!");

mockMvc.perform(createMessage)
        .andExpect(status().is3xxRedirection())
        .andExpect(redirectedUrl("/messages/123"));
Kotlin
val summaryParamName = "summary";
val textParamName = "text";
mockMvc.get("/messages/form").andExpect {
    xpath("//input[@name='$summaryParamName']") { exists() }
    xpath("//textarea[@name='$textParamName']") { exists() }
}
mockMvc.post("/messages/") {
    param(summaryParamName, "Spring Rocks")
    param(textParamName, "In case you didn't know, Spring Rocks!")
}.andExpect {
    status().is3xxRedirection()
    redirectedUrl("/messages/123")
}

这将减少我们的测试不正确通过的风险,但仍有一些问题:

  • 如果我们的页面上有多个表单怎么办?诚然,我们可以更新我们的XPath表达式,但随着我们考虑到更多的因素,它们会变得更加复杂: 这些字段的类型是否正确?这些字段是否启用?等等。

  • 另一个问题是,我们正在做双倍的工作,这是我们所期望的。我们必须首先验证视图,然后用我们刚刚验证的参数提交视图。理想情况下,这可以一次完成。

  • 最后,我们仍然无法解释一些事情。例如,如果表单中有我们希望测试的JavaScript验证,怎么办?

总的问题是,测试一个网页并不涉及单一的互动。相反,它是用户如何与网页互动以及该网页如何与其他资源互动的组合。例如,一个表单视图的结果被用作用户创建信息的输入。此外,我们的表单视图有可能使用影响页面行为的额外资源,如JavaScript验证。

集成测试来拯救?

为了解决前面提到的问题,我们可以进行端到端的集成测试,但这有一些缺点。考虑测试让我们翻阅 message 的视图。我们可能需要以下测试:

  • 当 message 为空时,我们的页面是否会向用户显示一个通知,表明没有结果?

  • 我们的页面是否正确地显示了一条 message?

  • 我们的页面是否正确支持分页?

为了设置这些测试,我们需要确保我们的数据库包含适当的 message。这就导致了一些额外的挑战:

  • 确保适当的 message 在数据库中是很繁琐的。(考虑到外键约束)。

  • 测试会变得缓慢,因为每个测试都需要确保数据库处于正确的状态。

  • 由于我们的数据库需要处于一个特定的状态,我们不能并行地运行测试。

  • 对自动生成的ID、时间戳和其他项目进行断言可能很困难。

这些挑战并不意味着我们应该完全放弃端到端的集成测试。相反,我们可以通过重构我们的详细测试来减少端到端集成测试的数量,使用运行速度更快、更可靠、没有副作用的 mock service。然后,我们可以实现少量的真正的端到端集成测试,验证简单的工作流程,以确保所有东西都能正常工作。

进入 HtmlUnit 整合

那么,我们如何才能在测试页面的交互之间取得平衡,并在我们的测试套件中仍然保留良好的性能呢?答案是:"通过整合MockMvc和HtmlUnit"。

HtmlUnit 整合选项

当你想把MockMvc和HtmlUnit整合时,你有很多选择:

  • MockMvc 和 HtmlUnit: 如果你想使用原始的HtmlUnit库,请使用这个选项。

  • MockMvc 和 WebDriver: 使用这个选项可以在集成和端到端测试之间缓解开发和重用代码。

  • MockMvc 和 Geb: 如果你想使用Groovy进行测试,简化开发,并在集成和端到端测试之间重复使用代码,请使用该选项。

7.12.2. MockMvc 和 HtmlUnit

本节描述了如何整合MockMvc和HtmlUnit。如果你想使用原始的HtmlUnit库,请使用这个选项。

MockMvc 和 HtmlUnit 的设置

首先,确保你已经包含了 net.sourceforge.htmlunit:htmlunit 的测试依赖。为了在Apache HttpComponents 4.5以上版本中使用 HtmlUnit,你需要使用HtmlUnit 2.18或者更高。

我们可以通过使用 MockMvcWebClientBuilder 轻松创建一个与 MockMvc 集成的HtmlUnit WebClient,如下所示:

Java
WebClient webClient;

@BeforeEach
void setup(WebApplicationContext context) {
    webClient = MockMvcWebClientBuilder
            .webAppContextSetup(context)
            .build();
}
Kotlin
lateinit var webClient: WebClient

@BeforeEach
fun setup(context: WebApplicationContext) {
    webClient = MockMvcWebClientBuilder
            .webAppContextSetup(context)
            .build()
}
这是一个使用 MockMvcWebClientBuilder 的简单例子。关于高级用法,请看 高级的 MockMvcWebClientBuilder.。

这确保了任何引用 localhost 作为服务器的URL都被引导到我们 的MockMvc 实例,而不需要真正的HTTP连接。任何其他的URL都是通过使用网络连接来请求的,和平常一样。这让我们可以轻松地测试CDN的使用。

MockMvc 和 HtmlUnit 的用法

现在我们可以像平时一样使用HtmlUnit,但不需要将我们的应用程序部署到Servlet容器中。例如,我们可以通过以下方式请求视图创建一个 message:

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

一旦我们有了对 HtmlPage 的引用,我们就可以填写表单并提交,以创建一个 message,如下例所示:

Java
HtmlForm form = createMsgFormPage.getHtmlElementById("messageForm");
HtmlTextInput summaryInput = createMsgFormPage.getHtmlElementById("summary");
summaryInput.setValueAttribute("Spring Rocks");
HtmlTextArea textInput = createMsgFormPage.getHtmlElementById("text");
textInput.setText("In case you didn't know, Spring Rocks!");
HtmlSubmitInput submit = form.getOneHtmlElementByAttribute("input", "type", "submit");
HtmlPage newMessagePage = submit.click();
Kotlin
val form = createMsgFormPage.getHtmlElementById("messageForm")
val summaryInput = createMsgFormPage.getHtmlElementById("summary")
summaryInput.setValueAttribute("Spring Rocks")
val textInput = createMsgFormPage.getHtmlElementById("text")
textInput.setText("In case you didn't know, Spring Rocks!")
val submit = form.getOneHtmlElementByAttribute("input", "type", "submit")
val newMessagePage = submit.click()

最后,我们可以验证一个新的消息是否被成功创建。下面的断言使用 AssertJ 库:

Java
assertThat(newMessagePage.getUrl().toString()).endsWith("/messages/123");
String id = newMessagePage.getHtmlElementById("id").getTextContent();
assertThat(id).isEqualTo("123");
String summary = newMessagePage.getHtmlElementById("summary").getTextContent();
assertThat(summary).isEqualTo("Spring Rocks");
String text = newMessagePage.getHtmlElementById("text").getTextContent();
assertThat(text).isEqualTo("In case you didn't know, Spring Rocks!");
Kotlin
assertThat(newMessagePage.getUrl().toString()).endsWith("/messages/123")
val id = newMessagePage.getHtmlElementById("id").getTextContent()
assertThat(id).isEqualTo("123")
val summary = newMessagePage.getHtmlElementById("summary").getTextContent()
assertThat(summary).isEqualTo("Spring Rocks")
val text = newMessagePage.getHtmlElementById("text").getTextContent()
assertThat(text).isEqualTo("In case you didn't know, Spring Rocks!")

前面的代码在很多方面改进了我们的 MockMvc测试 。首先,我们不再需要明确地验证我们的表单,然后创建一个看起来像表单的请求。相反,我们请求表单,填写并提交它,从而大大减少了开销。

另一个重要因素是, HtmlUnit 使用 Mozilla Rhino 引擎 来评估JavaScript。这意味着我们也可以测试页面中JavaScript的行为。

关于使用 HtmlUnit 的更多信息,请参见 HtmlUnit 文档

高级的 MockMvcWebClientBuilder

在迄今为止的例子中,我们以最简单的方式使用了 MockMvcWebClientBuilder,即基于Spring TestContext 框架为我们加载的 WebApplicationContext 构建一个 WebClient。这种方法在下面的例子中得到了重复:

Java
WebClient webClient;

@BeforeEach
void setup(WebApplicationContext context) {
    webClient = MockMvcWebClientBuilder
            .webAppContextSetup(context)
            .build();
}
Kotlin
lateinit var webClient: WebClient

@BeforeEach
fun setup(context: WebApplicationContext) {
    webClient = MockMvcWebClientBuilder
            .webAppContextSetup(context)
            .build()
}

我们还可以指定额外的配置选项,如下例所示:

Java
WebClient webClient;

@BeforeEach
void setup() {
    webClient = MockMvcWebClientBuilder
        // demonstrates applying a MockMvcConfigurer (Spring Security)
        .webAppContextSetup(context, springSecurity())
        // for illustration only - defaults to ""
        .contextPath("")
        // By default MockMvc is used for localhost only;
        // the following will use MockMvc for example.com and example.org as well
        .useMockMvcForHosts("example.com","example.org")
        .build();
}
Kotlin
lateinit var webClient: WebClient

@BeforeEach
fun setup() {
    webClient = MockMvcWebClientBuilder
        // demonstrates applying a MockMvcConfigurer (Spring Security)
        .webAppContextSetup(context, springSecurity())
        // for illustration only - defaults to ""
        .contextPath("")
        // By default MockMvc is used for localhost only;
        // the following will use MockMvc for example.com and example.org as well
        .useMockMvcForHosts("example.com","example.org")
        .build()
}

作为替代方案,我们可以通过单独配置 MockMvc 实例并将其提供给 MockMvcWebClientBuilder 来进行完全相同的设置,如下所示:

Java
MockMvc mockMvc = MockMvcBuilders
        .webAppContextSetup(context)
        .apply(springSecurity())
        .build();

webClient = MockMvcWebClientBuilder
        .mockMvcSetup(mockMvc)
        // for illustration only - defaults to ""
        .contextPath("")
        // By default MockMvc is used for localhost only;
        // the following will use MockMvc for example.com and example.org as well
        .useMockMvcForHosts("example.com","example.org")
        .build();
Kotlin
// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed

这就比较啰嗦了,但是,通过用 MockMvc 实例构建 WebClient,我们就有了 MockMvc 的全部功能,触手可及。

关于创建 MockMvc 实例的其他信息,请看 设置的选择

7.12.3. MockMvc 和 WebDriver

在前面的章节中,我们已经看到了如何将MockMvc与原始的HtmlUnit APIs结合使用。在这一节中,我们在Selenium WebDriver 中使用额外的抽象来使事情变得更加简单。

为什么选择 WebDriver 和 MockMvc?

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

尽管是 Selenium 的一部分,WebDriver并不要求Selenium服务器来运行你的测试。

假设我们需要确保一条 message 被正确创建。测试包括寻找HTML表单的输入元素,填写它们,并进行各种断言。

这种方法导致了许多单独的测试,因为我们也想测试错误条件。例如,我们要确保如果我们只填写了表单的一部分,就会出现错误。如果我们填写了整个表单,之后应该显示新创建的message。

如果其中一个字段被命名为 “summary”,我们可能会有类似以下的东西在我们的测试中的多个地方重复:

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

那么,如果我们把 id 改成 smmry 会怎么样呢?这样做将迫使我们更新我们所有的测试来纳入这一变化。这违反了DRY原则,所以我们最好将这段代码提取到自己的方法中,如下所示:

Java
public HtmlPage createMessage(HtmlPage currentPage, String summary, String text) {
    setSummary(currentPage, summary);
    // ...
}

public void setSummary(HtmlPage currentPage, String summary) {
    HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary");
    summaryInput.setValueAttribute(summary);
}
Kotlin
fun createMessage(currentPage: HtmlPage, summary:String, text:String) :HtmlPage{
    setSummary(currentPage, summary);
    // ...
}

fun setSummary(currentPage:HtmlPage , summary: String) {
    val summaryInput = currentPage.getHtmlElementById("summary")
    summaryInput.setValueAttribute(summary)
}

这样做可以确保我们在改变用户界面时不必更新所有的测试。

我们甚至可以更进一步,把这个逻辑放在一个代表我们当前所在的 HtmlPageObject 中,就像下面的例子所示:

Java
public class CreateMessagePage {

    final HtmlPage currentPage;

    final HtmlTextInput summaryInput;

    final HtmlSubmitInput submit;

    public CreateMessagePage(HtmlPage currentPage) {
        this.currentPage = currentPage;
        this.summaryInput = currentPage.getHtmlElementById("summary");
        this.submit = currentPage.getHtmlElementById("submit");
    }

    public <T> T createMessage(String summary, String text) throws Exception {
        setSummary(summary);

        HtmlPage result = submit.click();
        boolean error = CreateMessagePage.at(result);

        return (T) (error ? new CreateMessagePage(result) : new ViewMessagePage(result));
    }

    public void setSummary(String summary) throws Exception {
        summaryInput.setValueAttribute(summary);
    }

    public static boolean at(HtmlPage page) {
        return "Create Message".equals(page.getTitleText());
    }
}
Kotlin
    class CreateMessagePage(private val currentPage: HtmlPage) {

        val summaryInput: HtmlTextInput = currentPage.getHtmlElementById("summary")

        val submit: HtmlSubmitInput = currentPage.getHtmlElementById("submit")

        fun <T> createMessage(summary: String, text: String): T {
            setSummary(summary)

            val result = submit.click()
            val error = at(result)

            return (if (error) CreateMessagePage(result) else ViewMessagePage(result)) as T
        }

        fun setSummary(summary: String) {
            summaryInput.setValueAttribute(summary)
        }

        fun at(page: HtmlPage): Boolean {
            return "Create Message" == page.getTitleText()
        }
    }
}

以前,这种模式被称为 Page Object Pattern。虽然我们当然可以用 HtmlUnit 做到这一点,但WebDriver提供了一些工具,我们将在下面的章节中探讨,使这种模式更容易实现。

MockMvc 和 WebDriver 的设置

要在Spring MVC测试框架中使用Selenium WebDriver,请确保你的项目包括对 org.seleniumhq.selenium:selenium-htmlunit-driver 的测试依赖。

我们可以通过使用 MockMvcHtmlUnitDriverBuilder 轻松创建一个与MockMvc集成的Selenium WebDriver,如下图所示:

Java
WebDriver driver;

@BeforeEach
void setup(WebApplicationContext context) {
    driver = MockMvcHtmlUnitDriverBuilder
            .webAppContextSetup(context)
            .build();
}
Kotlin
lateinit var driver: WebDriver

@BeforeEach
fun setup(context: WebApplicationContext) {
    driver = MockMvcHtmlUnitDriverBuilder
            .webAppContextSetup(context)
            .build()
}
这是一个使用 MockMvcHtmlUnitDriverBuilder 的简单例子。对于更高级的用法,请看 高级的 MockMvcHtmlUnitDriverBuilder

前面的例子确保任何引用 localhost 作为服务器的URL都会被引导到我们的 MockMvc 实例,而不需要真正的HTTP连接。任何其他的URL都是通过使用网络连接来请求的,和平常一样。这让我们可以轻松地测试CDN的使用情况。

MockMvc 和 WebDriver 的用法

现在我们可以像平时一样使用WebDriver,但不需要将我们的应用程序部署到Servlet容器中。例如,我们可以通过以下方式请求视图创建一个 message:

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

然后我们可以填写表单并提交,创建一个message,如下所示

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

这通过利用 Page Object Pattern 改进了我们的 HtmlUnit测试 的设计。正如我们在《为什么选择 WebDriver 和 MockMvc?》中提到的,我们可以在HtmlUnit中使用 Page Object Pattern,但在WebDriver中要容易得多。请看下面的 CreateMessagePage 实现:

Java
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);
    }
}
1 CreateMessagePage 扩展了 AbstractPage。我们不讨论 AbstractPage 的细节,但是,总的来说,它包含了我们所有页面的共同功能。例如,如果我们的应用程序有一个导航栏、全局错误信息和其他功能,我们可以将这些逻辑放在一个共享的位置。
2 我们为HTML页面中我们感兴趣的每个部分都有一个成员变量。这些都是 WebElement 的类型。WebDriver的 PageFactory 通过自动解析每个 WebElement,让我们从HtmlUnit版本的 CreateMessagePage 中移除大量的代码。 PageFactory#initElements(WebDriver,Class<T>)方法通过使用字段名并通过HTML页面中元素的 idname 来自动解析每个 WebElement
3 我们可以使用 @FindBy 注解 来覆盖默认的查找行为。我们的例子显示了如何使用 @FindBy 注解,用 css 选择器(input[type=submit])来查找我们的提交按钮。
Kotlin
class CreateMessagePage(private val driver: WebDriver) : AbstractPage(driver) { (1)

    (2)
    private lateinit var summary: WebElement
    private lateinit var text: WebElement

    @FindBy(css = "input[type=submit]") (3)
    private lateinit var submit: WebElement

    fun <T> createMessage(resultPage: Class<T>, summary: String, details: String): T {
        this.summary.sendKeys(summary)
        text.sendKeys(details)
        submit.click()
        return PageFactory.initElements(driver, resultPage)
    }
    companion object {
        fun to(driver: WebDriver): CreateMessagePage {
            driver.get("http://localhost:9990/mail/messages/form")
            return PageFactory.initElements(driver, CreateMessagePage::class.java)
        }
    }
}
1 CreateMessagePage 扩展了 AbstractPage。我们不讨论 AbstractPage 的细节,但是,总的来说,它包含了我们所有页面的共同功能。例如,如果我们的应用程序有一个导航栏、全局错误信息和其他功能,我们可以将这些逻辑放在一个共享的位置。
2 我们为HTML页面中我们感兴趣的每个部分都有一个成员变量。这些都是 WebElement 的类型。WebDriver的 PageFactory 通过自动解析每个 WebElement,让我们从HtmlUnit版本的 CreateMessagePage 中移除大量的代码。 PageFactory#initElements(WebDriver,Class<T>)方法通过使用字段名并通过HTML页面中元素的 idname 来自动解析每个 WebElement
3 我们可以使用 @FindBy 注解 来覆盖默认的查找行为。我们的例子显示了如何使用 @FindBy 注解,用 css 选择器(input[type=submit])来查找我们的提交按钮。

最后,我们可以验证一个新的 message 是否被成功创建。下面的断言使用 AssertJ 断言库:

Java
assertThat(viewMessagePage.getMessage()).isEqualTo(expectedMessage);
assertThat(viewMessagePage.getSuccess()).isEqualTo("Successfully created a new message");
Kotlin
assertThat(viewMessagePage.message).isEqualTo(expectedMessage)
assertThat(viewMessagePage.success).isEqualTo("Successfully created a new message")

我们可以看到,我们的 ViewMessagePage 让我们与我们的自定义 domain 模型互动。例如,它暴露了一个方法,返回一个 Message 对象:

Java
public Message getMessage() throws ParseException {
    Message message = new Message();
    message.setId(getId());
    message.setCreated(getCreated());
    message.setSummary(getSummary());
    message.setText(getText());
    return message;
}
Kotlin
fun getMessage() = Message(getId(), getCreated(), getSummary(), getText())

然后我们可以在我们的断言中使用丰富的 domain 对象。

最后,我们一定不要忘记在测试完成后关闭 WebDriver 实例,如下所示:

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

关于使用 WebDriver 的其他信息,请参见 Selenium WebDriver 文档

高级的 MockMvcHtmlUnitDriverBuilder

在迄今为止的例子中,我们以最简单的方式使用了 MockMvcHtmlUnitDriverBuilder,即基于Spring TestContext 框架为我们加载的 WebApplicationContext 构建一个 WebDriver。在此重复这一方法,具体如下:

Java
WebDriver driver;

@BeforeEach
void setup(WebApplicationContext context) {
    driver = MockMvcHtmlUnitDriverBuilder
            .webAppContextSetup(context)
            .build();
}
Kotlin
lateinit var driver: WebDriver

@BeforeEach
fun setup(context: WebApplicationContext) {
    driver = MockMvcHtmlUnitDriverBuilder
            .webAppContextSetup(context)
            .build()
}

我们还可以指定额外的配置选项,如下所示:

Java
WebDriver driver;

@BeforeEach
void setup() {
    driver = MockMvcHtmlUnitDriverBuilder
            // demonstrates applying a MockMvcConfigurer (Spring Security)
            .webAppContextSetup(context, springSecurity())
            // for illustration only - defaults to ""
            .contextPath("")
            // By default MockMvc is used for localhost only;
            // the following will use MockMvc for example.com and example.org as well
            .useMockMvcForHosts("example.com","example.org")
            .build();
}
Kotlin
lateinit var driver: WebDriver

@BeforeEach
fun setup() {
    driver = MockMvcHtmlUnitDriverBuilder
            // demonstrates applying a MockMvcConfigurer (Spring Security)
            .webAppContextSetup(context, springSecurity())
            // for illustration only - defaults to ""
            .contextPath("")
            // By default MockMvc is used for localhost only;
            // the following will use MockMvc for example.com and example.org as well
            .useMockMvcForHosts("example.com","example.org")
            .build()
}

作为一个替代方案,我们可以通过单独配置MockMvc实例并将其提供给 MockMvcHtmlUnitDriverBuilder 来进行完全相同的设置,如下所示:

Java
MockMvc mockMvc = MockMvcBuilders
        .webAppContextSetup(context)
        .apply(springSecurity())
        .build();

driver = MockMvcHtmlUnitDriverBuilder
        .mockMvcSetup(mockMvc)
        // for illustration only - defaults to ""
        .contextPath("")
        // By default MockMvc is used for localhost only;
        // the following will use MockMvc for example.com and example.org as well
        .useMockMvcForHosts("example.com","example.org")
        .build();
Kotlin
// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed

这就比较啰嗦了,但是,通过用 MockMvc 实例构建 WebDriver,我们可以随心所欲地使用MockMvc的全部功能。

关于创建 MockMvc 实例的其他信息,请看 设置的选择

7.12.4. MockMvc 和 Geb

在上一节中,我们看到了如何使用MockMvc和WebDriver。在这一节中,我们使用 Geb 来使我们的测试更加Groovy-er。

为什么选择 Geb 和 MockMvc?

Geb是由WebDriver支持的,所以它提供了许多与WebDriver 相同的好处。然而,Geb通过为我们处理一些模板代码,使事情变得更加简单。

MockMvc 和 Geb 的设置

我们可以通过使用 MockMvc 的 Selenium WebDriver 轻松地初始化一个Geb Browser,如下所示:

def setup() {
    browser.driver = MockMvcHtmlUnitDriverBuilder
        .webAppContextSetup(context)
        .build()
}
这是一个使用 MockMvcHtmlUnitDriverBuilder 的简单例子。对于更高级的用法,请看 高级的 MockMvcHtmlUnitDriverBuilder

This ensures that any URL referencing localhost as the server is directed to our MockMvc instance without the need for a real HTTP connection. Any other URL is requested by using a network connection as normal. This lets us easily test the use of CDNs.

这确保了任何引用 localhost 作为服务器的URL都会被引导到我们的 MockMvc 实例,而不需要真正的HTTP连接。任何其他的URL都是通过使用正常的网络连接来请求的。这让我们可以轻松地测试CDN的使用。

MockMvc 和 Geb 的用法

现在我们可以像平时一样使用Geb,但不需要将我们的应用程序部署到Servlet容器中。例如,我们可以通过以下方式请求视图创建一条 message:

to CreateMessagePage

然后我们可以填写表单并提交,创建一个message,如下所示:

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

任何未被识别的方法调用或属性访问或未被发现的引用都被转发到当前的 page 对象。这消除了很多我们在直接使用WebDriver时需要的模板代码。

与直接使用WebDriver一样,这通过使用 Page Object Pattern 改进了我们 HtmlUnit测试 的设计。如前所述,我们可以在HtmlUnit和WebDriver中使用 Page Object Pattern ,但在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 的 Navigator API 来选择我们感兴趣的内容。

最后,我们可以验证是否成功创建了一条新的 message,如下所示:

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

关于如何充分利用Geb的更多细节,请参阅《 Geb之书》用户手册。

8. 测试客户应用

你可以使用客户端测试来测试内部使用 RestTemplate 的代码。这个想法是声明预期的请求并提供 “stub” 响应,这样你就可以专注于孤立地测试代码(也就是说,不运行服务器)。下面的例子展示了如何做到这一点:

Java
RestTemplate restTemplate = new RestTemplate();

MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build();
mockServer.expect(requestTo("/greeting")).andRespond(withSuccess());

// Test code that uses the above RestTemplate ...

mockServer.verify();
Kotlin
val restTemplate = RestTemplate()

val mockServer = MockRestServiceServer.bindTo(restTemplate).build()
mockServer.expect(requestTo("/greeting")).andRespond(withSuccess())

// Test code that uses the above RestTemplate ...

mockServer.verify()

在前面的例子中,MockRestServiceServer(客户端REST测试的中心类)用一个自定义的 ClientHttpRequestFactory 来配置 RestTemplate,该工厂根据预期来断定实际请求,并返回 “stub” 响应。在这种情况下,我们期待一个对 /greeting 的请求,并希望返回一个带有 text/plain 内容的200响应。我们可以根据需要定义额外的预期请求和stub响应。当我们定义了预期请求和stub响应后,RestTemplate 可以像往常一样在客户端代码中使用。在测试结束时,可以使用 mockServer.verify() 来验证所有的预期都得到了满足。

默认情况下,请求是按照期望值声明的顺序被期望的。你可以在构建服务器时设置 ignoreExpectOrder 选项,在这种情况下,所有的期望值都会被检查(按顺序)以找到与给定请求的匹配。这意味着允许请求以任何顺序出现。下面的例子使用了 ignoreExpectOrder

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

即使默认情况下是无序的请求,每个请求也只允许运行一次。expect 方法提供了一个重载变体,它接受一个指定计数范围的 ExpectedCount 参数(例如,once, manyTimes, max, min, between,等等)。下面的例子使用了 times

Java
RestTemplate restTemplate = new RestTemplate();

MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build();
mockServer.expect(times(2), requestTo("/something")).andRespond(withSuccess());
mockServer.expect(times(3), requestTo("/somewhere")).andRespond(withSuccess());

// ...

mockServer.verify();
Kotlin
val restTemplate = RestTemplate()

val mockServer = MockRestServiceServer.bindTo(restTemplate).build()
mockServer.expect(times(2), requestTo("/something")).andRespond(withSuccess())
mockServer.expect(times(3), requestTo("/somewhere")).andRespond(withSuccess())

// ...

mockServer.verify()

注意,当 ignoreExpectOrder 没有被设置时(默认情况),因此,请求是按照声明的顺序被预期的,那么这个顺序只适用于任何预期请求中的第一个。例如,如果 "/something" 被期望两次,然后是 "/something" 三次,那么在有 "/something" 的请求之前,应该有一个 "/something" 的请求,但是,除了随后的 "/something" 和 "/something" ,请求可以在任何时间出现。

作为上述所有的替代方案,客户端测试支持也提供了一个 ClientHttpRequestFactory 实现,你可以将其配置到 RestTemplate 中,将其绑定到 MockMvc 实例上。这允许使用实际的服务器端逻辑来处理请求,但不需要运行一个服务器。下面的例子展示了如何做到这一点:

Java
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
this.restTemplate = new RestTemplate(new MockMvcClientHttpRequestFactory(mockMvc));

// Test code that uses the above RestTemplate ...
Kotlin
val mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build()
restTemplate = RestTemplate(MockMvcClientHttpRequestFactory(mockMvc))

// Test code that uses the above RestTemplate ...

在某些情况下,可能需要执行对远程服务的实际调用,而不是 mock 响应。下面的例子展示了如何通过 ExecutingResponseCreator 来做到这一点:

Java
RestTemplate restTemplate = new RestTemplate();

// Create ExecutingResponseCreator with the original request factory
ExecutingResponseCreator withActualResponse = new ExecutingResponseCreator(restTemplate.getRequestFactory());

MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build();
mockServer.expect(requestTo("/profile")).andRespond(withSuccess());
mockServer.expect(requestTo("/quoteOfTheDay")).andRespond(withActualResponse);

// Test code that uses the above RestTemplate ...

mockServer.verify();
Kotlin
val restTemplate = RestTemplate()

// Create ExecutingResponseCreator with the original request factory
val withActualResponse = new ExecutingResponseCreator(restTemplate.getRequestFactory())

val mockServer = MockRestServiceServer.bindTo(restTemplate).build()
mockServer.expect(requestTo("/profile")).andRespond(withSuccess())
mockServer.expect(requestTo("/quoteOfTheDay")).andRespond(withActualResponse)

// Test code that uses the above RestTemplate ...

mockServer.verify()

在前面的例子中,我们使用 RestTemplate 中的 ClientHttpRequestFactory 创建了 ExecutingResponseCreator,然后 MockRestServiceServer 用另一个 mock 响应的工厂替换了它。然后我们用两种响应来定义期望:

  • 一个针对 /profile 端点的 stub 200 响应(不会执行实际请求)。

  • 通过调用 /quoteOfTheDay 端点获得的响应。

在第二种情况下,请求是通过先前捕获的 ClientHttpRequestFactory 执行的。这就产生了一个响应,这个响应可能来自一个实际的远程服务器,这取决于 RestTemplate 最初是如何配置的。

8.1. 静态导入

与服务器端测试一样,客户端测试的 fluent API需要一些静态导入。通过搜索 MockRest*,可以很容易地找到这些。Eclipse用户应该将 MockRestRequestMatchers.*MockRestResponseCreators.* 作为 “favorite static members” 添加到 Eclipse preferences 中的 Java → Editor → Content Assist → Favorites。这允许在输入静态方法名称的第一个字符后使用内容辅助。其他IDE(如IntelliJ)可能不需要任何额外的配置。检查对静态成员的代码完成的支持情况。

8.2. 客户端REST测试的更多例子

Spring MVC Test自己的测试包括 示例测试 的客户端REST测试。

9. 附录

9.1. 注解

本节涵盖了你在测试Spring应用程序时可以使用的注解。它包括以下主题:

9.1.1. 标准注解的支持

以下注解支持Spring TestContext框架的所有配置的标准语义。请注意,这些注解不是专门针对测试的,可以在Spring框架的任何地方使用。

  • @Autowired

  • @Qualifier

  • @Value

  • @Resource (jakarta.annotation) 如果JSR-250存在的话

  • @ManagedBean (jakarta.annotation) 如果JSR-250存在的话

  • @Inject (jakarta.inject) 如果JSR-330存在的话

  • @Named (jakarta.inject) 如果JSR-330存在的话

  • @PersistenceContext (jakarta.persistence) 如果JPA存在的话

  • @PersistenceUnit (jakarta.persistence) 如果JPA存在的话

  • @Transactional (org.springframework.transaction.annotation) 具有 有限的属性支持

JSR-250 生命周期注解

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

如果一个测试类中的方法被 @PostConstruct 注解,该方法会在底层测试框架的任何之前的方法(例如,用JUnit Jupiter的 @BeforeEach 注解的方法)之前运行,这适用于测试类中的每个测试方法。另一方面,如果一个测试类中的方法被 @PreDestroy 注解,该方法就永远不会运行。因此,在一个测试类中,我们建议你使用底层测试框架的测试生命周期回调,而不是 @PostConstruct@PreDestroy

9.1.2. Spring 测试注解

Spring框架提供了以下一组Spring特定的注解,你可以在单元和集成测试中与TestContext框架一起使用。更多信息,包括默认属性值、属性别名和其他细节,请参见相应的javadoc。

Spring的测试注解包括以下内容:

@BootstrapWith

@BootstrapWith 是一个类级注解,你可以用它来配置Spring TestContext框架的引导方式。具体来说,你可以使用 @BootstrapWith 来指定一个自定义的 TestContextBootstrapper。更多细节请参见 引导TestContext框架 的部分。

@ContextConfiguration

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

资源 location 通常是位于classpath中的XML配置文件或Groovy脚本,而组件类通常是 @Configuration 类。然而,资源 location 也可以指文件系统中的文件和脚本,而组件类可以是 @Component 类、@Service 类等等。详情请见 组件类(Component Classes)

下面的例子显示了一个指向XML文件的 @ContextConfiguration 注解:

Java
@ContextConfiguration("/test-config.xml") (1)
class XmlApplicationContextTests {
    // class body...
}
1 指向一个XML文件。
Kotlin
@ContextConfiguration("/test-config.xml") (1)
class XmlApplicationContextTests {
    // class body...
}
1 指向一个XML文件。

下面的例子显示了一个指向一个类的 @ContextConfiguration 注解:

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

作为声明资源 location 或组件类的替代或补充,你可以使用 @ContextConfiguration 来声明 ApplicationContextInitializer 类。下面的例子展示了这样一个情况:

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

你也可以选择使用 @ContextConfiguration 来声明 ContextLoader 策略。然而,请注意,你通常不需要明确地配置加载器,因为默认的加载器支持 initializers 和资源 locations 或组件 classes

下面的例子同时使用了一个 location 和一个 loader:

Java
@ContextConfiguration(locations = "/test-context.xml", loader = CustomContextLoader.class) (1)
class CustomLoaderXmlApplicationContextTests {
    // class body...
}
1 同时配置一个 location 和一个自定义loader。
Kotlin
@ContextConfiguration("/test-context.xml", loader = CustomContextLoader::class) (1)
class CustomLoaderXmlApplicationContextTests {
    // class body...
}
1 同时配置一个location 和一个自定义loader。
@ContextConfiguration 提供了对继承资源 location 或配置类以及由超类或包围类声明的 context initializer 的支持。

更多细节请参见 Context 管理, @Nested 测试类的配置, 以及 @Contextconfiguration javadocs.

@WebAppConfiguration

@WebAppConfiguration 是一个类级注解,你可以用它来声明为集成测试加载的 ApplicationContext 应该是一个 WebApplicationContext。仅仅在测试类上存在 @WebAppConfiguration 就可以确保为测试加载 WebApplicationContext,使用默认值 "file:src/main/webapp" 作为Web应用程序 root 的路径(也就是资源基础路径)。资源基础路径在幕后被用来创建一个 MockServletContext,作为测试的 WebApplicationContextServletContext

下面的例子展示了如何使用 @WebAppConfiguration 注解:

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

要覆盖默认值,你可以通过使用隐含 value 属性指定一个不同的基本资源路径。classpath:file: 资源前缀都被支持。如果没有提供资源前缀,路径将被假定为文件系统资源。下面的例子显示了如何指定一个classpath资源:

Java
@ContextConfiguration
@WebAppConfiguration("classpath:test-web-resources") (1)
class WebAppTests {
    // class body...
}
1 指定一个 classpath resource。
Kotlin
@ContextConfiguration
@WebAppConfiguration("classpath:test-web-resources") (1)
class WebAppTests {
    // class body...
}
1 指定一个 classpath resource。

请注意,@WebAppConfiguration 必须与 @ContextConfiguration 一起使用,无论是在一个测试类中还是在一个测试类的层次结构中。请参阅 @WebAppConfiguration javadoc以了解更多细节。

@ContextHierarchy

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

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

如果你需要在测试类层次结构中合并或覆盖上下文层次结构的某个特定级别的配置,你必须通过在类层次结构中的每个相应级别上为 @ContextConfiguration 中的 name 属性提供相同的值来明确地命名该级别。参见 上下文(Context)层次结构@ContextHierarchy javadoc 获取更多的例子。

@ActiveProfiles

@ActiveProfiles 是一个类级注解,用于声明在加载集成测试的 ApplicationContext 时,哪些Bean定义配置文件(profile)应该是活动的。

下面的例子表明 dev profile应该是活动的: s

Java
@ContextConfiguration
@ActiveProfiles("dev") (1)
class DeveloperTests {
    // class body...
}
1 表示 dev profile 应该是活动的。
Kotlin
@ContextConfiguration
@ActiveProfiles("dev") (1)
class DeveloperTests {
    // class body...
}
1 表示 dev profile 应该是活动的。

下面的例子表明,devintegration profile 都应该是活动的:

Java
@ContextConfiguration
@ActiveProfiles({"dev", "integration"}) (1)
class DeveloperIntegrationTests {
    // class body...
}
1 表示 devintegration profile 应该是活动的。
Kotlin
@ContextConfiguration
@ActiveProfiles(["dev", "integration"]) (1)
class DeveloperIntegrationTests {
    // class body...
}
1 表示 devintegration profile 应该是活动的。
@ActiveProfiles 默认提供了对继承由超类和包围类声明的活动豆定义配置文件的支持。你也可以通过实现一个自定义的 ActiveProfilesResolver 并通过使用 @ActiveProfilesresolver 属性来注册它,从而以编程方式解决 active bean definition profile。
@TestPropertySource

@TestPropertySource 是一个类级别的注解,你可以用它来配置properties文件和内联properties的位置(location),以添加到为集成测试加载的 ApplicationContextEnvironment 中的 PropertySources 集合。

下面的例子演示了如何从classpath声明一个properties文件:

Java
@ContextConfiguration
@TestPropertySource("/test.properties") (1)
class MyIntegrationTests {
    // class body...
}
1 从classpath根部的 test.properties 获取属性。
Kotlin
@ContextConfiguration
@TestPropertySource("/test.properties") (1)
class MyIntegrationTests {
    // class body...
}
1 从classpath根部的 test.properties 获取属性。

下面的例子演示了如何声明内联 properties:

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

参见 带有测试属性源(Property Sources)的 Context 配置 中的例子和进一步细节。

@DynamicPropertySource

@DynamicPropertySource 是一个方法级注解,你可以用它来注册动态属性,将其添加到集成测试加载的 ApplicationContextEnvironment 中的 PropertySources 集中。当你不知道属性的值时,动态属性很有用—​例如,如果属性由外部资源管理,如由 Testcontainers 项目管理的容器。

下面的例子演示了如何注册一个动态属性:

Java
@ContextConfiguration
class MyIntegrationTests {

    static MyExternalServer server = // ...

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

    // tests ...
}
1 @DynamicPropertySource 来注解一个 static 方法。
2 接受一个 DynamicPropertyRegistry 作为参数。
3 注册一个动态的 server.port 属性,以便从服务器中延迟地检索。
Kotlin
@ContextConfiguration
class MyIntegrationTests {

    companion object {

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

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

    // tests ...
}
1 @DynamicPropertySource 来注解一个 static 方法。
2 接受一个 DynamicPropertyRegistry 作为参数。
3 注册一个动态的 server.port 属性,以便从服务器中延迟地检索。

更多细节请参见 使用动态属性源的 Context 配置

@DirtiesContext

@DirtiesContext 表示底层的Spring ApplicationContext 在测试的执行过程中被破坏了(也就是说,测试以某种方式修改或破坏了它—​例如,通过改变单体Bean的状态),应该被关闭。当一个应用程序上下文(application context)被标记为脏时,它就会从测试框架的缓存中删除并关闭。因此,对于任何需要具有相同配置元数据的上下文的后续测试,底层的Spring容器将被重建。

你可以在同一个类或类的层次结构中使用 @DirtiesContext 作为类级和方法级注解。在这种情况下,根据配置的 methodModeclassModeApplicationContext 在任何这种注解的方法之前或之后,以及在当前测试类之前或之后都被标记为脏。

下面的例子解释了在各种配置情况下,什么时候上下文(context)会被搅乱:

  • 在当前测试类之前,当在 class mode 设置为 BEFORE_CLASS 的类上声明时。

    Java
    @DirtiesContext(classMode = BEFORE_CLASS) (1)
    class FreshContextTests {
        // some tests that require a new Spring container
    }
    
    1 将当前测试类之前的上下文(context)破坏。
    Kotlin
    @DirtiesContext(classMode = BEFORE_CLASS) (1)
    class FreshContextTests {
        // some tests that require a new Spring container
    }
    
    1 将当前测试类之前的上下文(context)破坏。
  • 在当前测试类之后,当在 class mode 设置为 AFTER_CLASS(即默认的 class mode)的类上声明。

    Java
    @DirtiesContext (1)
    class ContextDirtyingTests {
        // some tests that result in the Spring container being dirtied
    }
    
    1 在当前测试类之后破坏上下文(Context)
    Kotlin
    @DirtiesContext (1)
    class ContextDirtyingTests {
        // some tests that result in the Spring container being dirtied
    }
    
    1 在当前测试类之后破坏上下文(Context).
  • 在当前测试类中的每个测试方法之前,当在 class mode 设置为 BEFORE_EACH_TEST_METHOD 的类上声明时。

    Java
    @DirtiesContext(classMode = BEFORE_EACH_TEST_METHOD) (1)
    class FreshContextTests {
        // some tests that require a new Spring container
    }
    
    1 在每个测试方法之前破坏上下文(Context).
    Kotlin
    @DirtiesContext(classMode = BEFORE_EACH_TEST_METHOD) (1)
    class FreshContextTests {
        // some tests that require a new Spring container
    }
    
    1 在每个测试方法之前破坏上下文(Context)
  • 在当前测试类的每个测试方法之后,当在 class mode 设置为 AFTER_EACH_TEST_METHOD 的类上声明时。

    Java
    @DirtiesContext(classMode = AFTER_EACH_TEST_METHOD) (1)
    class ContextDirtyingTests {
        // some tests that result in the Spring container being dirtied
    }
    
    1 在每个测试方法之后,破坏上下文(Context)。
    Kotlin
    @DirtiesContext(classMode = AFTER_EACH_TEST_METHOD) (1)
    class ContextDirtyingTests {
        // some tests that result in the Spring container being dirtied
    }
    
    1 在每个测试方法之后,破坏上下文(Context)。
  • 在当前测试之前,当在一个方法上声明时,method mode 设置为 BEFORE_METHOD

    Java
    @DirtiesContext(methodMode = BEFORE_METHOD) (1)
    @Test
    void testProcessWhichRequiresFreshAppCtx() {
        // some logic that requires a new Spring container
    }
    
    1 将当前测试方法之前的上下文(Context)破坏。
    Kotlin
    @DirtiesContext(methodMode = BEFORE_METHOD) (1)
    @Test
    fun testProcessWhichRequiresFreshAppCtx() {
        // some logic that requires a new Spring container
    }
    
    1 将当前测试方法之前的上下文(Context)破坏。
  • 在当前测试之后,当在一个方法上声明时,method mode 设置为 AFTER_METHOD(即,默认的 method mode)。

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

如果你在一个测试中使用 @DirtiesContext,其上下文被配置为带有 @ContextHierarchy 的上下文层次结构的一部分,你可以使用 hierarchyMode 标志来控制上下文缓存的清除方式。默认情况下,一个详尽的算法被用来清除上下文缓存,不仅包括当前层级,还包括与当前测试共享一个祖先上下文的所有其他上下文层级。所有位于共同祖先上下文的子层次中的 ApplicationContext 实例都被从上下文缓存中删除并关闭。如果穷举算法对于一个特定的用例来说是多余的,你可以指定更简单的当前级别算法,正如下面的例子所示。

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

class ExtendedTests extends BaseTests {

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

class ExtendedTests : BaseTests() {

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

关于 EXHAUSTIVECURRENT_LEVEL 算法的进一步细节,请参见 DirtiesContext.HierarchyMode javadoc。

@TestExecutionListeners

@TestExecutionListeners 用于为一个特定的测试类、其子类和其嵌套类注册监听器。如果你想全局注册一个监听器,你应该通过 TestExecutionListener 配置 中描述的自动发现机制来注册它。

下面的例子显示了如何注册两个 TestExecutionListener 的实现:

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

默认情况下,@TestExecutionListeners 提供了对从超类或包围类中继承监听器的支持。参见 @Nested 测试类的配置@TestExecutionListeners javadoc 以了解一个例子和进一步的细节。如果你发现你需要切换回使用默认的 TestExecutionListener 实现,请参阅 注册 TestExecutionListener 实现 中的说明。

@RecordApplicationEvents

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

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

参见 Application Events@RecordApplicationEvents javadoc 以了解一个例子和更多细节。

@Commit

@Commit 表示一个事务性测试方法的事务应该在测试方法完成后被提交。你可以用 @Commit 直接替代 @Rollback(false) 来更明确地传达代码的意图。与 @Rollback 类似,@Commit 也可以被声明为类级或方法级注解。

下面的例子展示了如何使用 @Commit 注解:

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

@Rollback 表示在测试方法完成后,事务性测试方法的事务是否应该被回滚。如果为 true,事务将被回滚。否则,事务被提交(参见 @Commit)。即使 @Rollback 没有明确声明,Spring TestContext 框架中集成测试的回滚也默认为 true

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

下面的例子导致一个测试方法的结果不被回滚(也就是说,结果被提交到数据库):

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

methods.

@BeforeTransaction 表明,对于那些通过使用Spring的 @Transactional 注解而被配置为在事务中运行的测试方法,注解的 void 方法应该在事务开始前运行。 @BeforeTransaction 方法不需要是 public 的,可以在基于Java 8的接口默认方法上声明。

下面的例子展示了如何使用 @BeforeTransaction 注解:

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

@AfterTransaction 表明,对于那些通过使用Spring的 @Transactional 注解而被配置为在事务中运行的测试方法,注解的 void 方法应在事务结束后运行。@AfterTransaction 方法不需要是 public 的,可以在基于Java 8的接口默认方法上声明。

Java
@AfterTransaction (1)
void afterTransaction() {
    // logic to be run after a transaction has ended
}
1 在一个事务之后运行这个方法。
Kotlin
@AfterTransaction (1)
fun afterTransaction() {
    // logic to be run after a transaction has ended
}
1 在一个事务之后运行这个方法。
@Sql

@Sql 用于注解测试类或测试方法,以配置在集成测试期间针对给定数据库运行的SQL脚本。下面的例子显示了如何使用它:

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

更多细节请参见 用 @Sql 声明式地执行SQL脚本

@SqlConfig

@SqlConfig 定义了元数据,用于确定如何解析和运行用 @Sql 注解配置的SQL脚本。下面的例子展示了如何使用它:

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

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

注意,方法级的 @SqlMergeMode 声明会覆盖类级的声明。

下面的例子展示了如何在类的层面上使用 @SqlMergeMode

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

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

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

下面的例子展示了如何在方法层面使用 @SqlMergeMode

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

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

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

@SqlGroup 是一个容器注解,它聚合了几个 @Sql 注解。你可以原生地使用 @SqlGroup 来声明几个嵌套的 @Sql 注解,或者你可以结合Java 8对可重复注解的支持来使用它,在同一个类或方法上可以多次声明 @Sql,隐含地生成这个容器注解。下面的例子展示了如何声明一个SQL group:

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

9.1.3. Spring JUnit 4测试注解

以下注解只有在与 SpringRunnerSpring 的 JUnit4 ruleSpring的JUnit 4支持类 一起使用时才被支持:

@IfProfileValue

@IfProfileValue 表示注解的测试在特定的测试环境中被启用。如果配置的 ProfileValueSource 为所提供的 name 返回一个匹配的 value,则测试被启用。否则,测试将被禁用,并且实际上被忽略。

你可以在类级、方法级或两者中应用 @IfProfileValue。对于该类或其子类中的任何方法, @IfProfileValue 的类级使用优先于方法级使用。具体来说,如果一个测试在类级和方法级都被启用,那么它就是被启用的。没有 @IfProfileValue 意味着测试是隐式启用的。这类似于JUnit 4的 @Ignore 注解的语义,除了 @Ignore 的存在总是禁用一个测试。

下面的例子显示了一个有 @IfProfileValue 注解的测试:

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

另外,你可以用一个 values 列表来配置 @IfProfileValue(具有 OR 语义),以便在JUnit 4环境中实现类似TestNG的测试组支持。考虑一下下面的例子:

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

@ProfileValueSourceConfiguration 是一个类级别的注解,它指定了在检索通过 @IfProfileValue 注解配置的配置文件值时要使用什么类型的 ProfileValueSource。如果没有为测试声明 @ProfileValueSourceConfiguration,则默认使用 SystemProfileValueSource。下面的例子显示了如何使用 @ProfileValueSourceConfiguration

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

@Timed 表示注解的测试方法必须在指定的时间段(以毫秒为单位)内完成执行。如果文本的执行时间超过了指定的时间段,则测试失败。

这个时间段包括运行测试方法本身,任何重复的测试(见 @Repeat),以及任何测试fixture的设置或拆除。下面的例子显示了如何使用它:

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

Spring的 @Timed 注解与JUnit 4的 @Test(timeout=…​) 支持有着不同的语义。具体来说,由于JUnit 4处理测试执行超时的方式(即在一个单独的 Thread 中执行测试方法),@Test(timeout=…​) 在测试时间过长的情况下会预先失败。另一方面,Spring的 @Timed 不会抢先让测试失败,而是等待测试完成后再失败。

@Repeat

@Repeat 表示注解的测试方法必须重复运行。测试方法的运行次数在注释中被指定。

重复执行的范围包括测试方法本身的执行,以及任何测试 fixture 的设置或拆除。当与 SpringMethodRule 一起使用时,该范围还包括 SpringMethodRule 实现对测试实例的准备。下面的例子展示了如何使用 @Repeat 注解:

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

9.1.4. Spring JUnit Jupiter 测试注解

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

@SpringJUnitConfig

@SpringJUnitConfig 是一个组成注解,它结合了JUnit Jupiter的 @ExtendWith(SpringExtension.class) 和 Spring TestContext 框架的 @ContextConfiguration。它可以作为 @ContextConfiguration 的替代品在类的层面上使用。关于配置选项,@ContextConfiguration@SpringJUnitConfig 之间的唯一区别是,组件类可以用 @SpringJUnitConfig 中的 value 属性来声明。

下面的例子展示了如何使用 @SpringJUnitConfig 注解来指定一个配置类:

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

下面的例子显示了如何使用 @SpringJUnitConfig 注解来指定一个配置文件的位置:

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

参见 Context 管理 以及 @SpringJUnitConfig@ContextConfiguration 的 javadoc 以了解更多细节。

@SpringJUnitWebConfig

@SpringJUnitWebConfig 是一个组成注解,它将JUnit Jupiter中的 @ExtendWith(SpringExtension.class) 与Spring TestContext框架中的 @ContextConfiguration@WebAppConfiguration 相结合。你可以在类的层面上使用它,作为 @ContextConfiguration@WebAppConfiguration 的直接替代。关于配置选项,@ContextConfiguration@SpringJUnitWebConfig 之间的唯一区别是,你可以通过使用 @SpringJUnitWebConfigvalue 属性来声明组件类。此外,你只能通过使用 @SpringJUnitWebConfig 中的 resourcePath 属性来覆盖 @WebAppConfigurationvalue 属性。

下面的例子展示了如何使用 @SpringJUnitWebConfig 注解来指定一个配置类:

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

下面的例子展示了如何使用 @SpringJUnitWebConfig 注解来指定一个配置文件的位置:

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

参见 Context 管理 以及 @SpringJUnitWebConfig, @ContextConfiguration,以及 @WebAppConfiguration 了解更多细节。

@TestConstructor

@TestConstructor 是一个类型级别的注解,用于配置测试类构造器的参数如何从测试的 ApplicationContext 中的组件自动装配。

如果 @TestConstructor 在一个测试类上不存在或元存在,将使用默认的测试构造器自动装配模式。关于如何改变默认模式的细节,见下面的提示。然而,请注意,构造函数上的 @Autowired 的局部声明优先于 @TestConstructor 和默认模式。

改变默认的测试构造器自动装配模式

默认的测试构造器自动装配模式可以通过将 spring.test.constructor.autowire.mode JVM 系统属性设置为 all 来改变。另外,默认模式可以通过 SpringProperties 机制设置。

从Spring Framework 5.3 开始,默认模式也可以被配置为 JUnit平台配置参数

如果 spring.test.constructor.autowire.mode 属性没有设置,测试类构造函数将不会自动自动装配。

从Spring框架 5.2 开始,@TestConstructor 只支持与 SpringExtension 一起使用JUnit Jupiter。请注意,SpringExtension 通常会自动为你注册—​例如,在使用 @SpringJUnitConfig@SpringJUnitWebConfig 等注解或Spring Boot Test的各种测试相关注解时。
@NestedTestConfiguration

@NestedTestConfiguration 是一个类型级别的注解,用于配置Spring测试配置注解在内部测试类的包围类层次结构中的处理方式。

如果 @NestedTestConfiguration 不存在或元存在于测试类、其超类型层次结构或其包围类层次结构中,将使用默认的包围配置继承模式。关于如何改变默认模式的细节,见下面的提示。

改变默认的包围式配置继承模式

默认的包围配置继承模式是 INHERIT,但可以通过设置 spring.test.enclosing.configuration JVM系统属性为 OVERRIDE 来改变。另外,也可以通过 SpringProperties 机制设置默认模式。

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

@NestedTestConfiguration 的使用通常只对JUnit Jupiter中的 @Nested 测试类有意义;但是,可能有其他支持Spring和嵌套测试类的测试框架会使用这个注解。

@Nested 测试类的配置 中的例子和进一步的细节。

@EnabledIf

@EnabledIf 用于发出信号,表明注解的JUnit Jupiter测试类或测试方法被启用,并且如果提供的表达式评估为 true,就应该运行。具体来说,如果 expression 评估为 Boolean.TRUE 或一个等于 trueString 串(忽略大小写),则测试被启用。当应用在类的层面上时,该类中的所有测试方法默认也会自动启用。

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

  • Spring 表达式语言(SpEL)表达式。比如说: @EnabledIf("#{systemProperties['os.name'].toLowerCase().contains('mac')}")

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

  • 文本字面量。比如说: @EnabledIf("true")

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

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

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

@EnabledOnMac 只是作为一个可能的例子。如果你有这种确切的用例,请使用JUnit Jupiter中内置的 @EnabledOnOs(MAC) 支持。

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

@DisabledIf

@DisabledIf 用于发出信号,如果提供的 expression 评估为 true,则注解的JUnit Jupiter测试类或测试方法被禁用,不应运行。具体来说,如果 expression 评估为 Boolean.TRUE 或一个等于 trueString(忽略大小写),则测试被禁用。当在类的层面上应用时,该类中的所有测试方法也会自动禁用。

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

  • Spring 表达式语言(SpEL)表达式。比如说: @DisabledIf("#{systemProperties['os.name'].toLowerCase().contains('mac')}")

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

  • 文本字面量。比如说: @DisabledIf("true")

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

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

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

@DisabledOnMac 只是作为一个可能的例子。如果你有这种确切的用例,请使用JUnit Jupiter中内置的 @DisabledOnOs(MAC) 支持。

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

9.1.5. 对测试的元注解支持

你可以使用大多数与测试相关的注解作为 元注解 来创建自定义的组成注解,并减少整个测试套件的配置重复。

你可以结合 TestContext框架 使用以下每个元注解。

  • @BootstrapWith

  • @ContextConfiguration

  • @ContextHierarchy

  • @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上支持)

请考虑以下例子:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Java
@TransactionalDevTestConfig
class OrderRepositoryTests { }

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

@TransactionalDevTestConfig
class UserRepositoryTests { }

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

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

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

Java
@TransactionalIntegrationTest
void saveOrder() { }

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

@TransactionalIntegrationTest
fun deleteOrder() { }

更多细节,请参见 Spring注解编程模型 wiki页面。

9.2. 更多资源

有关测试的更多信息,请参见以下资源:

  • JUnit: "一个对程序员友好的Java和JVM的测试框架"。由Spring框架在其测试套件中使用,并在 Spring TestContext Framework 中得到支持。

  • TestNG: 一个受JUnit启发的测试框架,增加了对测试组、数据驱动测试、分布式测试和其他功能的支持。在 Spring TestContext Framework 中支持

  • AssertJ: "Java的 fluent 断言",包括对Java 8 lambdas、流(streams)和许多其他功能的支持。

  • Mock Objects: 维基百科中的文章。

  • MockObjects.com: 专门介绍mock对象的网站,这是一种在测试驱动开发中改进代码设计的技术。

  • Mockito: 基于 Test Spypattern 的Java mock 库。由Spring框架在其测试套件中使用。

  • EasyMock: Java库 "通过使用Java的代理机制即时生成接口(和通过类扩展的对象),为其提供Mock对象"。

  • JMock: 支持用mock对象对Java代码进行测试驱动开发的库。

  • DbUnit: JUnit扩展(也可用于Ant和Maven),针对数据库驱动的项目,除其他外,在测试运行之间将你的数据库置于已知状态。

  • Testcontainers: 支持JUnit测试的Java库,为普通数据库、Selenium web browser或其他可以在Docker容器中运行的东西提供轻量级的、可抛弃的实例。

  • The Grinder: Java负载测试框架。

  • SpringMockK: 支持用Kotlin编写的Spring Boot集成测试,使用 MockK 而不是Mockito。