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

1. Introduction to Spring Testing

2. Unit Testing

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

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

2.1. Mock Objects

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

2.1.1. Environment

org.springmework.mock.env包包含EnvironmentPropertySource抽象的模拟实现(请参阅Bean定义概要PropertySource抽象)。MockEnvironmentMockPropertySource对于开发依赖于环境特定属性的代码的容器外测试非常有用。

2.1.2. JNDI

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

The mock JNDI support in the org.springframework.mock.jndi package is officially deprecated as of Spring Framework 5.2 in favor of complete solutions from third parties such as Simple-JNDI.

2.1.3. Servlet API

org.springfrawork.mock.web包包含一组全面的Servlet API模拟对象,这些对象对测试Web上下文、控制器和过滤器很有用。这些模拟对象是针对Spring的Web MVC框架使用的,通常比动态模拟对象(如EasyMock)或替代的Servlet API模拟对象(如模拟对象)更易于使用。

Since Spring Framework 6.0, the mock objects in org.springframework.mock.web are based on the Servlet 6.0 API.

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

2.1.4. Spring Web Reactive

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

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

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

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

2.2. Unit Testing Support Classes

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

2.2.1. General Testing Utilities

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

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

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

  • 允许私有受保护的字段访问的ORM框架(如JPA和Hibernate),而不是域实体中属性的公共setter方法。

  • Spring对批注(如@AuTower@Inject@Resource)支持,为私有受保护的字段、setter方法和配置方法提供依赖项注入。

  • 生命周期回调方法使用@PostConstruct@PreDestroy等注释。

TestSocketUtils是一个简单的实用程序,用于在本地主机上查找可用于集成测试场景的可用TCP端口。

TestSocketUtils可用于在可用随机端口上启动外部服务器的集成测试中。但是,这些实用程序无法保证给定端口的后续可用性,因此是不可靠的。与使用TestSocketUtils为服务器查找可用的本地端口不同,建议您依靠服务器的能力在它选择或由操作系统分配的随机临时端口上启动。要与该服务器交互,您应该向该服务器查询它当前使用的端口。

2.2.2. Spring MVC Testing Utilities

Unit testing Spring MVC Controllers
To unit test your Spring MVC Controller classes as POJOs, use ModelAndViewAssert combined with MockHttpServletRequest, MockHttpSession, and so on from Spring’s Servlet API mocks. For thorough integration testing of your Spring MVC and REST Controller classes in conjunction with your WebApplicationContext configuration for Spring MVC, use the Spring MVC Test Framework instead.

3. Integration Testing

这一节(本章的大部分剩余部分)介绍了Spring应用程序的集成测试。它包括以下主题:

3.1. Overview

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

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

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

Spring框架在Spring-test模块中为集成测试提供了一流的支持。实际JAR文件的名称可能包括发布版本,也可能是长的org.springFrawork.test形式,这取决于您从哪里获得它(有关说明,请参阅依赖项管理的部分)。该库包括org.springFrawork.test包,其中包含用于与Spring容器进行集成测试的有价值的类。此测试不依赖于应用程序服务器或其他部署环境。这类测试的运行速度比单元测试慢,但比等价的Selify测试或依赖于部署到应用程序服务器的远程测试快得多。

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

3.2. Goals of Integration Testing

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

接下来的几个部分描述了每个目标,并提供了指向实施和配置详细信息的链接。

3.2.1. Context Management and Caching

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

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

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

3.2.2. Dependency Injection of Test Fixtures

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

例如,考虑一个场景,其中我们有一个实现标题域实体的数据访问逻辑的类(HibernateTitleRepository)。我们想要编写测试以下方面的集成测试:

  • Spring配置:基本上,与HibernateTitleRepositoryBean的配置相关的一切都正确且存在吗?

  • Hibernate映射文件配置:映射是否正确,延迟加载设置是否正确?

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

请参阅使用TestContext框架注入测试装置的依赖项。

3.2.3. Transaction Management

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

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

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

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

3.2.4. Support Classes for Integration Testing

Spring TestContext框架提供了几个抽象支持类,它们简化了集成测试的编写。这些基本测试类提供了到测试框架的定义良好的挂钩,以及方便的实例变量和方法,使您可以访问:

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

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

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

请参阅TestContext框架的支持类。

3.3. JDBC Testing Support

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

  • CountRowsInTable(..):统计给定表中的行数。

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

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

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

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

AbstractTransactionalJUnit4SpringContextTestsAbstractTransactionalTestNGSpringContextTests提供了方便的方法,这些方法委托给<代码>JdbcTestUtils 中的上述方法。

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

3.4. Annotations

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

3.4.1. Spring Testing Annotations

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

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

@BootstrapWith

@BootstrapWith是一个类级批注,您可以使用它来配置如何引导Spring TestContext框架。具体地说,您可以使用@BootstRapWith指定一个定制的TestContextBootstRapper。有关更多详细信息,请参阅引导TestContext框架一节。

@ContextConfiguration

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

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

下面的示例显示了引用XML文件的@ConextConfiguration批注:

Java
Kotlin
@ContextConfiguration("/test-config.xml") (1)
class XmlApplicationContextTests {
    // class body...
}

              
1 Referring to an XML file.

下面的示例显示了引用类的@ConextConfiguration批注:

Java
Kotlin
@ContextConfiguration(classes = TestConfig.class) (1)
class ConfigClassApplicationContextTests {
    // class body...
}

              
1 Referring to a class.

作为替代方法或除了声明资源位置或组件类之外,您还可以使用@ContextConfiguration声明ApplicationContextInitializer类。以下示例显示了这样的情况:

Java
Kotlin
@ContextConfiguration(initializers = CustomContextInitializer.class) (1)
class ContextInitializerTests {
    // class body...
}

              
1 Declaring an initializer class.

您也可以选择使用@ConextConfiguration来声明ConextLoader策略。但是请注意,您通常不需要显式配置加载器,因为默认加载器支持初始值设定项和资源位置或组件

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

Java
Kotlin
@ContextConfiguration(locations = "/test-context.xml", loader = CustomContextLoader.class) (1)
class CustomLoaderXmlApplicationContextTests {
    // class body...
}

              
1 Configuring both a location and a custom loader.
@ContextConfiguration provides support for inheriting resource locations or configuration classes as well as context initializers that are declared by superclasses or enclosing classes.

有关更多详细信息,请参阅上下文管理@nesteed测试类配置@ConextConfigurationjavados。

@WebAppConfiguration

@WebAppConfiguration是一个类级批注,可用于声明为集成测试加载的ApplicationContext应为WebApplicationContext。仅在测试类上存在@WebAppConfiguration就可以确保为测试加载WebApplicationContext,并使用指向Web应用程序根的路径(即资源库路径)的默认值“file:src/main/webapp”。资源基本路径在后台用于创建MockServletContext,它用作测试的WebApplicationContextServletContext

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

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

              
1 The @WebAppConfiguration annotation.

要覆盖缺省值,可以使用隐式属性指定不同的基本资源路径。同时支持classpath:file:资源前缀。如果未提供资源前缀,则假定该路径为文件系统资源。以下示例显示如何指定类路径资源:

Java
Kotlin
@ContextConfiguration
@WebAppConfiguration("classpath:test-web-resources") (1)
class WebAppTests {
    // class body...
}

              
1 Specifying a classpath resource.

请注意,@WebAppConfiguration必须与@ConextConfiguration一起使用,无论是在单个测试类中还是在测试类层次结构中。有关更多详细信息,请参阅@WebAppConfigurationjavadoc。

@ContextHierarchy

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

Java
Kotlin
@ContextHierarchy({ @ContextConfiguration("/parent-config.xml"), @ContextConfiguration("/child-config.xml") })
class ContextHierarchyTests {
    // class body...
}

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

              

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

@ActiveProfiles

@ActiveProfiles是一个类级批注,用于声明在为集成测试加载ApplicationContext时,哪些Bean定义概要文件应该是活动的。

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

Java
Kotlin
@ContextConfiguration
@ActiveProfiles("dev") (1)
class DeveloperTests {
    // class body...
}

              
1 Indicate that the dev profile should be active.

以下示例指示dev集成配置文件都应处于活动状态:

Java
Kotlin
@ContextConfiguration
@ActiveProfiles({"dev", "integration"}) (1)
class DeveloperIntegrationTests {
    // class body...
}

              
1 Indicate that the dev and integration profiles should be active.
@ActiveProfiles provides support for inheriting active bean definition profiles declared by superclasses and enclosing classes by default. You can also resolve active bean definition profiles programmatically by implementing a custom ActiveProfilesResolver and registering it by using the resolver attribute of @ActiveProfiles.

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

@TestPropertySource

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

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

Java
Kotlin
@ContextConfiguration
@TestPropertySource("/test.properties") (1)
class MyIntegrationTests {
    // class body...
}

              
1 Get properties from test.properties in the root of the classpath.

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

Java
Kotlin
@ContextConfiguration
@TestPropertySource(properties = { "timezone = GMT", "port: 4242" }) (1)
class MyIntegrationTests {
    // class body...
}

              
1 Declare timezone and port properties.

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

@DynamicPropertySource

@DynamicPropertySource是方法级批注,可用于注册动态属性以添加到Environment中为集成测试加载的ApplicationContextPropertySources集中。当您事先不知道属性的值时,动态属性非常有用-例如,如果属性由外部资源管理,例如由Testtainers项目管理的容器。

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

Java
Kotlin
@ContextConfiguration
class MyIntegrationTests {

    static MyExternalServer server = // ...

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

    // tests ...
}

              
1 Annotate a static method with @DynamicPropertySource.
2 Accept a DynamicPropertyRegistry as an argument.
3 Register a dynamic server.port property to be retrieved lazily from the server.

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

@DirtiesContext

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

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

以下示例说明了在各种配置场景中,环境何时会被破坏:

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

    Java
    Kotlin
    @DirtiesContext(classMode = BEFORE_CLASS) (1)
    class FreshContextTests {
        // some tests that require a new Spring container
    }
    
                     
    1 Dirty the context before the current test class.
  • 在当前测试类之后,当在类模式设置为After_CLASS(即,默认类模式)的类上声明时。

    Java
    Kotlin
    @DirtiesContext (1)
    class ContextDirtyingTests {
        // some tests that result in the Spring container being dirtied
    }
    
                     
    1 Dirty the context after the current test class.
  • 在当前测试类中的每个测试方法之前,当在类模式设置为BEVER_EACH_TEST_METHOD的类上声明时。

    Java
    Kotlin
    @DirtiesContext(classMode = BEFORE_EACH_TEST_METHOD) (1)
    class FreshContextTests {
        // some tests that require a new Spring container
    }
    
                     
    1 Dirty the context before each test method.
  • 在当前测试类中的每个测试方法之后,当在类模式设置为After_each_test_method的类上声明时。

    Java
    Kotlin
    @DirtiesContext(classMode = AFTER_EACH_TEST_METHOD) (1)
    class ContextDirtyingTests {
        // some tests that result in the Spring container being dirtied
    }
    
                     
    1 Dirty the context after each test method.
  • 在当前测试之前,当在方法模式设置为BEFORE_METHOD的方法上声明时。

    Java
    Kotlin
    @DirtiesContext(methodMode = BEFORE_METHOD) (1)
    @Test
    void testProcessWhichRequiresFreshAppCtx() {
        // some logic that requires a new Spring container
    }
    
                     
    1 Dirty the context before the current test method.
  • 在当前测试之后,在方法模式设置为After_METHOD(即,默认方法模式)的方法上声明时。

    Java
    Kotlin
    @DirtiesContext (1)
    @Test
    void testProcessWhichDirtiesAppCtx() {
        // some logic that results in the Spring container being dirtied
    }
    
                     
    1 Dirty the context after the current test method.

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

Java
Kotlin
@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 Use the current-level algorithm.

有关<代码>穷举 和<代码>CURRENT_LEVEL 算法的更多详细信息,请参阅DirtiesContext.HierarchyModejavadoc。

@TestExecutionListeners

@TestExecutionListeners用于注册特定测试类、其子类及其嵌套类的侦听器。如果您希望全局注册侦听器,则应该通过TestExecutionListener配置中描述的自动发现机制进行注册。

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

Java
Kotlin
@ContextConfiguration
@TestExecutionListeners({CustomTestExecutionListener.class, AnotherTestExecutionListener.class}) (1)
class CustomTestExecutionListenerTests {
    // class body...
}

              
1 Register two TestExecutionListener implementations.

默认情况下,@TestExecutionListeners支持从超类或封闭类继承侦听器。有关示例和更多详细信息,请参阅@nested测试类配置@TestExecutionListenersjavadoc。如果您发现需要切换回使用默认的TestExecutionListener实现,请参阅注册TestExecutionListener实现中的说明。

@RecordApplicationEvents

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

可以通过测试内部的ApplicationEvents接口访问录制的事件。

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

@Commit

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

下面的示例显示如何使用@Commit批注:

Java
Kotlin
@Commit (1)
@Test
void testProcessWithoutRollback() {
    // ...
}

              
1 Commit the result of the test to the database.
@Rollback

@Rollback事务性测试方法的事务是否应该在测试方法完成后回滚。如果为True,则回滚事务。否则,事务被提交(另请参见@Commit)。在Spring TestContext框架中,集成测试的回滚默认为true,即使没有显式声明@Rollback

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

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

Java
Kotlin
@Rollback(false) (1)
@Test
void testProcessWithoutRollback() {
    // ...
}

              
1 Do not roll back the result.
@BeforeTransaction

@BeForeTransaction表示,对于已使用Spring的@Transaction注释配置为在事务内运行的测试方法,应在启动事务之前运行带注释的void方法。@BeForeTransaction方法不要求为公共,可以在基于Java 8的接口默认方法上声明。

