# 测试

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

# 1. Spring 测试简介

测试是 Enterprise 软件开发中不可缺少的一部分。本章重点讨论了 IOC 原则对单元测试的增值,以及 Spring 框架支持集成测试的好处。(在 Enterprise 中对测试的彻底处理超出了本参考手册的范围。)

# 2. 单元测试

与传统的 爪哇 EE 开发相比,依赖注入应该使你的代码更少地依赖于容器。组成应用程序的 POJO 应该在 JUnit 或 TestNG 测试中是可测试的,使用new操作符实例化对象,而不需要 Spring 或任何其他容器。你可以使用模拟对象(与其他有价值的测试技术一起使用)来孤立地测试你的代码。如果你遵循 Spring 的体系结构建议,那么你的代码库的干净分层和组件化将促进更容易的单元测试。例如,你可以通过截断或模拟 DAO 或存储库接口来测试服务层对象,而不需要在运行单元测试时访问持久性数据。

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

# 2.1.模拟对象

Spring 包括一些专门用于嘲弄的软件包:

# 2.1.1.环境

org.springframework.mock.env包包含EnvironmentPropertySource抽象的模拟实现(参见Bean Definition Profiles和[PropertySource抽象](core.html#beans-property-source-abstraction))。MockEnvironmentMockPropertySource对于开发依赖于环境特定属性的代码的容器外测试非常有用。

# 2.1.2.JNDI

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

org.springframework.mock.jndi包中的模拟 JNDI 支持是
在 Spring Framework5.2 中正式反对的,以支持来自第三方
的完整解决方案,例如SIMPLE-JNDI (opens new window)

# 2.1.3. Servlet 空气污染指数

org.springframework.mock.web包包含一组完整的 API 模拟对象,这些对象对于测试 Web 上下文、控制器和过滤器非常有用。这些模拟对象针对 Spring 的 Web MVC 框架的使用,并且通常比动态模拟对象(例如EasyMock (opens new window))或可选的 Servlet API 模拟对象(例如模拟对象 (opens new window))更方便地使用。

自 Spring Framework5.0 以来,基于 Servlet 4.0API,org.springframework.mock.web中的模拟对象是

Spring MVC 测试框架构建在模拟 Servlet API 对象上,以提供 Spring MVC 的集成测试框架。见MockMvc

# 2.1.4. Spring 网络反应

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

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

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

WebTestClient构建在模拟请求和响应的基础上,为在没有 HTTP 服务器的情况下测试 WebFlux 应用程序提供支持。客户机还可以用于与正在运行的服务器进行端到端测试。

# 2.2.单元测试支持类

Spring 包括许多可以帮助单元测试的类。它们可分为两类:

# 2.2.1.通用测试工具

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

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

  • 允许privateprotected字段访问的 ORM 框架(例如 JPA 和 Hibernate),而不是用于域实体中属性的publicsetter 方法。

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

  • 对于生命周期回调方法,使用@PostConstruct@PreDestroy之类的注释。

[AopTestUtils](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/test/util/aoptestutils.html)是与 AOP 相关的实用方法的集合。可以使用这些方法获得隐藏在一个或多个 Spring 代理后面的底层目标对象的引用。例如,如果你已经通过使用 EasyMock 或 Mockito 之类的库将 Bean 配置为动态模拟,并且该模拟被包装在 Spring 代理中,那么你可能需要直接访问底层模拟以在其上配置期望并执行验证。关于 Spring 的核心 AOP 实用程序,请参见[AopUtils](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/ AOP/support/aoputils.html)和[AopProxyUtils(https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/ AOP/aopyutils.html)。

# 2.2.2. Spring MVC 测试工具

org.springframework.test.web包包含[ModelAndViewAssert](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/test/web/modelandviewassert.html),你可以将其与 JUnit、TestNG 或处理 Spring MVC<gtr="435"/>对象的任何其他测试框架结合使用。

单元测试 Spring MVC 控制器

以单元测试你的 Spring MVCController类作为 POJO,使用ModelAndViewAssertMockHttpServletRequest结合,MockHttpSession,从 Spring 的Servlet API mocks以此类推。对于 Spring MVC 的
Spring MVC 和 RESTController类以及WebApplicationContext配置,要进行彻底的集成测试,请使用Spring MVC Test Framework代替。

# 3. 集成测试

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

# 3.1.概述

重要的是能够执行一些集成测试,而不需要部署到应用程序服务器或连接到其他 Enterprise 基础设施。这样做可以让你测试以下内容:

  • 你的 Spring IOC 容器上下文的正确接线。

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

Spring 框架为spring-test模块中的集成测试提供了一流的支持。实际 jar 文件的名称可能包括发布版本,也可能是长org.springframework.test格式,这取决于你从哪里获得它(有关解释,请参见抚养管理一节)。这个库包括org.springframework.test包,其中包含用于使用 Spring 容器进行集成测试的有价值的类。此测试不依赖于应用程序服务器或其他部署环境。这样的测试比单元测试运行得慢,但比同等的 Selenium 测试或依赖于部署到应用程序服务器的远程测试快得多。

单元和集成测试支持是以注释驱动的Spring TestContext Framework的形式提供的。TestContext 框架与实际使用的测试框架无关,该框架允许在各种环境中测试,包括 JUnit、TestNG 和其他环境。

# 3.2.集成测试的目标

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

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

# 3.2.1.上下文管理和缓存

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

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

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

请参阅使用 TestContext 框架的上下文管理上下文缓存

# 3.2.2.测试夹具的依赖注入

当 TestContext 框架加载应用程序上下文时,它可以通过使用依赖项注入来配置测试类的实例。这提供了一种方便的机制,可以通过使用应用程序上下文中预先配置的 bean 来设置测试装置。这里的一个很大的好处是,你可以跨各种测试场景重用应用程序上下文(例如,用于配置 Spring-托管对象图、事务代理、DataSource实例和其他),从而避免了为单个测试用例重复复杂的测试固定设置的需要。

例如,考虑一个场景,其中我们有一个类(HibernateTitleRepository),它实现了Title域实体的数据访问逻辑。我们希望编写测试以下领域的集成测试:

  • Spring 配置:基本上,与HibernateTitleRepository Bean 的配置相关的一切都是正确的和存在的吗?

  • Hibernate 映射文件配置:是否所有映射都正确,以及是否存在正确的延迟加载设置?

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

参见使用TestContext 框架的测试固定件的依赖注入。

# 3.2.3.事务管理

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

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

如果你想要提交一个事务(这是不寻常的,但在你想要填充或修改数据库的特定测试时偶尔会有用),那么你可以通过使用[@Commit](#Integration-Testing-Annotations)注释,告诉 TestContext 框架使事务提交,而不是回滚。

参见事务管理TestContext 框架

# 3.2.4.集成测试的支持类

Spring TestContext 框架提供了几个abstract支持类,这些类简化了集成测试的编写。这些基本测试类为测试框架提供了定义良好的挂钩,以及方便的实例变量和方法,使你能够访问:

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

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

此外,你可能希望创建你自己的定制的、应用程序范围的超类,其中包含特定于你的项目的实例变量和方法。

参见TestContext 框架的支持类。

# 3.3.JDBC 测试支持

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

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

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

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

  • deleteFromTableWhere(..):使用提供的WHERE子句从给定表中删除行。

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

[AbstractTransactionalJUnit4SpringContextTests](#testcontext-support-classes-junit4)和[AbstractTransactionalTestNGSpringContextTests](#testcontext-support-classes-testng)提供了方便的方法,这些方法在JdbcTestUtils中委托给上述方法。
spring-jdbc模块提供了对配置和启动嵌入式
数据库的支持,你可以在与数据库交互的集成测试中使用该数据库。

# 3.4.注解

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

# 3.4.1. Spring 测试注释

Spring 框架提供了以下一组 Spring 特定的注释,你可以在与 TestContext 框架结合的单元和集成测试中使用这些注释。有关更多信息,请参见相应的 爪哇doc,包括默认属性值、属性别名和其他详细信息。

Spring 的测试注释包括以下内容:

  • [@BootstrapWith](# Spring-testing-annotation-bootstrapwith)

  • [@ContextConfiguration](# Spring-testing-annotation-contextconfiguration)

  • [@WebAppConfiguration](# Spring-testing-annotation-webappconfiguration)

  • [@ContextHierarchy](# Spring-testing-annotation-contexthierarchy)

  • [@ActiveProfiles](# Spring-testing-annotation-ActiveProfiles)

  • [@TestPropertySource](# Spring-testing-annotation-testPropertySource)

  • [@DynamicPropertySource](# Spring-testing-annotation-dynamicpropertysource)

  • [@DirtiesContext](# Spring-testing-annotation-dirtiescontext)

  • [@TestExecutionListeners](# Spring-testing-annotation-testexecutionlisteners)

  • [@RecordApplicationEvents](# Spring-testing-annotation-recordapplicationEvents)

  • [@Commit](# Spring-testing-annotation-commit)

  • [@Rollback](# Spring-testing-annotation-rollback)

  • [@BeforeTransaction](# Spring-testing-annotation-beforeTransaction)

  • [@AfterTransaction](# Spring-testing-annotation-aftertransaction)

  • [@Sql](# Spring-testing-annotation-sql)

  • [@SqlConfig](# Spring-testing-annotation-sqlconfig)

  • [@SqlMergeMode](# Spring-testing-annotation-sqlmergemode)

  • [@SqlGroup](# Spring-testing-annotation-sqlgroup)

# @BootstrapWith

@BootstrapWith是一个类级注释,你可以使用它来配置如何引导 Spring TestContext 框架。具体地说,你可以使用@BootstrapWith来指定自定义的TestContextBootstrapper。有关更多详细信息,请参见引导 TestContext 框架一节。

# @ContextConfiguration

@ContextConfiguration定义了类级元数据,用于确定如何加载和配置用于集成测试的ApplicationContext。具体地说,@ContextConfiguration声明用于加载上下文的应用程序上下文资源locations或组件classes

资源位置通常是位于 Classpath 中的 XML 配置文件或 Groovy 脚本,而组件类通常是@Configuration类。然而,资源位置也可以引用文件系统中的文件和脚本,并且组件类可以是@Component类、@Service类等等。有关更多详细信息,请参见组件类

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

爪哇

@ContextConfiguration("/test-config.xml") (1)
class XmlApplicationContextTests {
    // class body...
}
1 指一个 XML 文件。

Kotlin

@ContextConfiguration("/test-config.xml") (1)
class XmlApplicationContextTests {
    // class body...
}
1 指一个 XML 文件。

下面的示例显示了引用类的@ContextConfiguration注释:

爪哇

@ContextConfiguration(classes = TestConfig.class) (1)
class ConfigClassApplicationContextTests {
    // class body...
}
1 指的是一门课。

Kotlin

@ContextConfiguration(classes = [TestConfig::class]) (1)
class ConfigClassApplicationContextTests {
    // class body...
}
1 指的是一门课。

作为一种替代方法,或者除了声明资源位置或组件类之外,还可以使用@ContextConfiguration声明ApplicationContextInitializer类。下面的示例展示了这样一个案例:

爪哇

@ContextConfiguration(initializers = CustomContextIntializer.class) (1)
class ContextInitializerTests {
    // class body...
}
1 声明初始化程序类。

Kotlin

@ContextConfiguration(initializers = [CustomContextIntializer::class]) (1)
class ContextInitializerTests {
    // class body...
}
1 声明初始化程序类。

你也可以选择使用@ContextConfiguration来声明ContextLoader策略。但是,请注意,你通常不需要显式地配置加载器,因为默认加载器支持initializers和资源locations或组件classes

下面的示例既使用了位置,也使用了加载器:

爪哇

@ContextConfiguration(locations = "/test-context.xml", loader = CustomContextLoader.class) (1)
class CustomLoaderXmlApplicationContextTests {
    // class body...
}
1 配置位置和自定义加载器。

Kotlin

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

请参阅上下文管理,[@Nested测试类配置](#testcontext-junit-jupiter-nested-test-configuration),以及@ContextConfiguration爪哇docs 以获取更多详细信息。

# @WebAppConfiguration

@WebAppConfiguration是一个类级注释,你可以使用它来声明为集成测试加载的ApplicationContext应该是WebApplicationContext。在测试类上仅存在@WebAppConfiguration就可以确保为测试加载WebApplicationContext,使用缺省值"file:src/main/webapp"作为 Web 应用程序的根路径(即资源基路径)。后台使用资源库路径创建MockServletContext,它是测试的ServletContextServletContext

下面的示例展示了如何使用@WebAppConfiguration注释:

爪哇

@ContextConfiguration
@WebAppConfiguration (1)
class WebAppTests {
    // class body...
}

Kotlin

@ContextConfiguration
@WebAppConfiguration (1)
class WebAppTests {
    // class body...
}
1 @WebAppConfiguration注释。

要覆盖默认值,可以使用隐式value属性指定不同的基本资源路径。同时支持classpath:file:资源前缀。如果没有提供资源前缀,则假定路径是一个文件系统资源。下面的示例展示了如何指定 Classpath 资源:

爪哇

@ContextConfiguration
@WebAppConfiguration("classpath:test-web-resources") (1)
class WebAppTests {
    // class body...
}
1 指定 Classpath 资源。

Kotlin

@ContextConfiguration
@WebAppConfiguration("classpath:test-web-resources") (1)
class WebAppTests {
    // class body...
}
1 指定 Classpath 资源。

请注意,@WebAppConfiguration必须与@ContextConfiguration一起使用,无论是在单个测试类中还是在测试类层次结构中。有关更多详细信息,请参见[@WebAppConfiguration](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/test/context/web/webappconfiguration.html)爪哇doc。

# @ContextHierarchy

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

爪哇

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

爪哇

@WebAppConfiguration
@ContextHierarchy({
    @ContextConfiguration(classes = AppConfig.class),
    @ContextConfiguration(classes = WebConfig.class)
})
class WebIntegrationTests {
    // class body...
}

Kotlin

@WebAppConfiguration
@ContextHierarchy(
        ContextConfiguration(classes = [AppConfig::class]),
        ContextConfiguration(classes = [WebConfig::class]))
class WebIntegrationTests {
    // class body...
}

如果需要合并或覆盖测试类层次结构中上下文层次结构的给定级别的配置,则必须在类层次结构中的每个对应级别上,通过向@ContextConfiguration中的name属性提供相同的值,显式地命名该级别。参见上下文层次结构和[@ContextHierarchy](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/test/contexthierarchy.html)爪哇doc 以获取更多示例。

# @ActiveProfiles

@ActiveProfiles是一种类级注释,用于声明在为集成测试加载ApplicationContext时哪些 Bean 定义配置文件应该处于活动状态。

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

爪哇

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

Kotlin

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

下面的示例表明,devintegration配置文件都应该处于活动状态:

爪哇

@ContextConfiguration
@ActiveProfiles({"dev", "integration"}) (1)
class DeveloperIntegrationTests {
    // class body...
}
1 指示devintegration配置文件应该处于活动状态。

Kotlin

@ContextConfiguration
@ActiveProfiles(["dev", "integration"]) (1)
class DeveloperIntegrationTests {
    // class body...
}
1 指示devintegration配置文件应该处于活动状态。
@ActiveProfiles提供了对继承活动 Bean 定义配置文件
的支持,该配置文件由超类声明并默认包含类。还可以通过实现自定义[ActiveProfilesResolver](#TestContext-CTX-Management-ENV-Profiles-ActiveProfilesResolver)并使用resolver``@ActiveProfiles的属性来以编程方式解析活动的
Bean 定义配置文件。

参见具有环境配置文件的上下文配置,[@Nested测试类配置](#TestContext-JUnit-Jupiter-Nested-Test-Configuration),以及[@ActiveProfiles](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/org/springframework/test/context/actevepries.html)的示例和进一步详细信息。

# @TestPropertySource

@TestPropertySource是一种类级注释,你可以使用它来配置要添加到PropertySources中的Environment集合中的属性文件和内联属性的位置,用于为集成测试加载ApplicationContext

下面的示例演示了如何从 Classpath 声明属性文件:

爪哇

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

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

爪哇

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

Kotlin

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

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

# @DynamicPropertySource

@DynamicPropertySource是一种方法级别的注释,你可以使用它来注册要添加到PropertySources中的ApplicationContext集合中的 dynamicproperties,用于为集成测试加载ApplicationContext。当你不知道属性的初始值时,动态属性是有用的——例如,如果属性是由外部资源管理的,比如由测试容器 (opens new window)项目管理的容器。

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

爪哇

@ContextConfiguration
class MyIntegrationTests {

    static MyExternalServer server = // ...

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

    // tests ...
}
1 @DynamicPropertySource注释static方法。
2 接受DynamicPropertyRegistry作为参数。
3 注册一个动态server.port属性,以便从服务器上懒洋洋地检索。

Kotlin

@ContextConfiguration
class MyIntegrationTests {

    companion object {

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

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

    // tests ...
}
1 @DynamicPropertySource注释static方法。
2 接受DynamicPropertyRegistry作为参数。
3 注册一个动态server.port属性,以便从服务器上懒洋洋地检索。

有关更多详细信息,请参见具有动态属性源的上下文配置

# @DirtiesContext

@DirtiesContext表示底层 Spring ApplicationContext在测试的执行过程中被弄脏了(也就是说,测试以某种方式修改或损坏了它——例如,通过更改单例 Bean 的状态),并且应该关闭。当一个应用程序上下文被标记为 dirty 时,它将从测试框架的缓存中删除并关闭。因此,对于需要具有相同配置元数据的上下文的任何后续测试,都会重新构建基础 Spring 容器。

可以在同一个类或类层次结构中同时使用@DirtiesContext作为类级和方法级的注释。在这种情况下,根据配置的methodModeclassMode,在任何此类注释方法之前或之后以及在当前测试类之前或之后将ApplicationContext标记为 dirty。

下面的示例解释了各种配置场景的上下文何时会被弄脏:

  • 在当前测试类之前,当在类上声明时,将类的模式设置为BEFORE_CLASS

    爪哇

    @DirtiesContext(classMode = BEFORE_CLASS) (1)
    class FreshContextTests {
        // some tests that require a new Spring container
    }
    
    1 在当前测试类之前弄脏上下文。

    Kotlin

    @DirtiesContext(classMode = BEFORE_CLASS) (1)
    class FreshContextTests {
        // some tests that require a new Spring container
    }
    
    1 在当前测试类之前弄脏上下文。
  • 在当前的测试类之后,当在一个类上声明具有设置为AFTER_CLASS的类模式时(即默认的类模式)。

    爪哇

    @DirtiesContext (1)
    class ContextDirtyingTests {
        // some tests that result in the Spring container being dirtied
    }
    
    1 在当前测试类之后,将上下文弄脏。

    Kotlin

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

    爪哇

    @DirtiesContext(classMode = BEFORE_EACH_TEST_METHOD) (1)
    class FreshContextTests {
        // some tests that require a new Spring container
    }
    
    1 在每个测试方法之前弄脏上下文。

    Kotlin

    @DirtiesContext(classMode = BEFORE_EACH_TEST_METHOD) (1)
    class FreshContextTests {
        // some tests that require a new Spring container
    }
    
    1 在每个测试方法之前弄脏上下文。
  • 在当前测试类中的每个测试方法之后,当在类上声明时,将类模式设置为AFTER_EACH_TEST_METHOD.

    爪哇

    @DirtiesContext(classMode = AFTER_EACH_TEST_METHOD) (1)
    class ContextDirtyingTests {
        // some tests that result in the Spring container being dirtied
    }
    
    1 在每种测试方法之后都要弄脏上下文。

    Kotlin

    @DirtiesContext(classMode = AFTER_EACH_TEST_METHOD) (1)
    class ContextDirtyingTests {
        // some tests that result in the Spring container being dirtied
    }
    
    1 在每种测试方法之后都要弄脏上下文。
  • 在当前测试之前,当对方法进行声明时,方法模式设置为BEFORE_METHOD

    爪哇

    @DirtiesContext(methodMode = BEFORE_METHOD) (1)
    @Test
    void testProcessWhichRequiresFreshAppCtx() {
        // some logic that requires a new Spring container
    }
    
    1 在当前测试方法之前弄脏上下文。

    Kotlin

    @DirtiesContext(methodMode = BEFORE_METHOD) (1)
    @Test
    fun testProcessWhichRequiresFreshAppCtx() {
        // some logic that requires a new Spring container
    }
    
    1 在当前测试方法之前弄脏上下文。
  • 在当前测试之后,当对方法进行声明时,方法模式设置为AFTER_METHOD(即默认的方法模式)。

    爪哇

    @DirtiesContext (1)
    @Test
    void testProcessWhichDirtiesAppCtx() {
        // some logic that results in the Spring container being dirtied
    }
    
    1 在当前测试方法之后,将上下文弄脏。

    Kotlin

    @DirtiesContext (1)
    @Test
    fun testProcessWhichDirtiesAppCtx() {
        // some logic that results in the Spring container being dirtied
    }
    
    1 在当前测试方法之后,将上下文弄脏。

如果你在一个测试中使用@DirtiesContext,该测试的上下文被配置为带有@ContextHierarchy的上下文层次结构的一部分,那么你可以使用hierarchyMode标志来控制如何清除上下文缓存。默认情况下,使用穷举算法来清除上下文缓存,不仅包括当前级别,还包括共享当前测试共有的祖先上下文的所有其他上下文层次结构。所有位于公共祖先上下文的子层次结构中的ApplicationContext实例都将从上下文缓存中删除并关闭。如果穷举算法对特定的用例来说过于强大,那么你可以指定更简单的当前级别算法,如下例所示。

爪哇

@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](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/test/annotation/dirtiescontext.therymode.html)爪哇doc。

# @TestExecutionListeners

@TestExecutionListeners定义了用于配置应该在TestContextManager中注册的TestExecutionListener实现的类级元数据。通常,@TestExecutionListeners@ContextConfiguration一起使用。

下面的示例显示了如何注册两个TestExecutionListener实现:

爪哇

@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测试类配置](#TestContext-JUnit-Jupiter-Nested-Test-Configuration)和[@TestExecutionListeners爪哇doc](https://DOCS. Spring.io/ Spring-Framework/DOCS/5.3.16/javadoc-api/org/SpringFramework/test/contexecutionListeners.html)中的示例和更多详细信息。

# @RecordApplicationEvents

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

可以通过测试中的ApplicationEventsAPI 访问记录的事件。

参见应用程序事件和[@RecordApplicationEvents爪哇doc](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/test/context/event/recordapplicationevents.html)以获取示例和更多详细信息。

# @Commit

@Commit表示事务性测试方法的事务应在测试方法完成后提交。你可以使用@Commit作为@Rollback(false)的直接替换,以更明确地传达代码的意图。类似于@Rollback@Commit也可以声明为类级或方法级注释。

下面的示例展示了如何使用@Commit注释:

爪哇

@Commit (1)
@Test
void testProcessWithoutRollback() {
    // ...
}
1 将测试结果提交给数据库。

Kotlin

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

@Rollback指示在测试方法完成后是否应该回滚事务测试方法的事务。如果true,则回滚事务。否则,事务将被提交(参见[@Commit](# Spring-testing-annotation-commit))。 Spring TestContext 框架中集成测试的回滚默认为true,即使没有显式声明@Rollback

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

下面的示例导致测试方法的结果不会被回滚(即,结果被提交到数据库中):

爪哇

@Rollback(false) (1)
@Test
void testProcessWithoutRollback() {
    // ...
}
1 不要回滚结果。

Kotlin

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

@BeforeTransaction表示对于已通过使用 Spring 的@Transactional注释配置为在事务中运行的测试方法,在事务启动之前应该运行带注释的void方法。@BeforeTransaction方法不需要是public,并且可以在基于 爪哇8 的接口默认方法上声明。

下面的示例展示了如何使用@BeforeTransaction注释:

爪哇

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

Kotlin

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

@AfterTransaction表示在事务结束后应该运行带注释的void方法,用于通过使用 Spring 的@Transactional注释配置为在事务中运行的测试方法。@AfterTransaction方法不需要是public,并且可以在基于 爪哇8 的接口默认方法上声明。

爪哇

@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 脚本。下面的示例展示了如何使用它:

爪哇

@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 脚本。下面的示例展示了如何使用它:

爪哇

@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

爪哇

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

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

Kotlin

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

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

下面的示例展示了如何在方法级别上使用@SqlMergeMode

Java

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

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

Kotlin

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

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

@SqlGroup是一个容器注释,它聚合了几个@Sql注释。你可以本地使用@SqlGroup来声明几个嵌套的@Sql注释,或者可以将其与 Java8 对可重复注释的支持结合使用,其中@Sql可以在同一个类或方法上声明几次,隐式地生成这个容器注释。下面的示例展示了如何声明 SQL 组:

Java

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

Kotlin

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

# 3.4.2.标准注释支持

Spring TestContext 框架的所有配置都使用标准语义支持以下注释。请注意,这些注释不是特定于测试的,并且可以在 Spring 框架中的任何地方使用。

  • @Autowired

  • @Qualifier

  • @Value

  • @Resource(javax.annotation)如果存在 JSR-250

  • @ManagedBean(javax.annotation)如果存在 JSR-250

  • @Inject如果存在 JSR-330

  • @Named如果存在 JSR-330

  • @PersistenceContext(javax.persistence)如果存在 JPA

  • @PersistenceUnit(javax.persistence)如果存在 JPA

  • @Required

  • @Transactionalwith有限的属性支持

JSR-250 生命周期注释

在 Spring TestContext 框架中,可以在@PostConstruct@PreDestroy中配置的任何应用程序组件上使用
标准语义。
但是,在实际的测试类中,这些生命周期注释的使用是有限的。

如果测试类中的方法使用@PostConstruct进行注释,则该方法在底层测试框架的任何方法之前运行
(例如,方法
使用 JUnit Jupiter 的@BeforeEach进行注释),这适用于
测试类中的每个测试方法。另一方面,如果测试类中的方法被注释为@PreDestroy,则该方法永远不会运行。因此,在测试类中,我们建议
使用来自底层测试框架的测试生命周期回调,而不是@PostConstruct@PreDestroy

# 3.4.3. Spring JUnit4 测试注释

以下注释仅在与SpringrunnerSpring’s JUnit 4 rulesSpring’s JUnit 4 support classes结合使用时才受支持:

  • [@IfProfileValue](#Integration-Testing-Annotations-JUnit4-IFProfileValue)

  • [@ProfileValueSourceConfiguration](#Integration-Testing-Annotations-JUnit4-ProfilEvaluesourceConfiguration)

  • [@Timed](#Integration-Testing-Annotations-JUnit4-Timed)

  • [@Repeat](#Integration-Testing-Annotations-JUnit4-Repeat)

# @IfProfileValue

@IfProfileValue表示针对特定的测试环境启用了带注释的测试。如果配置的ProfileValueSource返回所提供的value的匹配value,则启用测试。否则,该测试将被禁用,并实际上被忽略。

你可以在类级别、方法级别或两者都应用@IfProfileValue。对于该类或其子类中的任何方法,@IfProfileValue的类级用法优先于方法级用法。具体地说,如果在类级别和方法级别都启用了测试,则将启用该测试。缺少@IfProfileValue意味着隐式启用了测试。这类似于 JUnit4 的@Ignore注释的语义,只是@Ignore的存在总是禁用测试。

下面的示例显示了一个带有@IfProfileValue注释的测试:

Java

@IfProfileValue(name="java.vendor", value="Oracle Corporation") (1)
@Test
public void testProcessWhichRunsOnlyOnOracleJvm() {
    // some logic that should run only on Java VMs from Oracle Corporation
}
1 仅当 Java 供应商“甲骨文股份有限公司”时才运行此测试。

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 供应商“甲骨文股份有限公司”时才运行此测试。

或者,你可以使用@IfProfileValue的列表配置values(带有OR语义),以在 JUnit4 环境中实现对测试组的类似 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 使用自定义配置文件的值源。

Kotlin

@ProfileValueSourceConfiguration(CustomProfileValueSource::class) (1)
class CustomProfileValueSourceTests {
    // class body...
}
1 使用自定义配置文件的值源。
# @Timed

@Timed表示带注释的测试方法必须在指定的时间内(以毫秒为单位)完成执行。如果文本执行时间超过了指定的时间段,则测试失败。

该时间段包括测试方法本身的运行、测试的任何重复(参见@Repeat),以及测试夹具的任何设置或拆除。下面的示例展示了如何使用它:

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注释与 JUnit4 的@Test(timeout=…​)支持具有不同的语义。具体地说,由于 JUnit4 处理测试执行超时的方式(即通过在单独的Thread中执行测试方法),如果测试时间过长,@Test(timeout=…​)会抢先失败测试。 Spring 的@Timed,在另一方面,不会先发制人地使测试失败,而是等待测试完成后再失败。

# @Repeat

@Repeat表示带注释的测试方法必须重复运行。在注释中指定了要运行测试方法的次数。

要重复的执行范围包括测试方法本身的执行以及测试夹具的任何设置或拆除。当与[SpringMethodRule](#TestContext-JUnit4-Rules)一起使用时,范围还包括通过TestExecutionListener实现准备测试实例。下面的示例展示了如何使用@Repeat注释:

Java

@Repeat(10) (1)
@Test
public void testProcessRepeatedly() {
    // ...
}
1 把这个测验重复十次。

Kotlin

@Repeat(10) (1)
@Test
fun testProcessRepeatedly() {
    // ...
}
1 把这个测验重复十次。

# 3.4.4. Spring Junit Jupiter 测试注释

当与[SpringExtension](#TestContext-JUnit-Jupiter-Extension)和 JUnit Jupiter(即 JUnit5 中的编程模型)结合使用时,支持以下注释:

  • [@SpringJUnitConfig](#Integration-Testing-Annotations-JUnit-Jupiter-SpringJunitConfig)

  • [@SpringJUnitWebConfig](#Integration-Testing-Annotations-JUnit-Jupiter-SpringJunitWebConfig)

  • [@TestConstructor](#Integration-Testing-Annotations-TestConstructor)

  • [@NestedTestConfiguration](#Integration-Testing-Annotations-NestedTestConfiguration)

  • [@EnabledIf](#Integration-Testing-Annotations-JUnit-Jupiter-EnableDIF)

  • [@DisabledIf](#Integration-Testing-Annotations-JUnit-Jupiter-DisableDIF)

# @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 指定配置文件的位置。

参见上下文管理以及[@SpringJUnitConfig]的 Javadoc(https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/test/context/junit/junitconfig.html)和@ContextConfiguration以获取更多详细信息。

# @SpringJUnitWebConfig

@SpringJUnitWebConfig是一个组合注释,它结合了来自 JUnit Jupiter 的@ExtendWith(SpringExtension.class)和来自 Spring TestContext 框架的@WebAppConfiguration。你可以在类级别上使用它作为@ContextConfiguration@WebAppConfiguration的插入替换。关于配置选项,@ContextConfiguration@SpringJUnitWebConfig之间的唯一区别是,你可以使用value中的value属性来声明组件类。此外,只需在@SpringJUnitWebConfig中使用resourcePath属性,就可以覆盖value中的value属性。

下面的示例展示了如何使用@SpringJUnitWebConfig注释来指定一个配置类:

Java

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

Kotlin

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

下面的示例展示了如何使用@SpringJUnitWebConfig注释来指定配置文件的位置:

Java

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

Kotlin

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

参见上下文管理以及[@SpringJUnitWebConfig](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/SpringFramework/test/context/junit/junitwebconfig.html)的 Javadoc,[<@ContextConfiguration](https:///DOCS. Spring.io/ Spring-framework/ Spring-framework/javadoc/javadoc/juntextframework/org/juntextframework/cr/

# @TestConstructor

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

如果@TestConstructor在测试类上不存在或元存在,则将使用默认的测试构造函数 AutoWire 模式。有关如何更改默认模式的详细信息,请参见下面的技巧。但是,请注意,构造函数上的@Autowired的本地声明优先于@TestConstructor和默认模式。

更改默认的测试构造函数 AutoWire 模式

可以通过将spring.test.constructor.autowire.modeJVM 系统属性设置为all来更改默认的测试构造函数 AutoWire 模式。或者,
默认模式也可以通过[SpringProperties](Appendix.html#Appendix- Spring-Properties)机制设置。

自 Spring Framework5.3 起,默认模式也可以配置为JUnit 平台配置参数 (opens new window)

如果不设置spring.test.constructor.autowire.mode属性,则不会自动连接构造函数。
在 Spring Framework5.2 中,@TestConstructor只支持与
结合使用的SpringExtension。请注意,SpringExtension
通常会自动为你注册–例如,当使用诸如@SpringJUnitConfig@SpringJUnitWebConfig之类的注释或来自
Spring 引导测试的各种与测试相关的注释时。
# @NestedTestConfiguration

@NestedTestConfiguration是一种类型级别的注释,用于配置 Spring 测试配置注释在为内部测试类封装的类层次结构中的处理方式。

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

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

默认的封闭配置继承模式INHERIT,但是可以通过将@ContextConfigurationJVM 系统属性设置为OVERRIDE来更改。或者,可以通过[SpringProperties](Appendix.html#Appendix- Spring-Properties)机制设置默认模式。

Spring TestContext Framework为下面的注释提供了@NestedTestConfiguration语义。

  • [@BootstrapWith](# Spring-testing-annotation-bootstrapwith)

  • [@ContextConfiguration](# Spring-testing-annotation-contextconfiguration)

  • [@WebAppConfiguration](# Spring-testing-annotation-webappconfiguration)

  • [@ContextHierarchy](# Spring-testing-annotation-contexthierarchy)

  • [@ActiveProfiles](# Spring-测试-注释-ActiveProfiles)

  • [@TestPropertySource](# Spring-testing-annotation-testPropertySource)

  • [@DynamicPropertySource](# Spring-testing-annotation-dynamicpropertysource)

  • [@DirtiesContext](# Spring-testing-annotation-dirtiescontext)

  • [@TestExecutionListeners](# Spring-testing-annotation-testexecutionlisteners)

  • [@RecordApplicationEvents](# Spring-testing-annotation-recordapplicationEvents)

  • [@Transactional]

  • [@Commit](# Spring-testing-annotation-commit)

  • [@Rollback](# Spring-testing-annotation-rollback)

  • [@Sql](# Spring-testing-annotation-sql)

  • [@SqlConfig](# Spring-testing-annotation-sqlconfig)

  • [@SqlMergeMode](# Spring-testing-annotation-sqlmergemode)

  • [@TestConstructor](#Integration-Testing-Annotations-TestConstructor)

在 JUnit Jupiter 中,使用@NestedTestConfiguration通常仅在
@Nested测试类结合时才有意义;但是,可能存在支持 Spring 的其他测试
框架和使用这种
注释的嵌套测试类。

参见[@Nested测试类配置](#testcontext-junit-jupiter-nested-test-configuration)以获得示例和更多详细信息。

# @EnabledIf

@EnabledIf用于表示启用了注释的 JUnit Jupiter 测试类或测试方法,并且如果提供的expression计算为true,则应该运行该方法。具体地说,如果表达式的求值为Boolean.TRUEString等于true(忽略情况),则启用测试。当应用于类级别时,该类中的所有测试方法在默认情况下也会自动启用。

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

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

  • 在 Spring[Environment](core.html#beans-environment)中可用的属性的占位符。例如:@EnabledIf("${smoke.tests.enabled}")

  • 文字文字。例如:@EnabledIf("true")

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

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

爪哇

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

Kotlin

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

@DisabledIf用于表示注释的 JUnit Jupiter 测试类或测试方法已禁用,并且如果提供的expression计算为true,则不应运行该测试方法。具体地说,如果表达式的求值为Boolean.TRUEString等于true(忽略情况),则禁用测试。当应用于类级别时,该类中的所有测试方法也会自动禁用。

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

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

  • 在 Spring[Environment](core.html#beans-environment)中可用的属性的占位符。例如:@DisabledIf("${smoke.tests.disabled}")

  • 文字文字。例如:@DisabledIf("true")

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

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

爪哇

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

Kotlin

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

# 3.4.5.测试的元注释支持

你可以使用大多数与测试相关的注释元注释来创建定制的组合注释,并减少跨测试套件的配置重复。

你可以结合TestContext 框架使用以下每一项作为元注释。

  • @BootstrapWith

  • @ContextConfiguration

  • @ContextHierarchy

  • @ActiveProfiles

  • @TestPropertySource

  • @DirtiesContext

  • @WebAppConfiguration

  • @TestExecutionListeners

  • @Transactional

  • @BeforeTransaction

  • @AfterTransaction

  • @Commit

  • @Rollback

  • @Sql

  • @SqlConfig

  • @SqlMergeMode

  • @SqlGroup

  • @Repeat (仅在 JUnit4 上支持)

  • @Timed (仅在 JUnit4 上支持)

  • @IfProfileValue (仅在 JUnit4 上支持)

  • @ProfileValueSourceConfiguration (仅在 JUnit4 上支持)

  • @SpringJUnitConfig (仅在 Junit Jupiter 上支持)

  • @SpringJUnitWebConfig (仅在 Junit Jupiter 上支持)

  • @TestConstructor (仅在 Junit Jupiter 上支持)

  • @NestedTestConfiguration (仅在 Junit Jupiter 上支持)

  • @EnabledIf (仅在 Junit Jupiter 上支持)

  • @DisabledIf (仅在 Junit Jupiter 上支持)

考虑以下示例:

爪哇

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

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

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

如果我们发现我们在基于 JUnit4 的测试套件中重复了前面的配置,那么我们可以通过引入自定义组合注释来减少重复,该注释集中了 Spring 的公共测试配置,如下所示:

爪哇

@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注释来简化基于 JUnit4 的测试类的配置,如下所示:

爪哇

@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 的测试,我们可以进一步减少代码重复,因为 JUnit5 中的注释也可以用作元注释。考虑以下示例:

爪哇

@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 的公共测试配置,如下所示:

爪哇

@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 的各个测试类的配置,如下所示:

爪哇

@TransactionalDevTestConfig
class OrderRepositoryTests { }

@TransactionalDevTestConfig
class UserRepositoryTests { }

Kotlin

@TransactionalDevTestConfig
class OrderRepositoryTests { }

@TransactionalDevTestConfig
class UserRepositoryTests { }

由于 JUnit Jupiter 支持使用@Test@RepeatedTestParameterizedTest等作为元注释,因此你还可以在测试方法级别上创建自定义的组合注释。例如,如果我们希望创建一个组合注释,将来自 Junit Jupiter 的@Test@Tag注释与来自 Spring 的@Transactional注释结合在一起,那么我们可以创建一个@TransactionalIntegrationTest注释,如下所示:

爪哇

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Transactional
@Tag("integration-test") // org.junit.jupiter.api.Tag
@Test // org.junit.jupiter.api.Test
public @interface TransactionalIntegrationTest { }

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 的单个测试方法的配置,如下所示:

爪哇

@TransactionalIntegrationTest
void saveOrder() { }

@TransactionalIntegrationTest
void deleteOrder() { }

Kotlin

@TransactionalIntegrationTest
fun saveOrder() { }

@TransactionalIntegrationTest
fun deleteOrder() { }

有关更多详细信息,请参见Spring Annotation Programming Model (opens new window)维基页面。

# 3.5. Spring TestContext 框架

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

除了通用的测试基础设施之外,TestContext 框架还为 JUnit4、JUnit Jupiter(AKA 为 JUnit5)和 TestNG 提供了明确的支持。对于 JUnit4 和 TestNG, Spring 提供abstract支持类。此外, Spring 为 JUnit4 提供了自定义的 JUnitRunner和自定义的 JUnitRules,为 JUnit Jupiter 提供了自定义的Extension,允许你编写所谓的 POJO 测试类。扩展特定的类层次结构不需要 POJO 测试类,例如abstract支持类。

下一节将概述 TestContext 框架的内部内容。如果你只对使用框架感兴趣,而对使用自己的自定义侦听器或自定义加载器扩展框架不感兴趣,可以直接访问配置(上下文管理依赖注入事务管理)、支持类注释支持部分。

# 3.5.1.关键抽象

框架的核心包括依赖注入类和TestContextTestExecutionListenerSmartContextLoader接口。为每个测试类创建TestContextManager(例如,用于在 JUnit Jupiter 中的单个测试类中执行所有测试方法)。而TestContextManager则管理保存当前测试上下文的TestContext。随着测试的进行,TestContextManager还会更新TestContext的状态,并将其委托给TestExecutionListener实现,该实现通过提供依赖注入、管理事务等来实现实际的测试执行。aSmartContextLoader负责为给定的测试类加载ApplicationContext。有关更多信息和各种实现的示例,请参见javadoc (opens new window)和 Spring 测试套件。

# TestContext

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

# TestContextManager

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

  • 在特定测试框架的任何“类之前”或“所有之前”方法之前。

  • 测试实例后处理。

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

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

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

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

  • 在特定测试框架的任何“课后”或“毕竟”方法之后。

# TestExecutionListener

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

# 上下文加载程序

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

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

Spring 提供了以下实现方式:

  • TestContext:两个默认加载器之一,它在内部委托给一个AnnotationConfigContextLoader、一个GenericXmlContextLoader或一个GenericGroovyXmlContextLoader,这取决于为测试类声明的配置,或者取决于是否存在默认位置或默认配置类。仅当 Groovy 位于 Classpath 上时,才启用 Groovy 支持。

  • WebDelegatingSmartContextLoader:两个默认加载器之一,它在内部委托给一个AnnotationConfigWebContextLoader、一个GenericXmlWebContextLoader或一个GenericGroovyXmlWebContextLoader,这取决于为测试类声明的配置,或者取决于缺省位置或缺省配置类的存在。只有当测试类上存在@WebAppConfiguration时,才使用 WebContextLoader。仅当 Groovy 位于 Classpath 上时,才启用 Groovy 支持。

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

  • AnnotationConfigWebContextLoader:从组件类加载WebApplicationContext

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

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

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

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

# 3.5.2.引导 TestContext 框架

Spring TestContext 框架内部的默认配置对于所有常见的用例都足够了。但是,有时开发团队或第三方框架希望更改默认的ContextLoader,实现自定义的GenericXmlWebContextLoader或,增加ContextCustomizerFactoryTestExecutionListener实现的默认集,以此类推。对于这种对 TestContext 框架如何操作的低级控制, Spring 提供了一种引导策略。

ApplicationEventsTestExecutionListener定义了引导 TestContext 框架的 SPI。TestContextBootstrapperTestContextManager用于加载用于当前测试的TestExecutionListener实现,并构建其管理的TestContext。你可以直接使用@BootstrapWith或作为元注释,为测试类(或测试类层次结构)配置自定义引导策略。如果没有使用@BootstrapWith显式配置引导程序,则使用DefaultTestContextBootstrapperWebTestContextBootstrapper,这取决于@WebAppConfiguration的存在。

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

# 3.5.3.TestExecutionListener配置

Spring 提供了以下TestExecutionListener默认情况下注册的实现,其顺序完全如下:

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

  • GenericGroovyXmlWebContextLoader:处理“before”模式的@DirtiesContext注释。

  • ApplicationEventsTestExecutionListener:提供对[ApplicationEvents](#TestContext-Application-Events)的支持。

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

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

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

  • SqlScriptsTestExecutionListener:运行使用TransactionalTestExecutionListener注释配置的 SQL 脚本。

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

# 注册TestExecutionListener实现

你可以使用@TestExecutionListeners注释来注册测试类及其子类的TestExecutionListener实现。有关详细信息和示例,请参见注释支持和[@TestExecutionListeners](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/test/contexectionlisteners.html)的 爪哇doc。

# 默认TestExecutionListener实现的自动发现

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

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

# 排序TestExecutionListener实现

当 TestContext 框架通过前述SpringFactoriesLoader机制发现默认的TestExecutionListener实现时,实例化的侦听器将通过使用 Spring 的SpringFactoriesLoader进行排序,该命令使用 Spring 的Ordered接口和@Order注释进行排序。AbstractTestExecutionListener和 Spring 提供的所有默认TestExecutionListener实现用适当的值实现Ordered。因此,第三方框架和开发人员应该通过实现TestExecutionListener或声明@Order来确保其默认的TestExecutionListener实现是按正确的顺序注册的。关于分配给每个核心侦听器的值的详细信息,请参见 爪哇doc 获取核心默认getOrder()实现的TestExecutionListener方法。

# 合并TestExecutionListener实现

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

爪哇

@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 Framework4.1 中引入,DirtiesContextBeforeModesTestExecutionListener在 Spring Framework4.2 中引入。此外,像 Spring 引导和 Spring 安全之类的第三方框架通过使用前述的自动发现机制来注册它们自己的默认TestExecutionListener实现。

为了避免必须意识到并重新声明所有默认侦听器,你可以将@TestExecutionListenersmergeMode属性设置为MergeMode.MERGE_WITH_DEFAULTSMERGE_WITH_DEFAULTS表示本地声明的侦听器应该与默认侦听器合并。合并算法确保从列表中删除重复项,并确保合并后的侦听器集合根据AnnotationAwareOrderComparator的语义进行排序,如[OrderingTestExecutionListener实现](#TestContext-TEL-Config-Ordering)中所述。如果侦听器实现了Ordered,或者用@Order进行了注释,那么它可能会影响与默认值合并的位置。否则,在合并时,本地声明的侦听器将被追加到默认侦听器列表中。

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

爪哇

@ContextConfiguration
@TestExecutionListeners(
    listeners = MyCustomTestExecutionListener.class,
    mergeMode = MERGE_WITH_DEFAULTS
)
class MyTest {
    // class body...
}

Kotlin

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

# 3.5.4.应用程序事件

Spring Framework5.3.3 以来,TestContext Framework 提供了对记录应用程序事件中发布的ApplicationContext的支持,以便可以针对测试中的那些事件执行断言。在执行单个测试期间发布的所有事件都可以通过ApplicationEventsAPI 获得,该 API 允许你以java.util.Stream的形式处理事件。

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

  • 确保你的测试类是用[@RecordApplicationEvents](# Spring-testing-annotation-recordapplicationEvents)进行注释或元注释的。

  • 确保ApplicationEventsTestExecutionListener已注册。但是,请注意,ApplicationEventsTestExecutionListener是默认注册的,并且只有在你通过@TestExecutionListeners进行了自定义配置且不包括默认侦听器的情况下,才需要手动注册。

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

    • 当使用朱尼特木星的 SpringExtension时,可以在测试或生命周期方法中声明类型为ApplicationEvents的方法参数,作为测试类中@Autowired字段的替代。

下面的测试类使用SpringExtensionfor JUnit Jupiter 和AssertJ (opens new window)来断言在 Spring 管理的组件中调用方法时发布的应用程序事件的类型:

爪哇

@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 使用ApplicationEventsAPI 来计算发布了多少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 使用ApplicationEventsAPI 来计算发布了多少OrderSubmitted事件。

有关ApplicationEvents爪哇doc](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/test/context/event/applicationevents.html)的更多详细信息,请参见[ApplicationEventsapi。

# 3.5.5.测试执行事件

Spring Framework5.2 中引入的EventPublishingTestExecutionListener提供了一种实现自定义TestExecutionListener的替代方法。测试的ApplicationContext中的组件可以侦听EventPublishingTestExecutionListener发布的以下事件,每个事件对应于TestExecutionListenerAPI 中的一个方法。

  • BeforeTestClassEvent

  • PrepareTestInstanceEvent

  • BeforeTestMethodEvent

  • BeforeTestExecutionEvent

  • AfterTestExecutionEvent

  • AfterTestMethodEvent

  • AfterTestClassEvent

只有在ApplicationContext已经加载的情况下,这些事件才会发布。

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

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

  • @BeforeTestClass

  • @PrepareTestInstance

  • @BeforeTestMethod

  • @BeforeTestExecution

  • @AfterTestExecution

  • @AfterTestMethod

  • @AfterTestClass

# 异常处理

默认情况下,如果测试执行事件侦听器在使用事件时抛出异常,则该异常将传播到使用中的底层测试框架(例如 JUnit 或 TestNG)。例如,如果消耗BeforeTestMethodEvent导致异常,则相应的测试方法将作为异常的结果而失败。相反,如果异步测试执行事件侦听器抛出异常,则异常将不会传播到底层测试框架。有关异步异常处理的更多详细信息,请参阅类级 爪哇doc for@EventListener

# 异步侦听器

如果希望特定的测试执行事件侦听器异步处理事件,可以使用 Spring 的[regular@Asyncsupport](integration.html#schooling-annotation-support-async)。有关更多详细信息,请参见类级 爪哇doc@EventListener

# 3.5.6.上下文管理

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

| |@AutoWired ApplicationContext

作为实现ApplicationContextAware接口的一种替代方案,你可以通过
@Autowired一个字段或 setter 方法上的

java=“1231”/>@Autowired<1223">>><gt="gt=">gt=”1228"|1|Injecting the ApplicationContext.|
|-----|-----------------------------------|

Kotlin

<br/>@SpringJUnitConfig<br/>class MyTest {<br/><br/> @Autowired (1)<br/> lateinit var applicationContext: ApplicationContext<br/><br/> // class body...<br/>}<br/>

|1|Injecting the ApplicationContext.|
|-----|-----------------------------------|

Similarly, if your test is configured to load a WebApplicationContext, you can inject
the web application context into your test, as follows:

爪哇

<br/>@SpringJUnitWebConfig (1)<br/>class MyWebAppTest {<br/><br/> @Autowired (2)<br/> WebApplicationContext wac;<br/><br/> // class body...<br/>}<br/>

|1|Configuring the WebApplicationContext.|
|-----|----------------------------------------|
|2| Injecting the WebApplicationContext. |

Kotlin

<br/>@SpringJUnitWebConfig (1)<br/>class MyWebAppTest {<br/><br/> @Autowired (2)<br/> lateinit var wac: WebApplicationContext<br/> // class body...<br/>}<br/>

|1|Configuring the WebApplicationContext.|
|-----|----------------------------------------|
|2| Injecting the WebApplicationContext. |

Dependency injection by using @Autowired is provided by theDependencyInjectionTestExecutionListener, which is configured by default
(see Dependency Injection of Test Fixtures).| |-----|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |1|注入ApplicationContext。| |1|注入ApplicationContext。| |1|配置WebApplicationContext。| |2|注入WebApplicationContext。| |1|配置WebApplicationContext。| |2|注入WebApplicationContext。|

使用 TestContext 框架的测试类不需要扩展任何特定的类或实现特定的接口来配置它们的应用程序上下文。相反,配置是通过在类级别声明@ContextConfiguration注释来实现的。如果你的测试类没有显式声明应用程序上下文资源位置或组件类,那么配置的ContextLoader将决定如何从默认位置或默认配置类加载上下文。除了上下文资源位置和组件类之外,还可以通过应用程序上下文初始化器来配置应用程序上下文。

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

# 使用 XML 资源的上下文配置

要通过使用 XML 配置文件为测试加载ApplicationContext,请用@ContextConfiguration注释测试类,并使用包含 XML 配置元数据资源位置的数组配置locations属性。普通或相对路径(例如,context.xml)被视为相对于定义测试类的包的 Classpath 资源。以斜杠开头的路径被视为绝对 Classpath 位置(例如,/org/example/config.xml)。表示资源 URL 的路径(即,带有classpath:file:http:等前缀的路径)被使用就像现在一样

爪哇

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

Kotlin

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

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

爪哇

@ExtendWith(SpringExtension.class)
@ContextConfiguration({"/app-config.xml", "/test-config.xml"}) (1)
class MyTest {
    // class body...
}
1 指定 XML 文件而不使用location属性。

Kotlin

@ExtendWith(SpringExtension::class)
@ContextConfiguration("/app-config.xml", "/test-config.xml") (1)
class MyTest {
    // class body...
}
1 指定 XML 文件而不使用location属性。

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

爪哇

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

Kotlin

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

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

启用 Groovy 脚本支持value
在 Spring
testContext 框架中使用 Groovy 脚本加载ApplicationContext的支持是自动启用的,如果 Groovy 在 Classpath 上。

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

爪哇

@ExtendWith(SpringExtension.class)
// ApplicationContext will be loaded from "/AppConfig.groovy" and
// "/TestConfig.groovy" in the root of the classpath
@ContextConfiguration({"/AppConfig.groovy", "/TestConfig.Groovy"}) (1)
class MyTest {
    // class body...
}

Kotlin

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

如果从@ContextConfiguration注释中省略locationsvalue属性,TestContext 框架将尝试检测默认的 Groovy 脚本。具体地说,GenericGroovyXmlContextLoaderGenericGroovyXmlWebContextLoader基于测试类的名称检测缺省位置。如果你的类名为com.example.MyTest,那么 Groovy 上下文加载程序将从"classpath:com/example/MyTestContext.groovy"加载应用程序上下文。下面的示例展示了如何使用默认值:

爪哇

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

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

下面的列表显示了如何在集成测试中合并这两个测试:






<<<>r=“1319”>r=“>”><gt=">r=”1319“/>r=”r=“>”>“><gt=”r="
# 具有组件类的上下文配置

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

爪哇

@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@ServiceWebApplicationContext注释的类,或其他原型注释)。

* 一个用javax.inject注释的 JSR-330 兼容的类。

* 任何包含@Bean-methods 的类。

* 任何其他打算在<80]组件中注册为 Spring 组件的类(即,一个 Spring gt=“1347”/>r=“1331”," 可能利用不使用 Spring 注释的单个构造函数的自动连接


参见[@Configuration](https://DOCS. Spring.io/ Spring-framework/5.3.16/javadog/javadoc-api-api/org/5.3.16/javadoc-api-api-api/org/org/javadoc-api-api/org/org/spramework/Springframework/contem/context/context/context/context/cont 特别注意
@Bean精简模式的讨论。

如果省略@ContextConfiguration注释中的classes属性,TestContext 框架将尝试检测缺省配置类的存在。具体地说,AnnotationConfigContextLoaderAnnotationConfigWebContextLoader检测满足配置类实现要求的测试类的所有static嵌套类,如在[@Configuration](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/context/context/annation/configuration.html)中指定的。请注意,配置类的名称是任意的。此外,如果需要,一个测试类可以包含多个static嵌套配置类。在下面的示例中,OrderServiceTest类声明了一个名为static的嵌套配置类,该配置类自动用于为测试类加载ApplicationContext:

爪哇

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

    @Configuration
    static class Config {

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

    @Autowired
    OrderService orderService;

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

}
1 从嵌套的Config类加载配置信息。

Kotlin

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

    @Autowired
    lateinit var orderService: OrderService

    @Configuration
    class Config {

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

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

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

此外,一些第三方框架(例如 Spring Boot)提供了用于同时从不同类型的资源(例如,XML 配置文件、Groovy 脚本和类)加载的一流支持。 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,但是,你仍然可以自由地包含或导入其他类型的配置。

# 带有上下文初始化器的上下文配置

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

爪哇

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

Kotlin

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

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

爪哇

@ExtendWith(SpringExtension.class)
// ApplicationContext will be initialized by EntireAppInitializer
// which presumably registers beans in the context
@ContextConfiguration(initializers = EntireAppInitializer.class) (1)
class MyTest {
    // class body...
}
1 仅使用初始化器指定配置。

Kotlin

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

@ContextConfiguration支持布尔inheritLocationsinheritInitializers属性,这些属性表示是否应该继承由超类声明的资源位置或组件类和上下文初始化器。这两个标志的默认值都是true。这意味着测试类继承了资源位置或组件类,以及由任何超类声明的上下文初始化器。具体地说,测试类的资源位置或组件类被追加到由超类声明的资源位置或注释类的列表中。类似地,给定测试类的初始化器被添加到由测试超类定义的初始化器集合中。因此,子类可以选择扩展资源位置、组件类或上下文初始化器。

如果inheritLocationsinheritInitializers中的inheritInitializers属性被设置为false,则资源位置或组件类和上下文初始化器分别用于测试类的影子和有效替换由超类定义的配置。

在 Spring Framework5.3 中,测试配置也可以从包含
类中继承。有关详细信息,请参见[@Nested测试类配置]。

在下一个使用 XML 资源位置的示例中,ApplicationContextExtendedTestApplicationContext按照这个顺序从base-config.xmlextended-config.xml加载。因此,在extended-config.xml中定义的 bean 可以覆盖(即替换)在base-config.xml中定义的 bean。下面的示例展示了一个类如何扩展另一个类,并同时使用自己的配置文件和超类的配置文件:

爪哇

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

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

Kotlin

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

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

类似地,在下一个使用组件类的示例中,ApplicationContextExtendedTest是从BaseConfigExtendedConfig类按此顺序加载的。因此,在ExtendedConfig中定义的 bean 可以覆盖(即替换)在BaseConfig中定义的 bean。下面的示例展示了一个类如何扩展另一个类,并同时使用自己的配置类和超类的配置类:

爪哇

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

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

Kotlin

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

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

在下一个使用上下文初始化器的示例中,ApplicationContextExtendedTest通过使用BaseInitializerExtendedInitializer进行初始化。然而,请注意,初始化器被调用的顺序取决于它们是实现 Spring 的Ordered接口,还是使用 Spring 的@Order注释或标准的@Priority注释。下面的示例展示了一个类如何扩展另一个类,并同时使用自己的初始化器和超类的初始化器:

爪哇

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

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

Kotlin

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

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

Spring 框架具有对环境和配置文件(AKA“ Bean 定义配置文件”)的概念的一流支持,并且集成测试可以被配置为针对各种测试场景激活特定的 Bean 定义配置文件。这是通过用@ActiveProfiles注释一个测试类并提供一个配置文件列表来实现的,这些配置文件在为测试加载ApplicationContext时应该被激活。

你可以将@ActiveProfiles用于SmartContextLoaderSPI 的任何实现,但是@ActiveProfiles不支持较早的ContextLoaderSPI 的实现。

考虑使用 XML 配置和@Configuration类的两个示例:

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

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

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

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

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

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

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

</beans>

爪哇

@ExtendWith(SpringExtension.class)
// ApplicationContext 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配置文件中。

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

有时,将 bean 分配到default配置文件中是有用的。只有当没有其他配置文件被特别激活时,默认配置文件中的 bean 才会被包含。你可以使用它来定义要在应用程序的默认状态下使用的“fallback”bean。例如,你可以显式地为devproduction配置文件提供数据源,但在这两个配置文件都不是活动的情况下,将内存中数据源定义为默认值。

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

爪哇

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

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

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

爪哇

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

爪哇

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

爪哇

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

爪哇

@SpringJUnitConfig({
        TransferServiceConfig.class,
        StandaloneDataConfig.class,
        JndiDataConfig.class,
        DefaultDataConfig.class})
@ActiveProfiles("dev")
class TransferServiceTest {

    @Autowired
    TransferService transferService;

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

Kotlin

@SpringJUnitConfig(
        TransferServiceConfig::class,
        StandaloneDataConfig::class,
        JndiDataConfig::class,
        DefaultDataConfig::class)
@ActiveProfiles("dev")
class TransferServiceTest {

    @Autowired
    lateinit var transferService: TransferService

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

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

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

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

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

  • DefaultDataConfig:为默认的嵌入式数据库定义一个dataSource,以防没有配置文件处于活动状态。

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

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

在 Spring Framework5.3 中,测试配置也可以从包含
类中继承。有关详细信息,请参见[@Nested测试类配置]。

爪哇

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

爪哇

// "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属性,该属性可用于禁用活动配置文件的继承,如下例所示:

爪哇

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

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

  • 当前的操作系统。

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

  • 某些环境变量的存在。

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

  • 其他问题。

要以编程方式解析活动 Bean 定义配置文件,你可以实现一个自定义的ActiveProfilesResolver,并使用resolverresolver属性对其进行注册。有关更多信息,请参见相应的javadoc (opens new window)。下面的示例演示如何实现和注册自定义OperatingSystemActiveProfilesResolver:

爪哇

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

爪哇

public class OperatingSystemActiveProfilesResolver implements ActiveProfilesResolver {

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

Kotlin

class OperatingSystemActiveProfilesResolver : ActiveProfilesResolver {

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

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

你可以使用@TestPropertySourceSmartContextLoaderSPI 的任何实现一起使用@TestPropertySource,但是@TestPropertySource不支持与较早的ContextLoaderSPI 的实现一起使用
SmartContextLoader通过getPropertySourceLocations()中的<gt r=“1495”和<gt=“1494”/>方法访问合并的测试属性源值
# 声明测试属性源

可以使用locationsvalue的属性配置测试属性文件。

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

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

下面的示例使用了一个测试属性文件:

爪哇

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

Kotlin

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

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

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

  • key=value

  • key:value

  • key value

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

爪哇

@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 Framework5.2 中,@TestPropertySource可以用作可重复注释
这意味着可以在单个
测试类上有多个@TestPropertySource的声明,随着locationsproperties后面的@TestPropertySource注释覆盖了前面的@TestPropertySource注释,
此外,你可以在一个测试类上声明多个组合注释,每个组合注释
meta 注释为@TestPropertySource,并且所有那些@TestPropertySource声明都将贡献给你的测试属性源。

直接呈现@TestPropertySource注释总是优先于
meta-present@TestPropertySource注释。换句话说,locationsproperties来自直接存在的@TestPropertySource注释将覆盖locationsproperties来自@TestPropertySource用作元注释的注释。
# 默认属性文件检测

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

# 优先权

测试属性的优先级高于在操作系统环境、Java 系统属性或应用程序通过使用@PropertySource或编程方式声明性地添加的属性源中定义的属性。因此,可以使用测试属性来选择性地覆盖从系统和应用程序属性源加载的属性。此外,与从资源位置加载的属性相比,内联属性具有更高的优先级。但是,请注意,通过[@DynamicPropertySource]注册的属性(#testcontext-ctx-management-dynamic-property-sources)比通过@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属性,这些属性表示超类声明的属性文件和内联属性的资源位置是否应该被继承。这两个标志的默认值都是true。这意味着测试类继承了由任何超类声明的位置和内联属性。具体地说,测试类的位置和内联属性被追加到超类声明的位置和内联属性之后。因此,子类可以选择扩展位置和内联属性。请注意,后面出现的属性是与前面出现的相同名称的 shadow(即覆盖)属性。此外,前面提到的优先规则也适用于继承的测试属性源。

如果inheritLocationsinheritProperties中的inheritProperties属性设置为false,则位置或内联属性分别用于测试类的影子和有效地替换由超类定义的配置。

在 Spring Framework5.3 中,测试配置也可以从包含
类中继承。有关详细信息,请参见[@Nested测试类配置]。

在下一个示例中,只使用base.properties文件作为测试属性源加载ApplicationContextforBaseTest。相比之下,ApplicationContextExtendedTest是通过使用base.propertiesextended.properties文件作为测试属性源位置加载的。下面的示例展示了如何通过使用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() {
    // ...
}

在下一个示例中,只使用内联的key1属性加载ApplicationContextforBaseTest。与此相反,ApplicationContextExtendedTest是通过使用内联的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() {
    // ...
}
# 具有动态属性源的上下文配置

从 Spring Framework5.2.5 开始,TestContext 框架通过@DynamicPropertySource注释为动态属性提供支持。此注释可用于集成测试,这些测试需要在Environment中的PropertySources集合中添加具有动态值的属性,用于为集成测试加载ApplicationContext

@DynamicPropertySource注释及其支持的基础结构是
,最初的设计目的是允许基于测试容器 (opens new window)的测试中的属性很容易地暴露在
Spring 集成测试中。然而,这个特性也可以用于任何形式的
外部资源,其生命周期被维护在测试的ApplicationContext之外。

与在类级别应用的[@TestPropertySource](#testcontext-ctx-management-property-sources)注释不同,@DynamicPropertySource必须应用到一个static方法,该方法接受一个DynamicPropertyRegistry参数,该参数用于将名称-值对添加到Environment。值是动态的,并通过Supplier提供,只有在解析属性时才调用该属性。通常,方法引用用于提供值,如下面的示例所示,该示例使用 TestContainers 项目来管理 Spring ApplicationContext之外的 Redis 容器。托管 Redis 容器的 IP 地址和端口可通过redis.hostredis.port属性提供给测试ApplicationContext中的组件。这些属性可以通过 Spring 的Environment抽象进行访问,也可以直接注入到 Spring 管理的组件中——例如,分别通过@Value("${redis.host}")@Value("${redis.port}")

如果在基类中使用@DynamicPropertySource,并发现子类
中的测试失败,因为子类之间的动态属性发生了变化,那么你可能需要用[
](# Spring-testing-annotation-dirtiescontext)对基类进行注释,以
确保每个子类都具有正确的动态ApplicationContext属性。

Java

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

    @Container
    static RedisContainer redis = new RedisContainer();

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

    // tests ...

}

Kotlin

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

    companion object {

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

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

    // tests ...

}
# 优先权

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

# 装入WebApplicationContext

要指示 TestContext 框架加载WebApplicationContext而不是标准的ApplicationContext,你可以用@WebAppConfiguration注释相应的测试类。

测试类上的@WebAppConfiguration指示 TestContext 框架应该为集成测试加载WebApplicationContext。在后台,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。类似地,如果在不指定资源locations的情况下声明@ContextConfiguration,组件classes,或上下文initializers, Spring 试图通过使用约定(即WacTests-context.xml在与WacTests类相同的包中或静态嵌套@Configuration类)来检测配置的存在。

下面的示例显示了如何显式地声明具有@WebAppConfiguration的资源库路径和具有@ContextConfiguration的 XML 资源位置:

Java

@ExtendWith(SpringExtension.class)

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

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

Kotlin

@ExtendWith(SpringExtension::class)

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

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

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

下面的示例表明,我们可以通过指定 Spring 资源前缀来覆盖这两种注释的默认资源语义:

Java

@ExtendWith(SpringExtension.class)

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

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

Kotlin

@ExtendWith(SpringExtension::class)

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

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

将本例中的注释与前面的示例进行对比。

使用 Web 模拟

为了提供全面的 Web 测试支持,TestContext 框架有一个ServletTestExecutionListener,默认情况下启用。当针对WebApplicationContext进行测试时,此[TestExecutionListener](#testcontext-key-abstractions)通过在每个测试方法之前使用 Spring Web 的RequestContextHolder设置默认的线程-本地状态,并基于配置有@WebAppConfiguration的基本资源路径创建MockHttpServletResponseServletWebRequestServletTestExecutionListener还确保MockHttpServletResponseServletWebRequest可以被注入到测试实例中,并且,一旦测试完成,它将清理线程本地状态。

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

Java

@SpringJUnitWebConfig
class WacTests {

    @Autowired
    WebApplicationContext wac; // cached

    @Autowired
    MockServletContext servletContext; // cached

    @Autowired
    MockHttpSession session;

    @Autowired
    MockHttpServletRequest request;

    @Autowired
    MockHttpServletResponse response;

    @Autowired
    ServletWebRequest webRequest;

    //...
}

Kotlin

@SpringJUnitWebConfig
class WacTests {

    @Autowired
    lateinit var wac: WebApplicationContext // cached

    @Autowired
    lateinit var servletContext: MockServletContext // cached

    @Autowired
    lateinit var session: MockHttpSession

    @Autowired
    lateinit var request: MockHttpServletRequest

    @Autowired
    lateinit var response: MockHttpServletResponse

    @Autowired
    lateinit var webRequest: ServletWebRequest

    //...
}
# 上下文缓存

一旦 TestContext Framework 为测试加载ApplicationContext(或WebApplicationContext),该上下文将被缓存,并在所有随后的测试中重用,这些测试在相同的测试套件中声明相同的唯一上下文配置。要理解缓存的工作原理,重要的是要理解“唯一”和“测试套件”的含义。

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

  • 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

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

测试套件和分叉进程

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

要受益于缓存机制,所有测试都必须在相同的进程或测试
套件中运行。这可以通过在 IDE 中作为一个组执行所有测试来实现。类似地,
在使用 Ant、 Maven 或 Gradle 之类的构建框架执行测试时,重要的是要确保构建框架不会在测试之间分叉。例如,
如果将 Maven surefire 插件的[forkMode](https:// Maven.apache.org/plugins/ Maven-surefire-plugin/test-mojo.html#forkmode)设置为alwayspertest,则 TestContext 框架
不能在测试类之间运行<gtcache 上下文,结果是构建过程的速度明显慢得多。

上下文缓存的大小是有界的,默认的最大大小为 32。每当达到最大大小时,就会使用最近使用的驱逐策略来驱逐和关闭陈旧的上下文。通过设置名为spring.test.context.cache.maxSize的 JVM 系统属性,可以从命令行或构建脚本配置最大大小。作为一种选择,你可以通过[SpringProperties](acception.html#acception- Spring-properties)机制设置相同的属性。

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

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

ApplicationContext 生命周期和控制台日志

当需要调试使用 Spring TestContext 框架执行的测试时,可以对
控制台输出进行有用的分析(即输出到SYSOUTSYSERR流)。一些构建工具和 IDE 能够将控制台输出与给定的
测试相关联;但是,某些控制台输出不能很容易地与给定的测试相关联。

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

测试的ApplicationContext通常是在准备测试
类的实例时加载的,例如,要在测试实例的@Autowired字段中执行依赖项注入。这意味着在
初始化ApplicationContext期间触发的任何控制台日志记录通常不能与
单独的测试方法关联。然而,如果在执行根据[@DirtiesContext](# Spring-testing-annotation-dirtiescontext)语义的测试方法之前立即关闭了上下文,则在执行
测试方法之前将加载一个新的上下文实例。在后一种情况下,IDE 或构建工具可能会将
控制台日志记录与单独的测试方法关联起来。

ApplicationContext对于测试可以通过以下场景中的一种来关闭。

* 上下文是根据@DirtiesContext语义关闭的。

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

* 当 JVM 关闭钩子时,上下文将通过 JVM 关闭钩子关闭。对于测试套件
终止。

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

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

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

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

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

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

ControllerIntegrationTests通过声明由两个级别组成的上下文层次结构,表示 Spring MVC Web 应用程序的典型集成测试场景,一个用于根WebApplicationContext(通过使用TestAppConfig``@Configuration类加载),一个用于调度器 Servlet WebApplicationContext(通过使用WebConfig类加载)。自动连接到测试实例的WebApplicationContext是子上下文(即层次结构中最低的上下文)。下面的清单展示了这个配置场景:

Java

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

    @Autowired
    WebApplicationContext wac;

    // ...
}

Kotlin

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

    @Autowired
    lateinit var wac: WebApplicationContext

    // ...
}

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

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

爪哇

@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 框架合并child层次结构级别的上下文配置,方法是确保@ContextConfiguration中的name属性中声明的名称都是child。结果是加载了三个应用程序上下文:一个用于/app-config.xml,一个用于/user-config.xml,一个用于{"/user-config.xml", "/order-config.xml"}。与前面的示例一样,从/app-config.xml加载的应用程序上下文被设置为从/user-config.xml{"/user-config.xml", "/order-config.xml"}加载的上下文的父上下文。下面的清单展示了这个配置场景:

爪哇

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

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

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加载的上下文。下面的清单展示了这个配置场景:

爪哇

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

@ContextHierarchy(
    @ContextConfiguration(
        name = "child",
        locations = "/test-user-config.xml",
        inheritLocations = false
))
class ExtendedTests extends BaseTests {}

Kotlin

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

@ContextHierarchy(
        ContextConfiguration(
                name = "child",
                locations = ["/test-user-config.xml"],
                inheritLocations = false
        ))
class ExtendedTests : BaseTests() {}
在上下文层次结构

中玷污上下文如果你在一个测试中使用@DirtiesContext,该测试的上下文被配置为
上下文层次结构的一部分,那么你可以使用hierarchyMode标志来控制如何清除上下文缓存
。有关更多详细信息,请参见@DirtiesContext中对Spring Testing Annotations的讨论和[@DirtiesContext](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/test/annotation/dirtiescontext.html)的讨论。

# 3.5.7.测试夹具的依赖注入

当你使用DependencyInjectionTestExecutionListener(它是默认配置的)时,你的测试实例的依赖项是从你配置了@ContextConfiguration或相关注释的应用程序上下文中的 bean 注入的。你可以使用 setter 注入、字段注入,或者两者都使用,这取决于你选择了哪些注释,以及是否将它们放置在 setter 方法或字段上。如果你正在使用 JUnit Jupiter,你也可以选择使用构造函数注入(参见[Dependency Injection withSpringExtension](#TestContext-JUnit-Jupiter-DI))。为了与 Spring 的基于注释的注入支持保持一致,还可以使用 Spring 的@Autowired注释或来自 JSR-330 的@Inject注释进行字段和 setter 注入。

对于 JUnit Jupiter 以外的测试框架,TestContext 框架不
参与测试类的实例化。因此,对于构造函数使用@Autowired@Inject对测试类没有影响。
尽管在生产代码中不鼓励现场注入,但在测试代码中,现场注入实际上是
非常自然的。这种差异的基本原理是,你将永远不会直接实例化你的测试类
。因此,不需要能够在测试类上调用
构造函数或 setter 方法。

因为@Autowired用于执行按类型自动布线,所以如果你有多个相同类型的 Bean 定义,那么对于那些特定的 bean,你不能依赖这种方法。在这种情况下,可以将@Autowired@Qualifier结合使用。你还可以选择将@Inject@Named结合使用。或者,如果你的测试类具有对其ApplicationContext的访问权限,则可以使用(例如)对applicationContext.getBean("titleRepository", TitleRepository.class)的调用来执行显式查找。

如果不希望将依赖注入应用于测试实例,请不要使用@Autowired@Inject注释域或 setter 方法。或者,可以通过显式地使用@TestExecutionListeners配置类并从侦听器列表中省略DependencyInjectionTestExecutionListener.class来完全禁用依赖注入。

考虑测试HibernateTitleRepository类的场景,如Goals部分所概述的那样。接下来的两个代码清单演示了@Autowired在字段和 setter 方法上的使用。在所有示例代码列表之后,将介绍应用程序上下文配置。

以下代码列表中的依赖注入行为不是 JUnit
Jupiter 特有的。相同的 DI 技术可以与任何支持的测试
框架结合使用。

以下示例调用静态断言方法,例如assertNotNull()
,但不预先处理与Assertions的调用。在这种情况下,假设
方法是通过import static声明正确导入的,该声明在
示例中未显示。

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

爪哇

@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 注入,如下所示:

爪哇

@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注释(即repository-config.xml)引用的相同 XML 上下文文件。以下显示了此配置:

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

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

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

</beans>
如果你是从 Spring 提供的测试基类进行扩展的,而测试基类恰好在它的一个 setter 方法上使用@Autowired,那么你可能在应用程序上下文中定义了多个受影响的
类型的 bean(例如,多个DataSourcebean)。在
这样的情况下,可以重写 setter 方法,并使用@Qualifier注释来表示特定的目标 Bean,如下所示(但也要确保将其委托给超类中重写的
方法):

java





<><1893"r="gt="1914"/><gt15"/>指定的限定词表示指定的限定符[lt=”lt=“lt=”lt=“lt=”lt=“lt17”/><lt=“lt=”lt="lt= 它的值与相应的<bean>定义中的<qualifier>声明相匹配。 Bean name
被用作回退限定符值,因此你也可以有效地通过这里的名称指向特定的
Bean(如前面所示,假设myDataSource是 Bean id)。

# 3.5.8.测试请求和会话范围的 bean

Spring 从早期开始就支持请求和会话范围的 bean,你可以通过以下步骤来测试你的请求范围和会话范围的 bean:

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

  • 将模拟请求或会话注入到你的测试实例中,并根据需要准备测试装置。

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

  • 对模拟执行断言。

下一个代码片段显示了登录用例的 XML 配置。请注意,userService Bean 对请求作用域loginAction Bean 具有依赖关系。而且,LoginAction是通过使用Spel 表达式实例化的,后者从当前的 HTTP 请求中检索用户名和密码。在测试中,我们希望通过 TestContext 框架管理的模拟来配置这些请求参数。下面的清单显示了这个用例的配置:

请求范围 Bean 配置

<beans>

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

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

</beans>

RequestScopedBeanTests中,我们将UserService(即被测试的对象)和MockHttpServletRequest注入到我们的测试实例中。在我们的requestScope()测试方法中,我们通过在提供的MockHttpServletRequest中设置请求参数来设置测试夹具。当在我们的userService上调用loginUser()方法时,我们确信用户服务可以访问当前MockHttpServletRequest的请求范围loginAction(即我们只设置参数的那个)。然后,我们可以根据用户名和密码的已知输入,针对结果执行断言。下面的清单展示了如何做到这一点:

爪哇

@SpringJUnitWebConfig
class RequestScopedBeanTests {

    @Autowired UserService userService;
    @Autowired MockHttpServletRequest request;

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

        LoginResults results = userService.loginUser();
        // assert results
    }
}

Kotlin

@SpringJUnitWebConfig
class RequestScopedBeanTests {

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

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

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

下面的代码片段与我们前面看到的请求范围的代码片段类似 Bean。然而,这一次,userService Bean 对会话范围userPreferences Bean 具有依赖关系。请注意,UserPreferences Bean 是通过使用 SPEL 表达式实例化的,该表达式从当前 HTTP 会话中检索主题。在测试中,我们需要在由 TestContext 框架管理的模拟会话中配置一个主题。下面的示例展示了如何做到这一点:

会话范围 Bean 配置

<beans>

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

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

</beans>

SessionScopedBeanTests中,我们将UserServiceMockHttpSession注入到我们的测试实例中。在我们的sessionScope()测试方法中,我们通过在提供的MockHttpSession中设置预期的theme属性来设置测试夹具。当在我们的userService上调用processUserPreferences()方法时,我们确信用户服务可以访问当前MockHttpSession的会话范围userPreferences,并且我们可以根据配置的主题对结果执行断言。下面的示例展示了如何做到这一点:

爪哇

@SpringJUnitWebConfig
class SessionScopedBeanTests {

    @Autowired UserService userService;
    @Autowired MockHttpSession session;

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

        Results results = userService.processUserPreferences();
        // assert results
    }
}

Kotlin

@SpringJUnitWebConfig
class SessionScopedBeanTests {

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

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

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

# 3.5.9.事务管理

在 TestContext 框架中,事务由TransactionalTestExecutionListener管理,这是默认配置的,即使你没有在测试类上显式声明@TestExecutionListeners。但是,要启用对事务的支持,你必须在ApplicationContext中配置一个PlatformTransactionManager Bean,它加载了@ContextConfiguration语义(更多详细信息将在后面提供)。此外,你必须在测试的类级别或方法级别声明 Spring 的@Transactional注释。

# 测试管理事务

测试管理事务是通过使用TransactionalTestExecutionListener或通过使用TestTransaction(在后面描述)以声明式方式管理的事务。你不应该将这样的事务与 Spring 管理的事务(那些直接由 Spring 在ApplicationContext中为测试加载的事务中管理的事务)或应用程序管理的事务(那些在由测试调用的应用程序代码中以编程方式管理的事务)混淆。 Spring-管理事务和应用程序管理事务通常参与测试管理事务。但是,如果 Spring-managed 或 application-managed 事务配置为除REQUIREDSUPPORTS以外的任何传播类型,则应谨慎使用(有关详细信息,请参见事务传播的讨论)。

在结合 Spring 的测试管理事务使用来自测试框架
的任何形式的抢占超时时时时时
时,必须谨慎。

具体来说, Spring 的测试支持将事务状态绑定到当前线程(通过
ajava.lang.ThreadLocal变量)在此之前调用当前的测试方法。如果
测试框架在新线程中调用当前测试方法以支持
抢占超时,则在当前测试方法内执行的任何操作都将不是在测试管理事务中调用
。因此,任何此类操作
的结果都不会被测试管理事务回滚。相反,这样的动作
将被承诺给持久性存储——例如,关系数据库——甚至
,尽管测试管理事务被适当地回滚了 Spring。

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

*Junit4 的@Test(timeout = …​)支持而TimeOutrule

*Junit Jupiter 的assertTimeoutPreemptively(…​)方法在org.junit.jupiter.api.Assertionsclass

*testng 的@Test(timeOut = …​)支持
# 启用和禁用事务

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

Attribute 支持测试管理的事务
value and transactionManager 是的
propagation 只支持Propagation.NOT_SUPPORTEDPropagation.NEVER
isolation
timeout
readOnly
rollbackFor and rollbackForClassName 否:用TestTransaction.flagForRollback()代替
noRollbackFor and noRollbackForClassName 否:用TestTransaction.flagForCommit()代替
方法级别的生命周期方法——例如,用 JUnit Jupiter 的@BeforeEach@AfterEach注释的方法——在测试管理事务中运行。在另一种
方法上,使用组件级和类级生命周期方法——例如,使用
Junit Jupiter 的@BeforeAll@AfterAll注释的方法,以及使用 Testng 的@BeforeSuite@AfterSuite注释的方法,或者@AfterClass—是不是
测试管理事务中运行。

如果你需要在
事务中以组件级或类级生命周期方法运行代码,你可能希望将相应的PlatformTransactionManager注入到
你的测试类中,然后将其与TransactionTemplate用于程序化的
事务管理。

请注意,[AbstractTransactionalJUnit4SpringContextTests](#TestContext-Support-Classes-JUnit4)和[<gtr="2023"/>](#TestContext-Support-Classes-TestNG)是为类级别的事务支持而预先配置的。

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

爪哇

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

    @Autowired
    HibernateUserRepository repository;

    @Autowired
    SessionFactory sessionFactory;

    JdbcTemplate jdbcTemplate;

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

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

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

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

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

    private void assertNumUsers(int expected) {
        assertEquals("Number of rows in the [user] table.", expected, countRowsInTable("user"));
    }
}

Kotlin

@SpringJUnitConfig(TestConfig::class)
@Transactional
class HibernateUserRepositoryTests {

    @Autowired
    lateinit var repository: HibernateUserRepository

    @Autowired
    lateinit var sessionFactory: SessionFactory

    lateinit var jdbcTemplate: JdbcTemplate

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

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

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

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

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

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

正如在事务回滚和提交行为中所解释的那样,在createUser()方法运行后不需要清理数据库,因为对数据库所做的任何更改都会由TransactionalTestExecutionListener自动回滚。

# 事务回滚和提交行为

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

# 程序化事务管理

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

下面的示例演示了TestTransaction的一些特性。关于[TestTransaction](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/test/context/transaction/testtransaction.html)的更多详细信息,请参见 爪哇doc。

爪哇

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

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

        deleteFromTables("user");

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

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

    protected void assertNumUsers(int expected) {
        assertEquals("Number of rows in the [user] table.", expected, countRowsInTable("user"));
    }
}

Kotlin

@ContextConfiguration(classes = [TestConfig::class])
class ProgrammaticTransactionManagementTests : AbstractTransactionalJUnit4SpringContextTests() {

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

        deleteFromTables("user")

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

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

    protected fun assertNumUsers(expected: Int) {
        assertEquals("Number of rows in the [user] table.", expected, countRowsInTable("user"))
    }
}
# 在事务之外运行代码

有时,你可能需要在事务性测试方法之前或之后运行特定的代码,但要在事务性上下文之外运行,例如,要在运行测试之前验证初始数据库状态,或者在测试运行之后验证预期的事务提交行为(如果测试被配置为提交事务)。TransactionalTestExecutionListener支持针对此类场景的@BeforeTransaction@AfterTransaction注释。你可以使用这些注释之一对测试类中的任何void方法或测试接口中的任何void默认方法进行注释,并且TransactionalTestExecutionListener确保你的 before transaction 方法或 after transaction 方法在适当的时间运行。

任何之前的方法(例如用 JUnit Jupiter 的@BeforeEach注释的方法)
和任何之后的方法(例如用 JUnit Jupiter 的@AfterEach注释的方法)都是
在事务中运行的。此外,对于未配置为在
事务中运行的测试方法,不运行带有@BeforeTransaction@AfterTransaction注释的方法。
# 配置事务管理器

TransactionalTestExecutionListener期望在 Spring ApplicationContext中定义一个PlatformTransactionManager用于测试。如果在测试的ApplicationContext中有多个PlatformTransactionManager实例,则可以使用@Transactional("myTxMgr")@Transactional(transactionManager = "myTxMgr")声明限定符,或者TransactionManagementConfigurer可以通过@Configuration类实现。查询[爪哇doc forTestContextTransactionUtils.retrieveTransactionManager()](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/test/contexttransactionutils.html#retrivetransactionmanager-org.spramework.test.context.tcontext.tcontext-tcontext-justcontext-java.lang.string-)以获取有关在测试中查找事务管理器的算法的详细信息,该算法用于在测试的<<

# 演示所有与事务相关的注释

下面的基于 JUnit Jupiter 的示例显示了一个虚拟的集成测试场景,该场景突出显示了所有与事务相关的注释。该示例的目的不是演示最佳实践,而是演示如何使用这些注释。有关更多信息和配置示例,请参见注释支持小节。[用于@Sql的事务管理](#TestContext-Executing-SQL-Declarationly-TX)包含一个附加示例,该示例使用@Sql执行带有默认事务回滚语义的声明性 SQL 脚本。下面的示例显示了相关的注释:

爪哇

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

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

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

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

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

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

}

Kotlin

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

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

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

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

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

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

}
在测试 ORM 代码

时避免误报当你测试处理 Hibernate 会话状态或 JPA
持久性上下文的应用程序代码时,请确保刷新运行该代码的测试方法
中的底层工作单元。未能刷新底层工作单元可能会产生错误的
正:你的测试通过了,但是相同的代码在动态的生产
环境中抛出了异常。请注意,这适用于任何维护
工作的内存单元的 ORM 框架。在以下基于 Hibernate 的示例测试用例中,一种方法演示了
假阳性,而另一种方法则正确地公开了刷新
会话的结果:

爪哇
<br/>// ...<br/><br/>@Autowired<br/>SessionFactory sessionFactory;<br/><br/>@Transactional<br/>@Test // no expected exception!<br/>public void falsePositive() {<br/> updateEntityInHibernateSession();<br/> // False positive: an exception will be thrown once the Hibernate<br/> // Session is finally flushed (i.e., in production code)<br/>}<br/><br/>@Transactional<br/>@Test(expected = ...)<br/>public void updateWithSessionFlush() {<br/> updateEntityInHibernateSession();<br/> // Manual flush is required to avoid false positive in test<br/> sessionFactory.getCurrentSession().flush();<br/>}<br/><br/>// ...<br/>


<br/>// ...<br/><br/>@Autowired<br/>lateinit var sessionFactory: SessionFactory<br/><br/>@Transactional<br/>@Test // no expected exception!<br/>fun falsePositive() {<br/> updateEntityInHibernateSession()<br/> // False positive: an exception will be thrown once the Hibernate<br/> // Session is finally flushed (i.e., in production code)<br/>}<br/><br/>@Transactional<br/>@Test(expected = ...)<br/>fun updateWithSessionFlush() {<br/> updateEntityInHibernateSession()<br/> // Manual flush is required to avoid false positive in test<br/> sessionFactory.getCurrentSession().flush()<br/>}<br/><br/>// ...<br/><gt="2085"/><>
<"><"><"><"gt="gt="2085">>><">>><<<"gt=2085">>>>>>>><<<<<"gt=

# 3.5.10.执行 SQL 脚本

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

虽然在ApplicationContext加载时初始化一个用于测试曾经的数据库是非常有用的,但有时能够修改数据库期间集成测试是必不可少的。下面的部分解释了如何在集成测试期间以编程方式和声明式方式运行 SQL 脚本。

# 以编程方式执行 SQL 脚本

Spring 提供了以下用于在集成测试方法中以编程方式执行 SQL 脚本的选项。

  • org.springframework.jdbc.datasource.init.ScriptUtils

  • org.springframework.jdbc.datasource.init.ResourceDatabasePopulator

  • org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests

  • org.springframework.test.context.testng.AbstractTransactionalTestNGSpringContextTests

ScriptUtils提供了一组用于处理 SQL 脚本的静态实用程序方法,主要用于框架内的内部使用。但是,如果你需要完全控制 SQL 脚本的解析和运行方式,ScriptUtils可能比后面介绍的其他一些替代方案更适合你的需要。有关更多详细信息,请参见javadoc (opens new window)中的单个方法。

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

爪哇

@Test
void databaseTest() {
    ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
    populator.addScripts(
            new ClassPathResource("test-schema.sql"),
            new ClassPathResource("test-data.sql"));
    populator.setSeparator("@@");
    populator.execute(this.dataSource);
    // 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 脚本。类似地,[executeSqlScript(..)](#testcontext-support-classes-junit4)和[AbstractTransactionalTestNGSpringContextTests](#testcontext-support-classes-testng)中的executeSqlScript(..)方法在内部使用ResourceDatabasePopulator来运行 SQL 脚本。有关更多详细信息,请参见 爪哇doc 获取各种executeSqlScript(..)方法。

# 使用 @sql 声明式执行 SQL 脚本

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

方法级别@Sql声明默认重写类级别声明。然而,作为 Spring Framework5.2 的
,该行为可以通过@SqlMergeMode配置为每个测试类或每个
测试方法。有关更多详细信息,请参见[使用@SqlMergeMode合并和覆盖配置]。
# 路径资源语义

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

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

爪哇

@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的多个实例。对于 爪哇8,你可以使用@Sql作为可重复的注释。否则,你可以使用@SqlGroup注释作为显式容器来声明@Sql的多个实例。

下面的示例展示了如何使用@Sql作为 爪哇8 的可重复注释:

爪哇

@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中。对于 爪哇8 及以上版本,@SqlGroup的使用是可选的,但是你可能需要使用@SqlGroup来与其他 JVM 语言(如 Kotlin)兼容。

爪哇

@Test
@SqlGroup({
    @Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`")),
    @Sql("/test-user-data.sql")
)}
void userTest() {
    // run code that uses the test schema and test data
}

Kotlin

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

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

爪哇

@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中的每个属性都有一个隐含的默认值,该默认值在相应属性的 爪哇doc 中有文档说明。由于 爪哇 语言规范中为注释属性定义的规则,遗憾的是,不可能为注释属性赋值null。因此,为了支持对继承的全局配置的重写,@SqlConfig属性具有显式默认值""(用于字符串),{}(用于数组),或DEFAULT(用于枚举)。这种方法允许@SqlConfig的本地声明通过提供""{}DEFAULT以外的值,选择性地覆盖@SqlConfig的全局声明中的单个属性。当局部@SqlConfig属性不提供除""{}DEFAULT以外的显式值时,全局@SqlConfig属性将被继承。因此,显式的局部配置重写全局配置。

@Sql@SqlConfig提供的配置选项与ScriptUtilsResourceDatabasePopulator支持的配置选项等价,但它们是由<jdbc:initialize-database/>XML 名称空间元素提供的那些选项的超集。参见[@Sql](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/test/context/jdbc/sql.html)和[@SqlConfig](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api-api/org/context/jdbc/jdbc/sqlconframework.html)中的各个属性的 爪哇doc,以获取详细信息。

**@Sql的事务管理 **

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

如果SqlScriptsTestExecutionListener用于检测DataSourcePlatformTransactionManager并推断事务语义的算法不适合你的需要,则可以通过设置dataSourcetransactionManager@SqlConfig属性来指定显式名称。此外,你可以通过设置@SqlConfigtransactionMode属性来控制事务传播行为(例如,是否应该在独立的事务中运行脚本)。尽管对@Sql的事务管理所支持的所有选项的彻底讨论超出了本参考手册的范围,但对于[@SqlConfig](https://DOCS. Spring.io/ Spring-Framework/DOCS/5.3.16/javadoc-api/org/SpringFramework/tFramework/tFramework/tform/springframework/test/test/contextbc/jdbc/sqlconfig.html)和[SqlScriptsTestExecutionListener](DOCS.[ps:/ Spring.io/[[framework. Spring-fram 下面的示例展示了一个典型的测试场景,该场景使用 JUnit Jupiter 和事务测试@Sql:

爪哇

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

    final JdbcTemplate jdbcTemplate;

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

    @Test
    @Sql("/test-data.sql")
    void usersTest() {
        // 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自动回滚(有关详细信息,请参见事务管理)。

#@SqlMergeMode合并和覆盖配置

在 Spring Framework5.2 中,可以将方法级@Sql声明与类级声明合并。例如,这允许你为每个测试类提供一次数据库模式或一些公共测试数据的配置,然后为每个测试方法提供额外的、特定于用例的测试数据。要启用@Sql合并,请用@SqlMergeMode(MERGE)注释你的测试类或测试方法。要禁用特定测试方法(或特定测试子类)的合并,可以通过@SqlMergeMode(OVERRIDE)切换回默认模式。有关示例和更多详细信息,请参阅[@SqlMergeMode注释文档部分](# Spring-testing-annotation-sqlmergemode)。

# 3.5.11.并行测试执行

Spring Framework5.0 引入了在使用 Spring TestContext 框架时在单个 JVM 内并行执行测试的基本支持。通常,这意味着大多数测试类或测试方法可以并行运行,而不需要对测试代码或配置进行任何更改。

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

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

如果测试:

  • 使用 Spring Framework 的@DirtiesContext支持。

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

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

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

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

这可能是由于使用@DirtiesContext或由于从ContextCache中自动驱逐所致。如果@DirtiesContext是罪魁祸首,则需要找到一种方法来
避免使用@DirtiesContext,或者将此类测试排除在并行执行之外。如果
已超过ContextCache的最大大小,则可以增加缓存的最大大小
。详见上下文缓存讨论。
Spring TestContext 框架中的并行测试执行只有在以下情况下才是可能的:
底层TestContext实现提供了一个复制构造函数,如
中所解释的[TestContext]的 爪哇doc(https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/test/text/context.html)。 Spring 中使用的DefaultTestContext提供了这样的构造函数。但是,如果使用提供自定义
实现的TestContext第三方库,则需要
验证它是否适合并行测试执行。

# 3.5.12.TestContext 框架支持类

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

# Spring JUnit4Runner

Spring TestContext 框架通过自定义运行器(在 JUnit4.12 或更高版本上支持)提供与 JUnit4 的完全集成。通过使用@RunWith(SpringJUnit4ClassRunner.class)或更短的@RunWith(SpringRunner.class)变体注释测试类,开发人员可以实现基于标准 JUnit4 的单元和集成测试,并同时获得 TestContext 框架的好处,例如对加载应用程序上下文、测试实例的依赖注入、事务性测试方法执行的支持,等等。如果希望将 Spring TestContext 框架与可选的运行器(例如 JUnit4 的Runner)或第三方运行器(例如)一起使用,则可以选择使用。

下面的代码清单显示了配置使用自定义 Spring Runner运行的测试类的最低要求:

爪哇

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

    @Test
    public void testMethod() {
        // test logic...
    }
}

Kotlin

@RunWith(SpringRunner::class)
@TestExecutionListeners
class SimpleTest {

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

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

# Spring JUnit4 规则

org.springframework.test.context.junit4.rules包提供了以下 JUnit4 规则(JUnit4.12 或更高版本支持):

  • SpringClassRule

  • SpringMethodRule

SpringClassRule是支持 Spring TestContext 框架的类级特性的 JUnitTestRule,而SpringMethodRule是支持 Spring TestContext 框架的实例级和方法级特性的 JUnitMethodRule

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

要支持 TestContext 框架的全部功能,你必须将SpringClassRuleSpringMethodRule合并。下面的示例展示了在集成测试中声明这些规则的正确方法:

爪哇

// 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()
    }
}
# JUnit4 支持类

org.springframework.test.context.junit4包为基于 JUnit4 的测试用例(JUnit4.12 或更高版本支持)提供了以下支持类:

  • AbstractJUnit4SpringContextTests

  • AbstractTransactionalJUnit4SpringContextTests

AbstractJUnit4SpringContextTests是一个抽象基测试类,它将 Spring TestContext 框架与 JUnit4 环境中显式的ApplicationContext测试支持集成在一起。扩展AbstractJUnit4SpringContextTests时,可以访问protected``applicationContext实例变量,可以使用该变量执行显式 Bean 查找或测试整个上下文的状态。

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

这些类为扩展提供了方便。如果不希望你的测试类
绑定到 Spring 特定的类层次结构,则可以使用
Spring’s
JUnit rules
配置你自己的自定义测试类。
# 朱尼特木星的 SpringExtension

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

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

  • 用于测试构造函数、测试方法和测试生命周期回调方法的依赖注入。有关更多详细信息,请参见[dependency injection withSpringExtension]。

  • 基于 SPEL 表达式、环境变量、系统属性等对条件测试执行 (opens new window)的强大支持。有关更多详细信息和示例,请参见@EnabledIf@DisabledIf中的文档。

  • 自定义合成的注释结合了来自 Spring 和 JUnit Jupiter 的注释。有关更多详细信息,请参见@TransactionalDevTestConfig@TransactionalIntegrationTest中的示例。

下面的代码清单显示了如何配置一个测试类,以便将SpringExtension@ContextConfiguration结合使用:

爪哇

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

由于还可以使用 JUnit5 中的注释作为元注释, Spring 提供了@SpringJUnitConfig@SpringJUnitWebConfig组合注释,以简化测试ApplicationContext和 JUnit Jupiter 的配置。

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

爪哇

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

爪哇

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

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

Kotlin

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

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

有关更多详细信息,请参见@SpringJUnitConfig@SpringJUnitWebConfig中的文档。

# 依赖注入与SpringExtension

SpringExtension实现了来自 JUnit Jupiter 的[ParameterResolver](https://junit.org/junit5/DOCS/current/user-guide/#extensions-parameter-resolution)扩展 API,它允许 Spring 为测试构造函数、测试方法和测试生命周期回调方法提供依赖注入。

具体地说,SpringExtension可以将来自测试的ApplicationContext的依赖关系注入到使用@BeforeAll@AfterAll@BeforeEach@AfterEach@RepeatedTest@RepeatedTest@ParameterizedTest等注释的测试构造函数和方法中。

# 构造函数注入

如果 JUnit Jupiter 测试类的构造函数中的特定参数类型为ApplicationContext(或其子类型),或者被注释或 meta 注释为@Autowired@Qualifier,或@Value, Spring 将该特定参数的值注入相应的 Bean 或来自测试的ApplicationContext的值。

Spring 还可以被配置为在一个测试类构造函数被认为是可自动连接的情况下自动连接用于该构造函数的所有参数。如果满足以下条件之一(按优先顺序),则构造函数被认为是可自动连接的。

  • 构造函数用@Autowired注释。

  • @TestConstructor在测试类上存在或元存在,并且autowireMode属性设置为ALL

  • 默认的测试构造函数 AutoWire 模式已更改为ALL

有关@TestConstructor的使用以及如何更改全局测试构造函数 AutoWire 模式的详细信息,请参见[@TestConstructor](#Integration-Testing-Annotations-TestConstructor)。

如果一个测试类的构造函数被认为是可自动连接,则 Spring
承担解决构造函数中所有参数的参数的责任。
因此,在 JUnit Jupiter 注册的其他ParameterResolver都不能为这样的构造函数解析
参数。
测试类的构造函数注入不能与 JUnit
Jupiter 的@TestInstance(PER_CLASS)支持一起使用如果@DirtiesContext是用来关闭
测试的ApplicationContext测试方法之前或之后的
的,原因是@TestInstance(PER_CLASS)指示 JUnit Jupiter 在测试方法调用之间缓存测试
实例。因此,测试实例将保留
对 bean 的引用,这些 bean 最初是从ApplicationContext注入的,而
随后被关闭。由于在这种情况下测试类的构造函数只会被调用
一次,因此依赖项注入将不会再次发生,而随后的测试
将与来自闭合的ApplicationContext的 bean 进行交互,这可能会导致错误。

@DirtiesContext与“测试方法之前”或“测试方法之后”的模式在
中与@TestInstance(PER_CLASS)结合,必须将 Spring
中的依赖项配置为通过字段或 setter 注入提供,以便它们可以在测试
方法调用之间重新注入。

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

爪哇

@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](#Integration-Testing-Annotations-TestConstructor)),我们可以在前面的示例中省略构造函数上@Autowired的声明,从而产生以下结果。

爪哇

@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(或其子类型),或被注释或 meta 注释为@Autowired@Qualifier,或@Value, Spring 从测试的ApplicationContext中将该特定参数的值注入相应的 Bean。

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

爪哇

@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()测试方法中注入依赖项。

爪哇

@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 Framework5.0 以来,Spring TestContext Framework一直支持在 JUnit Jupiter 中的@Nested测试类上使用与测试相关的注释;然而,在 Spring Framework5.3 之前,类级测试配置注释并不像来自超类那样来自封闭类的继承

Spring Framework5.3 引入了用于从封闭类继承测试类配置的第一类支持,并且这样的配置将在默认情况下被继承。要从默认的INHERIT模式更改为OVERRIDE模式,你可以用@Nested注释单个@Nested测试类。显式的@NestedTestConfiguration声明将应用于带注释的测试类及其任何子类和嵌套的类。因此,你可以用@NestedTestConfiguration注释顶级测试类,这将递归地应用于它的所有嵌套测试类。

为了允许开发团队将缺省模式更改为OVERRIDE——例如,为了与 Spring Framework5.0 到 5.2 兼容——缺省模式可以通过 JVM 系统属性或 Classpath 根中的spring.properties文件进行全局更改。有关详细信息,请参见“更改默认的封闭配置继承模式”注释。

尽管下面的“Hello World”示例非常简单,但它展示了如何在顶级类上声明公共配置,该类由其@Nested测试类继承。在这个特定的示例中,只继承了TestConfig配置类。每个嵌套测试类提供自己的一组活动配置文件,从而为每个嵌套测试类提供一个不同的ApplicationContext(有关详细信息,请参见上下文缓存)。请参阅支持的注释列表,以查看哪些注释可以在@Nested测试类中继承。

爪哇

@SpringJUnitConfig(TestConfig.class)
class GreetingServiceTests {

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

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

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

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

Kotlin

@SpringJUnitConfig(TestConfig::class)
class GreetingServiceTests {

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

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

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

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

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

  • AbstractTestNGSpringContextTests

  • AbstractTransactionalTestNGSpringContextTests

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

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

这些类为扩展提供了方便。如果你不希望你的测试类
被绑定到 Spring 特定的类层次结构,那么你可以通过使用@ContextConfiguration@TestExecutionListeners以此类推,并通过
手动使用TestContextManager来配置你自己的自定义测试类。请参阅源代码
AbstractTestNGSpringContextTests,以获得如何测试类的示例。

# 3.6.WebTestClient

WebTestClient是一个用于测试服务器应用程序的 HTTP 客户机。它封装了 Spring 的WebClient,并使用它来执行请求,但公开了一个用于验证响应的测试 facade。WebTestClient可用于执行端到端的 HTTP 测试。 Spring MVC 和 Spring WebFlux 应用程序也可用于在没有运行服务器的情况下通过模拟服务器来测试请求和响应对象。

Kotlin 用户:参见本节相关的WebTestClient的使用。

# 3.6.1.设置

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

# 绑定到控制器

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

对于 WebFlux 应用程序,使用以下方法加载与WebFlux 爪哇 配置等价的基础设施,注册给定的控制器,并创建Webhandler 链来处理请求:

爪哇

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

Kotlin

val client = WebTestClient.bindToController(TestController()).build()

对于 Spring MVC,使用以下方法委托给StandalOneMockmvcBuilder (opens new window)以加载与WebMVC 爪哇 配置等价的基础设施,注册给定的控制器,并创建MockMvc的实例来处理请求:

爪哇

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

Kotlin

val client = MockMvcWebTestClient.bindToController(TestController()).build()
# 绑定到ApplicationContext

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

对于 WebFlux,使用以下方法将 Spring ApplicationContext传递到WebHttphandlerBuilder (opens new window),以创建Webhandler 链来处理请求:

爪哇

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

    WebTestClient client;

    @BeforeEach
    void setUp(ApplicationContext context) {  (2)
        client = WebTestClient.bindToApplicationContext(context).build(); (3)
    }
}
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 (opens new window),以创建MockMvc实例来处理请求:

爪哇

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

    @Autowired
    WebApplicationContext wac; (2)

    WebTestClient client;

    @BeforeEach
    void setUp() {
        client = MockMvcWebTestClient.bindToApplicationContext(this.wac).build(); (3)
    }
}
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
# 绑定到路由器功能

这种设置允许你通过模拟请求和响应对象来测试功能端点,而不需要运行服务器。

对于 WebFlux,使用以下方法委托RouterFunctions.toWebHandler来创建服务器设置以处理请求:

爪哇

RouterFunction<?> route = ...
client = WebTestClient.bindToRouterFunction(route).build();

Kotlin

val route: RouterFunction<*> = ...
val client = WebTestClient.bindToRouterFunction(route).build()

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

# 绑定到服务器

此设置连接到正在运行的服务器,以执行完整的端到端 HTTP 测试:

爪哇

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

Kotlin

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

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

爪哇

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

Kotlin

client = WebTestClient.bindToController(TestController())
        .configureClient()
        .baseUrl("/test")
        .build()

# 3.6.2.写作测试

WebTestClient提供与WebClient相同的 API,直到使用exchange()执行请求为止。请参阅WebClient文档中的示例,以了解如何准备包含表单数据、多部分数据等任何内容的请求。

在调用exchange()之后,WebTestClient偏离了WebClient,而是继续使用工作流来验证响应。

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

爪哇

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

Kotlin

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

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

爪哇

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

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

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

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

  • expectBody():将byte[]或空体解码为JSON 内容

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

爪哇

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)

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

爪哇

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:

爪哇

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()
当你需要用泛型解码到目标类型时,请查找接受[
](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/core/core/parameterizedtyreference.html)而不是<gtr="2502"/>)的重载方法<gtr=“2503”。
# 无内容

如果预期响应不包含内容,则可以断言如下:

爪哇

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

如果你想忽略响应内容,那么下面的内容将在没有任何断言的情况下发布:

爪哇

client.get().uri("/persons/123")
        .exchange()
        .expectStatus().isNotFound()
        .expectBody(Void.class);

Kotlin

client.get().uri("/persons/123")
        .exchange()
        .expectStatus().isNotFound
        .expectBody<Unit>()
# JSON 内容

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

要用JSONAssert (opens new window)验证完整的 JSON 内容:

爪哇

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 (opens new window)验证 JSON 内容:

爪哇

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

Kotlin

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

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

爪哇

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

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

爪哇

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

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

Kotlin

val eventFlux = result.getResponseBody()

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

WebTestClient是一个 HTTP 客户机,因此它只能验证客户机响应中的内容,包括状态、头和主体。

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

爪哇

// 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 服务器响应断言:

爪哇

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

Kotlin

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

# 3.7.MockMVC

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

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

# 3.7.1.概述

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

Spring MVC 测试框架,也称为MockMvc,旨在为 Spring MVC 控制器提供更完整的测试,而无需运行服务器。它通过调用DispacherServlet并从spring-test模块传递“mock” implementations of the Servlet API来实现这一点,该模块在不运行服务器的情况下复制完整的 Spring MVC 请求处理。

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

# 静态导入

当直接使用 MockMVC 来执行请求时,你将需要用于以下方面的静态导入:

  • MockMvcBuilders.*

  • MockMvcRequestBuilders.*

  • MockMvcResultMatchers.*

  • MockMvcResultHandlers.*

记住这一点的一个简单方法是搜索MockMvc*。如果使用 Eclipse,请确保在 Eclipse 首选项中也添加上面的“最喜欢的静态成员”。

当通过WebTestClient使用 mockmvc 时,不需要静态导入。WebTestClient提供了一个没有静态导入的 Fluent API。

# 设置选项

MockMVC 可以通过以下两种方式之一设置。一种方法是直接指向你想要测试的控制器,并以编程方式配置 Spring MVC 基础设施。第二个是指向具有 Spring MVC 和控制器基础设施的 Spring 配置。

要设置用于测试特定控制器的 mockmvc,请使用以下方法:

爪哇

class MyWebTests {

    MockMvc mockMvc;

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

    // ...

}

Kotlin

class MyWebTests {

    lateinit var mockMvc : MockMvc

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

    // ...

}

或者,你也可以在通过WebTestClient进行测试时使用此设置,该设置将委托给与上面所示相同的构建器。

要通过 Spring 配置设置 mockmvc,请使用以下方法:

爪哇

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

    MockMvc mockMvc;

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

    // ...

}

Kotlin

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

    lateinit var mockMvc: MockMvc

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

    // ...

}

或者,你也可以在通过WebTestClient进行测试时使用此设置,该设置将委托给与上面所示相同的构建器。

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

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

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

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

爪哇

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

    @Autowired
    AccountService accountService;

    MockMvc mockMvc;

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

    // ...

}

Kotlin

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

    @Autowired
    lateinit var accountService: AccountService

    lateinit mockMvc: MockMvc

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

    // ...

}

另一方面,standaloneSetup更接近于单位检验。它一次测试一个控制器。你可以手动为控制器注入模拟依赖项,并且它不涉及加载 Spring 配置。这样的测试更多地关注于样式,并使其更容易地看到正在测试的控制器,是否需要任何特定的 MVC 配置来工作,等等。standaloneSetup也是编写临时测试以验证特定行为或调试问题的一种非常方便的方法。

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

# 设置功能

无论你使用哪个 MockMVC Builder,所有MockMvcBuilder实现都提供了一些常见且非常有用的特性。例如,你可以为所有请求声明一个Accept头,并期望在所有响应中的状态为 200,以及Content-Type头,如下所示:

爪哇

// 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 会话。你可以按以下方式使用它:

爪哇

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

参见 爪哇doc 的[ConfigurableMockMvcBuilder](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/test/web/ Servlet/setup/confirablemcmvcbuilder.html),以获得所有 MockMVC Builder 特性的列表,或使用 IDE 探索可用的选项。

# 执行请求

本节展示了如何单独使用 MockMVC 来执行请求和验证响应。如果通过WebTestClient使用 mockmvc,请查看写作测试上的相应部分。

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

爪哇

// static import of MockMvcRequestBuilders.*

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

Kotlin

import org.springframework.test.web.servlet.post

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

你还可以执行内部使用MockMultipartHttpServletRequest的文件上传请求,这样就不会实际解析多个部分的请求。相反,你必须将其设置为类似于以下示例:

Java

mockMvc.perform(multipart("/doc").file("a1", "ABC".getBytes("UTF-8")));

Kotlin

import org.springframework.test.web.servlet.multipart

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

你可以用 URI 模板样式指定查询参数,如下例所示:

Java

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

Kotlin

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

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

Java

mockMvc.perform(get("/hotels").param("thing", "somewhere"));

Kotlin

import org.springframework.test.web.servlet.get

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

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

在大多数情况下,最好是将上下文路径和 Servlet 路径留在请求 URI 之外。如果必须使用完整的请求 URI 进行测试,请确保相应地设置contextPathservletPath,以使请求映射工作,如下例所示:

Java

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

Kotlin

import org.springframework.test.web.servlet.get

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

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

Java

class MyWebTests {

    MockMvc mockMvc;

    @BeforeEach
    void setup() {
        mockMvc = standaloneSetup(new AccountController())
            .defaultRequest(get("/")
            .contextPath("/app").servletPath("/main")
            .accept(MediaType.APPLICATION_JSON)).build();
    }
}

Kotlin

// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed

前面的属性影响通过MockMvc实例执行的每个请求。如果在给定的请求中也指定了相同的属性,那么它将重写默认值。这就是为什么默认请求中的 HTTP 方法和 URI 无关紧要的原因,因为它们必须在每个请求中指定。

# 定义期望

可以通过在执行请求后追加一个或多个andExpect(..)调用来定义期望,如下例所示。一旦一种预期落空,就不会再有其他预期了。

Java

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

mockMvc.perform(get("/accounts/1")).andExpect(status().isOk());

Kotlin

import org.springframework.test.web.servlet.get

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

可以通过在执行请求后追加andExpectAll(..)来定义多个期望,如下例所示。与andExpect(..)相反,andExpectAll(..)保证将断言所有提供的期望,并将跟踪和报告所有故障。

Java

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

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

MockMvcResultMatchers.*提供了一些期望,其中一些期望进一步嵌套了更详细的期望。

预期分为两大类。第一类断言验证响应的属性(例如,响应状态、标题和内容)。这些是可以断言的最重要的结果。

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

以下测试断言绑定或验证失败:

Java

mockMvc.perform(post("/persons"))
    .andExpect(status().isOk())
    .andExpect(model().attributeHasErrors("person"));

Kotlin

import org.springframework.test.web.servlet.post

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

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

Java

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

Kotlin

import org.springframework.test.web.servlet.post

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

只要请求处理不会导致未处理的异常,print()方法就会将所有可用的结果数据打印到System.out。还有一个log()方法和print()方法的两个附加变体,一个接受OutputStream,另一个接受Writer。例如,调用print(System.err)将结果数据打印到System.err,而调用print(myWriter)将结果数据打印到自定义写入器。如果希望记录结果数据而不是打印结果,则可以调用log()方法,该方法将结果数据记录为DEBUG日志类别下的单个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 (opens new window)创建的超媒体链接时,可以使用 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 (opens new window)创建的超媒体链接时,可以使用 XPath 表达式来验证生成的链接:

Java

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

Kotlin

val ns = mapOf("ns" to "http://www.w3.org/2005/Atom")
mockMvc.get("/handle") {
    accept(MediaType.APPLICATION_XML)
}.andExpect {
    xpath("/person/ns:link[@rel='self']/@href", ns) {
        string("http://localhost:8080/people")
    }
}
# 异步请求

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

Servlet 3.0 异步请求supported in Spring MVC通过退出 Servlet 容器线程并允许应用程序异步计算响应来工作,在此之后进行异步分派以在 Servlet 容器线程上完成处理。

Spring 在 MVC 测试中,异步请求可以通过首先断言产生的异步值,然后手动执行异步调度,最后验证响应来进行测试。下面是返回DeferredResultCallable或反应类型(例如反应器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 手动执行异步分派(因为没有正在运行的容器)
5 验证最终响应

Kotlin

@Test
fun test() {
    var mvcResult = mockMvc.get("/path").andExpect {
        status().isOk() (1)
        request { asyncStarted() } (2)
        // TODO Remove unused generic parameter
        request { asyncResult<Nothing>("body") } (3)
    }.andReturn()

    mockMvc.perform(asyncDispatch(mvcResult)) (4)
            .andExpect {
                status().isOk() (5)
                content().string("body")
            }
}
1 检查响应状态仍未更改
2 异步处理肯定已经启动了。
3 等待并断言异步结果
4 手动执行异步分派(因为没有正在运行的容器)
5 验证最终响应
# 流式响应

测试流响应(例如服务器发送的事件)的最佳方法是通过WebTestClient,它可以用作测试客户端,以连接到MockMvc实例,从而在 Spring MVC 控制器上执行测试,而无需运行服务器。例如:

爪哇

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 中也是支持的,在这里你可以测试正在运行的服务器 (opens new window)

# 过滤注册

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

爪哇

mockMvc = standaloneSetup(new PersonController()).addFilters(new CharacterEncodingFilter()).build();

Kotlin

// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed

通过spring-test中的MockFilterChain调用注册过滤器,最后一个过滤器将委托给DispatcherServlet

# MockMVC 与端到端测试

MockMVC 是基于来自spring-test模块的 Servlet API 模拟实现构建的,并且不依赖于正在运行的容器。因此,与实际运行客户机和实时服务器的完整端到端集成测试相比,存在一些差异。

思考这个问题最简单的方法是从空白MockHttpServletRequest开始。无论你向它添加什么,请求都会变成什么。可能会让你感到惊讶的是,默认情况下没有上下文路径;没有jsessionidcookie;没有转发、错误或异步分派;因此,没有实际的 JSP 呈现。相反,“转发”和“重定向”URL 保存在MockHttpServletResponse中,并且可以在期望的情况下断言。

这意味着,如果使用 JSP,你可以验证请求被转发到的 JSP 页面,但不呈现 HTML。换句话说,不会调用 JSP。但是,请注意,所有不依赖于转发的其他呈现技术,例如 ThymeLeaf 和 FreeMarker,都会按照预期的那样将 HTML 呈现给响应主体。对于通过@ResponseBody方法呈现 JSON、XML 和其他格式,也是如此。

或者,你可以考虑使用@SpringBootTest从 Spring 启动的完整的端到端集成测试支持。见Spring Boot Reference Guide (opens new window)

每种方法都有优点和缺点。 Spring MVC 测试中提供的选项是从经典单元测试到完全集成测试的不同规模的停止。可以肯定的是, Spring MVC 测试中的所有选项都不属于经典单元测试的范畴,但它们更接近于经典单元测试。例如,你可以通过将模拟的服务注入控制器来隔离 Web 层,在这种情况下,你只能通过DispatcherServlet来测试 Web 层,但要使用实际的 Spring 配置,因为你可能会在与上面的层隔离的情况下测试数据访问层。此外,你还可以使用独立设置,一次只关注一个控制器,并手动提供使其工作所需的配置。

使用 Spring MVC 测试时的另一个重要区别是,从概念上讲,这样的测试是服务器端的,因此你可以检查使用了什么处理程序,如果异常是用 HandleRexCeptionResolver 处理的,模型的内容是什么,有哪些绑定错误,以及其他细节。这意味着更容易编写预期值,因为服务器不是一个不透明的框,就像通过实际的 HTTP 客户机进行测试时一样。这通常是经典单元测试的一个优势:它更容易编写、推理和调试,但不会取代对完全集成测试的需求。同时,重要的是不要忽视这样一个事实,即反应是最重要的检查事项。简而言之,即使在同一个项目中,这里也有多种测试风格和策略的空间。

# 进一步的例子

该框架自己的测试包括许多样本测试 (opens new window),旨在展示如何单独或通过WebTestClient (opens new window)使用 MockMVC。浏览这些示例以获得更多的想法。

# 3.7.2.htmlunit 集成

Spring 提供MockMvcHtmlUnit (opens new window)之间的集成。这简化了在使用基于 HTML 的视图时执行端到端测试的过程。这种集成使你能够:

MockMVC 使用不依赖于 Servlet 容器
的模板化技术(例如,ThymeLeaf、FreeMarker 和其他),但不适用于 JSP,因为
它们依赖于 Servlet 容器。
# 为什么要进行 HTMLUnit 集成?

我脑海中浮现的最明显的问题是“我为什么需要这个?”最好通过探索一个非常基本的示例应用程序来找到答案。假设你有一个 Spring MVC Web 应用程序,该应用程序在Message对象上支持增删改查操作。该应用程序还支持对所有消息进行分页。你会如何去测试它呢?

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

爪哇

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

mockMvc.perform(createMessage)
        .andExpect(status().is3xxRedirection())
        .andExpect(redirectedUrl("/messages/123"));

Kotlin

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

如果我们想测试允许我们创建消息的窗体视图,该怎么办?例如,假设我们的表单看起来像以下片段:

<form id="messageForm" action="/messages/" method="post">
    <div class="pull-right"><a href="/messages/">Messages</a></div>

    <label for="summary">Summary</label>
    <input type="text" class="required" id="summary" name="summary" value="" />

    <label for="text">Message</label>
    <textarea id="text" name="text"></textarea>

    <div class="form-actions">
        <input type="submit" value="Create" />
    </div>
</form>

我们如何确保我们的表单产生正确的请求来创建新消息?一次幼稚的尝试可能类似于以下几点:

爪哇

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

Kotlin

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

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

爪哇

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 表达式,但是随着我们考虑更多因素,它们变得更加复杂:字段是正确的类型吗?字段启用了吗?以此类推。

  • 另一个问题是,我们正在做的工作是预期的两倍。我们必须首先验证视图,然后使用刚刚验证过的相同参数提交视图。理想情况下,这可以一次完成。

  • 最后,我们仍然无法解释某些事情。例如,如果表单也有我们希望测试的 爪哇Script 验证呢?

总的问题是,测试一个 Web 页面并不涉及一个单独的交互。相反,它是用户如何与 Web 页面交互以及该 Web 页面如何与其他资源交互的组合。例如,表单视图的结果被用作用户创建消息的输入。此外,我们的表单视图可能会使用影响页面行为的额外资源,例如 爪哇Script 验证。

# 整合测试的拯救?

为了解决前面提到的问题,我们可以执行端到端集成测试,但这有一些缺点。考虑测试这个视图,它可以让我们在消息中进行页面浏览。我们可能需要进行以下测试:

  • 我们的页面是否向用户显示通知,以表明当消息为空时没有可用的结果?

  • 我们的页面是否正确地显示了一条消息?

  • 我们的页面是否适当地支持分页?

要设置这些测试,我们需要确保我们的数据库包含正确的消息。这导致了一些额外的挑战:

  • 确保数据库中有正确的消息是很乏味的。(考虑一下国外的关键限制。

  • 测试可能会变得很慢,因为每个测试都需要确保数据库处于正确的状态。

  • 由于我们的数据库需要处于特定的状态,因此我们不能并行运行测试。

  • 对自动生成的 ID、时间戳等项执行断言可能很困难。

这些挑战并不意味着我们应该完全放弃端到端集成测试。相反,我们可以通过重构详细的测试,使用运行速度更快、更可靠且没有副作用的模拟服务,来减少端到端集成测试的数量。然后,我们可以实现少量真正的端到端集成测试,这些测试验证简单的工作流,以确保所有工作都正确地结合在一起。

# 进入 HTMLUnit 集成

那么,我们如何在测试页面的交互和在测试套件中保持良好性能之间实现平衡呢?答案是:“通过整合 MockMVC 和 HTMLUnit。”

# htmlunit 集成选项

当你想要将 mockmvc 与 htmlunit 集成在一起时,你有许多选项:

  • mockmvc 和 htmlunit:如果你想使用原始的 htmlUnit 库,请使用此选项。

  • MockMVC 和 WebDriver:使用此选项可以简化开发,并在集成和端到端测试之间重用代码。

  • MOCKMVC 和 GEB:如果你想使用 Groovy 进行测试、简化开发以及在集成和端到端测试之间重用代码,请使用此选项。

# mockmvc 和 htmlunit

本节介绍如何集成 MockMVC 和 HTMLUnit。如果你想使用原始的 HTMLUnit 库,请使用此选项。

# mockmvc 和 htmlunit 设置

首先,确保包含了对net.sourceforge.htmlunit:htmlunit的测试依赖项。为了在 Apache HttpComponents4.5+ 中使用 HTMLUnit,你需要使用 HTMLUnit2.18 或更高版本。

通过使用MockMvcWebClientBuilder,我们可以轻松地创建一个与 MockMVC 集成的 htmlUnitWebClient,如下所示:

爪哇

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的一个简单示例。关于高级用法,
参见[advancedMockMvcWebClientBuilder](# Spring-mvc-test-server-htmlunit-mah-advanced-builder)。

这确保了将localhost作为服务器引用的任何 URL 都指向我们的MockMvc实例,而不需要真正的 HTTP 连接。正常情况下,通过使用网络连接来请求任何其他 URL。这让我们很容易测试 CDNS 的使用情况。

# mockmvc 和 htmlunit 的使用

现在,我们可以像通常那样使用 HTMLUnit,但不需要将我们的应用程序部署到 Servlet 容器中。例如,我们可以请求该视图创建带有以下内容的消息:

爪哇

HtmlPage createMsgFormPage = webClient.getPage("http://localhost/messages/form");

Kotlin

val createMsgFormPage = webClient.getPage("http://localhost/messages/form")
默认的上下文路径是""。或者,我们可以指定上下文路径,
,如[advancedMockMvcWebClientBuilder](# Spring-mvc-test-server-htmlunit-mah-advanced-builder)中所述。

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

爪哇

HtmlForm form = createMsgFormPage.getHtmlElementById("messageForm");
HtmlTextInput summaryInput = createMsgFormPage.getHtmlElementById("summary");
summaryInput.setValueAttribute("Spring Rocks");
HtmlTextArea textInput = createMsgFormPage.getHtmlElementById("text");
textInput.setText("In case you didn't know, Spring Rocks!");
HtmlSubmitInput submit = form.getOneHtmlElementByAttribute("input", "type", "submit");
HtmlPage newMessagePage = submit.click();

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 (opens new window)库:

爪哇

assertThat(newMessagePage.getUrl().toString()).endsWith("/messages/123");
String id = newMessagePage.getHtmlElementById("id").getTextContent();
assertThat(id).isEqualTo("123");
String summary = newMessagePage.getHtmlElementById("summary").getTextContent();
assertThat(summary).isEqualTo("Spring Rocks");
String text = newMessagePage.getHtmlElementById("text").getTextContent();
assertThat(text).isEqualTo("In case you didn't know, Spring Rocks!");

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 引擎 (opens new window)来评估 爪哇Script。这意味着我们还可以在页面中测试 爪哇Script 的行为。

有关使用 htmlunit 的更多信息,请参见htmlunit 文档 (opens new window)

# 高级MockMvcWebClientBuilder

在到目前为止的示例中,我们已经以尽可能简单的方式使用MockMvcWebClientBuilder,通过基于 Spring TestContext 框架为我们加载的WebApplicationContext构建WebClient。下面的示例重复了这种方法:

爪哇

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

我们还可以指定其他配置选项,如下例所示:

爪哇

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

爪哇

MockMvc mockMvc = MockMvcBuilders
        .webAppContextSetup(context)
        .apply(springSecurity())
        .build();

webClient = MockMvcWebClientBuilder
        .mockMvcSetup(mockMvc)
        // for illustration only - defaults to ""
        .contextPath("")
        // By default MockMvc is used for localhost only;
        // the following will use MockMvc for example.com and example.org as well
        .useMockMvcForHosts("example.com","example.org")
        .build();

Kotlin

// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed

这更详细,但是,通过使用MockMvc实例构建WebClient实例,我们可以在指尖获得 MockMVC 的全部功能。

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

在前面的部分中,我们已经了解了如何将 MockMVC 与原始的 HTMLUnitAPI 结合使用。在本节中,我们在 SeleniumWebDriver (opens new window)中使用了额外的抽象,以使事情变得更简单。

# 为什么是 Webdriver 和 MockMVC?

我们已经可以使用 HTMLUnit 和 MockMVC 了,那么为什么我们要使用 WebDriver 呢?Selenium WebDriver 提供了一个非常优雅的 API,可以让我们轻松地组织代码。为了更好地展示它是如何工作的,我们将在本节中探讨一个示例。

尽管是Selenium (opens new window)的一部分,WebDriver 并不需要
的 Selenium 服务器来运行测试。

假设我们需要确保消息是正确创建的。测试包括查找 HTML 表单输入元素,填写它们,并做出各种断言。

这种方法会导致许多单独的测试,因为我们也希望测试错误条件。例如,如果我们只填写表单的一部分,我们希望确保得到一个错误。如果我们填写了整个表单,那么新创建的消息将在之后显示。

如果其中一个字段被命名为“Summary”,那么我们可能会在测试中的多个地方重复类似于以下内容的内容:

爪哇

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

Kotlin

val summaryInput = currentPage.getHtmlElementById("summary")
summaryInput.setValueAttribute(summary)

那么,如果我们将id更改为smmry会发生什么呢?这样做将迫使我们更新所有的测试,以纳入这一变化。这违反了 dry 原则,因此我们最好将该代码提取到它自己的方法中,如下所示:

爪哇

public HtmlPage createMessage(HtmlPage currentPage, String summary, String text) {
    setSummary(currentPage, summary);
    // ...
}

public void setSummary(HtmlPage currentPage, String summary) {
    HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary");
    summaryInput.setValueAttribute(summary);
}

Kotlin

fun createMessage(currentPage: HtmlPage, summary:String, text:String) :HtmlPage{
    setSummary(currentPage, summary);
    // ...
}

fun setSummary(currentPage:HtmlPage , summary: String) {
    val summaryInput = currentPage.getHtmlElementById("summary")
    summaryInput.setValueAttribute(summary)
}

这样做可以确保在更改 UI 时不需要更新所有的测试。

我们甚至可以更进一步,将这个逻辑放在一个Object中,该逻辑表示我们当前所在的HtmlPage,如下例所示:

爪哇

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

以前,这种模式被称为页面对象模式 (opens new window)。虽然我们可以通过 HTMLUnit 实现这一点,但 WebDriver 提供了一些工具,我们将在下面的小节中对这些工具进行探讨,以使这种模式更容易实现。

# MockMVC 和 WebDriver 设置

要在 Spring MVC 测试框架中使用 Selenium WebDriver,请确保你的项目包含对org.seleniumhq.selenium:selenium-htmlunit-driver的测试依赖关系。

我们可以通过使用MockMvcHtmlUnitDriverBuilder很容易地创建与 MockMVC 集成的 Selenium WebDriver,如下例所示:

爪哇

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的一个简单示例。有关更高级的
用法,请参见[advancedMockMvcHtmlUnitDriverBuilder](# Spring-mvc-test-server-htmlunit-webdriver-advanced-builder)。

前面的示例确保将引用localhost作为服务器的任何 URL 都指向我们的MockMvc实例,而不需要真正的 HTTP 连接。正常情况下,通过使用网络连接来请求任何其他 URL。这让我们很容易测试 CDNS 的使用情况。

# MockMVC 和 WebDriver 的使用

现在,我们可以像通常那样使用 WebDriver,但不需要将我们的应用程序部署到 Servlet 容器中。例如,我们可以请求该视图创建带有以下内容的消息:

爪哇

CreateMessagePage page = CreateMessagePage.to(driver);

Kotlin

val page = CreateMessagePage.to(driver)

然后,我们可以填写表单并提交它来创建一条消息,如下所示:

爪哇

ViewMessagePage viewMessagePage =
        page.createMessage(ViewMessagePage.class, expectedSummary, expectedText);

Kotlin

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

通过利用页面对象模式,这改进了htmlunit 测试的设计。正如我们在为什么是 Webdriver 和 MockMVC?中提到的,我们可以在 HTMLUnit 中使用 Page 对象模式,但是使用 WebDriver 要容易得多。考虑以下CreateMessagePage实现:

爪哇

public class CreateMessagePage
        extends AbstractPage { (1)

    (2)
    private WebElement summary;
    private WebElement text;

    (3)
    @FindBy(css = "input[type=submit]")
    private WebElement submit;

    public CreateMessagePage(WebDriver driver) {
        super(driver);
    }

    public <T> T createMessage(Class<T> resultPage, String summary, String details) {
        this.summary.sendKeys(summary);
        this.text.sendKeys(details);
        this.submit.click();
        return PageFactory.initElements(driver, resultPage);
    }

    public static CreateMessagePage to(WebDriver driver) {
        driver.get("http://localhost:9990/mail/messages/form");
        return PageFactory.initElements(driver, CreateMessagePage.class);
    }
}
1 CreateMessagePage扩展了AbstractPage。我们没有讨论AbstractPage的详细信息,但是,总而言之,它包含了我们所有页面的通用功能。
例如,如果我们的应用程序具有导航栏、全局错误消息和其他
功能,我们可以将此逻辑放置在共享位置。
2 对于 HTML 页面中我们感兴趣的每个部分,我们都有一个成员变量
。这些是WebElement型。Webdriver 的[PageFactory](https://github.com/seleniumhq/selenium/wiki/pageFactory)让我们通过自动解析CreateMessagePage每个WebElement,从 htmlunit 版本的CreateMessagePage中删除大量代码。[PageFactory#initElements(WebDriver,Class<T>)](https://seleniumhq.github.io/selenium/DOCS/api/java/org/openqa/selenium/support/pageFactory.html#initelements-org.openqa.selenium.webdriver-java.lang.class-)方法通过使用字段名并查找
由页面中元素的idname自动解析每个WebElement
3 我们可以使用[@FindBy注释](https://github.com/seleniumhq/selenium/wiki/pageFactory#making-the-example-work-using-annotations)来覆盖默认的查找行为。我们的示例展示了如何使用@FindBy注释来使用css选择器(input[type=submit])查找我们的 Submit 按钮。

Kotlin

class CreateMessagePage(private val driver: WebDriver) : AbstractPage(driver) { (1)

    (2)
    private lateinit var summary: WebElement
    private lateinit var text: WebElement

    (3)
    @FindBy(css = "input[type=submit]")
    private lateinit var submit: WebElement

    fun <T> createMessage(resultPage: Class<T>, summary: String, details: String): T {
        this.summary.sendKeys(summary)
        text.sendKeys(details)
        submit.click()
        return PageFactory.initElements(driver, resultPage)
    }
    companion object {
        fun to(driver: WebDriver): CreateMessagePage {
            driver.get("http://localhost:9990/mail/messages/form")
            return PageFactory.initElements(driver, CreateMessagePage::class.java)
        }
    }
}
1 CreateMessagePage扩展了AbstractPage。我们没有讨论AbstractPage的详细信息,但是,总而言之,它包含了我们所有页面的通用功能。
例如,如果我们的应用程序具有导航栏、全局错误消息和其他
功能,我们可以将此逻辑放置在共享位置。
2 对于 HTML 页面中我们感兴趣的每个部分,我们都有一个成员变量
。它们是WebElement型。Webdriver 的[PageFactory](https://github.com/seleniumhq/selenium/wiki/pageFactory)让我们通过自动解析WebElement每个WebElement,从 htmlunit 版本的CreateMessagePage中删除大量代码。[PageFactory#initElements(WebDriver,Class<T>)](https://seleniumhq.github.io/selenium/DOCS/api/java/org/openqa/selenium/support/pageFactory.html#initelements-org.openqa.selenium.webdriver-java.lang.class-)方法通过使用字段名并查找
由页面中元素的idname自动解析每个<gt r=“2696”。
3 我们可以使用[@FindBy注释](https://github.com/seleniumhq/selenium/wiki/pageFactory#making-the-example-work-using-annotations)来覆盖默认的查找行为。我们的示例展示了如何使用@FindBy注释来使用css选择器(input[type=submit])查找我们的 Submit 按钮。

最后,我们可以验证新消息是否已成功创建。以下断言使用AssertJ (opens new window)断言程序库:

爪哇

assertThat(viewMessagePage.getMessage()).isEqualTo(expectedMessage);
assertThat(viewMessagePage.getSuccess()).isEqualTo("Successfully created a new message");

Kotlin

assertThat(viewMessagePage.message).isEqualTo(expectedMessage)
assertThat(viewMessagePage.success).isEqualTo("Successfully created a new message")

我们可以看到,我们的ViewMessagePage允许我们与自定义域模型进行交互。例如,它公开了一个返回Message对象的方法:

爪哇

public Message getMessage() throws ParseException {
    Message message = new Message();
    message.setId(getId());
    message.setCreated(getCreated());
    message.setSummary(getSummary());
    message.setText(getText());
    return message;
}

Kotlin

fun getMessage() = Message(getId(), getCreated(), getSummary(), getText())

然后,我们可以在断言中使用富域对象。

最后,当测试完成时,我们一定不要忘记关闭WebDriver实例,如下所示:

爪哇

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

Kotlin

@AfterEach
fun destroy() {
    if (driver != null) {
        driver.close()
    }
}

有关使用 WebDriver 的更多信息,请参见 SeleniumWebDriver 文档 (opens new window)

# 高级MockMvcHtmlUnitDriverBuilder

在到目前为止的示例中,我们已经以尽可能简单的方式使用MockMvcHtmlUnitDriverBuilder,通过基于 Spring TestContext 框架为我们加载的WebApplicationContext构建WebDriver。这种方法在此重复如下:

爪哇

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

我们还可以指定其他配置选项,如下所示:

爪哇

WebDriver driver;

@BeforeEach
void setup() {
    driver = MockMvcHtmlUnitDriverBuilder
            // demonstrates applying a MockMvcConfigurer (Spring Security)
            .webAppContextSetup(context, springSecurity())
            // for illustration only - defaults to ""
            .contextPath("")
            // By default MockMvc is used for localhost only;
            // the following will use MockMvc for example.com and example.org as well
            .useMockMvcForHosts("example.com","example.org")
            .build();
}

Kotlin

lateinit var driver: WebDriver

@BeforeEach
fun setup() {
    driver = MockMvcHtmlUnitDriverBuilder
            // demonstrates applying a MockMvcConfigurer (Spring Security)
            .webAppContextSetup(context, springSecurity())
            // for illustration only - defaults to ""
            .contextPath("")
            // By default MockMvc is used for localhost only;
            // the following will use MockMvc for example.com and example.org as well
            .useMockMvcForHosts("example.com","example.org")
            .build()
}

作为一种选择,我们可以通过分别配置MockMvc实例并将其提供给MockMvcHtmlUnitDriverBuilder来执行完全相同的设置,如下所示:

Java

MockMvc mockMvc = MockMvcBuilders
        .webAppContextSetup(context)
        .apply(springSecurity())
        .build();

driver = MockMvcHtmlUnitDriverBuilder
        .mockMvcSetup(mockMvc)
        // for illustration only - defaults to ""
        .contextPath("")
        // By default MockMvc is used for localhost only;
        // the following will use MockMvc for example.com and example.org as well
        .useMockMvcForHosts("example.com","example.org")
        .build();

Kotlin

// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed

这更详细,但是,通过使用MockMvc实例构建WebDriver,我们可以在指尖获得 MockMVC 的全部功能。

有关创建MockMvc实例的更多信息,请参见设置选项
# MOCKMVC 和 GEB

在上一节中,我们了解了如何在 WebDriver 中使用 MockMVC。在这一节中,我们使用Geb (opens new window)使我们的测试更加 Groovy-er。

# 为什么是 GEB 和 MOCKMVC?

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

# mockmvc 和 geb 设置

我们可以使用使用使用 MockMVC 的 Selenium WebDriver 轻松地初始化 GEBBrowser,如下所示:

def setup() {
    browser.driver = MockMvcHtmlUnitDriverBuilder
        .webAppContextSetup(context)
        .build()
}
这是使用MockMvcHtmlUnitDriverBuilder的一个简单示例。有关更高级的
用法,请参见[advancedMockMvcHtmlUnitDriverBuilder](# Spring-mvc-test-server-htmlunit-webdriver-advanced-builder)。

这确保将引用localhost作为服务器的任何 URL 指向我们的MockMvc实例,而不需要真正的 HTTP 连接。正常情况下,通过使用网络连接来请求任何其他 URL。这让我们可以很容易地测试 CDNS 的使用情况。

# MockMVC 和 GEB 的使用

现在,我们可以像通常那样使用 GEB,但不需要将我们的应用程序部署到 Servlet 容器中。例如,我们可以请求该视图创建带有以下内容的消息:

to CreateMessagePage

然后,我们可以填写表单并提交它来创建一条消息,如下所示:

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

未找到的任何未识别的方法调用或属性访问或引用都将转发到当前页对象。这删除了我们在直接使用 WebDriver 时所需的大量样板代码。

与直接使用 WebDriver 一样,通过使用页面对象模式,这改进了htmlunit 测试的设计。正如前面提到的,我们可以在 HTMLUnit 和 WebDriver 中使用页面对象模式,但是使用 GEB 则更容易。考虑一下我们新的基于 Groovy 的CreateMessagePage实现:

class CreateMessagePage extends Page {
    static url = 'messages/form'
    static at = { assert title == 'Messages : Create'; true }
    static content =  {
        submit { $('input[type=submit]') }
        form { $('form') }
        errors(required:false) { $('label.error, .alert-error')?.text() }
    }
}

我们的CreateMessagePage扩展了Page。我们不讨论Page的详细信息,但总而言之,它包含了我们所有页面的通用功能。我们定义了一个可以在其中找到此页面的 URL。这让我们可以导航到该页面,如下所示:

to CreateMessagePage

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

then:
at CreateMessagePage
errors.contains('This field is required.')
我们在闭包中使用断言,这样我们就可以确定问题出在哪里
,如果我们在错误的页面上。

接下来,我们创建一个content闭包,该闭包指定页面中所有感兴趣的区域。我们可以使用jQuery-ish Navigator API (opens new window)来选择我们感兴趣的内容。

最后,我们可以验证新消息是否已成功创建,如下所示:

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

有关如何最大限度地利用 GEB 的更多详细信息,请参见GEB 之书 (opens new window)用户手册。

# 3.8.测试客户端应用程序

你可以使用客户端测试来测试内部使用RestTemplate的代码。其思想是声明预期的请求并提供“存根”响应,这样你就可以专注于孤立地测试代码(也就是说,在不运行服务器的情况下)。下面的示例展示了如何做到这一点:

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 测试的中心类)使用自定义的RestTemplate配置ClientHttpRequestFactory,该自定义配置根据预期断言实际请求并返回“存根”响应。在这种情况下,我们期望请求/greeting,并希望返回带有text/plain内容的 200 响应。我们可以根据需要定义额外的期望请求和存根响应。当我们定义期望的请求和存根响应时,RestTemplate可以像往常一样在客户端代码中使用。在测试结束时,可以使用mockServer.verify()来验证所有的期望都已满足。

默认情况下,请求的预期顺序是声明期望的顺序。在构建服务器时,可以设置ignoreExpectOrder选项,在这种情况下,将检查所有期望(按顺序)以找到给定请求的匹配项。这意味着请求可以按任何顺序提出。下面的示例使用ignoreExpectOrder:

Java

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

Kotlin

server = MockRestServiceServer.bindTo(restTemplate).ignoreExpectOrder(true).build()

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

Java

RestTemplate restTemplate = new RestTemplate();

MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build();
mockServer.expect(times(2), requestTo("/something")).andRespond(withSuccess());
mockServer.expect(times(3), requestTo("/somewhere")).andRespond(withSuccess());

// ...

mockServer.verify();

Kotlin

val restTemplate = RestTemplate()

val mockServer = MockRestServiceServer.bindTo(restTemplate).build()
mockServer.expect(times(2), requestTo("/something")).andRespond(withSuccess())
mockServer.expect(times(3), requestTo("/somewhere")).andRespond(withSuccess())

// ...

mockServer.verify()

请注意,当ignoreExpectOrder未设置(默认值),因此,请求是按照声明的顺序进行的,那么该顺序仅适用于任何预期请求中的第一个。例如,如果“/something”预期两次,然后是“/somewhere”三次,那么在向“/somewhere”发出请求之前,应该有一个对“/something”的请求,但是,除了随后的“/something”和“/somewhere”之外,请求可以随时出现。

作为上述所有方法的替代方案,客户端测试支持还提供了ClientHttpRequestFactory实现,你可以将其配置为RestTemplate,以将其绑定到MockMvc实例。这允许使用实际的服务器端逻辑处理请求,但不需要运行服务器。下面的示例展示了如何做到这一点:

Java

MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
this.restTemplate = new RestTemplate(new MockMvcClientHttpRequestFactory(mockMvc));

// Test code that uses the above RestTemplate ...

Kotlin

val mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build()
restTemplate = RestTemplate(MockMvcClientHttpRequestFactory(mockMvc))

// Test code that uses the above RestTemplate ...

# 3.8.1.静态导入

与服务器端测试一样,客户端测试的 Fluent API 需要一些静态导入。搜索MockRest*很容易找到。Eclipse 用户应该在 Java Editor Content Assist Favorites 下的 Eclipse 首选项中添加MockRestRequestMatchers.*MockRestResponseCreators.*作为“最喜欢的静态成员”。这允许在输入静态方法名的第一个字符后使用内容辅助。其他 IDE(例如 IntelliJ)可能不需要任何额外的配置。检查对静态成员的代码完成的支持。

# 3.8.2.客户端 REST 测试的进一步示例

Spring MVC 测试自己的测试包括示例测试 (opens new window)客户端 REST 测试。

# 4. 更多资源

有关测试的更多信息,请参见以下参考资料: