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

1.Spring测试简介

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

2.单元测试

与传统 Java EE 开发相比,依赖注入应该使您的代码对容器的依赖程度更低。组成您的应用程序的 POJO 应该可以在 JUnit 或 TestNG 测试中进行测试,使用new 运算符实例化对象,无需 Spring 或任何其他容器。您可以使用模拟对象 (结合其他有价值的测试技术)来单独测试您的代码。如果您遵循 Spring 的架构建议,那么代码库的干净分层和组件化将有助于更轻松的单元测试。例如,您可以通过存根或模拟 DAO 或存储库接口来测试服务层对象,而无需在运行单元测试时访问持久数据。

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

2.1。模拟对象

Spring 包含许多专用于模拟的包:

2.1.1。环境

org.springframework.mock.env包包含 EnvironmentPropertySource抽象的模拟实现(请参阅 Bean 定义配置文件PropertySource抽象)。 MockEnvironment并且MockPropertySource对于为依赖于环境特定属性的代码开发容器外测试很有用。

2.1.2. JNDI

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

包中的模拟 JNDI 支持从org.springframework.mock.jndiSpring Framework 5.2 开始正式弃用,转而支持来自第三方的完整解决方案,例如Simple-JNDI

2.1.3。小服务程序 API

org.springframework.mock.web包包含一组全面的 Servlet API 模拟对象,可用于测试 Web 上下文、控制器和过滤器。这些模拟对象的目标是与 Spring 的 Web MVC 框架一起使用,并且通常比动态模拟对象(例如EasyMock)或替代 Servlet API 模拟对象(例如MockObjects)更方便使用。

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

Spring MVC 测试框架建立在模拟 Servlet API 对象之上,为 Spring MVC 提供集成测试框架。请参阅MockMvc

2.1.4。Spring Web 反应式

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

两者都从相同的抽象基MockServerHttpRequestMockServerHttpResponse扩展为特定于服务器的实现,并与它们共享行为。例如,模拟请求一经创建便不可变,但您可以使用mutate()from 方法ServerHttpRequest创建修改后的实例。

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

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

2.2. 单元测试支持类

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

2.2.1。通用测试工具

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

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

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

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

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

AopTestUtils是 AOP 相关实用方法的集合。您可以使用这些方法来获取对隐藏在一个或多个 Spring 代理后面的底层目标对象的引用。例如,如果您使用 EasyMock 或 Mockito 等库将 bean 配置为动态模拟,并且模拟被包装在 Spring 代理中,您可能需要直接访问底层模拟以对其配置期望并执行验证. 有关 Spring 的核心 AOP 实用程序,请参阅AopUtilsAopProxyUtils

2.2.2。Spring MVC 测试工具

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

单元测试 Spring MVC 控制器
要将 Spring MVCController类作为 POJO 进行单元测试,请使用Spring 的 Servlet API 模拟ModelAndViewAssert中的 with MockHttpServletRequest、等组合。 要结合 Spring MVC 的配置对 Spring MVC 和 REST类进行彻底的集成测试,请改用Spring MVC 测试框架MockHttpSessionControllerWebApplicationContext

3. 集成测试

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

3.1。概述

能够执行一些集成测试而无需部署到您的应用程序服务器或连接到其他企业基础架构,这一点很重要。这样做可以让您测试以下内容:

  • Spring IoC 容器上下文的正确连接。

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

spring-testSpring Framework 为模块中的集成测试提供了一流的支持 。实际 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 配置元数据的资源位置数组(通常在类路径中)或用于配置应用程序的组件类数组。web.xml这些位置或类与生产部署的其他配置文件中指定的位置或类相同或相似。

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

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

3.2.2. 测试夹具的依赖注入

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

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

  • Spring 配置:基本上,与 HibernateTitleRepositorybean 配置相关的所有内容都正确且存在吗?

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

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

请参阅使用TestContext 框架对测试装置进行依赖注入 。

3.2.3。事务管理

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

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

@Commit如果您希望提交事务(不寻常,但在您希望特定测试填充或修改数据库时偶尔有用),您可以使用注释告诉 TestContext 框架导致事务提交而不是回滚 。

请参阅使用TestContext 框架进行事务管理。

3.2.4。集成测试的支持类

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

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

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

此外,您可能希望使用特定于您的项目的实例变量和方法创建自己的自定义应用程序超类。

请参阅TestContext 框架的支持类。

3.3. JDBC 测试支持

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

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

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

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

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

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

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

3.4. 注释

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

3.4.1。Spring 测试注解

Spring 框架提供了以下一组 Spring 特定的注释,您可以在单元和集成测试中与 TestContext 框架一起使用它们。有关更多信息,请参阅相应的 javadoc,包括默认属性值、属性别名和其他详细信息。

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

@BootstrapWith

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

@ContextConfiguration

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

资源位置通常是位于类路径中的 XML 配置文件或 Groovy 脚本,而组件类通常是@Configuration类。但是,资源位置也可以指文件系统中的文件和脚本,组件类可以是@Component类、@Service类等。有关详细信息,请参阅 组件类

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

java
@ContextConfiguration("/test-config.xml") (1)
class XmlApplicationContextTests {
    // class body...
}
1 引用 XML 文件。
科特林
@ContextConfiguration("/test-config.xml") (1)
class XmlApplicationContextTests {
    // class body...
}
1 引用 XML 文件。

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

java
@ContextConfiguration(classes = TestConfig.class) (1)
class ConfigClassApplicationContextTests {
    // class body...
}
1 引用一个类。
科特林
@ContextConfiguration(classes = [TestConfig::class]) (1)
class ConfigClassApplicationContextTests {
    // class body...
}
1 引用一个类。

作为声明资源位置或组件类的替代或补充,您可以使用@ContextConfiguration声明ApplicationContextInitializer类。以下示例显示了这种情况:

java
@ContextConfiguration(initializers = CustomContextIntializer.class) (1)
class ContextInitializerTests {
    // class body...
}
1 声明一个初始化类。
科特林
@ContextConfiguration(initializers = [CustomContextIntializer::class]) (1)
class ContextInitializerTests {
    // class body...
}
1 声明一个初始化类。

您也可以选择使用@ContextConfiguration来声明ContextLoader策略。但是请注意,您通常不需要显式配置加载器,因为默认加载器支持initializersresourcelocations或 component classes

以下示例同时使用位置和加载程序:

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

有关详细信息,请参阅上下文管理@Nested测试类配置@ContextConfiguration javadocs。

@WebAppConfiguration

@WebAppConfiguration是一个类级别的注释,您可以使用它来声明 ApplicationContext为集成测试加载的应该是WebApplicationContext. 测试类的存在@WebAppConfiguration确保 a WebApplicationContext被加载用于测试,使用 Web 应用程序根路径的默认值 "file:src/main/webapp"(即资源基路径)。资源基本路径在幕后用于创建一个 MockServletContext,作为ServletContext测试的 WebApplicationContext.

以下示例显示了如何使用@WebAppConfiguration注解:

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

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

java
@ContextConfiguration
@WebAppConfiguration("classpath:test-web-resources") (1)
class WebAppTests {
    // class body...
}
1 指定类路径资源。
科特林
@ContextConfiguration
@WebAppConfiguration("classpath:test-web-resources") (1)
class WebAppTests {
    // class body...
}
1 指定类路径资源。

请注意,@WebAppConfiguration必须在 @ContextConfiguration单个测试类或测试类层次结构中与 结合使用。有关详细信息,请参阅 @WebAppConfiguration javadoc。

@ContextHierarchy

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

java
@ContextHierarchy({
    @ContextConfiguration("/parent-config.xml"),
    @ContextConfiguration("/child-config.xml")
})
class ContextHierarchyTests {
    // class body...
}
科特林
@ContextHierarchy(
    ContextConfiguration("/parent-config.xml"),
    ContextConfiguration("/child-config.xml"))
class ContextHierarchyTests {
    // class body...
}
java
@WebAppConfiguration
@ContextHierarchy({
    @ContextConfiguration(classes = AppConfig.class),
    @ContextConfiguration(classes = WebConfig.class)
})
class WebIntegrationTests {
    // class body...
}
科特林
@WebAppConfiguration
@ContextHierarchy(
        ContextConfiguration(classes = [AppConfig::class]),
        ContextConfiguration(classes = [WebConfig::class]))
class WebIntegrationTests {
    // class body...
}

如果您需要在测试类层次结构中合并或覆盖上下文层次结构的给定级别的配置,则必须通过为类层次结构中每个相应级别的name属性提供相同的值来明确命名该级别。@ContextConfiguration有关更多示例,请参阅上下文层次结构@ContextHierarchyjavadoc。

@ActiveProfiles

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

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

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

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

java
@ContextConfiguration
@ActiveProfiles({"dev", "integration"}) (1)
class DeveloperIntegrationTests {
    // class body...
}
1 指示devintegration配置文件应该处于活动状态。
科特林
@ContextConfiguration
@ActiveProfiles(["dev", "integration"]) (1)
class DeveloperIntegrationTests {
    // class body...
}
1 指示devintegration配置文件应该处于活动状态。
@ActiveProfiles提供对继承超类声明的活动 bean 定义配置文件和默认封闭类的支持。您还可以通过实现自定义 ActiveProfilesResolverresolver使用@ActiveProfiles.

有关示例和更多详细信息,请参阅使用环境配置文件的上下文配置@Nested测试类配置@ActiveProfilesjavadoc。

@TestPropertySource

@TestPropertySource是一个类级别的注释,您可以使用它来配置属性文件和内联属性的位置,这些属性将被添加到 为集成测试加载的集合PropertySources中。EnvironmentApplicationContext

以下示例演示如何从类路径声明属性文件:

java
@ContextConfiguration
@TestPropertySource("/test.properties") (1)
class MyIntegrationTests {
    // class body...
}
1 test.properties从类路径的根中获取属性。
科特林
@ContextConfiguration
@TestPropertySource("/test.properties") (1)
class MyIntegrationTests {
    // class body...
}
1 test.properties从类路径的根中获取属性。

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

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

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

@DynamicPropertySource

@DynamicPropertySource是一个方法级别的注释,您可以使用它来注册 要添加到集合中的动态属性,以便为集成测试加载。当您预先不知道属性的值时,动态属性很有用 - 例如,如果属性由外部资源管理,例如由 Testcontainers项目管理的容器。PropertySourcesEnvironmentApplicationContext

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

java
@ContextConfiguration
class MyIntegrationTests {

    static MyExternalServer server = // ...

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

    // tests ...
}
1 用注释static方法@DynamicPropertySource
2 接受 aDynamicPropertyRegistry作为参数。
3 注册一个动态server.port属性,以便从服务器中延迟检索。
科特林
@ContextConfiguration
class MyIntegrationTests {

    companion object {

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

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

    // tests ...
}
1 用注释static方法@DynamicPropertySource
2 接受 aDynamicPropertyRegistry作为参数。
3 注册一个动态server.port属性,以便从服务器中延迟检索。

有关详细信息,请参阅使用动态属性源的上下文配置

@DirtiesContext

@DirtiesContext表示底层 SpringApplicationContext在执行测试期间已被弄脏(即测试以某种方式修改或破坏了它 - 例如,通过更改单例 bean 的状态)并且应该关闭。当应用程序上下文被标记为脏时,它会从测试框架的缓存中删除并关闭。因此,为任何需要具有相同配置元数据的上下文的后续测试重新构建底层 Spring 容器。

您可以@DirtiesContext在同一类或类层次结构中同时用作类级别和方法级别的注解。在这种情况下,ApplicationContext在任何此类带注释的方法之前或之后以及当前测试类之前或之后将 标记为脏,具体取决于配置的methodModeand classMode

以下示例说明了各种配置方案何时会弄脏上下文:

  • 在当前测试类之前,当在类模式设置为 BEFORE_CLASS.

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

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

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

    java
    @DirtiesContext(classMode = AFTER_EACH_TEST_METHOD) (1)
    class ContextDirtyingTests {
        // some tests that result in the Spring container being dirtied
    }
    1 在每个测试方法之后弄脏上下文。
    科特林
    @DirtiesContext(classMode = AFTER_EACH_TEST_METHOD) (1)
    class ContextDirtyingTests {
        // some tests that result in the Spring container being dirtied
    }
    1 在每个测试方法之后弄脏上下文。
  • 在当前测试之前,在方法模式设置为 BEFORE_METHOD.

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

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

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

java
@ContextHierarchy({
    @ContextConfiguration("/parent-config.xml"),
    @ContextConfiguration("/child-config.xml")
})
class BaseTests {
    // class body...
}

class ExtendedTests extends BaseTests {

    @Test
    @DirtiesContext(hierarchyMode = CURRENT_LEVEL) (1)
    void test() {
        // some logic that results in the child context being dirtied
    }
}
1 使用当前级别的算法。
科特林
@ContextHierarchy(
    ContextConfiguration("/parent-config.xml"),
    ContextConfiguration("/child-config.xml"))
open class BaseTests {
    // class body...
}

class ExtendedTests : BaseTests() {