下面的示例显示如何使用@BeForeTransaction批注:

Java
Kotlin
@BeforeTransaction (1)
void beforeTransaction() {
    // logic to be run before a transaction is started
}

              
1 Run this method before a transaction.
@AfterTransaction

@AfterTransaction表示,对于已使用Spring的@Transaction注释配置为在事务内运行的测试方法,应在事务结束后运行带注释的void方法。@AfterTransaction方法不要求为公共,可以在基于Java 8的接口默认方法上声明。

Java
Kotlin
@AfterTransaction (1)
void afterTransaction() {
    // logic to be run after a transaction has ended
}

              
1 Run this method after a transaction.
@Sql

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

Java
Kotlin
@Test
@Sql({"/test-schema.sql", "/test-user-data.sql"}) (1)
void userTest() {
    // run code that relies on the test schema and test data
}

              
1 Run two scripts for this test.

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

@SqlConfig

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

Java
Kotlin
@Test
@Sql( scripts = "/test-user-data.sql", config = @SqlConfig(commentPrefix = "`", separator = "@@") (1) )
void userTest() {
    // run code that relies on the test data
}

              
1 Set the comment prefix and the separator in SQL scripts.
@SqlMergeMode

@SqlMergeMode用于标注测试类或测试方法,配置方法级@SQL声明是否与类级@SQL声明合并。如果测试类或测试方法上未声明@SqlMergeMode,则默认情况下将使用覆盖合并模式。使用覆盖模式,方法级@SQL声明将有效地覆盖类级@SQL声明。

请注意,方法级@SqlMergeModel声明覆盖类级声明。

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

Java
Kotlin
@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 Set the @Sql merge mode to MERGE for all test methods in the class.

以下示例说明如何在方法级别使用@SqlMergeMode

Java
Kotlin
@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 Set the @Sql merge mode to MERGE for a specific test method.
@SqlGroup

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

Java
Kotlin
@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 Declare a group of SQL scripts.

3.4.2. Standard Annotation Support

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

  • @自动连接

  • @限定符

  • @Value

  • @Resource(jakarta.注解)(如果存在JSR-250

  • @ManagedBean(jakarta.注解)(如果存在JSR-250

  • 如果存在JSR-330,则@Inject(jakarta.inject)

  • 如果存在JSR-330,则@已命名(jakarta.inject)

  • @PersistenceContext(jakarta.Persistence)(如果存在JPA

  • @PersistenceUnit(jakarta.Persistence)(如果存在JPA

  • @Transaction(org.springFramework.Transaction.Annotation)有限属性支持

JSR-250 Lifecycle Annotations

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

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

3.4.3. Spring JUnit 4 Testing Annotations

@IfProfileValue

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

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

下面的示例显示具有@IfProfileValue批注的测试:

Java
Kotlin
@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 Run this test only when the Java vendor is "Oracle Corporation".

或者,您可以使用列表(使用OR语义)配置@IfProfileValue,以在JUnit4环境中实现对测试组的类似于TestNG的支持。请考虑以下示例:

Java
Kotlin
@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 Run this test for unit tests and integration tests.
@ProfileValueSourceConfiguration

@ProfileValueSourceConfiguration是一个类级批注,它指定在检索通过@IfProfileValue批注配置的配置文件值时使用哪种类型的ProfileValueSource。如果没有为测试声明@ProfileValueSourceConfiguration,则默认使用SystemProfileValueSource。以下示例说明如何使用@ProfileValueSourceConfiguration

Java
Kotlin
@ProfileValueSourceConfiguration(CustomProfileValueSource.class) (1)
public class CustomProfileValueSourceTests {
    // class body...
}

              
1 Use a custom profile value source.
@Timed

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

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

Java
Kotlin
@Timed(millis = 1000) (1)
public void testProcessWithOneSecondTimeout() {
    // some logic that should not take longer than 1 second to run
}

              
1 Set the time period for the test to one second.

Spring的@Timed注释的语义与JUnit4的@Test(TimeOut=…)不同​)支持。具体地说,由于JUnit4处理测试执行超时的方式(即通过在单独的<代码>线程 中执行测试方法),<代码>@测试(TimeOut=…​)如果测试花费的时间太长,则会抢先使测试失败。另一方面,Spring的@timed不会抢先通过测试,而是等待测试完成后才会失败。

@Repeat

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

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

Java
Kotlin
@Repeat(10) (1)
@Test
public void testProcessRepeatedly() {
    // ...
}

              
1 Repeat this test ten times.

3.4.4. Spring JUnit Jupiter Testing Annotations

当与SpringExtension和JUnitJupiter(即JUnit5中的编程模型)结合使用时,支持以下批注:

@SpringJUnitConfig

@SpringJUnitConfig是一个组合批注,它将来自JUnitJupiter的@ExtendWith(SpringExtension.class)与来自Spring TestContext框架的@ConextConfiguration组合在一起。它可以在类级别用作@ConextConfiguration的临时替代。关于配置选项,@ConextConfiguration@SpringJUnitConfig的唯一区别是组件类可以在@SpringJUnitConfig中用属性声明。

以下示例显示如何使用@SpringJUnitConfig批注指定配置类:

Java
Kotlin
@SpringJUnitConfig(TestConfig.class) (1)
class ConfigurationClassJUnitJupiterSpringTests {
    // class body...
}

              
1 Specify the configuration class.

下面的示例显示如何使用@SpringJUnitConfig批注指定配置文件的位置:

Java
Kotlin
@SpringJUnitConfig(locations = "/test-config.xml") (1)
class XmlJUnitJupiterSpringTests {
    // class body...
}

              
1 Specify the location of a configuration file.

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

@SpringJUnitWebConfig

@SpringJUnitWebConfig是一个组合批注,它将来自JUnitJupiter的@ExtendWith(SpringExtension.class)与来自Spring TestContext框架的@ConextConfiguration@WebAppConfiguration组合在一起。您可以在类级别使用它作为@ConextConfiguration@WebAppConfiguration的临时替代。关于配置选项,@ConextConfiguration@SpringJUnitWebConfig的唯一区别是可以通过@SpringJUnitWebConfig中的属性声明组件类。此外,您只能通过使用@SpringJUnitWebConfig中的resource cePath属性来覆盖@WebAppConfiguration中的属性。

以下示例显示如何使用@SpringJUnitWebConfig批注指定配置类:

Java
Kotlin
@SpringJUnitWebConfig(TestConfig.class) (1)
class ConfigurationClassJUnitJupiterSpringWebTests {
    // class body...
}

              
1 Specify the configuration class.

下面的示例显示如何使用@SpringJUnitWebConfig批注指定配置文件的位置:

Java
Kotlin
@SpringJUnitWebConfig(locations = "/test-config.xml") (1)
class XmlJUnitJupiterSpringWebTests {
    // class body...
}

              
1 Specify the location of a configuration file.

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

@TestConstructor

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

如果测试类上不存在@TestConstructor或元存在,则将使用默认的测试构造函数自动布线模式。有关如何更改默认模式的详细信息,请参阅下面的提示。然而,请注意,构造函数上的@AuTower的本地声明优先于@TestConstructor和默认模式。

Changing the default test constructor autowire mode

通过将代码JVM系统属性设置为<spring.test.constructor.autowire.mode>ALL,可以更改默认的测试构造函数自动连接模式。或者,可以通过SpringProperties机制设置默认模式。

从Spring Framework5.3开始,默认模式也可以配置为JUnit平台配置参数

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

As of Spring Framework 5.2, @TestConstructor is only supported in conjunction with the SpringExtension for use with JUnit Jupiter. Note that the SpringExtension is often automatically registered for you – for example, when using annotations such as @SpringJUnitConfig and @SpringJUnitWebConfig or various test-related annotations from Spring Boot Test.
@NestedTestConfiguration

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

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

Changing the default enclosing configuration inheritance mode

默认的包含配置继承模式是<spring.test.enclosing.configuration>继承 ,但可以通过将CodeJVM系统属性设置为<代码>覆盖 来更改它。或者,可以通过SpringProperties机制设置默认模式。

Spring TestContext框架遵循以下批注的@NestedTestConfiguration语义。

The use of @NestedTestConfiguration typically only makes sense in conjunction with @Nested test classes in JUnit Jupiter; however, there may be other testing frameworks with support for Spring and nested test classes that make use of this annotation.

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

@EnabledIf

@EnabledIf用于表示带注释的JUnit Jupiter测试类或测试方法已启用,如果提供的表达式的计算结果为true,则应运行该测试类或测试方法。具体地说,如果表达式的计算结果为Boolan.TRUE字符串等于true(忽略大小写),则启用测试。当应用于类级别时,该类中的所有测试方法在默认情况下也会自动启用。

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

  • Spring表达式语言(Spel)表达式。例如:@EnabledIf(“#{systemProperties[‘os.name’].toLowerCase().contains(‘mac’)}”)

  • 在Spring环境中可用的属性的占位符。例如:@EnabledIf(“${smoke.tests.enabled}”)

  • 文本文字。例如:@EnabledIf(“true”)

然而,请注意,不是属性占位符动态解析结果的文本文本没有实际价值,因为@EnabledIf(“False”)等同于@Disable@EnabledIf(“true”)在逻辑上没有意义。

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

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

              

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

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

@DisabledIf

@DisabledIf用于通知带注释的JUnit Jupiter测试类或测试方法被禁用,如果提供的表达式的计算结果为true,则不应运行该测试类或测试方法。具体地说,如果表达式的计算结果为Boolan.TRUE字符串等于true(忽略大小写),则测试将被禁用。在类级别应用时,该类中的所有测试方法也会自动禁用。

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

  • Spring表达式语言(Spel)表达式。例如:@DisabledIf(“#{systemProperties[‘os.name’].toLowerCase().contains(‘mac’)}”)

  • 在Spring环境中可用的属性的占位符。例如:@DisabledIf(“${smoke.tests.disabled}”)

  • 文本文字。例如:@DisabledIf(“true”)

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

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

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

              

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

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

3.4.5. Meta-Annotation Support for Testing

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

您可以将以下各项作为元注释与TestContext框架结合使用。

  • @BootstrapWith

  • @ConextConfiguration

  • @ConextHierarchy

  • @ActiveProfiles

  • @TestPropertySource

  • @DirtiesContext

  • @WebAppConfiguration

  • @TestExecutionListeners

  • @事务性

  • @BeForeTransaction

  • @AfterTransaction

  • @Commit

  • @回滚

  • @SQL

  • @SqlConfig

  • @SqlMergeMode

  • @SqlGroup

  • @Repeat(仅在JUnit4上受支持)

  • @Timed(仅在JUnit4上受支持)

  • @IfProfileValue(仅在JUnit4上受支持)

  • @ProfileValueSourceConfiguration(仅JUnit4支持)

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

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

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

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

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

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

请考虑以下示例:

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

             

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

Java
Kotlin
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
public @interface TransactionalDevTestConfig { }

             

然后,我们可以使用定制的@TransactionalDevTestConfig注释来简化各个基于JUnit4的测试类的配置,如下所示:

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

@RunWith(SpringRunner.class)
@TransactionalDevTestConfig
public class UserRepositoryTests { }

             

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

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

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

             

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

Java
Kotlin
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(SpringExtension.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
public @interface TransactionalDevTestConfig { }

             

然后,我们可以使用定制的@TransactionalDevTestConfig注释来简化各个基于JUnitJupiter的测试类的配置,如下所示:

Java
Kotlin
@TransactionalDevTestConfig
class OrderRepositoryTests { }

@TransactionalDevTestConfig
class UserRepositoryTests { }

             

由于JUnit Jupiter支持使用@Test@RepeatedTest参数化测试等作为元批注,因此您还可以在测试方法级别创建自定义组合批注。例如,如果我们希望创建一个组合批注,该批注将来自JUnit Jupiter的@Test@tag批注与来自Spring的@Transaction批注结合在一起,我们可以创建一个@TransactionalIntegrationTest批注,如下所示:

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

             

然后,我们可以使用定制的@TransactionalIntegrationTest注释来简化各个基于JUnitJupiter的测试方法的配置,如下所示:

Java
Kotlin
@TransactionalIntegrationTest
void saveOrder() { }

@TransactionalIntegrationTest
void deleteOrder() { }

             

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

3.5. Spring TestContext Framework

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

除了通用的测试基础设施,TestContext框架还提供了对JUnit4、JUnitJupiter(又名JUnit5)和TestNG的显式支持。对于JUnit4和TestNG,Spring提供了抽象支持类。此外,Spring为JUnit4提供了一个定制的JUnitRunner和定制的JUnit规则,并为JUnit Jupiter提供了一个定制的扩展,让您可以编写所谓的POJO测试类。POJO测试类不需要扩展特定的类层次结构,例如抽象支持类。

以下部分概述了TestContext框架的内部结构。如果您只对使用框架感兴趣,而对使用您自己的定制侦听器或定制加载器扩展它不感兴趣,那么可以直接转到配置(上下文管理依赖注入事务管理)、支持类注释支持部分。

3.5.1. Key Abstractions

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

TestContext

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

TestContextManager

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

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

  • 测试实例后处理。

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

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

  • 紧接在测试方法执行之后但在测试拆卸之前。

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

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

TestExecutionListener

TestExecutionListener定义用于响应监听器注册到的TestConextManager发布的测试执行事件的API。参见TestExecutionListener配置

Context Loaders

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

SmartConextLoaderConextLoader接口的扩展,它取代了原始的最小ConextLoaderSPI。具体地说,SmartConextLoader可以选择处理资源位置、组件类或上下文初始值设定项。此外,SmartConextLoader可以设置活动的Bean定义配置文件,并在它加载的上下文中测试属性源。

Spring提供了以下实现:

  • DelegatingSmartConextLoader:两个默认加载器之一,它在内部委托给AnnotationConfigConextLoaderGenericXmlConextLoaderGenericGroovyXmlConextLoader,具体取决于为测试类声明的配置,或者取决于是否存在默认位置或默认配置类。只有当Groovy位于类路径上时,才会启用Groovy支持。

  • WebDelegatingSmartConextLoader AnnotationConfigWebContextLoader,>:它是两个默认加载器之一,它根据为测试类声明的配置或是否存在默认位置或默认配置类,在内部将一个<GenericGroovyXmlWebContextLoader,>GenericXmlWebConextLoader委托给它。仅当测试类上存在@WebAppConfiguration时,才使用WebConextLoader。只有当Groovy位于类路径上时,才会启用Groovy支持。

  • AnnotationConfigConextLoader:从组件类加载标准ApplicationContext

  • AnnotationConfigWebContextLoader:从组件类加载WebApplicationContext

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

  • GenericGroovyXmlWebContextLoader:从资源位置加载WebApplicationContext,这些资源位置要么是Groovy脚本,要么是XML配置文件。

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

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

3.5.2. Bootstrapping the TestContext Framework

Spring TestContext框架内部的默认配置足以满足所有常见用例。然而,有时开发团队或第三方框架想要更改默认的ConextLoader,实现自定义的TestContextConextCache,扩充ConextCustomizerFactoryTestExecutionListener的默认实现集,等等。对于这种对TestContext框架如何操作的低级控制,Spring提供了一种自举策略。

TestContextBootstrapper定义用于引导TestContext框架的SPI。TestConextManager使用TestContextBootstrapper加载当前测试的TestExecutionListener实现,并构建它管理的TestContext。您可以使用@BootstRapWith为测试类(或测试类层次结构)配置定制的引导策略,可以直接使用,也可以作为元注释使用。如果没有使用@BootstrapWith显式配置引导程序,则根据是否存在@WebAppConfiguration,使用DefaultTestContextBootstrapperWebTestContextBootstrapper

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

3.5.3. TestExecutionListener Configuration

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

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

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

  • ApplicationEventsTestExecutionListener:支持ApplicationEvents

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

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

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

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

  • EventPublishingTestExecutionListener:将测试执行事件发布到测试的<代码>应用程序上下文 (请参阅测试执行事件)。

Registering TestExecutionListener Implementations

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

Switching to default TestExecutionListener implementations

如果您扩展了一个用@TestExecutionListeners注释的类,并且需要切换到使用默认的侦听器集,则可以使用以下代码注释您的类。

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

                   
Automatic Discovery of Default TestExecutionListener Implementations

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

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

Merging TestExecutionListener Implementations

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

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

              

这种方法的挑战在于,它要求开发人员准确地知道默认情况下注册了哪些监听器。此外,默认侦听器的集合可以在不同的版本中更改。例如,<DirtiesContextBeforeModesTestExecutionListener> >SqlScriptsTestExecutionListener 是在Spring Framework4.1中引入的,而SqlScriptsTestExecutionListener是在Spring Framework4.2中引入的。此外,像Spring Boot和Spring Security这样的第三方框架通过使用前面提到的自动发现机制来注册它们自己的默认TestExecutionListener实现。

为了避免知道并重新声明所有默认侦听器,您可以将@TestExecutionListenersmergeMode属性设置为MergeMode.MERGE_WITH_DEFAULTSMERGE_WITH_DEFAULTS表示本地声明的监听程序应该与默认监听程序合并。合并算法确保从列表中删除重复项,并根据AnnotationAwareOrderCompator的语义对合并后的侦听器的结果集进行排序,如排序TestExecutionListener实现中所述。如果侦听器实现Order或使用@Order注释,它可能会影响它与默认值合并的位置。否则,本地声明的侦听器将在合并时附加到默认侦听器列表中。

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

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

              

3.5.4. Application Events

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

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

  • 确保您的测试类使用@RecordApplicationEvents.进行了注释或元注释

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

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

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

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

Java
Kotlin
@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 Annotate the test class with @RecordApplicationEvents.
2 Inject the ApplicationEvents instance for the current test.
3 Use the ApplicationEvents API to count how many OrderSubmitted events were published.

有关ApplicationEvents接口的详细信息,请参阅ApplicationEventsjavadoc

3.5.5. Test Execution Events

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

  • BeForeTestClassEvent

  • 准备测试实例事件

  • BeForeTestMethodEvent

  • BeForeTestExecutionEvent

  • AfterTestExecutionEvent

  • AfterTestMethodEvent

  • AfterTestClassEvent

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

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

因此,只有在另一个TestExecutionListener加载了ApplicationContext之后,才会发布BeForeTestClassEvent。例如,注册了一组默认的TestExecutionListener实现后,将不会为使用特定测试ApplicationContext的第一个测试类发布BeForeTestClassEvent,但将为同一测试套件中使用相同测试ApplicationContext的任何后续测试类发布BeForeTestClassEvent,因为当后续测试类运行时,该上下文将已经加载(只要上下文没有通过上下文缓存或max-Size icevtion策略被移除)。

如果要确保始终为每个测试类发布BeForeTestClassEvent,则需要注册一个TestExecutionListener,它在beForeTestClass回调中加载ApplicationContext,并且必须在EventPublishingTestExecutionListener.之前注册TestExecutionListener

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

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

  • @BeForeTestClass

  • @PrepareTestInstance

  • @BeForeTestMethod

  • @BeForeTestExecution

  • @AfterTestExecution

  • @AfterTestMethod

  • @AfterTestClass

Exception Handling

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

Asynchronous Listeners

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

3.5.6. Context Management

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

@Autowired ApplicationContext

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

Java
Kotlin
@SpringJUnitConfig
class MyTest {

    @Autowired (1)
    ApplicationContext applicationContext;

    // class body...
}

                  
1 Injecting the ApplicationContext.

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

Java
Kotlin
@SpringJUnitWebConfig (1)
class MyWebAppTest {

    @Autowired (2)
    WebApplicationContext wac;

    // class body...
}

                  
1 Configuring the WebApplicationContext.
2 Injecting the WebApplicationContext.

通过使用<DependencyInjectionTestExecutionListener,>@AuTower的依赖项注入是由默认配置的代码提供的(参见测试装置的依赖项注入)。

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

以下各节解释如何使用Spring的@ConextConfiguration注释,通过使用XML配置文件、Groovy脚本、组件类(通常是@Configuration类)或上下文初始化器来配置测试ApplicationContext。或者,您也可以针对高级用例实现和配置您自己的自定义SmartConextLoader

Context Configuration with XML resources

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

Java
Kotlin
@ExtendWith(SpringExtension.class)
// ApplicationContext will be loaded from "/app-config.xml" and
// "/test-config.xml" in the root of the classpath
@ContextConfiguration(locations={"/app-config.xml", "/test-config.xml"}) (1)
class MyTest {
    // class body...
}

              
1 Setting the locations attribute to a list of XML files.

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

Java
Kotlin
@ExtendWith(SpringExtension.class)
@ContextConfiguration({"/app-config.xml", "/test-config.xml"}) (1)
class MyTest {
    // class body...
}

              
1 Specifying XML files without using the location attribute.

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

Java
Kotlin
@ExtendWith(SpringExtension.class)
// ApplicationContext will be loaded from
// "classpath:com/example/MyTest-context.xml"
@ContextConfiguration (1)
class MyTest {
    // class body...
}

              
1 Loading configuration from the default location.
Context Configuration with Groovy Scripts

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

Enabling Groovy script support
Support for using Groovy scripts to load an ApplicationContext in the Spring TestContext Framework is enabled automatically if Groovy is on the classpath.

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

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

              
1 Specifying the location of Groovy configuration files.

如果您在@ConextConfiguration注释中同时省略了位置属性,那么TestContext框架将尝试检测默认的Groovy脚本。具体地说,GenericGroovyXmlConextLoaderGenericGroovyXmlWebConextLoader根据测试类的名称检测默认位置。如果您的类被命名为com.example.MyTest,则Groovy上下文加载器从“classpath:com/example/MyTestContext.groovy”.加载您的应用程序上下文以下示例显示如何使用默认设置:

Java
Kotlin
@ExtendWith(SpringExtension.class)
// ApplicationContext will be loaded from
// "classpath:com/example/MyTestContext.groovy"
@ContextConfiguration (1)
class MyTest {
    // class body...
}

              
1 Loading configuration from the default location.
Declaring XML configuration and Groovy scripts simultaneously

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

下面的清单显示了如何在集成测试中将两者结合起来:

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

                   
Context Configuration with Component Classes

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

Java
Kotlin
@ExtendWith(SpringExtension.class)
// ApplicationContext will be loaded from AppConfig and TestConfig
@ContextConfiguration(classes = {AppConfig.class, TestConfig.class}) (1)
class MyTest {
    // class body...
}

              
1 Specifying component classes.
Component Classes

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

  • @configuration注释的类。

  • 组件(即,使用@Component@Service@Repository或其他构造型注释标注的类)。

  • 一个符合JSR-330的类,用jakarta.inject批注。

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

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

有关组件类的配置和语义的更多信息,请参见@configuration@Bean的javadoc,其中特别关注@BeanLite模式的讨论。

如果您在@ConextConfiguration注释中省略了CLASS属性,那么TestContext框架会尝试检测默认配置类的存在。具体地说,AnnotationConfigConextLoaderAnnotationConfigWebConextLoader检测满足配置类实现要求的测试类的所有静态类,如@configurationjavadoc中指定的。请注意,配置类的名称是任意的。此外,如果需要,一个测试类可以包含多个静态嵌套配置类。在下面的示例中,OrderServiceTest类声明了一个名为Config静态嵌套配置类,该类自动用于加载测试类的ApplicationContext

Java
Kotlin
@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 Loading configuration information from the nested Config class.
Mixing XML, Groovy Scripts, and Component Classes

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

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

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

Context Configuration with Context Initializers

若要使用上下文初始值设定项为您的测试配置ApplicationContext,请使用@ConextConfiguration注释您的测试类,并使用包含对实现ApplicationContextInitializer的类的引用的数组来配置Initializers属性。然后使用声明的上下文初始值设定项来初始化为测试加载的ConfigurableApplicationContext。请注意,每个声明的初始值设定项支持的具体ConfigurableApplicationContext类型必须与正在使用的SmartConextLoader创建的ApplicationContext类型兼容(通常是GenericApplicationContext)。此外,调用初始化器的顺序取决于它们是实现了Spring的Order接口,还是使用了Spring的@Order批注或标准的@优先级批注。以下示例显示如何使用初始值设定项:

Java
Kotlin
@ExtendWith(SpringExtension.class)
// ApplicationContext will be loaded from TestConfig
// and initialized by TestAppCtxInitializer
@ContextConfiguration( classes = TestConfig.class, initializers = TestAppCtxInitializer.class) (1)
class MyTest {
    // class body...
}

              
1 Specifying configuration by using a configuration class and an initializer.

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

Java
Kotlin
@ExtendWith(SpringExtension.class)
// ApplicationContext will be initialized by EntireAppInitializer
// which presumably registers beans in the context
@ContextConfiguration(initializers = EntireAppInitializer.class) (1)
class MyTest {
    // class body...
}

              
1 Specifying configuration by using only an initializer.
Context Configuration Inheritance

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

如果@ConextConfiguration中的inheritLocationsinheritInitializers属性设置为FALSE,则测试类的资源位置或组件类以及上下文初始值设定项将分别隐藏并有效替换超类定义的配置。

As of Spring Framework 5.3, test configuration may also be inherited from enclosing classes. See @Nested test class configuration for details.

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

Java
Kotlin
@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 Configuration file defined in the superclass.
2 Configuration file defined in the subclass.

类似地,在下一个使用组件类的示例中,ExtendedTestApplicationContext是从BaseConfigExtendedConfig类加载的。因此,ExtendedConfig中定义的Bean可以重写(即替换)BaseConfig中定义的Bean。下面的示例显示一个类如何扩展另一个类并同时使用其自己的配置类和超类的配置类:

Java
Kotlin
// 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 Configuration class defined in the superclass.
2 Configuration class defined in the subclass.

在下一个使用上下文初始化器的示例中,使用BaseInitializerExtendedInitializer初始化ExtendedTestApplicationContext。但是请注意,调用初始化器的顺序取决于它们是实现了Spring的Order接口,还是使用了Spring的@Order批注或标准的@优先级批注。下面的示例说明一个类如何扩展另一个类并同时使用其自己的初始值设定项和超类的初始值设定项:

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

// ApplicationContext will be initialized by BaseInitializer
// and ExtendedInitializer
@SpringJUnitConfig(initializers = ExtendedInitializer.class) (2)
class ExtendedTest extends BaseTest {
    // class body...
}

              
1 Initializer defined in the superclass.
2 Initializer defined in the subclass.
Context Configuration with Environment Profiles

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

You can use @ActiveProfiles with any implementation of the SmartContextLoader SPI, but @ActiveProfiles is not supported with implementations of the older ContextLoader SPI.

考虑两个具有XML配置和@Configuration类的示例:

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

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

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

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

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

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

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

</beans>
              
Java
Kotlin
@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
    }
}

              

TransferServiceTest运行时,其ApplicationContext将从类路径根目录下的app-config.xml配置文件中加载。如果您检查app-config.xml,您可以看到bactRepositoryBean依赖于DataSourceBean。但是,DataSource未定义为顶级Bean。相反,DataSource定义了三次:生产配置文件中、开发配置文件中和默认配置文件中。

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

有时将Bean分配给默认配置文件很有用。只有当没有特别激活其他配置文件时,才会包含默认配置文件中的Bean。您可以使用它来定义要在应用程序的默认状态中使用的“回退”Bean。例如,您可以显式地为开发生产配置文件提供数据源,但在这两个配置文件都不活动时将内存中的数据源定义为默认数据源。

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

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

              
Java
Kotlin
@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");
    }
}

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

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

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

    @Autowired
    TransferService transferService;

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

              

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

  • TransferServiceConfig:通过依赖注入获取一个DataSource,使用@AuTower

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

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

  • DefaultDataConfig:在没有配置文件处于活动状态的情况下,为默认嵌入式数据库定义一个DataSource

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

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

As of Spring Framework 5.3, test configuration may also be inherited from enclosing classes. See @Nested test class configuration for details.
Java
Kotlin
@SpringJUnitConfig({ TransferServiceConfig.class, StandaloneDataConfig.class, JndiDataConfig.class, DefaultDataConfig.class})
@ActiveProfiles("dev")
abstract class AbstractIntegrationTest {
}

              
Java
Kotlin
// "dev" profile inherited from superclass
class TransferServiceTest extends AbstractIntegrationTest {

    @Autowired
    TransferService transferService;

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

              

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

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

              

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

  • 当前操作系统。

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

  • 某些环境变量的存在。

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

  • 其他方面的担忧。

要以编程方式解析活动的Bean定义配置文件,您可以实现一个自定义的ActiveProfilesResolver,并使用@ActiveProfiles解析器属性注册它。有关更多信息,请参阅相应的javadoc。以下示例演示如何实现和注册自定义OperatingSystemActiveProfilesResolver

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

              
Java
Kotlin
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};
    }
}

              
Context Configuration with Test Property Sources

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