    @Test
    @DirtiesContext(hierarchyMode = CURRENT_LEVEL) (1)
    fun test() {
        // some logic that results in the child context being dirtied
    }
}
1 使用当前级别的算法。

有关EXHAUSTIVECURRENT_LEVEL算法的更多详细信息,请参阅 DirtiesContext.HierarchyMode javadoc。

@TestExecutionListeners

@TestExecutionListeners定义类级元数据,用于配置 TestExecutionListener应向 TestContextManager. 通常,@TestExecutionListeners与 结合使用 @ContextConfiguration

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

java
@ContextConfiguration
@TestExecutionListeners({CustomTestExecutionListener.class, AnotherTestExecutionListener.class}) (1)
class CustomTestExecutionListenerTests {
    // class body...
}
1 注册两个TestExecutionListener实现。
科特林
@ContextConfiguration
@TestExecutionListeners(CustomTestExecutionListener::class, AnotherTestExecutionListener::class) (1)
class CustomTestExecutionListenerTests {
    // class body...
}
1 注册两个TestExecutionListener实现。

默认情况下,@TestExecutionListeners支持从超类或封闭类继承侦听器。有关示例和更多详细信息,请参阅 @Nested测试类配置@TestExecutionListeners javadoc 。

@RecordApplicationEvents

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

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

有关示例和更多详细信息,请参阅应用程序事件@RecordApplicationEvents javadoc 。

@Commit

@Commit指示事务测试方法的事务应在测试方法完成后提交。您可以将@Commit其用作@Rollback(false)更明确地传达代码意图的直接替代品。类似于@Rollback,@Commit也可以声明为类级别或方法级别的注解。

以下示例显示了如何使用@Commit注解:

java
@Commit (1)
@Test
void testProcessWithoutRollback() {
    // ...
}
1 将测试结果提交到数据库。
科特林
@Commit (1)
@Test
fun testProcessWithoutRollback() {
    // ...
}
1 将测试结果提交到数据库。
@Rollback

@Rollback指示事务测试方法的事务是否应在测试方法完成后回滚。如果true,则事务回滚。否则,事务将被提交(另请参见 参考资料 @Commit)。Spring TestContext Framework 中集成测试的回滚默认为true即使@Rollback未显式声明。

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

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

java
@Rollback(false) (1)
@Test
void testProcessWithoutRollback() {
    // ...
}
1 不要回滚结果。
科特林
@Rollback(false) (1)
@Test
fun testProcessWithoutRollback() {
    // ...
}
1 不要回滚结果。
@BeforeTransaction

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

以下示例显示了如何使用@BeforeTransaction注解:

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

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

java
@AfterTransaction (1)
void afterTransaction() {
    // logic to be run after a transaction has ended
}
1 在事务后运行此方法。
科特林
@AfterTransaction (1)
fun afterTransaction() {
    // logic to be run after a transaction has ended
}
1 在事务后运行此方法。
@Sql

@Sql用于注释测试类或测试方法以配置 SQL 脚本以在集成测试期间针对给定数据库运行。以下示例显示了如何使用它:

java
@Test
@Sql({"/test-schema.sql", "/test-user-data.sql"}) (1)
void userTest() {
    // run code that relies on the test schema and test data
}
1 为此测试运行两个脚本。
科特林
@Test
@Sql("/test-schema.sql", "/test-user-data.sql") (1)
fun userTest() {
    // run code that relies on the test schema and test data
}
1 为此测试运行两个脚本。

有关详细信息,请参阅使用 @Sql以声明方式执行 SQL 脚本。

@SqlConfig

@SqlConfig定义用于确定如何解析和运行使用@Sql注释配置的 SQL 脚本的元数据。以下示例显示了如何使用它:

java
@Test
@Sql(
    scripts = "/test-user-data.sql",
    config = @SqlConfig(commentPrefix = "`", separator = "@@") (1)
)
void userTest() {
    // run code that relies on the test data
}
1 在 SQL 脚本中设置注释前缀和分隔符。
科特林
@Test
@Sql("/test-user-data.sql", config = SqlConfig(commentPrefix = "`", separator = "@@")) (1)
fun userTest() {
    // run code that relies on the test data
}
1 在 SQL 脚本中设置注释前缀和分隔符。
@SqlMergeMode

@SqlMergeMode用于注释测试类或测试方法,以配置方法级@Sql声明是否与类级声明合并@Sql。如果 @SqlMergeMode未在测试类或测试方法上声明,则OVERRIDE默认使用合并模式。使用该OVERRIDE模式,方法级别的@Sql声明将有效地覆盖类级别的@Sql声明。

请注意,方法级别的@SqlMergeMode声明会覆盖类级别的声明。

以下示例显示了如何@SqlMergeMode在类级别使用。

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

    @Test
    @Sql("/user-test-data-001.sql")
    void standardUserProfile() {
        // run code that relies on test data set 001
    }
}
1 为类中的所有测试方法设置@Sql合并模式。MERGE
科特林
@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
科特林
@SpringJUnitConfig(TestConfig::class)
@Sql("/test-schema.sql")
class UserTests {

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

@SqlGroup是聚合多个注解的容器@Sql注解。您可以使用@SqlGroup本机声明多个嵌套@Sql注解,也可以将它与 Java 8 对可重复注解的支持结合使用,其中@Sql可以在同一个类或方法上多次声明,隐式生成此容器注解。以下示例显示了如何声明 SQL 组:

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 脚本。
科特林
@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 Framework 的所有配置都支持以下注解,具有标准语义。请注意,这些注释不是特定于测试的,可以在 Spring 框架中的任何地方使用。

  • @Autowired

  • @Qualifier

  • @Value

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

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

  • @Inject(javax.inject) 如果存在 JSR-330

  • @Named(javax.inject) 如果存在 JSR-330

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

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

  • @Required