您可以将@TestPropertySource用于SmartConextLoaderSPI的任何实现,但较旧的ConextLoaderSPI的实现不支持@TestPropertySource

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

Declaring Test Property Sources

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

传统的和基于XML语言的属性文件格式都支持代码转换,例如“classpath:/com/example/test.properties”或“file:///path/to/file.xml”.“ - 

每条路径都被解释为一个Spring资源。普通路径(例如,“test.properties”)被视为相对于定义测试类的包的类路径资源。以斜杠开头的路径被视为绝对类路径资源(例如:“/org/Example/test.xml”)。使用指定的资源协议加载引用URL的路径(例如,以classpath:file:http:为前缀的路径)。不允许使用资源位置通配符(如*/.properties):每个位置的计算结果必须恰好为一个.properties.xml资源。

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

Java
Kotlin
@ContextConfiguration
@TestPropertySource("/test.properties") (1)
class MyIntegrationTests {
    // class body...
}

               
1 Specifying a properties file with an absolute path.

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

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

  • key=值

  • 键:值

  • 密钥值

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

Java
Kotlin
@ContextConfiguration
@TestPropertySource(properties = {"timezone = GMT", "port: 4242"}) (1)
class MyIntegrationTests {
    // class body...
}

               
1 Setting two properties by using two variations of the key-value syntax.

从Spring Framework5.2开始,@TestPropertySource可以作为可重复的批注。这意味着您可以在一个测试类上有多个@TestPropertySource声明,其中后面的@TestPropertySource注释中的Locations属性注释覆盖了前面@TestPropertySource注释中的那些注释。

此外,您可以在一个测试类上声明多个合成批注,每个批注都用@TestPropertySource进行了元批注,所有这些@TestPropertySource声明都将为您的测试属性源做出贡献。

直接呈现@TestPropertySource批注始终优先于元呈现@TestPropertySource批注。换句话说,来自直接呈现的@TestPropertySource批注的位置属性将覆盖用作元批注的@TestPropertySource批注中的位置属性

Default Properties File Detection

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

Precedence

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

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

Java
Kotlin
@ContextConfiguration
@TestPropertySource( locations = "/test.properties", properties = {"timezone = GMT", "port: 4242"} )
class MyIntegrationTests {
    // class body...
}

               
Inheriting and Overriding Test Property Sources

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

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

As of Spring Framework 5.3, test configuration may also be inherited from enclosing classes. See @Nested test class configuration for details.

在下一个示例中,仅使用base.Properties文件作为测试属性源来加载BaseTestApplicationContext。相比之下,ExtendedTestApplicationContext是通过使用base.Properties扩展的.Properties文件作为测试属性源位置来加载的。下面的示例说明如何使用属性文件在子类及其超类中定义属性:

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

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

               

在下一个示例中,仅使用内联的key1属性加载BaseTestApplicationContext。相比之下,ExtendedTestApplicationContext是使用内联的key1key2属性加载的。下面的示例说明如何使用内联属性在子类及其超类中定义属性:

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

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

               
Context Configuration with Dynamic Property Sources

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

@DynamicPropertySource注释及其支持的基础设施最初设计用于允许来自Testtainers的测试的属性轻松公开给Spring集成测试。但是,此功能也可以与其生命周期在测试的ApplicationContext之外维护的任何形式的外部资源一起使用。

与在类级别应用的@TestPropertySource批注不同,@DynamicPropertySource必须应用于接受单个DynamicPropertyRegistry参数的静态方法,该参数用于将名称-值对添加到Environment。值是动态的,并通过供应商提供,该供应商仅在解析属性时调用。通常,方法引用用于提供值,如下面的示例所示,该示例使用TestContainers项目在SpringApplicationContext外部管理Redis容器。托管Redis容器的IP地址和端口通过redis.hostredis.port属性提供给测试的ApplicationContext中的组件。这些属性可以通过Spring的Environment抽象访问,也可以直接注入到Spring管理的组件中--例如,分别通过@Value(“${redis.host}”)@Value(“${redis.port}”)

如果您在基类中使用@DynamicPropertySource,并且发现由于子类之间的动态属性更改而导致子类中的测试失败,则可能需要使用@DirtiesContext来注释您的基类,以确保每个子类都获得其自己的具有正确动态属性的ApplicationContext

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

    @Container
    static RedisContainer redis = new RedisContainer();

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

    // tests ...

}

              
Precedence

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

Loading a WebApplicationContext

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

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

注意,Spring对WebApplicationContext实现的测试支持与对标准ApplicationContext实现的支持不相上下。当使用WebApplicationContext进行测试时,您可以使用@ConextConfiguration声明XML配置文件、Groovy脚本或@Configuration类。您还可以自由使用任何其他测试注释,例如@ActiveProfiles@TestExecutionListeners@SQL@Rollback等。

本节中的其余示例显示了用于加载WebApplicationContext的一些不同配置选项。下面的示例显示了TestContext框架对约定优先于配置的支持:

Java
Kotlin
@ExtendWith(SpringExtension.class)

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

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

              

如果您使用@WebAppConfiguration注释测试类,而没有指定资源基础路径,则资源路径实际上默认为file:src/main/webapp。类似地,如果您声明@ConextConfiguration而没有指定资源位置、组件或上下文初始值设定项,则Spring会尝试使用约定(即WacTest-context.xmlWacTest类或静态嵌套的@Configuration类位于同一个包中)来检测配置的存在。

以下示例显示如何使用@WebAppConfiguration显式声明资源基本路径,并使用@ConextConfiguration显式声明XML资源位置:

Java
Kotlin
@ExtendWith(SpringExtension.class)

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

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

              

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

下面的示例显示,我们可以通过指定一个Spring资源前缀来覆盖这两个批注的默认资源语义:

Java
Kotlin
@ExtendWith(SpringExtension.class)

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

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

              

将本例中的注释与上一个示例进行对比。

Working with Web Mocks

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

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

Java
Kotlin
@SpringJUnitWebConfig
class WacTests {

    @Autowired
    WebApplicationContext wac; // cached

    @Autowired
    MockServletContext servletContext; // cached

    @Autowired
    MockHttpSession session;

    @Autowired
    MockHttpServletRequest request;

    @Autowired
    MockHttpServletResponse response;

    @Autowired
    ServletWebRequest webRequest;

    //...
}

              
Context Caching

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

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

  • 位置(来自@ConextConfiguration)

  • (来自@ConextConfiguration)

  • contextInitializerClasss(来自@ConextConfiguration)

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

  • ConextLoader(来自@ConextConfiguration)

  • 父级(来自@ConextHierarchy)

  • 活动配置文件(来自@ActiveProfiles)

  • PropertySourceLocations(来自@TestPropertySource)

  • PropertySourceProperties(来自@TestPropertySource)

  • resource BasePath(来自@WebAppConfiguration)

例如,如果TestClassA@ConextConfigurationLocations(或Value)属性指定了{“app-config.xml”,“test-config.xml”},则TestContext框架加载相应的ApplicationContext,并将其存储在静态上下文缓存中的一个仅基于这些位置的键下。因此,如果TestClassB还通过继承显式或隐式地为其位置定义了{“app-config.xml”,“test-config.xml”},但没有定义@WebAppConfiguration、不同的ConextLoader、不同的活动配置文件、不同的上下文初始值设定项、不同的测试属性源或不同的父上下文,则两个测试类共享相同的ApplicationContext。这意味着加载应用程序上下文的设置成本只发生一次(每个测试套件),并且后续测试执行速度要快得多。

Test suites and forked processes

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

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

上下文高速缓存的大小以默认的最大大小32为界。只要达到最大大小,就会使用最近最少使用(LRU)逐出策略来逐出和关闭陈旧的上下文。通过设置名为spring.test.context.cache.maxSize.的jvm系统属性,可以从命令行或构建脚本配置最大大小或者,您可以通过SpringProperties机制设置相同的属性。

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

在测试损坏应用程序上下文并需要重新加载的情况下(例如,通过修改应用程序对象的Bean定义或状态),您可以使用@DirtiesContext注释您的测试类或测试方法(请参阅Spring测试注释@DirtiesContext的讨论)。这将指示Spring从缓存中删除上下文并重新构建应用程序上下文,然后再运行需要相同应用程序上下文的下一个测试。请注意,对@DirtiesContext注释的支持是由DirtiesContextBeforeModesTestExecutionListenerDirtiesContextTestExecutionListener,提供的,它们在默认情况下处于启用状态。

ApplicationContext lifecycle and console logging

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

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

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

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

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

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

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

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

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

Context Hierarchies

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

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

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

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

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

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

    @Autowired
    WebApplicationContext wac;

    // ...
}

                

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

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

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

                

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

本例中的类显示了命名层次结构级别的使用,以便合并上下文层次结构中特定级别的配置。BaseTests在层次结构中定义两个级别:ExtendedTests扩展BaseTest,并指示Spring TestContext框架合并层次结构的上下文配置,方法是确保@ConextConfiguration中的name属性中声明的名称都是。结果是加载了三个应用程序上下文:一个用于/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
Kotlin
@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 {}

                

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

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

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

                
Dirtying a context within a context hierarchy
If you use @DirtiesContext in a test whose context is configured as part of a context hierarchy, you can use the hierarchyMode flag to control how the context cache is cleared. For further details, see the discussion of @DirtiesContext in Spring Testing Annotations and the @DirtiesContext javadoc.

3.5.7. Dependency Injection of Test Fixtures

当您使用DependencyInjectionTestExecutionListener(它是默认配置的)时,测试实例的依赖项是从您使用@ConextConfiguration或相关注释配置的应用程序上下文中的Bean注入的。您可以使用setter注入和/或字段注入,这取决于您选择了哪些批注,以及您是将它们放在setter方法还是字段上。如果您使用的是JUnitJupiter,您还可以选择使用构造函数注入(参见依赖项注入和SpringExtension)。为了与Spring的基于注释的注入支持保持一致,您还可以使用Spring的@AuTower注释或来自JSR-330的@Inject注释来进行字段和setter注入。

For testing frameworks other than JUnit Jupiter, the TestContext framework does not participate in instantiation of the test class. Thus, the use of @Autowired or @Inject for constructors has no effect for test classes.
Although field injection is discouraged in production code, field injection is actually quite natural in test code. The rationale for the difference is that you will never instantiate your test class directly. Consequently, there is no need to be able to invoke a public constructor or setter method on your test class.

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

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

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

下面的示例调用静态断言方法,如assertNotNull(),但没有使用断言作为调用的前缀。在这种情况下,假设该方法是通过示例中未显示的导入静态声明正确导入的。

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

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

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

    @Test
    void findById() {
        Title title = titleRepository.findById(new Long(10));
        assertNotNull(title);
    }
}

             

或者,您也可以将类配置为使用@AuTower进行setter注入,如下所示:

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

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

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

    @Test
    void findById() {
        Title title = titleRepository.findById(new Long(10));
        assertNotNull(title);
    }
}

             

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

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

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

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

</beans>
             

如果您从Spring提供的测试基类进行扩展,而该基类恰好在其某个setter方法上使用了@AuTower,则您可能在应用程序上下文中定义了多个受影响类型的Bean(例如,多个DataSourceBean)。在这种情况下,您可以覆盖setter方法并使用@限定符注释来指示特定的目标Bean,如下所示(但也要确保委托给超类中被覆盖的方法):

Java
Kotlin
// ...

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

// ...

                  

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

3.5.8. Testing Request- and Session-scoped Beans

从早年起,Spring就支持请求和会话范围的Bean,您可以通过以下步骤测试请求范围和会话范围的Bean:

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

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

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

  • 针对模拟执行断言。

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

Request-scoped bean configuration
<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>
             

RequestScopedBeanTest中,我们将UserService(即被测试的主题)和MockHttpServletRequest注入到我们的测试实例中。在questScope()测试方法中,我们通过在提供的MockHttpServletRequest中设置请求参数来设置测试夹具。当在userService上调用loginUser()方法时,我们确信用户服务可以访问当前MockHttpServletRequest的请求范围loginAction(即,我们刚刚在其中设置了参数)。然后,我们可以根据用户名和密码的已知输入对结果执行断言。以下清单显示了如何执行此操作:

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

             

下面的代码片段类似于我们之前看到的请求作用域Bean的代码片段。但是,这一次,userServiceBean依赖于会话范围的UserPreferencesBean。注意,UserPreferencesBean是通过使用Spel表达式实例化的,该表达式从当前的HTTP会话中检索主题。在我们的测试中,我们需要在由TestContext框架管理的模拟会话中配置一个主题。以下示例显示了如何执行此操作:

Session-scoped bean configuration
<beans>

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

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

</beans>
             

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

Java
Kotlin
@SpringJUnitWebConfig
class SessionScopedBeanTests {

    @Autowired UserService userService;
    @Autowired MockHttpSession session;

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

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

             

3.5.9. Transaction Management

在TestContext框架中,事务由默认配置的代码管理,即使您没有在测试类上显式声明<TransactionalTestExecutionListener,>@TestExecutionListeners。然而,要启用对事务的支持,您必须在ApplicationContext中配置一个PlatformTransactionManagerBean,该Bean加载了@ConextConfiguration语义(稍后将提供更多详细信息)。此外,您必须在测试的类或方法级别声明Spring的@Transaction注释。

Test-managed Transactions

测试管理事务是通过使用TransactionalTestExecutionListener以声明方式或使用TestTransaction(稍后介绍)以编程方式管理的事务。不应将此类事务与Spring管理的事务(由Spring在为测试加载的ApplicationContext中直接管理的事务)或应用程序管理的事务(在测试调用的应用程序代码中以编程方式管理的事务)混淆。Spring管理的事务和应用程序管理的事务通常参与测试管理的事务。但是,如果使用RequiredSupport以外的任何传播类型配置Spring管理的或应用程序管理的事务(有关详细信息,请参阅事务传播的讨论),请务必小心。

Preemptive timeouts and test-managed transactions

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

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

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

  • JUnit4的@Test.(超时=…​)支持和超时规则

  • JUnitJupiter的断言超时抢占(…​)<代码>org.juit.jupiter.api.Assertions类中的方法

  • TestNG的@Test.(超时=…​)支持

Enabling and Disabling Transactions

使用@Transaction注释测试方法会导致测试在一个事务中运行,默认情况下,该事务在测试完成后自动回滚。如果一个测试类用@Transaction注释,则该类层次结构中的每个测试方法都在一个事务中运行。未使用@Transaction注释的测试方法(在类或方法级别)不在事务内运行。注意,测试生命周期方法不支持@Transaction--例如,使用JUnit Jupiter的@BeForeAll@BeForeEach等注释的方法。此外,使用@Transaction注释但将传播属性设置为NOT_SUPPORTEDNever的测试不会在事务中运行。

Table 1. @Transactional attribute support
Attribute Supported for test-managed transactions

事务管理器

传播

仅支持Propagation.NOT_SupportPropagation.NEVER

隔离

不是

超时

不是

只读

不是

ROLLBACK FORROLLBACK ForClassName

否:改用TestTransaction.flagForRollback()

noRollback fornoRollackForClassName

否:改用TestTransaction.FlagForCommit()

方法级生命周期方法--例如,用JUnitJupiter的@BeForeEach@AfterEach注释的方法--在测试管理的事务中运行。另一方面,套件级别和类级别的生命周期方法--例如,用JUnit Jupiter的@BeForeAll@Afterall注释的方法,以及用TestNG的@BeForeSuite@AfterSuite@BeForeClass@AfterClass注释的方法--都是而不是在测试管理的事务中运行。

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

请注意,AbstractTransactionalJUnit4SpringContextTestsAbstractTransactionalTestNGSpringContextTests已预先配置为在类级别提供事务支持。

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

Java
Kotlin
@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"));
    }
}

              
Transaction Rollback and Commit Behavior

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

Programmatic Transaction Management

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

下面的示例演示了TestTransaction的一些功能。有关详细信息,请参阅TestTransaction的javadoc。

Java
Kotlin
@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"));
    }
}

              
Running Code Outside of a Transaction

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

Any before methods (such as methods annotated with JUnit Jupiter’s @BeforeEach) and any after methods (such as methods annotated with JUnit Jupiter’s @AfterEach) are run within a transaction. In addition, methods annotated with @BeforeTransaction or @AfterTransaction are not run for test methods that are not configured to run within a transaction.
Configuring a Transaction Manager

TransactionalTestExecutionListener期望在SpringApplicationContext中为测试定义一个PlatformTransactionManagerBean。如果测试的ApplicationContext中有多个PlatformTransactionManager实例,您可以使用@Transaction(“myTxMgr”)@Transaction(TransactionManager=“myTxMgr”)声明一个限定符,或者TransactionManagementConfigurer可以由@Configuration类实现。有关用于在测试的<TestContextTransactionUtils.retrieveTransactionManager()>ApplicationContext中查找事务管理器的算法的详细信息,请参考javadoc。

Demonstration of All Transaction-related Annotations

下面的基于JUnitJupiter的示例显示了一个虚构的集成测试场景,其中突出显示了所有与事务相关的注释。该示例的目的不是演示最佳实践,而是演示如何使用这些注释。有关详细信息和配置示例,请参阅注释支持一节。@SQL事务管理包含一个附加示例,该示例使用@SQL以默认的事务回滚语义执行声明性SQL脚本。以下示例显示了相关批注:

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

}

              
Avoid false positives when testing ORM code

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

Java
Kotlin
// ...

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

// ...

                   

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

Java
Kotlin
// ...

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

// ...

                   
Testing ORM entity lifecycle callbacks

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

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

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

Java
Kotlin
// ...

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

// ...

                   

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

3.5.10. Executing SQL Scripts

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

虽然在加载ApplicationContext时初始化一次用于测试的数据库非常有用,但有时必须能够在集成测试期间修改数据库。以下各节说明如何在集成测试期间以编程方式和声明方式运行SQL脚本。

Executing SQL scripts programmatically

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

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

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

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

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

ScriptUtils提供用于使用SQL脚本的静态实用程序方法集合,主要用于框架内部使用。但是,如果您需要完全控制如何解析和运行SQL脚本,ScriptUtils可能比后面介绍的其他一些替代方案更适合您的需要。有关详细信息,请参阅ScriptUtils中的各个方法的javadoc

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

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

              

请注意,ResourceDatabasePopator在内部委托ScriptUtils解析和运行SQL脚本。类似地,AbstractTransactionalJUnit4SpringContextTestsAbstractTransactionalTestNGSpringContextTests>中的ecuteSqlScrip(..)方法在内部使用ResourceDatabasePopator来运行SQL脚本。有关详细信息,请参阅各种ecuteSqlScript(..)方法的Javadoc。

Executing SQL scripts declaratively with @Sql

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

Method-level @Sql declarations override class-level declarations by default. As of Spring Framework 5.2, however, this behavior may be configured per test class or per test method via @SqlMergeMode. See Merging and Overriding Configuration with @SqlMergeMode for further details.
Path Resource Semantics

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

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

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

               
Default Script Detection

如果未指定SQL脚本或语句,则会尝试检测默认脚本,具体取决于声明@SQL的位置。如果无法检测到默认值,则抛出IllegalStateException

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

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

Declaring Multiple @Sql Sets

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

以下示例显示如何将@SQL用作Java 8的可重复批注:

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

               

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

下面的示例与前面的示例相同,只是@SQL声明组合在@SqlGroup中。在Java 8和更高版本中,@SqlGroup的使用是可选的,但您可能需要使用@SqlGroup以与其他JVM语言(如Kotlin)兼容。

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

               
Script Execution Phases

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

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

               

请注意,IsolatedAfter_TEST_METHOD分别是从Sql.TransactionModelSql.ExecutionState静态导入的。

Script Configuration with @SqlConfig

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

@SQL@SqlConfig提供的配置选项等同于ScriptUtilsResourceDatabasePopator支持的配置选项,但它们是<;jdbc:initialize-database/>;XML命名空间元素提供的配置选项的超集。详见@SQL@SqlConfig中各个属性的javadoc。

@SQL的事务管理

默认情况下,SqlScriptsTestExecutionListener为使用@SQL配置的脚本推断所需的事务语义。具体地说,根据<TransactionalTestExecutionListener>@SqlConfig中的Transaction属性的配置值以及测试的ApplicationContext中是否存在PlatformTransactionManager,在现有的Spring管理的事务中(例如,由测试的代码>@Transaction由代码管理的测试的事务)或在独立的事务中运行SQL脚本。但是,作为最低要求,测试的ApplicationContext中必须存在javax.sql.DataSource

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

Java
Kotlin
@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.");
    }
}

               

注意,在运行usersTest()方法之后,不需要清理数据库,因为TransactionalTestExecutionListener会自动回滚对数据库所做的任何更改(无论是在测试方法中还是在/test-data.sql脚本中)(有关详细信息,请参阅事务管理)。

Merging and Overriding Configuration with @SqlMergeMode

从Spring Framework5.2开始,可以将方法级@SQL声明与类级声明合并。例如,这允许您为每个测试类提供一次数据库模式或一些公共测试数据的配置,然后为每个测试方法提供额外的、特定于用例的测试数据。要启用@SQL合并,请使用@SqlMergeMode(Merge)注释您的测试类或测试方法。要禁用特定测试方法(或特定测试子类)的合并,您可以通过@SqlMergeMode(Override)切换回默认模式。有关示例和更多详细信息,请参阅@SqlMergeMode批注文档部分

3.5.11. Parallel Test Execution

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

For details on how to set up parallel test execution, see the documentation for your testing framework, build tool, or IDE.

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

如果测试符合以下条件,请不要并行运行测试:

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

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

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

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

如果并行测试执行失败,并出现异常,声明当前测试的ApplicationContext不再处于活动状态,这通常意味着ApplicationContext已从另一个线程中的上下文缓存中删除。

这可能是由于使用了@DirtiesContext,也可能是由于自动从上下文缓存中逐出。如果@DirtiesContext是罪魁祸首,您要么需要找到避免使用@DirtiesContext的方法,要么从并行执行中排除此类测试。如果已超过上下文缓存的最大大小,则可以增加缓存的最大大小。有关详细信息,请参阅上下文缓存的讨论。

Parallel test execution in the Spring TestContext Framework is only possible if the underlying TestContext implementation provides a copy constructor, as explained in the javadoc for TestContext. The DefaultTestContext used in Spring provides such a constructor. However, if you use a third-party library that provides a custom TestContext implementation, you need to verify that it is suitable for parallel test execution.

3.5.12. TestContext Framework Support Classes

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

Spring JUnit 4 Runner

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

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

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

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

              

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

Spring JUnit 4 Rules

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

  • SpringClassRule

  • SpringMethodRule

SpringClassRule是支持Spring TestContext框架类级功能的JUnitTestRule,而SpringMethodRule是支持Spring TestContext框架实例级和方法级功能的JUnit方法规则

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

若要支持TestContext框架的全部功能,您必须将SpringClassRuleSpringMethodRule组合在一起。以下示例显示在集成测试中声明这些规则的正确方法:

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

              
JUnit 4 Support Classes

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

  • AbstractJUnit4SpringContext测试

  • AbstractTransactionalJUnit4SpringContextTests

AbstractJUnit4SpringConextTests是一个抽象的基本测试类,它在JUnit4环境中集成了Spring TestContext框架和显式ApplicationContext测试支持。当您扩展AbstractJUnit4SpringContextTests,时,您可以访问<代码>受保护的 <代码>应用上下文 实例变量,您可以使用该变量来执行显式的Bean查找或测试整个上下文的状态。

AbstractTransactionalJUnit4SpringContextTests是<代码>AbstractJUnit4SpringConextTests的抽象事务扩展,它为JDBCAccess添加了一些方便的功能。此类需要在ApplicationContext中定义一个javax.sql.DataSourceBean和一个PlatformTransactionManagerBean。当您扩展AbstractTransactionalJUnit4SpringContextTests,时,您可以访问受保护的JDBCDEMPLATE实例变量,您可以使用该变量来运行SQL语句来查询数据库。在运行与数据库相关的应用程序代码之前和之后,您都可以使用这样的查询来确认数据库状态,而Spring确保这样的查询在与应用程序代码相同的事务范围内运行。当与ORM工具结合使用时,务必避免误报。正如JDBC测试支持中所提到的,AbstractTransactionalJUnit4SpringContextTests还通过使用前面提到的jdbcTemplate提供了委托给JdbcTestUtils中的方法的方便方法。此外,AbstractTransactionalJUnit4SpringContextTests提供了一个ecuteSqlScrip(..)方法,用于针对配置的数据源运行SQL脚本。

These classes are a convenience for extension. If you do not want your test classes to be tied to a Spring-specific class hierarchy, you can configure your own custom test classes by using @RunWith(SpringRunner.class) or Spring’s JUnit rules.
SpringExtension for JUnit Jupiter

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

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

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

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

  • 定制合成的批注结合了来自Spring和JUnitJupiter的批注。有关详细信息,请参阅Meta-Annotation Support for Testing中的@TransactionalDevTestConfig@TransactionalIntegrationTest示例。

以下代码清单显示如何将测试类配置为将SpringExtension@ConextConfiguration结合使用:

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

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

              

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

下面的示例使用@SpringJUnitConfig减少上一个示例中使用的配置量:

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

              

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

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

              

有关更多详细信息,请参阅Spring JUnitJupiter测试注释中的@SpringJUnitConfig@SpringJUnitWebConfig的文档。

Dependency Injection with SpringExtension

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

具体地说,SpringExtension可以将测试的ApplicationContext中的依赖项注入到测试构造函数和方法中,这些构造函数和方法用@BeForeAll@AfForeAll@BeForeEach@AfterEach@RepeatedTest@参数测试等标注。

Constructor Injection