  • @Transactional(org.springframework.transaction.annotation) 具有有限的属性支持

JSR-250 生命周期注解

@PostConstruct在 Spring TestContext Framework 中,您可以@PreDestroyApplicationContext. 但是,这些生命周期注释在实际测试类中的用途有限。

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

3.4.3. Spring JUnit 4 测试注解

仅当与 SpringRunnerSpring 的 JUnit 4 规则Spring 的 JUnit 4 支持类一起使用时,才支持以下注释:

@IfProfileValue

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

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

以下示例显示了具有@IfProfileValue注释的测试:

java
@IfProfileValue(name="java.vendor", value="Oracle Corporation") (1)
@Test
public void testProcessWhichRunsOnlyOnOracleJvm() {
    // some logic that should run only on Java VMs from Oracle Corporation
}
1 仅当 Java 供应商为“Oracle Corporation”时才运行此测试。
科特林
@IfProfileValue(name="java.vendor", value="Oracle Corporation") (1)
@Test
fun testProcessWhichRunsOnlyOnOracleJvm() {
    // some logic that should run only on Java VMs from Oracle Corporation
}
1 仅当 Java 供应商为“Oracle Corporation”时才运行此测试。

或者,您可以配置@IfProfileValue一个列表values(带有OR 语义),以在 JUnit 4 环境中实现对测试组的类似 TestNG 的支持。考虑以下示例:

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 为单元测试和集成测试运行此测试。
科特林
@IfProfileValue(name="test-groups", values=["unit-tests", "integration-tests"]) (1)
@Test
fun testProcessWhichRunsForUnitOrIntegrationTestGroups() {
    // some logic that should run only for unit and integration test groups
}
1 为单元测试和集成测试运行此测试。
@ProfileValueSourceConfiguration

@ProfileValueSourceConfiguration是一个类级别的注释,它指定在检索通过注释ProfileValueSource配置的配置文件值时要使用 的类型。@IfProfileValue如果@ProfileValueSourceConfiguration没有为测试声明,SystemProfileValueSource默认使用。下面的例子展示了如何使用@ProfileValueSourceConfiguration

java
@ProfileValueSourceConfiguration(CustomProfileValueSource.class) (1)
public class CustomProfileValueSourceTests {
    // class body...
}
1 使用自定义配置文件值源。
科特林
@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 将测试的时间段设置为一秒。
科特林
@Timed(millis = 1000) (1)
fun testProcessWithOneSecondTimeout() {
    // some logic that should not take longer than 1 second to run
}
1 将测试的时间段设置为一秒。

Spring 的注解与 JUnit 4 的 支持@Timed具有不同的语义。具体来说,@Test(timeout=…​)由于 JUnit 4 处理测试执行超时的方式(即通过在单独 的 . 另一方面,Spring不会先发制人地使测试失败,而是在失败之前等待测试完成。Thread@Test(timeout=…​)@Timed

@Repeat

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

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

java
@Repeat(10) (1)
@Test
public void testProcessRepeatedly() {
    // ...
}
1 重复这个测试十次。
科特林
@Repeat(10) (1)
@Test
fun testProcessRepeatedly() {
    // ...
}
1 重复这个测试十次。

3.4.4。Spring JUnit Jupiter 测试注解

SpringExtension与 JUnit Jupiter(即 JUnit 5 中的编程模型)结合使用时,支持以下注解 :

@SpringJUnitConfig

@SpringJUnitConfig是一个组合注释,它结合 @ExtendWith(SpringExtension.class)了 JUnit Jupiter 和@ContextConfigurationSpring TestContext Framework。它可以在课堂级别用作@ContextConfiguration. 关于配置选项,@ContextConfiguration和之间的唯一区别是@SpringJUnitConfig组件类可以使用.value@SpringJUnitConfig

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

java
@SpringJUnitConfig(TestConfig.class) (1)
class ConfigurationClassJUnitJupiterSpringTests {
    // class body...
}
1 指定配置类。
科特林
@SpringJUnitConfig(TestConfig::class) (1)
class ConfigurationClassJUnitJupiterSpringTests {
    // class body...
}
1 指定配置类。

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

java
@SpringJUnitConfig(locations = "/test-config.xml") (1)
class XmlJUnitJupiterSpringTests {
    // class body...
}
1 指定配置文件的位置。
科特林
@SpringJUnitConfig(locations = ["/test-config.xml"]) (1)
class XmlJUnitJupiterSpringTests {
    // class body...
}
1 指定配置文件的位置。

有关详细信息,请参阅上下文管理以及 javadoc 。@SpringJUnitConfig@ContextConfiguration

@SpringJUnitWebConfig

@SpringJUnitWebConfig是一个组合注释,它结合 @ExtendWith(SpringExtension.class)了 JUnit Jupiter@ContextConfiguration@WebAppConfigurationSpring TestContext Framework。@ContextConfiguration您可以在课堂级别使用它作为和的替代品@WebAppConfiguration@ContextConfiguration 关于配置选项,和的唯一区别@SpringJUnitWebConfig是可以使用 中的 value属性来声明组件类@SpringJUnitWebConfig。此外,您只能通过使用 中的属性 来覆盖value 属性。@WebAppConfigurationresourcePath@SpringJUnitWebConfig

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

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

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

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

有关更多详细信息,请参阅上下文管理以及用于 @SpringJUnitWebConfig@ContextConfiguration和 的 javadoc @WebAppConfiguration

@TestConstructor

@TestConstructor是一个类型级别的注释,用于配置测试类构造函数的参数如何从测试的 ApplicationContext.

如果@TestConstructor在测试类中不存在或元存在,则将使用默认的测试构造函数自动装配模式。有关如何更改默认模式的详细信息,请参阅下面的提示。但是请注意,@Autowired构造函数的局部声明优先于@TestConstructor默认模式和默认模式。

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

可以通过将 JVM 系统属性设置为 来更改默认的测试构造函数自动装配模式。或者,可以通过该 机制设置默认模式。spring.test.constructor.autowire.modeallSpringProperties

从 Spring Framework 5.3 开始,默认模式也可以配置为 JUnit Platform 配置参数

如果spring.test.constructor.autowire.mode未设置该属性,则不会自动自动装配测试类构造函数。

从 Spring Framework 5.2 开始,@TestConstructor仅支持SpringExtension与 JUnit Jupiter 一起使用。请注意,SpringExtension通常会自动为您注册 - 例如,当使用 Spring Boot Test 中的@SpringJUnitConfig@SpringJUnitWebConfig/或各种与测试相关的注释等注释时。
@NestedTestConfiguration

@NestedTestConfiguration是一个类型级别的注解,用于配置如何在内部测试类的封闭类层次结构中处理 Spring 测试配置注解。

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

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

默认的封闭配置继承模式INHERIT,但可以通过将spring.test.enclosing.configurationJVM 系统属性 设置为 来更改它OVERRIDE。或者,可以通过该 SpringProperties机制设置默认模式。

Spring TestContext 框架尊重@NestedTestConfiguration以下注释的语义。

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

有关示例和更多详细信息,请参阅@Nested测试类配置。

@EnabledIf

@EnabledIfexpression用于表示带注释的 JUnit Jupiter 测试类或测试方法已启用,如果提供的评估结果为,则应运行true。具体来说,如果表达式的计算结果等于Boolean.TRUEString等于true (忽略大小写),则启用测试。在类级别应用时,该类中的所有测试方法也会默认自动启用。

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

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

  • Spring 中可用属性的占位符Environment。例如:@EnabledIf("${smoke.tests.enabled}")

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

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

您可以@EnabledIf用作元注释来创建自定义组合注释。例如,您可以@EnabledOnMac按如下方式创建自定义注解:

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

@EnabledOnMac仅作为可能的示例。如果您有确切的用例,请使用 JUnit Jupiter 中的内置@EnabledOnOs(MAC)支持。

从 JUnit 5.7 开始,JUnit Jupiter 也有一个名为@EnabledIf. 因此,如果您希望使用 Spring 的@EnabledIf支持,请确保从正确的包中导入注释类型。

@DisabledIf

@DisabledIfexpression用于表示带注释的 JUnit Jupiter 测试类或测试方法已禁用,如果提供的评估结果为 ,则不应运行true。具体来说,如果表达式的计算结果等于Boolean.TRUEString等于true(忽略大小写),则禁用测试。在类级别应用时,该类中的所有测试方法也会自动禁用。

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

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

  • Spring 中可用属性的占位符Environment。例如:@DisabledIf("${smoke.tests.disabled}")

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

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

您可以@DisabledIf用作元注释来创建自定义组合注释。例如,您可以@DisabledOnMac按如下方式创建自定义注解:

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

@DisabledOnMac仅作为可能的示例。如果您有确切的用例,请使用 JUnit Jupiter 中的内置@DisabledOnOs(MAC)支持。

从 JUnit 5.7 开始,JUnit Jupiter 也有一个名为@DisabledIf. 因此,如果您希望使用 Spring 的@DisabledIf支持,请确保从正确的包中导入注释类型。

3.4.5。用于测试的元注释支持

您可以将大多数与测试相关的注释用作 元注释来创建自定义组合注释并减少测试套件中的配置重复。

  • @BootstrapWith

  • @ContextConfiguration

  • @ContextHierarchy

  • @ActiveProfiles

  • @TestPropertySource

  • @DirtiesContext

  • @WebAppConfiguration

  • @TestExecutionListeners

  • @Transactional

  • @BeforeTransaction

  • @AfterTransaction

  • @Commit

  • @Rollback

  • @Sql

  • @SqlConfig

  • @SqlMergeMode

  • @SqlGroup

  • @Repeat (仅在 JUnit 4 上支持)

  • @Timed (仅在 JUnit 4 上支持)

  • @IfProfileValue (仅在 JUnit 4 上支持)

  • @ProfileValueSourceConfiguration (仅在 JUnit 4 上支持)

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

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

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

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

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

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

考虑以下示例:

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

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

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

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

java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
public @interface TransactionalDevTestConfig { }
科特林
@Target(AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
@ActiveProfiles("dev")
@Transactional
annotation class TransactionalDevTestConfig { }

然后我们可以使用我们的自定义@TransactionalDevTestConfig注解来简化单个基于 JUnit 4 的测试类的配置,如下所示:

java
@RunWith(SpringRunner.class)
@TransactionalDevTestConfig
public class OrderRepositoryTests { }

@RunWith(SpringRunner.class)
@TransactionalDevTestConfig
public class UserRepositoryTests { }
科特林
@RunWith(SpringRunner::class)
@TransactionalDevTestConfig
class OrderRepositoryTests

@RunWith(SpringRunner::class)
@TransactionalDevTestConfig
class UserRepositoryTests

如果我们使用 JUnit Jupiter 编写测试,我们可以进一步减少代码重复,因为 JUnit 5 中的注解也可以用作元注解。考虑以下示例:

java
@ExtendWith(SpringExtension.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
class OrderRepositoryTests { }

@ExtendWith(SpringExtension.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
class UserRepositoryTests { }
科特林
@ExtendWith(SpringExtension::class)
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
@ActiveProfiles("dev")
@Transactional
class OrderRepositoryTests { }

@ExtendWith(SpringExtension::class)
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
@ActiveProfiles("dev")
@Transactional
class UserRepositoryTests { }

如果我们发现我们在基于 JUnit Jupiter 的测试套件中重复上述配置,我们可以通过引入一个自定义组合注解来减少重复,该注解集中了 Spring 和 JUnit Jupiter 的通用测试配置,如下所示:

java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(SpringExtension.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
public @interface TransactionalDevTestConfig { }
科特林
@Target(AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@ExtendWith(SpringExtension::class)
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
@ActiveProfiles("dev")
@Transactional
annotation class TransactionalDevTestConfig { }

然后我们可以使用我们的自定义@TransactionalDevTestConfig注解来简化单个基于 JUnit Jupiter 的测试类的配置,如下所示:

java
@TransactionalDevTestConfig
class OrderRepositoryTests { }

@TransactionalDevTestConfig
class UserRepositoryTests { }
科特林
@TransactionalDevTestConfig
class OrderRepositoryTests { }

@TransactionalDevTestConfig
class UserRepositoryTests { }

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

java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Transactional
@Tag("integration-test") // org.junit.jupiter.api.Tag
@Test // org.junit.jupiter.api.Test
public @interface TransactionalIntegrationTest { }
科特林
@Target(AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@Transactional
@Tag("integration-test") // org.junit.jupiter.api.Tag
@Test // org.junit.jupiter.api.Test
annotation class TransactionalIntegrationTest { }

然后我们可以使用我们的自定义@TransactionalIntegrationTest注解来简化单个基于 JUnit Jupiter 的测试方法的配置,如下所示:

java
@TransactionalIntegrationTest
void saveOrder() { }

@TransactionalIntegrationTest
void deleteOrder() { }
科特林
@TransactionalIntegrationTest
fun saveOrder() { }

@TransactionalIntegrationTest
fun deleteOrder() { }

有关更多详细信息,请参阅 Spring Annotation Programming Model wiki 页面。

3.5. Spring TestContext 框架

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

除了通用测试基础架构之外,TestContext 框架还为 JUnit 4、JUnit Jupiter (AKA JUnit 5) 和 TestNG 提供了明确的支持。对于 JUnit 4 和 TestNG,Spring 提供了abstract支持类。此外,Spring为 JUnit 4 和JUnit Jupiter 提供了自定义 JUnitRunner和自定义 JUnit ,让您可以编写所谓的 POJO 测试类。POJO 测试类不需要扩展特定的类层次结构,例如支持类。RulesExtensionabstract

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

3.5.1。关键抽象

框架的核心由TestContextManager类和 TestContext, TestExecutionListener, 和SmartContextLoader接口组成。A TestContextManager为每个测试类创建(例如,用于在 JUnit Jupiter 中的单个测试类中执行所有测试方法)。反过来TestContextManager,管理一个TestContext保存当前测试上下文的 a。随着测试的 进行,它TestContextManager还会更新状态TestContext并委托给TestExecutionListener实现,后者通过提供依赖注入、管理事务等来检测实际的测试执行。A SmartContextLoader负责为ApplicationContext给定的测试类加载一个。有关各种实现的更多信息和示例,请参阅javadoc和 Spring 测试套件。

TestContext

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

TestContextManager

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

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

  • 测试实例后处理。

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

  • 紧接在执行测试方法之前但在测试设置之后。

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

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

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

TestExecutionListener

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

上下文加载器

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

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

Spring 提供了以下实现:

  • DelegatingSmartContextLoader: 两个默认加载器之一,它在内部委托AnnotationConfigContextLoadera 、 aGenericXmlContextLoader或 a GenericGroovyXmlContextLoader,这取决于为测试类声明的配置或默认位置或默认配置类的存在。仅当 Groovy 在类路径上时才启用 Groovy 支持。

  • WebDelegatingSmartContextLoader: 两个默认加载器之一,它在内部委托AnnotationConfigWebContextLoadera 、 aGenericXmlWebContextLoader或 a GenericGroovyXmlWebContextLoader,这取决于为测试类声明的配置或默认位置或默认配置类的存在。仅当测试类中存在网络ContextLoader时才使用网络。@WebAppConfiguration仅当 Groovy 在类路径上时才启用 Groovy 支持。

  • AnnotationConfigContextLoaderApplicationContext:从组件类中加载标准。

  • AnnotationConfigWebContextLoaderWebApplicationContext:从组件类中加载一个。

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

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

  • GenericXmlContextLoaderApplicationContext:从 XML 资源位置加载标准。

  • GenericXmlWebContextLoaderWebApplicationContext:从 XML 资源位置加载一个。

3.5.2. 引导 TestContext 框架

Spring TestContext Framework 内部的默认配置足以满足所有常见用例。但是,有时开发团队或第三方框架想要更改默认值ContextLoader、实现自定义TestContextContextCache、增加默认集 ContextCustomizerFactoryTestExecutionListener实现等等。对于 TestContext 框架如何运行的这种低级控制,Spring 提供了一种引导策略。

TestContextBootstrapper定义用于引导 TestContext 框架的 SPI。A TestContextBootstrapper用于TestContextManager加载 TestExecutionListener当前测试的实现并构建 TestContext它管理的测试。@BootstrapWith您可以直接使用或作为元注释为测试类(或测试类层次结构)配置自定义引导策略。如果引导程序未通过 using 显式配置, @BootstrapWith则使用DefaultTestContextBootstrapperWebTestContextBootstrapper,具体取决于@WebAppConfiguration.

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

3.5.3. TestExecutionListener配置

Spring 提供了以下TestExecutionListener默认注册的实现,完全按照以下顺序:

  • ServletTestExecutionListener: 为 WebApplicationContext.

  • DirtiesContextBeforeModesTestExecutionListener:处理@DirtiesContext “之前”模式的注释。

  • ApplicationEventsTestExecutionListener: 提供对 ApplicationEvents.

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

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

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

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

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

注册TestExecutionListener实现

您可以使用注解TestExecutionListener为测试类及其子类注册实现。 有关详细信息和示例,@TestExecutionListeners请参阅 注解支持和 javadoc 。@TestExecutionListeners

自动发现默认TestExecutionListener实现

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

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

排序TestExecutionListener实现

当 TestContext 框架通过上述TestExecutionListener机制发现默认实现时,实例化的侦听器使用 Spring 进行排序,Spring 遵循Spring 的接口和 注解进行排序。Spring 提供的所有默认 实现都使用适当的值实现。因此,第三方框架和开发人员应该确保他们的默认实现通过实现或声明以正确的顺序注册。有关分配给每个核心侦听器的值的详细信息,请参阅 核心默认实现的方法的 javadoc 。 SpringFactoriesLoaderAnnotationAwareOrderComparatorOrdered@OrderAbstractTestExecutionListenerTestExecutionListenerOrderedTestExecutionListenerOrdered@OrdergetOrder()TestExecutionListener

合并TestExecutionListener实现

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

java
@ContextConfiguration
@TestExecutionListeners({
    MyCustomTestExecutionListener.class,
    ServletTestExecutionListener.class,
    DirtiesContextBeforeModesTestExecutionListener.class,
    DependencyInjectionTestExecutionListener.class,
    DirtiesContextTestExecutionListener.class,
    TransactionalTestExecutionListener.class,
    SqlScriptsTestExecutionListener.class
})
class MyTest {
    // class body...
}
科特林
@ContextConfiguration
@TestExecutionListeners(
    MyCustomTestExecutionListener::class,
    ServletTestExecutionListener::class,
    DirtiesContextBeforeModesTestExecutionListener::class,
    DependencyInjectionTestExecutionListener::class,
    DirtiesContextTestExecutionListener::class,
    TransactionalTestExecutionListener::class,
    SqlScriptsTestExecutionListener::class
)
class MyTest {
    // class body...
}

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

为避免必须了解并重新声明所有默认侦听器,您可以将 的 mergeMode属性设置@TestExecutionListenersMergeMode.MERGE_WITH_DEFAULTSMERGE_WITH_DEFAULTS指示本地声明的侦听器应与默认侦听器合并。合并算法确保从列表中删除重复项,并确保合并的侦听器的结果集根据 的语义进行排序AnnotationAwareOrderComparator,如Ordering TestExecutionListenerImplementations中所述。如果侦听器实现Ordered或使用 注释@Order,它可以影响它与默认值合并的位置。否则,本地声明的侦听器会在合并时附加到默认侦听器列表中。

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

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

3.5.4。应用程序事件

从 Spring Framework 5.3.3 开始,TestContext 框架支持记录 在 中发布的 应用程序事件ApplicationContext,以便可以在测试中针对这些事件执行断言。在执行单个测试期间发布的所有事件都可以通过ApplicationEventsAPI 获得,它允许您将事件作为 java.util.Stream.

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

  • 确保您的测试类使用 @RecordApplicationEvents.

  • 确保ApplicationEventsTestExecutionListener已注册。但是请注意,这是默认注册的,只有在您具有不包含默认侦听器的ApplicationEventsTestExecutionListener自定义配置时才需要手动注册 。@TestExecutionListeners

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

以下测试类使用SpringExtensionJUnit Jupiter 和 AssertJ来断言在调用 Spring 管理的组件中的方法时发布的应用程序事件的类型:

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

    @Autowired
    OrderService orderService;

    @Autowired
    ApplicationEvents events; (2)

    @Test
    void submitOrder() {
        // Invoke method in OrderService that publishes an event
        orderService.submitOrder(new Order(/* ... */));
        // Verify that an OrderSubmitted event was published
        long numEvents = events.stream(OrderSubmitted.class).count(); (3)
        assertThat(numEvents).isEqualTo(1);
    }
}
1 用 注释测试类@RecordApplicationEvents
2 ApplicationEvents为当前测试注入实例。
3 使用ApplicationEventsAPI 计算OrderSubmitted发布的事件数量。
科特林
@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发布的事件数量。

有关API的更多详细信息,请参阅 ApplicationEvents javadoc 。ApplicationEvents

3.5.5。测试执行事件

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

  • BeforeTestClassEvent

  • PrepareTestInstanceEvent

  • BeforeTestMethodEvent

  • BeforeTestExecutionEvent

  • AfterTestExecutionEvent

  • AfterTestMethodEvent

  • AfterTestClassEvent

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

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

因此, a在被另一个 加载BeforeTestClassEvent之前不会发布 。例如,在注册了默认实现集后, 不会为使用特定测试的第一个测试类发布 a ,但为自上下文以来使用相同测试的同一测试套件中的任何后续测试类发布 a将在后续测试类运行时已经加载(只要上下文尚未从via 或 max-size eviction 策略中删除)。ApplicationContextTestExecutionListenerTestExecutionListenerBeforeTestClassEventApplicationContextBeforeTestClassEvent ApplicationContextContextCache@DirtiesContext

如果您希望确保BeforeTestClassEvent始终为每个测试类发布 a ,则需要 在回调中注册TestExecutionListener加载 的a ,并且必须 .ApplicationContextbeforeTestClassTestExecutionListenerEventPublishingTestExecutionListener

同样,如果用于在给定测试类中的最后一个测试方法之后从上下文缓存@DirtiesContext中删除, 则不会为该测试类发布 。ApplicationContextAfterTestClassEvent

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

  • @BeforeTestClass

  • @PrepareTestInstance

  • @BeforeTestMethod

  • @BeforeTestExecution

  • @AfterTestExecution

  • @AfterTestMethod

  • @AfterTestClass

异常处理

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

异步侦听器

如果希望特定的测试执行事件监听器异步处理事件,可以使用 Spring 的常规 @Async支持。有关更多详细信息,请参阅类级别的 javadoc 以获取 @EventListener.

3.5.6。上下文管理

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

@Autowired 应用程序上下文

作为实现接口的替代方法,您可以通过字段或 setter 方法上ApplicationContextAware的注释为测试类注入应用程序上下文,如以下示例所示:@Autowired

java
@SpringJUnitConfig
class MyTest {

    @Autowired (1)
    ApplicationContext applicationContext;

    // class body...
}
1 注入ApplicationContext.
科特林
@SpringJUnitConfig
class MyTest {

    @Autowired (1)
    lateinit var applicationContext: ApplicationContext

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

同样,如果您的测试被配置为加载一个WebApplicationContext,您可以将 Web 应用程序上下文注入到您的测试中,如下所示:

java
@SpringJUnitWebConfig (1)
class MyWebAppTest {

    @Autowired (2)
    WebApplicationContext wac;

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

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

通过 using 进行的依赖注入@Autowired是由 提供的 DependencyInjectionTestExecutionListener,它是默认配置的(请参阅测试夹具的依赖注入)。

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

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

使用 XML 资源进行上下文配置

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

java
@ExtendWith(SpringExtension.class)
// ApplicationContext will be loaded from "/app-config.xml" and
// "/test-config.xml" in the root of the classpath
@ContextConfiguration(locations={"/app-config.xml", "/test-config.xml"}) (1)
class MyTest {
    // class body...
}
1 将位置属性设置为 XML 文件列表。
科特林
@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 将位置属性设置为 XML 文件列表。

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

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

如果您同时省略注释中的locationsvalue属性 @ContextConfiguration,TestContext 框架会尝试检测默认的 XML 资源位置。具体来说,GenericXmlContextLoader根据 GenericXmlWebContextLoader测试类的名称检测默认位置。如果您的类名为com.example.MyTestGenericXmlContextLoader则从 加载您的应用程序上下文"classpath:com/example/MyTest-context.xml"。以下示例显示了如何执行此操作:

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

ApplicationContext要使用使用 Groovy Bean 定义 DSL的 Groovy 脚本加载测试,您可以使用 包含 Groovy 脚本资源位置的数组来注释测试类@ContextConfiguration并配置locationsor属性。Groovy 脚本的资源查找语义与为XML 配置文件value描述的相同 。

启用 Groovy 脚本支持
ApplicationContext如果 Groovy 在类路径上,则自动启用 对使用 Groovy 脚本在 Spring TestContext Framework 中加载的支持。

以下示例显示了如何指定 Groovy 配置文件:

java
@ExtendWith(SpringExtension.class)
// ApplicationContext will be loaded from "/AppConfig.groovy" and
// "/TestConfig.groovy" in the root of the classpath
@ContextConfiguration({"/AppConfig.groovy", "/TestConfig.Groovy"}) (1)
class MyTest {
    // class body...
}
科特林
@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 配置文件的位置。

如果您在注释中同时省略locationsvalue属性@ContextConfiguration ,TestContext 框架会尝试检测默认的 Groovy 脚本。具体来说,GenericGroovyXmlContextLoader根据GenericGroovyXmlWebContextLoader 测试类的名称检测默认位置。如果您的类被命名 com.example.MyTest,Groovy 上下文加载器会从 "classpath:com/example/MyTestContext.groovy". 以下示例显示了如何使用默认值:

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

locations您可以使用 的orvalue属性同时声明 XML 配置文件和 Groovy 脚本@ContextConfiguration。如果配置的资源位置的路径以 结尾.xml,则使用 XmlBeanDefinitionReader. 否则,将使用 GroovyBeanDefinitionReader.

以下清单显示了如何在集成测试中结合两者:

java
@ExtendWith(SpringExtension.class)
// ApplicationContext will be loaded from
// "/app-config.xml" and "/TestConfig.groovy"
@ContextConfiguration({ "/app-config.xml", "/TestConfig.groovy" })
class MyTest {
    // class body...
}
科特林
@ExtendWith(SpringExtension::class)
// ApplicationContext will be loaded from
// "/app-config.xml" and "/TestConfig.groovy"
@ContextConfiguration("/app-config.xml", "/TestConfig.groovy")
class MyTest {
    // class body...
}
使用组件类的上下文配置

要使用组件类加载ApplicationContext测试(请参阅 基于 Java 的容器配置),您可以使用包含对组件类的引用的数组来注释测试类@ContextConfiguration并配置属性。classes以下示例显示了如何执行此操作:

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

术语“组件类”可以指以下任何一种:

  • 一个用 注释的类@Configuration

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

  • 使用注解进行注解的符合 JSR-330 的类javax.inject

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

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

@Configuration有关组件类的配置和 语义的更多信息,请参见 javadoc @Bean,特别注意@BeanLite Mode 的讨论。

如果您classes从注解中省略该属性@ContextConfiguration,TestContext 框架会尝试检测默认配置类的存在。具体来说,AnnotationConfigContextLoader检测AnnotationConfigWebContextLoader 测试static类中所有满足配置类实现要求的嵌套类,如 @Configurationjavadoc中所指定。请注意,配置类的名称是任意的。此外,如果需要,一个测试类可以包含多个static嵌套的配置类。在以下示例中,OrderServiceTest该类声明了一个static名为的嵌套配置类,该类Config自动用于加载ApplicationContext测试类:

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

    @Configuration
    static class Config {

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

    @Autowired
    OrderService orderService;

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

}
1 从嵌套Config类加载配置信息。
科特林
@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 管理的组件,反之亦然。

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

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

使用上下文初始化器的上下文配置

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

java
@ExtendWith(SpringExtension.class)
// ApplicationContext will be loaded from TestConfig
// and initialized by TestAppCtxInitializer
@ContextConfiguration(
    classes = TestConfig.class,
    initializers = TestAppCtxInitializer.class) (1)
class MyTest {
    // class body...
}
1 使用配置类和初始化程序指定配置。
科特林
@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 使用配置类和初始化程序指定配置。

您还可以完全省略 XML 配置文件、Groovy 脚本或组件类的@ContextConfiguration声明,而只声明 ApplicationContextInitializer类,然后这些类负责在上下文中注册 bean — 例如,通过以编程方式从 XML 文件或配置类中加载 bean 定义. 以下示例显示了如何执行此操作:

java
@ExtendWith(SpringExtension.class)
// ApplicationContext will be initialized by EntireAppInitializer
// which presumably registers beans in the context
@ContextConfiguration(initializers = EntireAppInitializer.class) (1)
class MyTest {
    // class body...
}
1 仅使用初始化程序指定配置。
科特林
@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. 这意味着测试类继承资源位置或组件类以及任何超类声明的上下文初始化器。具体来说,测试类的资源位置或组件类附加到由超类声明的资源位置或带注释的类的列表中。类似地,给定测试类的初始化器被添加到由测试超类定义的初始化器集中。因此,子类可以选择扩展资源位置、组件类或上下文初始化器。

如果inheritLocationsorinheritInitializers属性@ContextConfiguration 设置为false,则资源位置或组件类和上下文初始化器分别为测试类隐藏并有效替换超类定义的配置。

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

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

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

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

类似地,在下一个使用组件类的示例中,ApplicationContext for是按顺序从和类ExtendedTest加载的。因此,在 中定义的 Bean可以覆盖(即替换)在. 以下示例显示了一个类如何扩展另一个类并同时使用它自己的配置类和超类的配置类:BaseConfigExtendedConfigExtendedConfigBaseConfig

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

// ApplicationContext will be loaded from BaseConfig and ExtendedConfig
@SpringJUnitConfig(ExtendedConfig.class) (2)
class ExtendedTest extends BaseTest {
    // class body...
}
1 在超类中定义的配置类。
2 子类中定义的配置类。
科特林
// 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 子类中定义的配置类。

在下一个使用上下文初始化器的示例中,ApplicationContextfor ExtendedTest使用BaseInitializerand初始化ExtendedInitializer。但是请注意,调用初始化程序的顺序取决于它们是实现 Spring 的Ordered接口还是使用 Spring 的@Order注解或标准注解进行@Priority注解。下面的例子展示了一个类如何扩展另一个类并使用它自己的初始化器和超类的初始化器:

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

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

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

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

您可以@ActiveProfiles与 SPI 的任何实现一起使用SmartContextLoader ,但@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>
java
@ExtendWith(SpringExtension.class)
// ApplicationContext will be loaded from "classpath:/app-config.xml"
@ContextConfiguration("/app-config.xml")
@ActiveProfiles("dev")
class TransferServiceTest {

    @Autowired
    TransferService transferService;

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

    @Autowired
    lateinit var transferService: TransferService

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

运行时TransferServiceTest,它是从类路径根目录中的配置文件中ApplicationContext加载的 。app-config.xml如果您检查 app-config.xml,您可以看到该accountRepositorybean 对某个 bean 具有依赖关系 dataSource。但是,dataSource没有定义为顶级bean。而是 dataSource定义了 3 次:在production配置文件中、在dev配置文件中和在default配置文件中。

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

有时将 bean 分配给default配置文件很有用。仅当没有专门激活其他配置文件时,才会包含默认配置文件中的 Bean。您可以使用它来定义要在应用程序的默认状态下使用的“备用”bean。例如,您可以显式地为devproduction配置文件提供数据源,但是当这两个都不是活动的时,将内存中的数据源定义为默认值。

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

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

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

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

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

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

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

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

    @Autowired DataSource dataSource;

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

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

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

    @Autowired
    lateinit var dataSource: DataSource

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

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

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

    @Autowired
    TransferService transferService;

    @Test
    void testTransferService() {
        // test the transferService
    }
}
科特林
@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:dataSource通过使用 @Autowired.

  • StandaloneDataConfigdataSource:为适合开发人员测试的嵌入式数据库定义一个。

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

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

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

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

从 Spring Framework 5.3 开始,测试配置也可以从封闭类继承。有关详细信息,请参阅@Nested测试类配置
java
@SpringJUnitConfig({
        TransferServiceConfig.class,
        StandaloneDataConfig.class,
        JndiDataConfig.class,
        DefaultDataConfig.class})
@ActiveProfiles("dev")
abstract class AbstractIntegrationTest {
}
科特林
@SpringJUnitConfig(
        TransferServiceConfig::class,
        StandaloneDataConfig::class,
        JndiDataConfig::class,
        DefaultDataConfig::class)
@ActiveProfiles("dev")
abstract class AbstractIntegrationTest {
}
java
// "dev" profile inherited from superclass
class TransferServiceTest extends AbstractIntegrationTest {

    @Autowired
    TransferService transferService;

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

    @Autowired
    lateinit var transferService: TransferService

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

@ActiveProfiles还支持inheritProfiles可用于禁用活动配置文件继承的属性,如以下示例所示:

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

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

  • 当前的操作系统。

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

  • 某些环境变量的存在。

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

  • 其他顾虑。

要以编程方式解析活动 bean 定义配置文件,您可以实现自定义 并ActiveProfilesResolver使用. 有关详细信息,请参阅相应的 javadoc。以下示例演示了如何实现和注册自定义 :resolver@ActiveProfilesOperatingSystemActiveProfilesResolver

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

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

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

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

您可以@TestPropertySource与 SPI 的任何实现一起使用SmartContextLoader ,但@TestPropertySource不支持旧 ContextLoaderSPI 的实现。

通过. SmartContextLoader_ getPropertySourceLocations()_ getPropertySourceProperties()_ MergedContextConfiguration

声明测试属性源

您可以使用 的locationsvalue属性 来配置测试属性文件@TestPropertySource

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

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

以下示例使用测试属性文件:

java
@ContextConfiguration
@TestPropertySource("/test.properties") (1)
class MyIntegrationTests {
    // class body...
}
1 使用绝对路径指定属性文件。
科特林
@ContextConfiguration
@TestPropertySource("/test.properties") (1)
class MyIntegrationTests {
    // class body...
}
1 使用绝对路径指定属性文件。

properties您可以使用 的属性以键值对的形式配置内联属性 @TestPropertySource,如下例所示。所有键值对都作为具有最高优先级Environment的单个测试添加到封闭中 。PropertySource

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

  • key=value

  • key:value

  • key value

以下示例设置两个内联属性:

java
@ContextConfiguration
@TestPropertySource(properties = {"timezone = GMT", "port: 4242"}) (1)
class MyIntegrationTests {
    // class body...
}
1 使用键值语法的两种变体设置两个属性。
科特林
@ContextConfiguration
@TestPropertySource(properties = ["timezone = GMT", "port: 4242"]) (1)
class MyIntegrationTests {
    // class body...
}
1 使用键值语法的两种变体设置两个属性。

从 Spring Framework 5.2 开始,@TestPropertySource可以用作可重复的注解。这意味着您可以@TestPropertySource在单个测试类上拥有多个声明,locations并且properties后面的@TestPropertySource 注释会覆盖先前注释中的@TestPropertySource注释。

此外,您可以在一个测试类上声明多个组合注释,每个都使用 元注释@TestPropertySource,所有这些@TestPropertySource 声明都将有助于您的测试属性源。

直接呈现@TestPropertySource的注释总是优先于元呈现的@TestPropertySource注释。换句话说,locationsand propertiesfrom 直接存在的@TestPropertySource注解将覆盖 locationsand propertiesfrom@TestPropertySource用作元注解的注解。

默认属性文件检测

如果@TestPropertySource被声明为空注解(即,没有明确的locationsorproperties属性值),则尝试检测与声明注解的类相关的默认属性文件。例如,如果带注释的测试类是com.example.MyTest,则对应的默认属性文件是classpath:com/example/MyTest.properties. 如果无法检测到默认值, IllegalStateException则抛出 an。

优先级

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

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

java
@ContextConfiguration
@TestPropertySource(
    locations = "/test.properties",
    properties = {"timezone = GMT", "port: 4242"}
)
class MyIntegrationTests {
    // class body...
}
科特林
@ContextConfiguration
@TestPropertySource("/test.properties",
        properties = ["timezone = GMT", "port: 4242"]
)
class MyIntegrationTests {
    // class body...
}
继承和覆盖测试属性源

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

如果inheritLocationsorinheritProperties属性@TestPropertySource设置为false,则位置或内联属性分别为测试类隐藏并有效替换超类定义的配置。

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

在下一个示例中,仅使用 文件作为测试属性源来加载ApplicationContextfor 。相反, for是通过使用和 文件作为测试属性源位置来加载的。以下示例显示如何使用文件在子类及其超类中定义属性:BaseTestbase.propertiesApplicationContextExtendedTestbase.propertiesextended.propertiesproperties

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

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

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

在下一个示例中,仅使用内联属性加载ApplicationContextfor 。相反,for是通过使用内联和属性加载的。以下示例显示如何使用内联属性在子类及其超类中定义属性:BaseTestkey1ApplicationContextExtendedTestkey1key2

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

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

@TestPropertySource(properties = ["key2 = value2"])
@ContextConfiguration
class ExtendedTest : BaseTest() {
    // ...
}
具有动态属性源的上下文配置

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

@DynamicPropertySource注释及其支持基础设施最初旨在允许 基于Testcontainers的测试的属性轻松地暴露给 Spring 集成测试。但是,此功能也可以与任何形式的外部资源一起使用,其生命周期在测试的ApplicationContext.

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

如果您@DynamicPropertySource在基类中使用并发现子类中的测试失败,因为子类之间的动态属性发生变化,您可能需要注释您的基类,@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 ...

}
科特林
@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 系统属性或应用程序通过 using@PropertySource或以编程方式以声明方式添加的属性源加载的属性。因此,动态属性可用于选择性地覆盖通过 @TestPropertySource、系统属性源和应用程序属性源加载的属性。

加载一个WebApplicationContext

要指示 TestContext 框架加载 aWebApplicationContext而不是标准ApplicationContext,您可以使用 @WebAppConfiguration.

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

请注意,Spring 对实现的测试支持WebApplicationContext与其对标准ApplicationContext实现的支持相当。使用 . 进行测试时 WebApplicationContext,您可以@Configuration使用@ContextConfiguration. 您还可以自由使用任何其他测试注释,例如@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 {
    //...
}
科特林
@ExtendWith(SpringExtension::class)

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

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

如果您在@WebAppConfiguration未指定资源基路径的情况下使用注释测试类,则资源路径实际上默认为file:src/main/webapp. 同样,如果您在声明@ContextConfiguration时未指定 resource locations、 component classes或 context initializers,Spring 会尝试使用约定(即与类或静态嵌套类WacTests-context.xml在同一个包中)检测您的配置是否存在。WacTests@Configuration

以下示例显示如何使用 显式声明资源基路径 @WebAppConfiguration和使用 的 XML 资源位置@ContextConfiguration

java
@ExtendWith(SpringExtension.class)

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

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

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

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

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

以下示例显示我们可以通过指定 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 {
    //...
}
科特林
@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 Mocks

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

为测试加载后,您WebApplicationContext可能会发现需要与 Web 模拟进行交互——例如,设置测试夹具或在调用 Web 组件后执行断言。以下示例显示了哪些模拟可以自动装配到您的测试实例中。请注意,WebApplicationContextand MockServletContext都在测试套件中缓存,而其他模拟由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;

    //...
}
科特林
@SpringJUnitWebConfig
class WacTests {

    @Autowired
    lateinit var wac: WebApplicationContext // cached

    @Autowired
    lateinit var servletContext: MockServletContext // cached

    @Autowired
    lateinit var session: MockHttpSession

    @Autowired
    lateinit var request: MockHttpServletRequest

    @Autowired
    lateinit var response: MockHttpServletResponse

    @Autowired
    lateinit var webRequest: ServletWebRequest

    //...
}
上下文缓存

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

AnApplicationContext可以通过用于加载它的配置参数的组合来唯一标识。因此,配置参数的唯一组合用于生成缓存上下文的键。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

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

测试套件和分叉流程

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

要从缓存机制中受益,所有测试都必须在同一进程或测试套件中运行。这可以通过在 IDE 中作为一个组执行所有测试来实现。同样,在使用 Ant、Maven 或 Gradle 等构建框架执行测试时,确保构建框架不会在测试之间分叉很重要。例如,如果 forkMode Maven Surefire 插件的 设置为alwayspertest,则 TestContext 框架无法缓存测试类之间的应用程序上下文,从而导致构建过程运行速度明显变慢。

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

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

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

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

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

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

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

ApplicationContext测试可以通过以下场景之一关闭。

  • 上下文根据@DirtiesContext语义关闭。

  • 上下文已关闭,因为它已根据 LRU 逐出策略自动从缓存中逐出。

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

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

当 SpringApplicationContext通过 JVM 关闭挂钩关闭时,在关闭阶段执行的回调将在名为SpringContextShutdownHook. 因此,如果您希望禁用ApplicationContext通过 JVM 关闭挂钩关闭时触发的控制台日志记录,您可以向您的日志记录框架注册一个自定义过滤器,允许您忽略该线程启动的任何日志记录。

上下文层次结构

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

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

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

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

ControllerIntegrationTests通过声明一个包含两个级别的上下文层次结构来表示 Spring MVC Web 应用程序的典型集成测试场景,一个用于根WebApplicationContext(使用TestAppConfig @Configuration类加载),一个用于调度程序 servlet WebApplicationContext (使用WebConfig @Configuration类加载)。自动装配到测试实例中的WebApplicationContext 是子上下文(即层次结构中最低的上下文)。以下清单显示了此配置方案:

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

    @Autowired
    WebApplicationContext wac;

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

    @Autowired
    lateinit var wac: WebApplicationContext

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

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

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

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

@ContextHierarchy(@ContextConfiguration("/spring/rest-ws-config.xml"))
public class RestWebServiceTests extends AbstractWebTests {}
科特林
@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定义层次结构中的两个级别,parent并且child. ExtendedTests扩展BaseTests并指示 Spring TestContext 框架合并层次结构级别的上下文配置,方法是确保在属性中 child 声明的名称都是. 结果是加载了三个应用程序上下文:一个 for 、一个 for和一个 for 。与前面的示例一样,从中加载的应用程序上下文被设置为从中加载的上下文的父上下文,并且name@ContextConfigurationchild/app-config.xml/user-config.xml{"/user-config.xml", "/order-config.xml"}/app-config.xml/user-config.xml{"/user-config.xml", "/order-config.xml"}. 以下清单显示了此配置方案:

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

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

@ContextHierarchy(
    ContextConfiguration(name = "child", locations = ["/order-config.xml"])
)
class ExtendedTests : BaseTests() {}
具有覆盖的上下文层次结构配置的类层次结构

与前面的示例相比,此示例演示了如何通过将 inheritLocations标志设置为 来覆盖上下文层次结构中@ContextConfiguration给定命名级别的配置false。因此,应用程序上下文ExtendedTests仅从 加载,/test-user-config.xml并且其父级设置为从 加载的上下文/app-config.xml。以下清单显示了此配置方案:

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

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

@ContextHierarchy(
        ContextConfiguration(
                name = "child",
                locations = ["/test-user-config.xml"],
                inheritLocations = false
        ))
class ExtendedTests : BaseTests() {}
在上下文层次结构中弄脏上下文
如果您@DirtiesContext在其上下文被配置为上下文层次结构的一部分的测试中使用,则可以使用该hierarchyMode标志来控制如何清除上下文缓存。有关更多详细信息,请参阅Spring Testing Annotations和 javadoc @DirtiesContext中 的讨论。@DirtiesContext

3.5.7。测试夹具的依赖注入

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

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

因为@Autowired用于按类型执行自动装配,所以如果您有多个相同类型的 bean 定义,您不能依赖于这些特定 bean 的这种方法。在这种情况下,您可以@Autowired与 结合使用@Qualifier。您也可以选择@Inject@Named. 或者,如果您的测试类可以访问它的ApplicationContext,您可以通过使用(例如)调用来执行显式查找 applicationContext.getBean("titleRepository", TitleRepository.class)

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

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

以下代码清单中的依赖注入行为并非特定于 JUnit Jupiter。相同的 DI 技术可以与任何支持的测试框架结合使用。

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

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

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

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

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

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

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

或者,您可以配置类以@Autowired用于 setter 注入,如下所示:

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

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

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

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

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

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

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

前面的代码清单使用 @ContextConfiguration注释引用的相同 XML 上下文文件(即repository-config.xml)。下面显示了此配置:

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

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

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

</beans>

如果您从 Spring 提供的测试基类扩展,该基类碰巧 @Autowired在其 setter 方法之一上使用,您可能在应用程序上下文中定义了多个受影响类型的 bean(例如,多个DataSourcebean)。在这种情况下,您可以覆盖 setter 方法并使用@Qualifier注解来指示特定的目标 bean,如下所示(但请确保也委托给超类中的覆盖方法):

java
// ...

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

// ...
科特林
// ...

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

// ...

指定的限定符值指示DataSource要注入的特定 bean,将类型匹配集缩小到特定 bean。<qualifier>它的值与相应<bean>定义中的声明相匹配 。bean 名称用作备用限定符值,因此您还可以有效地通过名称指向特定的 bean(如前所示,假设myDataSource是 bean id)。

3.5.8。测试请求和会话范围的 Bean

Spring从早年开始就支持请求和会话范围的 bean,您可以按照以下步骤测试请求范围和会话范围的 bean

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

  • 将模拟请求或会话注入您的测试实例并根据需要准备测试夹具。

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

  • 对模拟执行断言。

下一个代码片段显示了登录用例的 XML 配置。请注意,该 userServicebean 依赖于请求范围的loginActionbean。此外, 通过使用从当前 HTTP 请求中检索用户名和密码的SpEL 表达式LoginAction来实例化。在我们的测试中,我们想通过TestContext框架管理的mock来配置这些请求参数。以下清单显示了此用例的配置:

请求范围的 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. 当loginUser()在我们的 上调用该方法时 userService,我们可以确保用户服务可以访问当前请求范围内的请求 loginActionMockHttpServletRequest即我们刚刚设置参数的那个)。然后,我们可以根据用户名和密码的已知输入对结果执行断言。以下清单显示了如何执行此操作:

java
@SpringJUnitWebConfig
class RequestScopedBeanTests {

    @Autowired UserService userService;
    @Autowired MockHttpServletRequest request;

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

        LoginResults results = userService.loginUser();
        // assert results
    }
}
科特林
@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 的代码片段。但是,这一次,userServicebean 依赖于会话范围的 userPreferencesbean。请注意,该UserPreferencesbean 是通过使用从当前 HTTP 会话中检索主题的 SpEL 表达式来实例化的。在我们的测试中,我们需要在 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中,我们将UserService和注入MockHttpSession到我们的测试实例中。在我们的sessionScope()测试方法中,我们通过设置提供的期望theme属性来设置我们的测试夹具MockHttpSession。当 processUserPreferences()在 our 上调用该方法时userService,我们可以确保用户服务可以访问userPreferences当前 的会话范围MockHttpSession,并且我们可以根据配置的主题对结果执行断言。以下示例显示了如何执行此操作:

java
@SpringJUnitWebConfig
class SessionScopedBeanTests {

    @Autowired UserService userService;
    @Autowired MockHttpSession session;

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

        Results results = userService.processUserPreferences();
        // assert results
    }
}
科特林
@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在测试类上显式声明也是如此。但是,要启用对事务的支持,您必须在加载语义的 bean 中配置一个PlatformTransactionManagerbean (稍后提供更多详细信息)。此外,您必须 在测试的类或方法级别声明 Spring 的注解。ApplicationContext@ContextConfiguration@Transactional

测试管理事务

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

抢先超时和测试管理事务

在将测试框架中的任何形式的抢占式超时与 Spring 的测试管理事务结合使用时,必须小心。

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

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

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

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

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

启用和禁用事务

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

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

valuetransactionManager

是的

propagation

Propagation.NOT_SUPPORTEDPropagation.NEVER受支持

isolation

timeout

readOnly

rollbackForrollbackForClassName

否:TestTransaction.flagForRollback()改为使用

noRollbackFornoRollbackForClassName

否:TestTransaction.flagForCommit()改为使用

方法级别的生命周期方法——例如,使用 JUnit Jupiter's @BeforeEachor注释的方法——@AfterEach在测试管理的事务中运行。另一方面,套件级和类级生命周期方法——例如,使用 JUnit Jupiter 的@BeforeAllor@AfterAll注释的方法和使用 TestNG 的 @BeforeSuite@AfterSuite@BeforeClass或注释的方法@AfterClass——不在测试管理的事务中运行。

如果您需要在事务中的套件级或类级生命周期方法中运行代码,您可能希望将相应的注入PlatformTransactionManager到您的测试类中,然后将其与TransactionTemplate编程事务管理一起使用。

请注意,AbstractTransactionalJUnit4SpringContextTestsAbstractTransactionalTestNGSpringContextTests 已针对类级别的事务支持进行了预配置。

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

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

    @Autowired
    HibernateUserRepository repository;

    @Autowired
    SessionFactory sessionFactory;

    JdbcTemplate jdbcTemplate;

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

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

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

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

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

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

    @Autowired
    lateinit var repository: HibernateUserRepository

    @Autowired
    lateinit var sessionFactory: SessionFactory

    lateinit var jdbcTemplate: JdbcTemplate

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

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

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

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

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

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

Transaction Rollback and Commit Behavior中所述,在方法运行后无需清理数据库createUser(),因为对数据库所做的任何更改都会由TransactionalTestExecutionListener.

事务回滚和提交行为

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

程序化事务管理

您可以使用TestTransaction. 例如,您可以使用TestTransaction测试方法内、之前方法和之后方法来启动或结束当前测试管理的事务,或者配置当前测试管理的事务以进行回滚或提交。TestTransaction只要启用 ,就会自动获得对 的支持TransactionalTestExecutionListener

以下示例演示了TestTransaction. 有关详细信息,请参阅 javadoc TestTransaction

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

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

        deleteFromTables("user");

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

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

    protected void assertNumUsers(int expected) {
        assertEquals("Number of rows in the [user] table.", expected, countRowsInTable("user"));
    }
}
科特林
@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"))
    }
}
在事务之外运行代码

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

任何 before 方法(例如使用 JUnit Jupiter's 注释的方法@BeforeEach)和任何 after 方法(例如使用 JUnit Jupiter's 注释的方法@AfterEach)都在事务中运行。此外,对于未配置为在事务中运行的测试方法,使用注释@BeforeTransaction@AfterTransaction不运行的方法。
配置事务管理器

TransactionalTestExecutionListener期望在 Spring中为测试PlatformTransactionManager定义一个 bean 。如果测试ApplicationContext中有多个实例,您可以使用or声明限定符,或者可以由 类实现。有关用于在测试中查找事务管理器的算法的详细信息,请参阅 javadocPlatformTransactionManagerApplicationContext@Transactional("myTxMgr")@Transactional(transactionManager = "myTxMgr")TransactionManagementConfigurer@ConfigurationTestContextTransactionUtils.retrieveTransactionManager()ApplicationContext

所有与交易相关的注释的演示

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

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

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

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

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

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

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

}
科特林
@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 的示例测试用例中,一种方法演示了误报,另一种方法正确公开了刷新会话的结果:

java
// ...

@Autowired
SessionFactory sessionFactory;

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

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

// ...
科特林
// ...

@Autowired
lateinit var sessionFactory: SessionFactory

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

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

// ...

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

java
// ...

@PersistenceContext
EntityManager entityManager;

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

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

// ...
科特林
// ...

@PersistenceContext
lateinit var entityManager:EntityManager

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

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

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

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

例如,当使用 JPA 时,@PostPersist除非在保存或更新实体后调用,否则不会调用@PreUpdate回调@PostUpdateentityManager.flush()类似地,如果实体已附加到当前工作单元(与当前持久性上下文相关联),则重新加载实体的尝试不会导致@PostLoad回调,除非entityManager.clear()在尝试重新加载实体之前调用。

以下示例显示如何刷新EntityManager以确保 @PostPersist在持久化实体时调用回调。@PostPersist已为示例中使用的实体注册了具有回调方法的实体侦听Person器。

java
// ...

@Autowired
JpaPersonRepository repo;

@PersistenceContext
EntityManager entityManager;

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

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

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

// ...
科特林
// ...

@Autowired
lateinit var repo: JpaPersonRepository

@PersistenceContext
lateinit var entityManager: EntityManager

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

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

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

// ...

有关使用所有 JPA 生命周期回调的工作示例,请参阅 Spring Framework 测试套件中的JpaEntityListenerTests

3.5.10。执行 SQL 脚本

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

尽管在加载时 初始化数据库以进行测试非常有用,但有时在集成测试期间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中的各个方法ScriptUtils

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

java
@Test
void databaseTest() {
    ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
    populator.addScripts(
            new ClassPathResource("test-schema.sql"),
            new ClassPathResource("test-data.sql"));
    populator.setSeparator("@@");
    populator.execute(this.dataSource);
    // run code that uses the test schema and data
}
科特林
@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内部委托 toScriptUtils用于解析和运行 SQL 脚本。类似地,内部 和内部的executeSqlScript(..)方法 使用 a来运行 SQL 脚本。有关更多详细信息,请参阅各种方法的 Javadoc。AbstractTransactionalJUnit4SpringContextTestsAbstractTransactionalTestNGSpringContextTestsResourceDatabasePopulatorexecuteSqlScript(..)

使用@Sql 以声明方式执行 SQL 脚本

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

默认情况下,方法级别的@Sql声明会覆盖类级别的声明。但是,从 Spring Framework 5.2 开始,可以通过每个测试类或每个测试方法配置此行为@SqlMergeMode。有关详细信息,请参阅 合并和覆盖配置@SqlMergeMode
路径资源语义

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

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

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

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