如果JUnitJupiter测试类的构造函数中的特定参数是ApplicationContext类型(或其子类型),或者使用@Autwire@限定符@Value进行注释或元注释,则Spring会使用测试的ApplicationContext中的相应Bean或值来注入该特定参数的值。

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

  • 该构造函数使用@Autwire进行注释。

  • @TestConstructor在测试类上存在或元存在,并且auTower ireMode属性设置为All

  • 默认的测试构造函数自动布线模式已更改为全部

有关@TestConstructor的用法以及如何更改全局测试构造函数自动布线模式的详细信息,请参阅@TestConstructor

If the constructor for a test class is considered to be autowirable, Spring assumes the responsibility for resolving arguments for all parameters in the constructor. Consequently, no other ParameterResolver registered with JUnit Jupiter can resolve parameters for such a constructor.

如果在测试方法之前或之后使用@DirtiesContext关闭测试的ApplicationContext,则测试类的构造函数注入不能与JUnit Jupiter的@TestInstance(Per_Class)支持一起使用。

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

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

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

Java
Kotlin
@SpringJUnitConfig(TestConfig.class)
class OrderServiceIntegrationTests {

    private final OrderService orderService;

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

    // tests that use the injected OrderService
}

               

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

如果spring.test.constructor.autowire.mode属性设置为all(参见@TestConstructor),我们可以省略上一个示例中构造函数上的@Autwire声明,结果如下所示。

Java
Kotlin
@SpringJUnitConfig(TestConfig.class)
class OrderServiceIntegrationTests {

    private final OrderService orderService;

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

    // tests that use the injected OrderService
}

               
Method Injection

如果JUnitJupiter测试方法或测试生命周期回调方法中的参数是ApplicationContext类型(或其子类型),或者使用@AuTower@限定符@Value进行注释或元注释,则Spring将使用测试的ApplicationContext中的相应Bean为该特定参数注入值。

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

Java
Kotlin
@SpringJUnitConfig(TestConfig.class)
class OrderServiceIntegrationTests {

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

               

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

下面的例子展示了如何让Spring和JUnitJupiter同时将依赖项注入placeOrderRepeatly()测试方法。

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

               

注意,使用来自JUnit Jupiter的@RepeatedTest使测试方法能够访问RepeatedInfo

@Nested test class configuration

从Spring Framework5.0开始,Spring TestContext框架就支持在JUnit Jupiter中的@嵌套测试类上使用与测试相关的批注;然而,直到Spring Framework5.3类级测试配置批注才像从超类继承一样从封闭类继承

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

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

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

Java
Kotlin
@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");
        }
    }
}

              
TestNG Support Classes

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

  • AbstractTestNGSpringConextTests

  • AbstractTransactionalTestNGSpringContextTests

AbstractTestNGSpringConextTests是一个抽象的基本测试类,它集成了Spring TestContext框架与TestNG环境中显式的ApplicationContext测试支持。当您扩展AbstractTestNGSpringContextTests,时,您可以访问<代码>受保护的 <代码>应用上下文 实例变量,您可以使用该变量来执行显式的Bean查找或测试整个上下文的状态。

AbstractTransactionalTestNGSpringContextTests是<代码>AbstractTestNGSpringConextTest的抽象事务扩展,它为JDBCAccess添加了一些方便的功能。此类需要在ApplicationContext中定义一个javax.sql.DataSourceBean和一个PlatformTransactionManagerBean。当您扩展AbstractTransactionalTestNGSpringContextTests,时,您可以访问受保护的JDBCDEMPLATE实例变量,您可以使用该变量来运行SQL语句来查询数据库。在运行与数据库相关的应用程序代码之前和之后,您都可以使用这样的查询来确认数据库状态,而Spring确保这样的查询在与应用程序代码相同的事务范围内运行。当与ORM工具结合使用时,务必避免误报。正如JDBC测试支持中所提到的,AbstractTransactionalTestNGSpringContextTests还通过使用前面提到的jdbcTemplate提供了委托给JdbcTestUtils中的方法的方便方法。此外,AbstractTransactionalTestNGSpringContextTests提供了一个ecuteSqlScrip(..)方法,用于针对配置的数据源运行SQL脚本。

These classes are a convenience for extension. If you do not want your test classes to be tied to a Spring-specific class hierarchy, you can configure your own custom test classes by using @ContextConfiguration, @TestExecutionListeners, and so on and by manually instrumenting your test class with a TestContextManager. See the source code of AbstractTestNGSpringContextTests for an example of how to instrument your test class.

3.5.13. Ahead of Time Support for Tests

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

该测试支持使用以下特性扩展了Spring的核心AOT支持

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

    • 提供对基于JUnitJupiter和JUnit4的测试类的显式支持,以及对TestNG和其他使用Spring的核心测试注释 - 的测试框架的隐式支持,只要测试是使用为当前项目注册的JUnit平台<代码>TestEngine运行的。

  • 构建时AOT处理:当前项目中的每个唯一测试ApplicationContext将为AOT处理刷新

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

AOT模式当前不支持@ConextHierarchy批注。

要提供特定于测试的运行时提示以在GraalVM本机映像中使用,您可以选择以下选项。

TestRunmeHintsRegister接口是核心RounmeHintsRegister接口的配套接口。如果您需要注册非特定于特定测试类的测试支持的全局提示,则倾向于在特定于测试的API上实现RounmeHintsRegister

如果实现自定义ConextLoader,则它必须实现AotConextLoader,以便提供AOT构建时处理和AOT运行时执行支持。但是请注意,由Spring框架和Spring Boot提供的所有上下文加载器实现都已经实现了AotConextLoader

如果您实现自定义 AotTestExecutionListener>TestExecutionListener,则它必须实现 Spring-test模块中的 SqlScriptsTestExecutionListener

3.6. WebTestClient

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

Kotlin users: See this section related to use of the WebTestClient.

3.6.1. Setup

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

Bind to Controller

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

对于WebFlux应用程序,使用以下工具加载相当于WebFlux Java配置的基础设施,注册给定的控制器,并创建WebHandler链来处理请求:

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

              

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

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

              
Bind to ApplicationContext

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

对于WebFlux,使用以下代码将SpringApplicationContext传递给WebHttpHandlerBuilder以创建处理请求的WebHandler链:

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

    WebTestClient client;

    @BeforeEach
    void setUp(ApplicationContext context) {  (2)
        client = WebTestClient.bindToApplicationContext(context).build(); (3)
    }
}

              
1 Specify the configuration to load
2 Inject the configuration
3 Create the WebTestClient

对于Spring MVC,在将Spring MockMvcBuilders.webAppContextSetup>ApplicationContext传递给 MockMvc实例来处理请求时,可以使用以下方法:

Java
Kotlin
@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 Specify the configuration to load
2 Inject the configuration
3 Create the WebTestClient
Bind to Router Function

此设置允许您通过模拟请求和响应对象测试功能端点,而无需运行服务器。

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

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

              

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

Bind to Server

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

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

              
Client Config

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

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

              

3.6.2. Writing Tests

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

在调用exchange()之后,WebTestClientWebClient不同,而是继续执行验证响应的工作流。

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

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

             

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

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

             

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

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

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

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

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

Java
Kotlin
client.get().uri("/persons")
        .exchange()
        .expectStatus().isOk()
        .expectBodyList(Person.class).hasSize(3).contains(person);

             

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

Java
Kotlin
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)... }); 
             

或者,您可以退出工作流并获取EntityExchangeResult

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

             
When you need to decode to a target type with generics, look for the overloaded methods that accept ParameterizedTypeReference instead of Class<T>.
No Content

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

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

              

如果您想忽略响应内容,下面的代码将不带任何断言地释放该内容:

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

              
JSON Content

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

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

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

              

使用JSONPath验证JSON内容:

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

              
Streaming Responses

要测试可能无限的流,如“Text/Event-stream”“app/x-ndjson”,请首先验证响应状态和Header,然后获得FlosExchangeResult

Java
Kotlin
FluxExchangeResult<MyEvent> result = client.get().uri("/events")
        .accept(TEXT_EVENT_STREAM)
        .exchange()
        .expectStatus().isOk()
        .returnResult(MyEvent.class);

              

现在,您已经准备好使用反应器测试中的StepVerier使用响应流:

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

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

              
MockMvc Assertions

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

在使用MockMvc服务器设置测试Spring MVC应用程序时,您可以选择对服务器响应执行进一步的断言。为此,首先在断言Body之后获取ExchangeResult

Java
Kotlin
// 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();

              

然后切换到MockMvc服务器响应断言:

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

              

3.7. MockMvc

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

MockMvc可以单独用于执行请求和验证响应。也可以通过WebTestClient使用MockMvc作为服务器来处理请求。WebTestClient的优势是可以选择使用更高级别的对象而不是原始数据,并且能够切换到针对活动服务器的完整端到端HTTP测试并使用相同的测试API。

3.7.1. Overview

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

Spring MVC测试框架,也称为MockMvc,旨在为无需运行服务器的Spring MVC控制器提供更完整的测试。它通过调用DispatcherServlet并从Spring-test模块传递Servlet API的“mock”实现,该模块复制了完整的Spring MVC请求处理,而无需运行服务器。

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

Static Imports

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

  • MockMvcBuilders。*

  • MockMvcRequestBuilders。*

  • MockMvcResultMatcher。*

  • MockMvcResultHandler。*

记住这一点的一个简单方法是搜索MockMvc*。如果使用的是Eclipse,请确保还将上面的成员添加为“最喜欢的静态成员”。

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

Setup Choices

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

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

Java
Kotlin
class MyWebTests {

    MockMvc mockMvc;

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

    // ...

}

              

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

要通过Spring配置设置MockMvc,请使用以下方法:

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

    MockMvc mockMvc;

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

    // ...

}

              

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

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

webAppConextSetup加载您的实际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
Kotlin
@SpringJUnitWebConfig(locations = "test-servlet-context.xml")
class AccountTests {

    @Autowired
    AccountService accountService;

    MockMvc mockMvc;

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

    // ...

}

              

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

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

Setup Features

无论您使用哪个MockMvc构建器,所有MockMvcBuilder实现都提供了一些常见且非常有用的功能。例如,您可以为所有请求声明Accept头,并在所有响应中预期状态为200和Content-Type头,如下所示:

Java
Kotlin
// 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();

              

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

Java
Kotlin
// static import of SharedHttpSessionConfigurer.sharedHttpSession

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

// Use mockMvc to perform requests...

              

参见ConfigurableMockMvcBuilder的javadoc获取所有MockMvc构建器特性的列表,或者使用集成开发环境来探索可用的选项。

Performing Requests

本节介绍如何单独使用MockMvc执行请求和验证响应。如果通过WebTestClient使用MockMvc,请参阅编写测试中的相应部分。

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

Java
Kotlin
// static import of MockMvcRequestBuilders.*

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

              

您还可以执行内部使用MockMultipartHttpServletRequest的文件上传请求,这样就不需要对分块请求进行实际解析。相反,您必须将其设置为类似于以下示例:

Java
Kotlin
mockMvc.perform(multipart("/doc").file("a1", "ABC".getBytes("UTF-8")));

              

您可以使用URI模板样式指定查询参数,如下例所示:

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

              

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

Java
Kotlin
mockMvc.perform(get("/hotels").param("thing", "somewhere"));

              

如果应用程序代码依赖于Servlet请求参数,并且不显式检查查询字符串(这是最常见的情况),则使用哪个选项并不重要。但是,请记住,URI模板提供查询参数在请求通过参数(…)提供的参数时被解码​)方法应已解码。

在大多数情况下,最好将上下文路径和Servlet路径排除在请求URI之外。如果您必须使用完整的请求URI进行测试,请确保相应地设置上下文路径servletPath,以便请求映射工作,如下例所示:

Java
Kotlin
mockMvc.perform(get("/app/main/hotels/{id}").contextPath("/app").servletPath("/main"))

              

在前面的示例中,为每个执行的请求设置上下文路径servletPath会很麻烦。相反,您可以设置默认请求属性,如下例所示:

Java
Kotlin
class MyWebTests {

    MockMvc mockMvc;

    @BeforeEach
    void setup() {
        mockMvc = standaloneSetup(new AccountController())
            .defaultRequest(get("/")
            .contextPath("/app").servletPath("/main")
            .accept(MediaType.APPLICATION_JSON)).build();
    }
}

              

上述属性影响通过MockMvc实例执行的每个请求。如果在给定请求上也指定了相同的属性,则它将覆盖缺省值。这就是为什么默认请求中的HTTP方法和URI无关紧要,因为它们必须在每个请求上指定。

Defining Expectations

您可以通过在执行请求后附加一个或多个和Expect(..)调用来定义预期,如下面的示例所示。一旦一个预期落空,就不会再断言其他预期。

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

mockMvc.perform(get("/accounts/1")).andExpect(status().isOk());

              

您可以通过在执行请求后添加和ExspectAll(..)来定义多个预期,如下例所示。与andExpect(..)相反,andExspectAll(..)保证所有提供的预期都将被断言,并且所有失败都将被跟踪和报告。

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

mockMvc.perform(get("/accounts/1")).andExpectAll(
    status().isOk(),
    content().contentType("application/json;charset=UTF-8"));

              

MockMvcResultMatcher.*提供了一些预期,其中一些进一步嵌套了更详细的预期。

预期分为两大类。第一类断言验证响应的属性(例如,响应状态、标头和内容)。这些是需要断言的最重要的结果。

第二类断言超越了回应。这些断言允许您检查Spring MVC特定的方面,例如哪个控制器方法处理了请求、是否引发并处理了异常、模型的内容是什么、选择了什么视图、添加了哪些闪存属性等等。它们还允许您检查特定于Servlet的方面,例如请求和会话属性。

以下测试断言绑定或验证失败:

Java
Kotlin
mockMvc.perform(post("/persons"))
    .andExpect(status().isOk())
    .andExpect(model().attributeHasErrors("person"));

              

很多时候,在编写测试时,转储已执行请求的结果是很有用的。您可以这样做,其中print()是来自MockMvcResultHandters的静态导入:

Java
Kotlin
mockMvc.perform(post("/persons"))
    .andDo(print())
    .andExpect(status().isOk())
    .andExpect(model().attributeHasErrors("person"));

              

只要请求处理不会导致未处理的异常,print()方法就会将所有可用的结果数据打印到System.out。还有一个log()方法和另外两个print()方法,一个接受OutputStream,另一个接受Writer。例如,调用print(System.err)将结果数据打印到System.err,而调用print(MyWriter)将结果数据打印到自定义编写器。如果希望记录结果数据而不是打印结果数据,可以调用<org.springframework.test.web.servlet.result>log()方法,该方法将结果数据作为单个调试消息记录在日志记录类别下。

在某些情况下,您可能希望直接访问结果并验证以其他方式无法验证的内容。这可以通过在所有其他预期之后追加.andReturn()来实现,如下面的示例所示:

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

              

如果所有测试都重复相同的预期,则可以在构建MockMvc实例时设置一次通用预期,如下例所示:

Java
Kotlin
standaloneSetup(new SimpleController())
    .alwaysExpect(status().isOk())
    .alwaysExpect(content().contentType("application/json;charset=UTF-8"))
    .build()

              

请注意,如果不创建单独的MockMvc实例,则始终应用通用预期,并且无法覆盖这些预期。

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

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

              

当XML响应内容包含使用Spring HATEOAS创建的超媒体链接时,您可以使用XPath表达式验证结果链接:

Java
Kotlin
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"));

              
Async Requests

本节介绍如何单独使用MockMvc来测试异步请求处理。如果通过WebTestClient使用MockMvc,则无需特殊操作即可使异步请求工作,因为WebTestClient会自动执行本节所述的操作。

在Spring MVC测试中,可以通过以下方式测试异步请求:首先断言生成的异步值,然后手动执行异步调度,最后验证响应。以下是对返回DeferredResultCallable或反应性类型(如反应器Mono)的控制器方法的示例测试:

Java
Kotlin
// 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 Check response status is still unchanged
2 Async processing must have started
3 Wait and assert the async result
4 Manually perform an ASYNC dispatch (as there is no running container)
5 Verify the final response
Streaming Responses

测试流响应(如服务器发送的事件)的最佳方式是通过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中也支持这一点,您可以测试正在运行的服务器

Filter Registrations

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

Java
Kotlin
mockMvc = standaloneSetup(new PersonController()).addFilters(new CharacterEncodingFilter()).build();

              

注册的筛选器通过Spring-test中的MockFilterChain调用,最后一个筛选器委托给DispatcherServlet

MockMvc vs End-to-End Tests

MockMVc构建在来自Spring-test模块的Servlet API模拟实现之上,并且不依赖于运行的容器。因此,与运行实际客户端和活动服务器的完全端到端集成测试相比,存在一些差异。

考虑这一点的最简单方法是从空白MockHttpServletRequest开始。无论您添加什么内容,请求都会变成这样。可能会让您大吃一惊的是,默认情况下没有上下文路径;没有jessionidcookie;没有转发、错误或异步分派;因此,没有实际的JSP呈现。相反,“转发”和“重定向”的URL保存在MockHttpServletResponse中,并且可以按照预期进行断言。

这意味着,如果您使用的是JSPs,您可以验证请求被转发到的JSP页,但不会呈现任何HTML。换句话说,不会调用该JSP。但是,请注意,所有其他不依赖于转发的呈现技术,如Thymeleaf和Freemarker,都会按照预期将HTML呈现给响应体。通过@ResponseBody方法呈现JSON、XML和其他格式也是如此。

或者,您可以考虑通过@SpringBootTest从Spring Boot获得完整的端到端集成测试支持。请参阅Spring Boot参考指南

每种方法都有利弊。Spring MVC测试提供了从经典单元测试到完全集成测试的不同级别的选项。可以肯定的是,Spring MVC测试中的所有选项都不属于经典单元测试的范畴,但它们更接近经典单元测试。例如,您可以通过将模拟服务注入控制器来隔离Web层,在这种情况下,您仅通过DispatcherServlet使用实际的Spring配置来测试Web层,因为您可能会将数据访问层与其上面的层隔离测试。此外,您还可以使用独立设置,一次只关注一个控制器,然后手动提供使其正常工作所需的配置。

使用Spring MVC测试时的另一个重要区别是,从概念上讲,这样的测试是在服务器端进行的,因此您可以检查使用了哪个处理程序、异常是否由HandlerExceptionResolver处理、模型的内容是什么、有哪些绑定错误,以及其他细节。这意味着更容易编写预期,因为服务器不像通过实际的HTTP客户端进行测试时那样是一个不透明的盒子。这通常是经典单元测试的优势:它更容易编写、推理和调试,但不会取代完全集成测试的需要。与此同时,重要的是不要忽视这样一个事实,即反应是最重要的检查。简而言之,即使在同一个项目中,也有多种测试样式和策略的空间。

Further Examples

该框架自己的测试包括许多样本测试旨在展示如何单独或通过WebTestClient使用MockMvc。浏览这些示例以了解更多想法。

3.7.2. HtmlUnit Integration

Spring提供了MockMvcHtmlUnit之间的集成。这简化了使用基于HTML的视图时执行端到端测试的过程。此集成使您能够:

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

  • 在页面中测试Java脚本。

  • 可以选择使用模拟服务进行测试,以加快测试速度。

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

MockMvc works with templating technologies that do not rely on a Servlet Container (for example, Thymeleaf, FreeMarker, and others), but it does not work with JSPs, since they rely on the Servlet container.
Why HtmlUnit Integration?

我脑海中浮现的最明显的问题是“我为什么需要这个?”最好的答案是通过研究一个非常基本的样例应用程序来找到。假设您有一个Spring MVC web应用程序,它支持对消息对象的CRUD操作。该应用程序还支持对所有消息进行寻呼。你将如何着手测试它?

使用Spring MVC测试,我们可以轻松地测试是否能够创建消息,如下所示:

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

mockMvc.perform(createMessage)
        .andExpect(status().is3xxRedirection())
        .andExpect(redirectedUrl("/messages/123"));

              

如果我们想要测试允许我们创建消息的表单视图,该怎么办?例如,假设我们的表单如下所示:

<form id="messageForm" action="/messages/" method="post">
    <div class="pull-right"><a href="/messages/">Messages</a></div>

    <label for="summary">Summary</label>
    <input type="text" class="required" id="summary" name="summary" value="" />

    <label for="text">Message</label>
    <textarea id="text" name="text"></textarea>

    <div class="form-actions">
        <input type="submit" value="Create" />
    </div>
</form>
              

如何确保我们的表单生成创建新消息的正确请求?一次天真的尝试可能如下所示:

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

              

这项测试有一些明显的缺陷。如果我们更新控制器以使用参数消息而不是文本,我们的表单测试将继续通过,即使HTML表单与控制器不同步。为了解决这个问题,我们可以组合我们的两个测试,如下所示:

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

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

mockMvc.perform(createMessage)
        .andExpect(status().is3xxRedirection())
        .andExpect(redirectedUrl("/messages/123"));

              

这将降低我们的测试错误通过的风险,但仍然存在一些问题:

  • 如果我们的页面上有多个表单,该怎么办?诚然,我们可以更新我们的XPath表达式,但当我们考虑更多因素时,它们会变得更加复杂:字段的类型是否正确?这些字段是否已启用?诸若此类。

  • 另一个问题是,我们正在做的工作是我们预期的两倍。我们必须首先验证视图,然后使用刚才验证的相同参数提交视图。理想情况下,这可以一次完成。

  • 最后,我们仍然无法解释一些事情。例如,如果表单具有我们也希望测试的JavaScript验证,该怎么办?

总体问题是,测试网页并不涉及单一的交互。相反,它是用户如何与网页交互以及该网页如何与其他资源交互的组合。例如,表单视图的结果用作用户创建消息的输入。此外,我们的表单视图可以潜在地使用影响页面行为的其他资源,例如JavaScript验证。

Integration Testing to the Rescue?

要解决前面提到的问题,我们可以执行端到端集成测试,但这有一些缺点。考虑测试允许我们浏览消息的视图。我们可能需要以下测试:

  • 当消息为空时,我们的页面是否向用户显示通知,指示没有可用的结果?

  • 我们的页面是否正确显示一条消息?

  • 我们的页面是否正确支持分页?

要设置这些测试,我们需要确保我们的数据库包含正确的消息。这导致了一些额外的挑战:

  • 确保数据库中有适当的消息可能会很繁琐。(考虑外键约束。)

  • 测试可能会变得很慢,因为每次测试都需要确保数据库处于正确的状态。

  • 因为我们的数据库需要处于特定的状态,所以我们不能并行运行测试。

  • 对自动生成的ID、时间戳等项执行断言可能很困难。

这些挑战并不意味着我们应该完全放弃端到端集成测试。相反,我们可以通过重构详细的测试以使用运行更快、更可靠且没有副作用的模拟服务来减少端到端集成测试的数量。然后,我们可以实施少量真正的端到端集成测试,以验证简单的工作流,以确保一切都能正常工作。

Enter HtmlUnit Integration

那么,我们如何在测试页面交互和在测试套件中保持良好性能之间取得平衡呢?答案是:“通过集成MockMvc和HtmlUnit。”

HtmlUnit Integration Options

当您想要将MockMvc与HtmlUnit集成时,您有许多选择:

  • MockMvc and HtmlUnit:如果希望使用原始HtmlUnit库,请使用此选项。

  • MockMvc和WebDriver:使用此选项可以简化集成和端到端测试之间的开发和代码重用。

  • MockMvc and Geb:如果希望使用Groovy进行测试、简化开发并在集成和端到端测试之间重用代码,请使用此选项。

MockMvc and HtmlUnit

本节介绍如何集成MockMvc和HtmlUnit。如果要使用原始HtmlUnit库,请使用此选项。

MockMvc and HtmlUnit Setup

首先,确保您已经包含了对net.sourceforge.htmlunit:htmlunit.的测试依赖项为了在ApacheHttpComponents 4.5+中使用HtmlUnit,您需要使用HtmlUnit2.18或更高版本。

我们可以使用MockMvcWebClientBuilder轻松创建与MockMvc集成的HtmlUnitWebClient,如下所示:

Java
Kotlin
WebClient webClient;

@BeforeEach
void setup(WebApplicationContext context) {
    webClient = MockMvcWebClientBuilder
            .webAppContextSetup(context)
            .build();
}

               
This is a simple example of using MockMvcWebClientBuilder. For advanced usage, see Advanced MockMvcWebClientBuilder.

这确保了引用本地主机作为服务器的任何URL都被定向到我们的MockMvc实例,而不需要真正的HTTP连接。正常情况下,任何其他URL都是通过使用网络连接请求的。这让我们可以很容易地测试CDN的使用。

MockMvc and HtmlUnit Usage

现在,我们可以像往常一样使用HtmlUnit,但不需要将应用程序部署到Servlet容器。例如,我们可以请求视图创建一条具有以下内容的消息:

Java
Kotlin
HtmlPage createMsgFormPage = webClient.getPage("http://localhost/messages/form");

               
The default context path is "". Alternatively, we can specify the context path, as described in Advanced MockMvcWebClientBuilder.

一旦我们有了对HtmlPage的引用,我们就可以填写表单并提交它来创建一条消息,如下面的示例所示:

Java
Kotlin
HtmlForm form = createMsgFormPage.getHtmlElementById("messageForm");
HtmlTextInput summaryInput = createMsgFormPage.getHtmlElementById("summary");
summaryInput.setValueAttribute("Spring Rocks");
HtmlTextArea textInput = createMsgFormPage.getHtmlElementById("text");
textInput.setText("In case you didn't know, Spring Rocks!");
HtmlSubmitInput submit = form.getOneHtmlElementByAttribute("input", "type", "submit");
HtmlPage newMessagePage = submit.click();

               

最后,我们可以验证是否成功创建了一条新消息。以下断言使用AssertJ库:

Java
Kotlin
assertThat(newMessagePage.getUrl().toString()).endsWith("/messages/123");
String id = newMessagePage.getHtmlElementById("id").getTextContent();
assertThat(id).isEqualTo("123");
String summary = newMessagePage.getHtmlElementById("summary").getTextContent();
assertThat(summary).isEqualTo("Spring Rocks");
String text = newMessagePage.getHtmlElementById("text").getTextContent();
assertThat(text).isEqualTo("In case you didn't know, Spring Rocks!");

               

前面的代码在许多方面改进了MockMvc测试。首先,我们不再需要显式地验证我们的表单,然后创建一个看起来像表单的请求。取而代之的是,我们请求表单、填写并提交表单,从而显著减少了开销。

另一个重要因素是HtmlUnit使用Mozilla Rhino引擎来评估JavaScript。这意味着我们还可以测试页面中的JavaScript行为。

有关使用HtmlUnit的其他信息,请参阅HtmlUnit文档

Advanced MockMvcWebClientBuilder

在到目前为止的示例中,我们已经以最简单的方式使用了MockMvcWebClientBuilder,方法是基于Spring TestContext框架为我们加载的WebApplicationContext构建一个WebClient。此方法在下面的示例中重复:

Java
Kotlin
WebClient webClient;

@BeforeEach
void setup(WebApplicationContext context) {
    webClient = MockMvcWebClientBuilder
            .webAppContextSetup(context)
            .build();
}

               

我们还可以指定其他配置选项,如下例所示:

Java
Kotlin
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();
}

               

或者,我们可以通过单独配置MockMvc实例并将其提供给MockMvcWebClientBuilder来执行完全相同的设置,如下所示:

Java
Kotlin
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();

               

这更加繁琐,但是,通过使用MockMvc实例构建WebClient,我们可以在指尖掌握MockMvc的全部功能。

For additional information on creating a MockMvc instance, see Setup Choices.
MockMvc and WebDriver

在前面的小节中,我们了解了如何将MockMvc与原始HtmlUnitAPI结合使用。在这一节中,我们使用SelifyWebDriver中的其他抽象来使事情变得更加简单。

Why WebDriver and MockMvc?

我们已经可以使用HtmlUnit和MockMvc了,那么为什么还要使用WebDriver呢?Selify WebDiverer提供了一个非常优雅的API,使我们可以轻松地组织代码。为了更好地展示它是如何工作的,我们在本节中探索一个示例。

Despite being a part of Selenium, WebDriver does not require a Selenium Server to run your tests.

假设我们需要确保正确创建一条消息。这些测试包括查找、填充这些输入元素,然后进行各种断言。

这种方法会导致许多单独的测试,因为我们还想测试错误条件。例如,我们希望确保如果只填写表单的一部分,就会出现错误。如果我们填写整个表单,随后应该会显示新创建的消息。

如果其中一个字段被命名为“SUMMART”,我们可能会在测试中的多个位置重复类似以下内容:

Java
Kotlin
HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary");
summaryInput.setValueAttribute(summary);

               

那么,如果我们将id更改为smmry会发生什么呢?这样做将迫使我们更新所有测试以纳入此更改。这违反了DRY原则,因此理想情况下,我们应该将此代码提取到它自己的方法中,如下所示:

Java
Kotlin
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);
}

               

这样做可以确保在更改UI时不必更新所有测试。

我们甚至可以更进一步,将此逻辑放在表示我们当前所在的HtmlPage对象中,如下面的示例所示:

Java
Kotlin
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());
    }
}

               

以前,这种模式称为Page Object模式。虽然我们当然可以使用HtmlUnit来实现这一点,但WebDriver提供了一些工具,我们将在下面几节中对这些工具进行探索,以使该模式更易于实现。

MockMvc and WebDriver Setup

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

通过使用MockMvcHtmlUnitDriverBuilder,我们可以轻松创建一个与MockMvc集成的Selify WebDriver,如下例所示:

Java
Kotlin
WebDriver driver;

@BeforeEach
void setup(WebApplicationContext context) {
    driver = MockMvcHtmlUnitDriverBuilder
            .webAppContextSetup(context)
            .build();
}

               
This is a simple example of using MockMvcHtmlUnitDriverBuilder. For more advanced usage, see Advanced MockMvcHtmlUnitDriverBuilder.

前面的示例确保将localhost作为服务器引用的任何URL都被定向到我们的MockMvc实例,而不需要真正的HTTP连接。正常情况下,任何其他URL都是通过使用网络连接请求的。这让我们可以很容易地测试CDN的使用。

MockMvc and WebDriver Usage

现在,我们可以像往常一样使用WebDriver,但不需要将应用程序部署到Servlet容器。例如,我们可以请求视图创建一条具有以下内容的消息:

Java
Kotlin
CreateMessagePage page = CreateMessagePage.to(driver);

               
1 CreateMessagePage extends the AbstractPage. We do not go over the details of AbstractPage, but, in summary, it contains common functionality for all of our pages. For example, if our application has a navigational bar, global error messages, and other features, we can place this logic in a shared location.
2 We have a member variable for each of the parts of the HTML page in which we are interested. These are of type WebElement. WebDriver’s PageFactory lets us remove a lot of code from the HtmlUnit version of CreateMessagePage by automatically resolving each WebElement. The PageFactory#initElements(WebDriver,Class<T>) method automatically resolves each WebElement by using the field name and looking it up by the id or name of the element within the HTML page.
3 We can use the @FindBy annotation to override the default lookup behavior. Our example shows how to use the @FindBy annotation to look up our submit button with a css selector (input[type=submit]).

然后,我们可以填写表单并提交以创建一条消息,如下所示:

Java
Kotlin
ViewMessagePage viewMessagePage =
        page.createMessage(ViewMessagePage.class, expectedSummary, expectedText);

               
1 CreateMessagePage extends the AbstractPage. We do not go over the details of AbstractPage, but, in summary, it contains common functionality for all of our pages. For example, if our application has a navigational bar, global error messages, and other features, we can place this logic in a shared location.
2 We have a member variable for each of the parts of the HTML page in which we are interested. These are of type WebElement. WebDriver’s PageFactory lets us remove a lot of code from the HtmlUnit version of CreateMessagePage by automatically resolving each WebElement. The PageFactory#initElements(WebDriver,Class<T>) method automatically resolves each WebElement by using the field name and looking it up by the id or name of the element within the HTML page.
3 We can use the @FindBy annotation to override the default lookup behavior. Our example shows how to use the @FindBy annotation to look up our submit button with a css selector (input[type=submit]).

这通过利用Page对象模式改进了HtmlUnit测试的设计。正如我们在为什么要使用WebDriver和MockMvc?中提到的,我们可以将Page对象模式与HtmlUnit一起使用,但使用WebDriver要容易得多。考虑以下CreateMessagePage实现:

Java
Kotlin
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);
    }
}

               

最后,我们可以验证是否成功创建了一条新消息。以下断言使用AssertJ断言库:

Java
Kotlin
assertThat(viewMessagePage.getMessage()).isEqualTo(expectedMessage);
assertThat(viewMessagePage.getSuccess()).isEqualTo("Successfully created a new message");

               

我们可以看到,ViewMessagePage允许我们与自定义域模型交互。例如,它公开了一个返回消息对象的方法:

Java
Kotlin
public Message getMessage() throws ParseException {
    Message message = new Message();
    message.setId(getId());
    message.setCreated(getCreated());
    message.setSummary(getSummary());
    message.setText(getText());
    return message;
}

               

然后,我们可以在断言中使用富领域对象。

最后,我们不要忘记在测试完成后关闭WebDriver实例,如下所示:

Java
Kotlin
@AfterEach
void destroy() {
    if (driver != null) {
        driver.close();
    }
}

               

有关使用WebDriver的其他信息,请参阅SelensWebDriver文档

Advanced MockMvcHtmlUnitDriverBuilder

在到目前为止的示例中,我们已经以最简单的方式使用了MockMvcHtmlUnitDriverBuilder,方法是基于Spring TestContext框架为我们加载的WebApplicationContext构建WebDriver。此方法在此处重复,如下所示:

Java
Kotlin
WebDriver driver;

@BeforeEach
void setup(WebApplicationContext context) {
    driver = MockMvcHtmlUnitDriverBuilder
            .webAppContextSetup(context)
            .build();
}

               

我们还可以指定其他配置选项,如下所示:

Java
Kotlin
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();
}

               

或者,我们可以通过单独配置MockMvc实例并将其提供给MockMvcHtmlUnitDriverBuilder来执行完全相同的设置,如下所示:

Java
Kotlin
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();

               

这更加繁琐,但是,通过使用MockMvc实例构建WebDriver,我们可以在指尖掌握MockMvc的全部功能。

For additional information on creating a MockMvc instance, see Setup Choices.
MockMvc and Geb

在上一节中,我们了解了如何在WebDriver中使用MockMvc。在本节中,我们使用geb来使我们的测试更加Groovy-er。

Why Geb and MockMvc?

GEB是由WebDriver支持的,因此它提供了许多我们从WebDriver获得的相同的好处。然而,Geb为我们处理了一些样板代码,从而使事情变得更加容易。

MockMvc and Geb Setup

我们可以很容易地使用使用MockMvc的Selify WebDriver来初始化一个Geb浏览器,如下所示:

def setup() {
    browser.driver = MockMvcHtmlUnitDriverBuilder
        .webAppContextSetup(context)
        .build()
}

               
This is a simple example of using MockMvcHtmlUnitDriverBuilder. For more advanced usage, see Advanced MockMvcHtmlUnitDriverBuilder.

这确保了引用本地主机作为服务器的任何URL都被定向到我们的MockMvc实例,而不需要真正的HTTP连接。任何其他URL都是通过正常使用网络连接请求的。这让我们可以很容易地测试CDN的使用。

MockMvc and Geb Usage

现在,我们可以像往常一样使用Geb,但不需要将应用程序部署到Servlet容器。例如,我们可以请求视图创建一条具有以下内容的消息:

to CreateMessagePage

               

然后,我们可以填写表单并提交以创建一条消息,如下所示:

when:
form.summary = expectedSummary
form.text = expectedMessage
submit.click(ViewMessagePage)

               

任何未找到的无法识别的方法调用、属性访问或引用都将转发到当前页对象。这消除了我们在直接使用WebDriver时所需的大量样板代码。

与直接使用WebDriver一样,这通过使用Page对象模式改进了HtmlUnit测试的设计。如前所述,我们可以将Page Object模式与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.')

               
We use an assertion in the closure so that we can determine where things went wrong if we were at the wrong page.

接下来,我们创建一个内容闭包,它指定页面中所有感兴趣的区域。我们可以使用jQuery-ish Navigator API选择我们感兴趣的内容。

最后,我们可以验证是否成功创建了一条新消息,如下所示:

then:
at ViewMessagePage
success == 'Successfully created a new message'
id
date
summary == expectedSummary
message == expectedMessage

               

有关如何充分利用Geb的更多详细信息,请参阅《Geb手册》用户手册。

3.8. Testing Client Applications

您可以使用客户端测试来测试在内部使用RestTemplate的代码。其思想是声明预期的请求并提供“存根”响应,以便您可以专注于隔离测试代码(即,无需运行服务器)。以下示例显示了如何执行此操作:

Java
Kotlin
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();

            

在前面的示例中,MockRestServiceServer(客户端REST测试的中心类)使用定制的ClientHttpRequestFactory配置RestTemplate,该客户端根据预期断言实际请求并返回“存根”响应。在本例中,我们期待一个对/greting的请求,并希望返回一个包含文本/纯文本内容的200响应。我们可以根据需要定义额外的预期请求和存根响应。当我们定义预期的请求和存根响应时,可以照常在客户端代码中使用RestTemplate。在测试结束时,可以使用mock Server.Verify()来验证是否满足了所有预期。

默认情况下,请求的预期顺序是声明期望的顺序。您可以在构建服务器时设置IGNOREEXPectOrder选项,在这种情况下,将检查所有预期(以便)找到与给定请求匹配的项。这意味着允许以任何顺序发送请求。下面的示例使用IGNORREEXPECtOrder

Java
Kotlin
server = MockRestServiceServer.bindTo(restTemplate).ignoreExpectOrder(true).build();

            

即使在默认情况下,对于无序请求,每个请求也只能运行一次。Expect方法提供了一个重载变量,该变量接受指定计数范围的ExspectedCount参数(例如,一次多次最大最小介于等等)。下面的示例使用

Java
Kotlin
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();

            

请注意,如果未设置IgnreExspectOrder(默认设置),因此请求应按声明顺序进行,则该顺序仅适用于任何预期请求中的第一个。例如,如果“/某事”应该出现两次,然后是“/某地”三次,那么在请求“/某地”之前,应该有一个对“/某事”的请求,但是,除了随后的“/某事”和“/某地”之外,请求可以随时到来。

作为上述所有功能的替代方案,客户端测试支持还提供了ClientHttpRequestFactory实现,您可以将其配置到RestTemplate中,以将其绑定到MockMvc实例。这允许使用实际的服务器端逻辑处理请求,而无需运行服务器。以下示例显示了如何执行此操作:

Java
Kotlin
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
this.restTemplate = new RestTemplate(new MockMvcClientHttpRequestFactory(mockMvc));

// Test code that uses the above RestTemplate ...

            

3.8.1. Static Imports

与服务器端测试一样,客户端测试的流畅API需要一些静态导入。通过搜索MockRest*可以很容易地找到它们。→用户应该将<代码>MockRestRequestMatcher.* 和<代码>MockRestResponseCreator.* 添加为“最喜欢的静态成员”,它们位于Java→编辑器→Content Assister→Favorites下。这允许在键入静态方法名称的第一个字符后使用内容助手。其他IDE(如IntelliJ)可能不需要任何额外的配置。检查对静态成员的代码完成支持。

3.8.2. Further Examples of Client-side REST Tests

Spring MVC测试自己的测试包括客户端REST测试的示例测试

4. Further Resources

有关测试的更多信息,请参阅以下资源:

  • JUnit:“面向Java的程序员友好测试框架”。由Spring框架在其测试套件中使用,并在Spring TestContext框架中受支持。

  • TestNG:一个受JUnit启发的测试框架,增加了对测试组、数据驱动测试、分布式测试和其他特性的支持。受Spring TestContext框架支持

  • AssertJ:“流畅的Java断言”,包括对Java 8 lambdas、Streams和其他特性的支持。

  • 模拟对象:维基百科上的文章。

  • MockObjects.com:专门用于模拟对象的网站,这是一种用于改进测试驱动开发中的代码设计的技术。

  • Mockito:基于测试间谍模式的Java模拟库。由Spring框架在其测试套件中使用。

  • EasyMock:Java库“为接口(和通过类扩展的对象)提供模拟对象,方法是使用Java的代理机制动态生成它们。”

  • JMock:支持带有模拟对象的Java代码的测试驱动开发的库。

  • DbUnit:JUnit扩展(也可与Ant和Maven一起使用),其目标是数据库驱动的项目,并在测试运行之间将数据库置于已知状态。

  • TestContainers:支持JUnit测试的Java库,提供常见数据库的轻量级、一次性实例、SelengeWeb浏览器或任何可以在Docker容器中运行的东西。

  • the Grinder:Java负载测试框架。

  • SpringMockK:支持使用MockK而不是Mockito用Kotlin编写的Spring Boot集成测试。