    @Test
    @Sql({"/test-schema.sql", "/test-user-data.sql"})
    void userTest() {
        // run code that uses the test schema and test data
    }
}
科特林
@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则抛出 an。

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

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

声明多个@Sql集合

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

以下示例展示了如何在@SqlJava 8 中用作可重复注解:

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

在前面示例中呈现的场景中,test-schema.sql脚本对单行注释使用不同的语法。

以下示例与前面的示例相同,不同之处在于@Sql 声明在@SqlGroup. 在 Java 8 及更高版本中,使用 @SqlGroup是可选的,但您可能需要使用@SqlGroup与其他 JVM 语言(如 Kotlin)兼容。

java
@Test
@SqlGroup({
    @Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`")),
    @Sql("/test-user-data.sql")
)}
void userTest() {
    // run code that uses the test schema and test data
}
科特林
@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,如以下示例所示:

java
@Test
@Sql(
    scripts = "create-test-data.sql",
    config = @SqlConfig(transactionMode = ISOLATED)
)
@Sql(
    scripts = "delete-test-data.sql",
    config = @SqlConfig(transactionMode = ISOLATED),
    executionPhase = AFTER_TEST_METHOD
)
void userTest() {
    // run code that needs the test data to be committed
    // to the database outside of the test's transaction
}
科特林
@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
}

请注意ISOLATED和分别从和AFTER_TEST_METHOD静态导入 。Sql.TransactionModeSql.ExecutionPhase

脚本配置与@SqlConfig

您可以使用@SqlConfig注解来配置脚本解析和错误处理。当在集成测试类上声明为类级注释时,@SqlConfig 用作测试类层次结构中所有 SQL 脚本的全局配置。当使用注解的config属性直接声明时@Sql@SqlConfig 用作封闭@Sql 注解内声明的 SQL 脚本的本地配置。中的每个属性@SqlConfig都有一个隐含的默认值,该值记录在相应属性的 javadoc 中。由于 Java 语言规范中为注解属性定义的规则,不幸的是,不能为null到注释属性。因此,为了支持继承的全局配置的覆盖,@SqlConfig属性具有显式默认值""(对于字符串)、{}(对于数组)或DEFAULT(对于枚举)。这种方法允许局部声明@SqlConfig选择性地覆盖全局声明中的单个属性,方法@SqlConfig是提供除""{}或之外的值DEFAULT。只要局部属性不提供除 、 或 之外的显式值,就会 @SqlConfig继承全局属性。因此,显式本地配置会覆盖全局配置。@SqlConfig""{}DEFAULT

@Sqland提供的配置选项@SqlConfig等价于 and 支持的配置选项ScriptUtils,但是它们是XML 命名空间元素ResourceDatabasePopulator提供的配置选项的超集。有关详细信息,请参阅和 <jdbc:initialize-database/>中的各个属性的 javadoc 。@Sql@SqlConfig

事务管理@Sql

默认情况下,SqlScriptsTestExecutionListener为使用配置的脚本推断所需的事务语义@Sql。具体来说,SQL 脚本在没有事务的情况下运行,在现有 Spring 管理的事务中(例如,由TransactionalTestExecutionListener注释为 的测试 管理的事务@Transactional)或在隔离的事务中,具体取决于配置的transactionMode属性值 in@SqlConfig和a PlatformTransactionManager在测试中的存在ApplicationContext。但是,作为最低要求, ajavax.sql.DataSource必须出现在测试的ApplicationContext.

如果用于SqlScriptsTestExecutionListener检测DataSourcePlatformTransactionManager推断事务语义的算法不适合您的需要,您可以通过设置的dataSourcetransactionManager 属性来指定显式名称@SqlConfigtransactionMode此外,您可以通过设置的属性来控制事务传播行为@SqlConfig(例如,脚本是否应该在隔离事务中运行)。尽管对所有支持的事务管理选项的全面讨论@Sql超出了本参考手册的范围,但 javadoc @SqlConfig提供 SqlScriptsTestExecutionListener 了详细信息,以下示例显示了使用 JUnit Jupiter 和事务测试的典型测试场景@Sql

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

    final JdbcTemplate jdbcTemplate;

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

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

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

    void assertNumUsers(int expected) {
        assertEquals(expected, countRowsInTable("user"),
            "Number of rows in the [user] table.");
    }
}
科特林
@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 Framework 5.2 开始,可以将方法级@Sql声明与类级声明合并。例如,这允许您为每个测试类提供一次数据库模式或一些常见测试数据的配置,然后为每个测试方法提供额外的、特定于用例的测试数据。要启用@Sql合并,请使用 . 注释您的测试类或测试方法@SqlMergeMode(MERGE)。要禁用特定测试方法(或特定测试子类)的合并,您可以通过 切换回默认模式@SqlMergeMode(OVERRIDE)。有关示例和更多详细信息,请参阅@SqlMergeMode注释文档部分

3.5.11。并行测试执行

Spring Framework 5.0 引入了在使用 Spring TestContext Framework 时在单个 JVM 中并行执行测试的基本支持。一般来说,这意味着大多数测试类或测试方法可以并行运行,而无需对测试代码或配置进行任何更改。

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

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

如果测试出现以下情况,请勿并行运行测试:

  • 使用 Spring Framework 的@DirtiesContext支持。

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

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

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

如果并行测试执行失败并出现异常,表明ApplicationContext 当前测试不再处于活动状态,这通常意味着 ApplicationContext已从ContextCache不同线程中删除。

这可能是由于使用@DirtiesContextContextCache. 如果@DirtiesContext是罪魁祸首,您要么需要找到一种方法来避免使用@DirtiesContext此类测试,要么将此类测试排除在并行执行之外。如果ContextCache已超过最大大小,则可以增加缓存的最大大小。有关详细信息,请参阅上下文缓存的讨论 。

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

3.5.12。TestContext 框架支持类

本节介绍支持 Spring TestContext Framework 的各种类。

Spring JUnit 4 Runner

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

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

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

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

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

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

Spring JUnit 4 规则

org.springframework.test.context.junit4.rules软件包提供以下 JUnit 4 规则(在 JUnit 4.12 或更高版本上受支持):

  • SpringClassRule

  • SpringMethodRule

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

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

要支持 TestContext 框架的全部功能,您必须将 a SpringClassRule与 a结合使用SpringMethodRule。以下示例显示了在集成测试中声明这些规则的正确方法:

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

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

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

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

    @Rule
    val springMethodRule = SpringMethodRule()

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

    companion object {
        @ClassRule
        val springClassRule = SpringClassRule()
    }
}
JUnit 4 支持类

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

  • AbstractJUnit4SpringContextTests

  • AbstractTransactionalJUnit4SpringContextTests

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

AbstractTransactionalJUnit4SpringContextTests是一个抽象的事务扩展, AbstractJUnit4SpringContextTests它为 JDBC 访问增加了一些便利功能。这个类期望一个javax.sql.DataSourcebean 和一个 PlatformTransactionManagerbean 被定义在ApplicationContext. 扩展时AbstractTransactionalJUnit4SpringContextTests,您可以访问一个protected jdbcTemplate实例变量,您可以使用该变量来运行 SQL 语句来查询数据库。您可以在运行与数据库相关的应用程序代码之前和之后使用此类查询来确认数据库状态,Spring 确保此类查询在与应用程序代码相同的事务范围内运行。与 ORM 工具结合使用时,请务必避免误报。如JDBC 测试支持中所述, AbstractTransactionalJUnit4SpringContextTests还提供了委托给方法的便捷方法JdbcTestUtils通过使用上述jdbcTemplate. 此外,AbstractTransactionalJUnit4SpringContextTests提供了 executeSqlScript(..)一种针对已配置的DataSource.

这些类便于扩展。如果您不希望您的测试类绑定到特定于 Spring 的类层次结构,您可以使用 Spring 的 JUnit 规则来配置您自己的自定义@RunWith(SpringRunner.class)测试
JUnit Jupiter 的 SpringExtension

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

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

  • 测试构造函数、测试方法和测试生命周期回调方法的依赖注入。有关更多详细信息,请参阅依赖注入SpringExtension

  • 对基于 SpEL 表达式、环境变量、系统属性等的条件测试执行的强大支持。有关更多详细信息和示例,请参阅Spring JUnit Jupiter Testing Annotations的文档@EnabledIf和其中@DisabledIf的 文档。

  • 自定义组合注释,结合了来自 Spring 和 JUnit Jupiter 的注释。有关更多详细信息,请参阅Meta-Annotation Support for Testing@TransactionalDevTestConfig中的和@TransactionalIntegrationTest示例 。

以下代码清单显示了如何配置测试类以与 SpringExtension结合使用@ContextConfiguration

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

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

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

由于您还可以在 JUnit 5 中将注解用作元注解,因此 Spring 提供了 @SpringJUnitConfig@SpringJUnitWebConfig组合注解来简化测试ApplicationContext和 JUnit Jupiter 的配置。

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

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

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

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

同样,以下示例用于@SpringJUnitWebConfig创建 WebApplicationContext用于 JUnit Jupiter 的:

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

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

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

有关详细信息,请参阅Spring JUnit Jupiter Testing Annotations的文档@SpringJUnitConfig和其中@SpringJUnitWebConfig的 文档。

依赖注入SpringExtension

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

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

构造函数注入

如果 JUnit Jupiter 测试类的构造函数中的特定参数是类型 ApplicationContext(或其子类型)或使用 @Autowired@Qualifier或注释或元注释@Value,则 Spring 会使用相应的 bean 或值注入该特定参数的值从测试的ApplicationContext.

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

  • 构造函数用 注释@Autowired

  • @TestConstructorautowireMode 在属性设置为的测试类上存在或元存在ALL

  • 默认测试构造函数自动装配模式已更改为ALL.

有关@TestConstructor使用 @TestConstructor以及如何更改全局测试构造函数 autowire 模式的详细信息,请参阅。

如果测试类的构造函数被认为是可自动装配的,那么 Spring 将负责解析构造函数中所有参数的参数。因此,ParameterResolver在 JUnit Jupiter 中注册的其他任何人都无法解析此类构造函数的参数。

如果用于关闭测试之前或之后的测试方法,则测试类的构造函数注入不能与 JUnit Jupiter 的@TestInstance(PER_CLASS)支持结合使用。@DirtiesContextApplicationContext

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

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

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

java
@SpringJUnitConfig(TestConfig.class)
class OrderServiceIntegrationTests {

    private final OrderService orderService;

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

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

请注意,此功能允许测试依赖项是final不可变的。

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

java
@SpringJUnitConfig(TestConfig.class)
class OrderServiceIntegrationTests {

    private final OrderService orderService;

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

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

如果 JUnit Jupiter 测试方法或测试生命周期回调方法中的参数属于类型ApplicationContext(或其子类型)或使用 、 或 进行注释或元注释 @Autowired@Qualifier@ValueSpring 使用来自的相应 bean 注入该特定参数的值测试的ApplicationContext.

在以下示例中,Spring 将OrderServicefromApplicationContext 加载的 fromTestConfig.class注入到deleteOrder()测试方法中:

java
@SpringJUnitConfig(TestConfig.class)
class OrderServiceIntegrationTests {

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

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

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

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

java
@SpringJUnitConfig(TestConfig.class)
class OrderServiceIntegrationTests {

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

        // use orderService from the test's ApplicationContext
        // and repetitionInfo from JUnit Jupiter
    }
}
科特林
@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
    }
}

请注意,使用@RepeatedTestfrom JUnit Jupiter 可以让测试方法访问RepetitionInfo.

@Nested测试类配置

自 Spring Framework 5.0 起,Spring TestContext Framework 就支持在 JUnit Jupiter 中的测试类上使用与测试相关的注解 ;@Nested但是,直到 Spring Framework 5.3 类级别的测试配置注释才像从超类那样继承自封闭类。

Spring Framework 5.3 引入了对从封闭类继承测试类配置的一流支持,默认情况下会继承此类配置。要从默认INHERIT模式更改为OVERRIDE模式,您可以使用 注释单个@Nested测试类 @NestedTestConfiguration(EnclosingConfiguration.OVERRIDE)。显式 @NestedTestConfiguration声明将应用于带注释的测试类以及它的任何子类和嵌套类。因此,您可以使用 注释顶级测试类@NestedTestConfiguration,这将递归地应用于其所有嵌套测试类。

为了允许开发团队将OVERRIDE默认模式更改为——例如,为了与 Spring Framework 5.0 到 5.2 兼容——可以通过 JVM 系统属性或spring.properties类路径根目录中的文件全局更改默认模式。有关详细信息,请参阅“更改默认封闭配置继承模式”说明。

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

java
@SpringJUnitConfig(TestConfig.class)
class GreetingServiceTests {

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

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

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

        @Test
        void hello(@Autowired GreetingService service) {
            assertThat(service.greetWorld()).isEqualTo("Hallo Welt");
        }
    }
}
科特林
@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 框架与ApplicationContextTestNG 环境中的显式测试支持集成在一起。当您扩展时AbstractTestNGSpringContextTests,您可以访问一个 protected applicationContext实例变量,您可以使用它来执行显式 bean 查找或测试整个上下文的状态。

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

这些类便于扩展。如果您不希望您的测试类绑定到特定于 Spring 的类层次结构,您可以使用 、 等配置您自己的自定义测试类@ContextConfiguration@TestExecutionListeners并使用TestContextManager. AbstractTestNGSpringContextTests有关如何检测测试类的示例, 请参阅的源代码。

3.6. WebTestClient

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

Kotlin 用户:请参阅 使用WebTestClient.

3.6.1. 设置

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

绑定到控制器

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

对于 WebFlux 应用程序,使用以下内容加载等效于 WebFlux Java 配置的基础架构,注册给定的控制器,并创建一个WebHandler 链 来处理请求:

java
WebTestClient client =
        WebTestClient.bindToController(new TestController()).build();
科特林
val client = WebTestClient.bindToController(TestController()).build()

对于 Spring MVC,使用以下委托给 StandaloneMockMvcBuilder 来加载等效于WebMvc Java config的基础设施,注册给定的控制器,并创建 MockMvc的实例来处理请求:

java
WebTestClient client =
        MockMvcWebTestClient.bindToController(new TestController()).build();
科特林
val client = MockMvcWebTestClient.bindToController(TestController()).build()
绑定到ApplicationContext

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

对于 WebFlux,在 SpringApplicationContext传递给 WebHttpHandlerBuilder的地方使用以下内容 来创建WebHandler 链来处理请求:

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

    WebTestClient client;

    @BeforeEach
    void setUp(ApplicationContext context) {  (2)
        client = WebTestClient.bindToApplicationContext(context).build(); (3)
    }
}
1 指定要加载的配置
2 注入配置
3 创建WebTestClient
科特林
@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,使用以下 SpringApplicationContext传递给 MockMvcBuilders.webAppContextSetup 来创建MockMvc实例来处理请求:

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

    @Autowired
    WebApplicationContext wac; (2)

    WebTestClient client;

    @BeforeEach
    void setUp() {
        client = MockMvcWebTestClient.bindToApplicationContext(this.wac).build(); (3)
    }
}
1 指定要加载的配置
2 注入配置
3 创建WebTestClient
科特林
@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来创建服务器设置来处理请求:

java
RouterFunction<?> route = ...
client = WebTestClient.bindToRouterFunction(route).build();
科特林
val route: RouterFunction<*> = ...
val client = WebTestClient.bindToRouterFunction(route).build()

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

绑定到服务器

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

java
client = WebTestClient.bindToServer().baseUrl("http://localhost:8080").build();
科特林
client = WebTestClient.bindToServer().baseUrl("http://localhost:8080").build()
客户端配置

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

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

3.6.2. 写作测试

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

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

要断言响应状态和标头,请使用以下命令:

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

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

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

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

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

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

  • expectBody():解码byte[]JSON 内容或空正文。

并对产生的更高级别的对象执行断言:

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

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

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

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

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

或者您可以退出工作流程并获得EntityExchangeResult

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

val result = client.get().uri("/persons/1")
        .exchange()
        .expectStatus().isOk
        .expectBody<Person>()
        .returnResult()
当您需要使用泛型解码为目标类型时,请查找接受 ParameterizedTypeReference 而不是Class<T>.
无内容

如果预期响应没有内容,您可以断言如下:

java
client.post().uri("/persons")
        .body(personMono, Person.class)
        .exchange()
        .expectStatus().isCreated()
        .expectBody().isEmpty();
科特林
client.post().uri("/persons")
        .bodyValue(person)
        .exchange()
        .expectStatus().isCreated()
        .expectBody().isEmpty()

如果你想忽略响应内容,下面会释放不带任何断言的内容:

java
client.get().uri("/persons/123")
        .exchange()
        .expectStatus().isNotFound()
        .expectBody(Void.class);
科特林
client.get().uri("/persons/123")
        .exchange()
        .expectStatus().isNotFound
        .expectBody<Unit>()
JSON 内容

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

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

java
client.get().uri("/persons/1")
        .exchange()
        .expectStatus().isOk()
        .expectBody()
        .json("{\"name\":\"Jane\"}")
科特林
client.get().uri("/persons/1")
        .exchange()
        .expectStatus().isOk()
        .expectBody()
        .json("{\"name\":\"Jane\"}")

使用JSONPath验证 JSON 内容:

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

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

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

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

现在您已准备好使用StepVerifierfrom使用响应流reactor-test

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

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

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

WebTestClient是一个 HTTP 客户端,因此它只能验证客户端响应中的内容,包括状态、标头和正文。

在使用 MockMvc 服务器设置测试 Spring MVC 应用程序时,您可以选择在服务器响应上执行进一步的断言。要做到这一点,首先要获得一个ExchangeResultafter 断言 body:

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

// For a response without a body
EntityExchangeResult<Void> result = client.get().uri("/path")
        .exchange()
        .expectBody().isEmpty();
科特林
// 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 服务器响应断言:

java
MockMvcWebTestClient.resultActionsFor(result)
        .andExpect(model().attribute("integer", 3))
        .andExpect(model().attribute("string", "a string value"));
科特林
MockMvcWebTestClient.resultActionsFor(result)
        .andExpect(model().attribute("integer", 3))
        .andExpect(model().attribute("string", "a string value"));

3.7. 模拟Mvc

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和传递 模块中的 Servlet API 的“模拟”实现来实现这一点,该 spring-test模块在没有运行服务器的情况下复制完整的 Spring MVC 请求处理。

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

静态导入

直接使用 MockMvc 执行请求时,您需要静态导入:

  • MockMvcBuilders.*

  • MockMvcRequestBuilders.*

  • MockMvcResultMatchers.*

  • MockMvcResultHandlers.*

一个容易记住的方法是搜索MockMvc*. 如果使用 Eclipse,请务必在 Eclipse 首选项中将上述内容添加为“最喜欢的静态成员”。

通过WebTestClient使用 MockMvc 时,您不需要静态导入。提供WebTestClient了一个流畅的 API,没有静态导入。

设置选择

MockMvc 可以通过以下两种方式之一进行设置。一种是直接指向要测试的控制器并以编程方式配置 Spring MVC 基础设施。第二个是指向 Spring 配置,其中包含 Spring MVC 和控制器基础设施。

要设置 MockMvc 以测试特定控制器,请使用以下命令:

java
class MyWebTests {

    MockMvc mockMvc;

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

    // ...

}
科特林
class MyWebTests {

    lateinit var mockMvc : MockMvc

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

    // ...

}

或者,您也可以在通过委托给同一个构建器的WebTestClient进行测试时使用此设置, 如上所示。

要通过 Spring 配置设置 MockMvc,请使用以下命令:

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

    MockMvc mockMvc;

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

    // ...

}
科特林
@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>

然后,您可以将模拟服务注入测试以设置和验证您的期望,如以下示例所示:

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

    @Autowired
    AccountService accountService;

    MockMvc mockMvc;

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

    // ...

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

    @Autowired
    lateinit var accountService: AccountService

    lateinit mockMvc: MockMvc

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

    // ...

}

另一方面,它standaloneSetup更接近于单元测试。它一次测试一个控制器。您可以手动注入具有模拟依赖项的控制器,并且不涉及加载 Spring 配置。此类测试更侧重于样式,并且可以更轻松地查看正在测试哪个控制器,是否需要任何特定的 Spring MVC 配置才能工作,等等。这standaloneSetup也是编写临时测试以验证特定行为或调试问题的一种非常方便的方法。

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

设置功能

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

java
// static import of MockMvcBuilders.standaloneSetup

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

此外,第三方框架(和应用程序)可以预先打包设置说明,例如MockMvcConfigurer. Spring 框架有一个这样的内置实现,它有助于跨请求保存和重用 HTTP 会话。您可以按如下方式使用它:

java
// static import of SharedHttpSessionConfigurer.sharedHttpSession

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

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

有关所有 MockMvc 构建器功能的列表,请参阅 javadoc, ConfigurableMockMvcBuilder 或使用 IDE 探索可用选项。

执行请求

本节展示如何单独使用 MockMvc 来执行请求和验证响应。如果通过 MockMvc 使用,WebTestClient请参阅有关 编写测试的相应部分。

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

java
// static import of MockMvcRequestBuilders.*

mockMvc.perform(post("/hotels/{id}", 42).accept(MediaType.APPLICATION_JSON));
科特林
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")));
科特林
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"));
科特林
mockMvc.get("/hotels?thing={thing}", "somewhere")

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

java
mockMvc.perform(get("/hotels").param("thing", "somewhere"));
科特林
import org.springframework.test.web.servlet.get

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

如果应用程序代码依赖于 Servlet 请求参数并且没有明确地检查查询字符串(通常是这种情况),那么您使用哪个选项都没有关系。但是请记住,URI 模板提供的查询参数已被解码,而通过该param(…​)方法提供的请求参数预计已被解码。

在大多数情况下,最好将上下文路径和 Servlet 路径留在请求 URI 之外。如果您必须使用完整的请求 URI 进行测试,请务必相应地设置contextPath andservletPath以便请求映射工作,如以下示例所示:

java
mockMvc.perform(get("/app/main/hotels/{id}").contextPath("/app").servletPath("/main"))
科特林
import org.springframework.test.web.servlet.get

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

在前面的示例中,为每个执行的请求设置contextPathand 会很麻烦。servletPath相反,您可以设置默认请求属性,如以下示例所示:

java
class MyWebTests {

    MockMvc mockMvc;

    @BeforeEach
    void setup() {
        mockMvc = standaloneSetup(new AccountController())
            .defaultRequest(get("/")
            .contextPath("/app").servletPath("/main")
            .accept(MediaType.APPLICATION_JSON)).build();
    }
}
科特林
// 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());
科特林
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"));
科特林
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"));
科特林
import org.springframework.test.web.servlet.post

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

只要请求处理不会导致未处理的异常,该print()方法就会将所有可用的结果数据打印到System.out. 还有一个log()方法和该方法的两个附加变体print(),一个接受OutputStreama ,一个接受 a Writer。例如,调用print(System.err)将结果数据打印到System.err,而调用print(myWriter)将结果数据打印到自定义写入器。如果您想要记录而不是打印结果数据,您可以调用该 方法,该方法将结果数据记录为日志类别下的log()单个DEBUG消息 。org.springframework.test.web.servlet.result

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

java
MvcResult mvcResult = mockMvc.perform(post("/persons")).andExpect(status().isOk()).andReturn();
// ...
科特林
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()
科特林
// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed

MockMvc请注意,始终应用共同的期望,并且在不创建单独的实例的情况下无法覆盖。

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

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

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

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

本节展示如何单独使用 MockMvc 来测试异步请求处理。如果通过 WebTestClient 使用MockMvc,则不需要做任何特殊的事情来使异步请求工作,因为它会WebTestClient自动执行本节中描述的操作。

Spring MVC 支持的Servlet 3.0 异步请求 通过退出 Servlet 容器线程并允许应用程序异步计算响应来工作,然后进行异步调度以完成 Servlet 容器线程上的处理。

在 Spring MVC Test 中,可以通过先断言生成的异步值,然后手动执行异步调度,最后验证响应来测试异步请求。以下是返回 , 或反应类型(例如 Reactor )的控制器方法的DeferredResult示例Callable测试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 验证最终响应
科特林
@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 控制器上执行测试。例如:

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

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

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

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

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

过滤注册

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

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

注册过滤器通过MockFilterChainfrom调用spring-test,最后一个过滤器委托给DispatcherServlet.

MockMvc 与端到端测试

MockMVc 建立在 spring-test模块中的 Servlet API 模拟实现之上,不依赖于正在运行的容器。因此,与使用实际客户端和实时服务器运行的完整端到端集成测试相比,存在一些差异。

考虑这一点的最简单方法是从空白开始MockHttpServletRequest。无论您添加什么,请求都会变成什么。可能会让你大吃一惊的是,默认情况下没有上下文路径;没有jsessionid饼干;没有转发、错误或异步调度;因此,没有实际的 JSP 渲染。相反,“转发”和“重定向”的 URL 保存在 中,MockHttpServletResponse并且可以根据期望进行断言。

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

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

每种方法都有优点和缺点。Spring MVC Test 中提供的选项是从经典单元测试到完全集成测试的不同阶段。可以肯定的是,Spring MVC Test 中没有一个选项属于经典单元测试的范畴,但它们更接近它。例如,您可以通过将模拟服务注入控制器来隔离 Web 层,在这种情况下,您仅通过DispatcherServlet实际的 Spring 配置来测试 Web 层,因为您可能会与上面的层隔离地测试数据访问层。此外,您可以使用独立设置,一次只关注一个控制器,然后手动提供使其工作所需的配置。

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

进一步的例子

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

3.7.2. HtmlUnit 集成

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

  • 使用HtmlUnitWebDriverGeb等工具轻松测试 HTML 页面, 无需部署到 Servlet 容器。

  • 在页面内测试 JavaScript。

  • 或者,使用模拟服务进行测试以加快测试速度。

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

MockMvc 使用不依赖于 Servlet 容器的模板技术(例如 Thymeleaf、FreeMarker 等),但它不适用于 JSP,因为它们依赖于 Servlet 容器。
为什么要集成 HtmlUnit?

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

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

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

mockMvc.perform(createMessage)
        .andExpect(status().is3xxRedirection())
        .andExpect(redirectedUrl("/messages/123"));
科特林
@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>

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

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

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

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

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

mockMvc.perform(createMessage)
        .andExpect(status().is3xxRedirection())
        .andExpect(redirectedUrl("/messages/123"));
科特林
val summaryParamName = "summary";
val textParamName = "text";
mockMvc.get("/messages/form").andExpect {
    xpath("//input[@name='$summaryParamName']") { exists() }
    xpath("//textarea[@name='$textParamName']") { exists() }
}
mockMvc.post("/messages/") {
    param(summaryParamName, "Spring Rocks")
    param(textParamName, "In case you didn't know, Spring Rocks!")
}.andExpect {
    status().is3xxRedirection()
    redirectedUrl("/messages/123")
}

这会降低我们的测试错误通过的风险,但是仍然存在一些问题:

  • 如果我们的页面上有多个表单怎么办?诚然,我们可以更新我们的 XPath 表达式,但是当我们考虑更多因素时,它们会变得更加复杂:这些字段的类型是否正确?字段是否启用?等等。

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

  • 最后,我们仍然无法解释一些事情。例如,如果表单有我们希望测试的 JavaScript 验证怎么办?

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

集成测试救援?

为了解决前面提到的问题,我们可以执行端到端的集成测试,但这有一些缺点。考虑测试让我们浏览消息的视图。我们可能需要以下测试:

  • 当消息为空时,我们的页面是否向用户显示通知以指示没有可用的结果?

  • 我们的页面是否正确显示了一条消息?

  • 我们的页面是否正确支持分页?

要设置这些测试,我们需要确保我们的数据库包含正确的消息。这导致了一些额外的挑战:

  • 确保正确的消息在数据库中可能很乏味。(考虑外键约束。)

  • 测试可能会变慢,因为每个测试都需要确保数据库处于正确的状态。

  • 由于我们的数据库需要处于特定状态,我们不能并行运行测试。

  • 对自动生成的 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 HttpComponents 4.5+ 中使用 HtmlUnit,您需要使用 HtmlUnit 2.18 或更高版本。

我们可以通过使用 轻松创建一个WebClient与 MockMvc 集成的 HtmlUnit, MockMvcWebClientBuilder如下所示:

java
WebClient webClient;

@BeforeEach
void setup(WebApplicationContext context) {
    webClient = MockMvcWebClientBuilder
            .webAppContextSetup(context)
            .build();
}
科特林
lateinit var webClient: WebClient

@BeforeEach
fun setup(context: WebApplicationContext) {
    webClient = MockMvcWebClientBuilder
            .webAppContextSetup(context)
            .build()
}
这是一个简单的使用示例MockMvcWebClientBuilder。有关高级用法,请参阅高级MockMvcWebClientBuilder

这确保了localhost作为服务器引用的任何 URL 都被定向到我们的 MockMvc实例,而无需真正的 HTTP 连接。正常情况下,使用网络连接请求任何其他 URL。这让我们可以轻松地测试 CDN 的使用。

MockMvc 和 HtmlUnit 用法

现在我们可以像往常一样使用 HtmlUnit,但无需将应用程序部署到 Servlet 容器。例如,我们可以请求视图创建一条带有以下内容的消息:

java
HtmlPage createMsgFormPage = webClient.getPage("http://localhost/messages/form");
科特林
val createMsgFormPage = webClient.getPage("http://localhost/messages/form")
默认上下文路径是"". 或者,我们可以指定上下文路径,如AdvancedMockMvcWebClientBuilder中所述。

一旦我们有了对 的引用HtmlPage,我们就可以填写表单并提交它以创建一条消息,如以下示例所示:

java
HtmlForm form = createMsgFormPage.getHtmlElementById("messageForm");
HtmlTextInput summaryInput = createMsgFormPage.getHtmlElementById("summary");
summaryInput.setValueAttribute("Spring Rocks");
HtmlTextArea textInput = createMsgFormPage.getHtmlElementById("text");
textInput.setText("In case you didn't know, Spring Rocks!");
HtmlSubmitInput submit = form.getOneHtmlElementByAttribute("input", "type", "submit");
HtmlPage newMessagePage = submit.click();
科特林
val form = createMsgFormPage.getHtmlElementById("messageForm")
val summaryInput = createMsgFormPage.getHtmlElementById("summary")
summaryInput.setValueAttribute("Spring Rocks")
val textInput = createMsgFormPage.getHtmlElementById("text")
textInput.setText("In case you didn't know, Spring Rocks!")
val submit = form.getOneHtmlElementByAttribute("input", "type", "submit")
val newMessagePage = submit.click()

最后,我们可以验证是否成功创建了一条新消息。以下断言使用AssertJ库:

java
assertThat(newMessagePage.getUrl().toString()).endsWith("/messages/123");
String id = newMessagePage.getHtmlElementById("id").getTextContent();
assertThat(id).isEqualTo("123");
String summary = newMessagePage.getHtmlElementById("summary").getTextContent();
assertThat(summary).isEqualTo("Spring Rocks");
String text = newMessagePage.getHtmlElementById("text").getTextContent();
assertThat(text).isEqualTo("In case you didn't know, Spring Rocks!");
科特林
assertThat(newMessagePage.getUrl().toString()).endsWith("/messages/123")
val id = newMessagePage.getHtmlElementById("id").getTextContent()
assertThat(id).isEqualTo("123")
val summary = newMessagePage.getHtmlElementById("summary").getTextContent()
assertThat(summary).isEqualTo("Spring Rocks")
val text = newMessagePage.getHtmlElementById("text").getTextContent()
assertThat(text).isEqualTo("In case you didn't know, Spring Rocks!")

前面的代码以多种方式改进了我们的 MockMvc 测试。首先,我们不再需要显式地验证我们的表单,然后创建一个看起来像表单的请求。相反,我们请求表单、填写并提交,从而显着减少开销。

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

有关使用 HtmlUnit 的更多信息,请参阅HtmlUnit 文档

先进的MockMvcWebClientBuilder

在到目前为止的示例中,我们MockMvcWebClientBuilder以最简单的方式使用,即WebClient基于WebApplicationContextSpring TestContext Framework 为我们加载的构建一个。在以下示例中重复此方法:

java
WebClient webClient;

@BeforeEach
void setup(WebApplicationContext context) {
    webClient = MockMvcWebClientBuilder
            .webAppContextSetup(context)
            .build();
}
科特林
lateinit var webClient: WebClient

@BeforeEach
fun setup(context: WebApplicationContext) {
    webClient = MockMvcWebClientBuilder
            .webAppContextSetup(context)
            .build()
}

我们还可以指定其他配置选项,如以下示例所示:

java
WebClient webClient;

@BeforeEach
void setup() {
    webClient = MockMvcWebClientBuilder
        // demonstrates applying a MockMvcConfigurer (Spring Security)
        .webAppContextSetup(context, springSecurity())
        // for illustration only - defaults to ""
        .contextPath("")
        // By default MockMvc is used for localhost only;
        // the following will use MockMvc for example.com and example.org as well
        .useMockMvcForHosts("example.com","example.org")
        .build();
}
科特林
lateinit var webClient: WebClient

@BeforeEach
fun setup() {
    webClient = MockMvcWebClientBuilder
        // demonstrates applying a MockMvcConfigurer (Spring Security)
        .webAppContextSetup(context, springSecurity())
        // for illustration only - defaults to ""
        .contextPath("")
        // By default MockMvc is used for localhost only;
        // the following will use MockMvc for example.com and example.org as well
        .useMockMvcForHosts("example.com","example.org")
        .build()
}

MockMvc 作为替代方案,我们可以通过单独配置实例并将其提供给来执行完全相同的设置MockMvcWebClientBuilder,如下所示:

java
MockMvc mockMvc = MockMvcBuilders
        .webAppContextSetup(context)
        .apply(springSecurity())
        .build();

webClient = MockMvcWebClientBuilder
        .mockMvcSetup(mockMvc)
        // for illustration only - defaults to ""
        .contextPath("")
        // By default MockMvc is used for localhost only;
        // the following will use MockMvc for example.com and example.org as well
        .useMockMvcForHosts("example.com","example.org")
        .build();
科特林
// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed

这更冗长,但是,通过WebClient使用MockMvc实例构建,我们可以轻松获得 MockMvc 的全部功能。

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

在前面的部分中,我们已经了解了如何将 MockMvc 与原始 HtmlUnit API 结合使用。在本节中,我们使用 Selenium WebDriver中的其他抽象来使事情变得更容易。

为什么选择 WebDriver 和 MockMvc?

我们已经可以使用 HtmlUnit 和 MockMvc,为什么还要使用 WebDriver?Selenium WebDriver 提供了一个非常优雅的 API,让我们可以轻松地组织我们的代码。为了更好地展示它的工作原理,我们将在本节中探讨一个示例。

尽管是Selenium的一部分,但 WebDriver 不需要 Selenium 服务器来运行您的测试。

假设我们需要确保正确创建消息。测试包括查找 HTML 表单输入元素、填写它们并做出各种断言。

这种方法会导致许多单独的测试,因为我们也想测试错误条件。例如,如果我们只填写表格的一部分,我们希望确保我们得到一个错误。如果我们填写了整个表格,之后应该会显示新创建的消息。

如果其中一个字段被命名为“summary”,我们可能会在测试中的多个位置重复出现类似于以下内容的内容:

java
HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary");
summaryInput.setValueAttribute(summary);
科特林
val summaryInput = currentPage.getHtmlElementById("summary")
summaryInput.setValueAttribute(summary)

那么如果我们改变idto会发生什么smmry?这样做会迫使我们更新所有测试以纳入此更改。这违反了 DRY 原则,因此我们最好将这段代码提取到自己的方法中,如下所示:

java
public HtmlPage createMessage(HtmlPage currentPage, String summary, String text) {
    setSummary(currentPage, summary);
    // ...
}

public void setSummary(HtmlPage currentPage, String summary) {
    HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary");
    summaryInput.setValueAttribute(summary);
}
科特林
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我们当前所在的 an 中,如以下示例所示:

java
public class CreateMessagePage {

    final HtmlPage currentPage;

    final HtmlTextInput summaryInput;

    final HtmlSubmitInput submit;

    public CreateMessagePage(HtmlPage currentPage) {
        this.currentPage = currentPage;
        this.summaryInput = currentPage.getHtmlElementById("summary");
        this.submit = currentPage.getHtmlElementById("submit");
    }

    public <T> T createMessage(String summary, String text) throws Exception {
        setSummary(summary);

        HtmlPage result = submit.click();
        boolean error = CreateMessagePage.at(result);

        return (T) (error ? new CreateMessagePage(result) : new ViewMessagePage(result));
    }

    public void setSummary(String summary) throws Exception {
        summaryInput.setValueAttribute(summary);
    }

    public static boolean at(HtmlPage page) {
        return "Create Message".equals(page.getTitleText());
    }
}
科特林
    class CreateMessagePage(private val currentPage: HtmlPage) {

        val summaryInput: HtmlTextInput = currentPage.getHtmlElementById("summary")

        val submit: HtmlSubmitInput = currentPage.getHtmlElementById("submit")

        fun <T> createMessage(summary: String, text: String): T {
            setSummary(summary)

            val result = submit.click()
            val error = at(result)

            return (if (error) CreateMessagePage(result) else ViewMessagePage(result)) as T
        }

        fun setSummary(summary: String) {
            summaryInput.setValueAttribute(summary)
        }

        fun at(page: HtmlPage): Boolean {
            return "Create Message" == page.getTitleText()
        }
    }
}

以前,这种模式被称为 页面对象模式。虽然我们当然可以使用 HtmlUnit 做到这一点,但 WebDriver 提供了一些我们将在以下部分中探讨的工具,以使这种模式更容易实现。

MockMvc 和 WebDriver 设置

要将 Selenium WebDriver 与 Spring MVC 测试框架一起使用,请确保您的项目包含对org.seleniumhq.selenium:selenium-htmlunit-driver.

MockMvcHtmlUnitDriverBuilder我们可以使用以下示例轻松创建与 MockMvc 集成的 Selenium WebDriver :

java
WebDriver driver;

@BeforeEach
void setup(WebApplicationContext context) {
    driver = MockMvcHtmlUnitDriverBuilder
            .webAppContextSetup(context)
            .build();
}
科特林
lateinit var driver: WebDriver

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

前面的示例确保任何引用localhost为服务器的 URL 都被定向到我们的MockMvc实例,而无需真正的 HTTP 连接。正常情况下,使用网络连接请求任何其他 URL。这让我们可以轻松地测试 CDN 的使用。

MockMvc 和 WebDriver 用法

现在我们可以像往常一样使用 WebDriver,但不需要将我们的应用程序部署到 Servlet 容器中。例如,我们可以请求视图创建一条带有以下内容的消息:

java
CreateMessagePage page = CreateMessagePage.to(driver);
科特林
val page = CreateMessagePage.to(driver)

然后我们可以填写表单并提交以创建消息,如下所示:

java
ViewMessagePage viewMessagePage =
        page.createMessage(ViewMessagePage.class, expectedSummary, expectedText);
科特林
val viewMessagePage =
    page.createMessage(ViewMessagePage::class, expectedSummary, expectedText)

这 通过利用页面对象模式改进了我们的HtmlUnit 测试的设计。正如我们在 为什么使用 WebDriver 和 MockMvc?,我们可以将页面对象模式与 HtmlUnit 一起使用,但使用 WebDriver 会容易得多。考虑以下 CreateMessagePage实现:

java
public class CreateMessagePage
        extends AbstractPage { (1)

    (2)
    private WebElement summary;
    private WebElement text;

    (3)
    @FindBy(css = "input[type=submit]")
    private WebElement submit;

    public CreateMessagePage(WebDriver driver) {
        super(driver);
    }

    public <T> T createMessage(Class<T> resultPage, String summary, String details) {
        this.summary.sendKeys(summary);
        this.text.sendKeys(details);
        this.submit.click();
        return PageFactory.initElements(driver, resultPage);
    }

    public static CreateMessagePage to(WebDriver driver) {
        driver.get("http://localhost:9990/mail/messages/form");
        return PageFactory.initElements(driver, CreateMessagePage.class);
    }
}
1 CreateMessagePage扩展AbstractPage. 我们不会详细介绍 的详细信息 AbstractPage,但总而言之,它包含我们所有页面的通用功能。例如,如果我们的应用程序有导航栏、全局错误消息和其他功能,我们可以将此逻辑放在共享位置。
2 对于我们感兴趣的 HTML 页面的每个部分,我们都有一个成员变量。这些是类型WebElement。WebDriver PageFactory让我们CreateMessagePage通过自动解析每个WebElement. 该 方法通过使用字段名称并通过HTML 页面中的元素的or查找它来PageFactory#initElements(WebDriver,Class<T>) 自动解析每个。WebElementidname
3 我们可以使用 @FindBy注解 来覆盖默认的查找行为。我们的示例展示了如何使用注解通过选择器 ( input[type=submit]@FindBy ) 查找我们的提交按钮。css
科特林
class CreateMessagePage(private val driver: WebDriver) : AbstractPage(driver) { (1)

    (2)
    private lateinit var summary: WebElement
    private lateinit var text: WebElement

    (3)
    @FindBy(css = "input[type=submit]")
    private lateinit var submit: WebElement

    fun <T> createMessage(resultPage: Class<T>, summary: String, details: String): T {
        this.summary.sendKeys(summary)
        text.sendKeys(details)
        submit.click()
        return PageFactory.initElements(driver, resultPage)
    }
    companion object {
        fun to(driver: WebDriver): CreateMessagePage {
            driver.get("http://localhost:9990/mail/messages/form")
            return PageFactory.initElements(driver, CreateMessagePage::class.java)
        }
    }
}
1 CreateMessagePage扩展AbstractPage. 我们不会详细介绍 的详细信息 AbstractPage,但总而言之,它包含我们所有页面的通用功能。例如,如果我们的应用程序有导航栏、全局错误消息和其他功能,我们可以将此逻辑放在共享位置。
2 对于我们感兴趣的 HTML 页面的每个部分,我们都有一个成员变量。这些是类型WebElement。WebDriver PageFactory让我们CreateMessagePage通过自动解析每个WebElement. 该 方法通过使用字段名称并通过HTML 页面中的元素的or查找它来PageFactory#initElements(WebDriver,Class<T>) 自动解析每个。WebElementidname
3 我们可以使用 @FindBy注解 来覆盖默认的查找行为。我们的示例展示了如何使用注解通过选择器 ( input[type=submit]@FindBy ) 查找我们的提交按钮。css

最后,我们可以验证是否成功创建了一条新消息。以下断言使用AssertJ断言库:

java
assertThat(viewMessagePage.getMessage()).isEqualTo(expectedMessage);
assertThat(viewMessagePage.getSuccess()).isEqualTo("Successfully created a new message");
科特林
assertThat(viewMessagePage.message).isEqualTo(expectedMessage)
assertThat(viewMessagePage.success).isEqualTo("Successfully created a new message")

我们可以看到,我们ViewMessagePage让我们与自定义域模型进行交互。例如,它公开了一个返回Message对象的方法:

java
public Message getMessage() throws ParseException {
    Message message = new Message();
    message.setId(getId());
    message.setCreated(getCreated());
    message.setSummary(getSummary());
    message.setText(getText());
    return message;
}
科特林
fun getMessage() = Message(getId(), getCreated(), getSummary(), getText())

然后我们可以在断言中使用丰富的域对象。

最后,我们一定不要忘记WebDriver在测试完成时关闭实例,如下所示:

java
@AfterEach
void destroy() {
    if (driver != null) {
        driver.close();
    }
}
科特林
@AfterEach
fun destroy() {
    if (driver != null) {
        driver.close()
    }
}

有关使用 WebDriver 的更多信息,请参阅 Selenium WebDriver 文档

先进的MockMvcHtmlUnitDriverBuilder

在到目前为止的示例中,我们MockMvcHtmlUnitDriverBuilder以最简单的方式使用,即WebDriver基于WebApplicationContextSpring TestContext Framework 为我们加载的构建一个。此处重复此方法,如下:

java
WebDriver driver;

@BeforeEach
void setup(WebApplicationContext context) {
    driver = MockMvcHtmlUnitDriverBuilder
            .webAppContextSetup(context)
            .build();
}
科特林
lateinit var driver: WebDriver

@BeforeEach
fun setup(context: WebApplicationContext) {
    driver = MockMvcHtmlUnitDriverBuilder
            .webAppContextSetup(context)
            .build()
}

我们还可以指定额外的配置选项,如下:

java
WebDriver driver;

@BeforeEach
void setup() {
    driver = MockMvcHtmlUnitDriverBuilder
            // demonstrates applying a MockMvcConfigurer (Spring Security)
            .webAppContextSetup(context, springSecurity())
            // for illustration only - defaults to ""
            .contextPath("")
            // By default MockMvc is used for localhost only;
            // the following will use MockMvc for example.com and example.org as well
            .useMockMvcForHosts("example.com","example.org")
            .build();
}
科特林
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();
科特林
// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed

这更冗长,但是,通过WebDriver使用MockMvc实例构建,我们可以轻松获得 MockMvc 的全部功能。

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

在上一节中,我们看到了如何将 MockMvc 与 WebDriver 一起使用。在本节中,我们使用Geb使我们的测试更符合 Groovy-er。

为什么选择 Geb 和 MockMvc?

Geb 由 WebDriver 提供支持,因此它提供了许多与 我们从 WebDriver 获得的相同的好处。然而,Geb 为我们处理了一些样板代码,使事情变得更加容易。

MockMvc 和 Geb 设置

我们可以Browser使用使用 MockMvc 的 Selenium WebDriver 轻松初始化 Geb,如下所示:

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

这确保了任何localhost作为服务器引用的 URL 都被定向到我们的 MockMvc实例,而无需真正的 HTTP 连接。正常使用网络连接请求任何其他 URL。这让我们可以轻松地测试 CDN 的使用。

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来选择我们感兴趣的内容。

最后,我们可以验证是否成功创建了一条新消息,如下:

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

有关如何充分利用 Geb 的更多详细信息,请参阅 The Book of Geb用户手册。

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();
科特林
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();
科特林
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();
科特林
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实现,您可以将其配置为 aRestTemplate以将其绑定到MockMvc实例。这允许使用实际的服务器端逻辑处理请求,但无需运行服务器。以下示例显示了如何执行此操作:

java
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
this.restTemplate = new RestTemplate(new MockMvcClientHttpRequestFactory(mockMvc));

// Test code that uses the above RestTemplate ...
科特林
val mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build()
restTemplate = RestTemplate(MockMvcClientHttpRequestFactory(mockMvc))

// Test code that uses the above RestTemplate ...

3.8.1. 静态导入

与服务器端测试一样,客户端测试的流畅 API 需要一些静态导入。这些很容易通过搜索找到MockRest*。Eclipse 用户应在 Java → Editor → Content Assist → Favorites 下的 Eclipse 首选项中添加 MockRestRequestMatchers.*MockRestResponseCreators.*作为“最喜欢的静态成员”。这允许在输入静态方法名称的第一个字符后使用内容辅助。其他 IDE(例如 IntelliJ)可能不需要任何额外的配置。检查对静态成员代码完成的支持。

3.8.2. 客户端 REST 测试的更多示例

Spring MVC Test 自己的测试包括 客户端 REST 测试的示例测试。

4. 更多资源

有关测试的更多信息,请参阅以下资源:

  • JUnit:“一个程序员友好的 Java 测试框架”。由 Spring Framework 在其测试套件中使用,并在 Spring TestContext Framework中得到支持。

  • TestNG:受 JUnit 启发的测试框架,增加了对测试组、数据驱动测试、分布式测试和其他功能的支持。在Spring TestContext 框架中支持

  • AssertJ:“Fluent assertions for Java”,包括对 Java 8 lambda、流和其他特性的支持。

  • 模拟对象:维基百科中的文章。

  • MockObjects.com:专门用于模拟对象的网站,这是一种在测试驱动开发中改进代码设计的技术。

  • Mockito:基于 Test Spy模式的 Java 模拟库。由 Spring Framework 在其测试套件中使用。

  • EasyMock:Java 库“通过使用 Java 的代理机制动态生成接口(和通过类扩展的对象)来提供模拟对象。”

  • JMock:支持使用模拟对象对 Java 代码进行测试驱动开发的库。

  • DbUnit:JUnit 扩展(也可与 Ant 和 Maven 一起使用),针对数据库驱动的项目,除其他外,将您的数据库置于测试运行之间的已知状态。

  • Testcontainers:支持 JUnit 测试的 Java 库,提供通用数据库、Selenium Web 浏览器或任何其他可以在 Docker 容器中运行的轻量级、一次性实例。

  • Grinder:Java 负载测试框架。

  • SpringMockK:支持使用MockK而不是 Mockito 用 Kotlin 编写的 Spring Boot 集成测试。


1. see XML Configuration