# 测试 本章介绍 Spring 对集成测试的支持和单元测试的最佳实践。 Spring 团队提倡测试驱动开发。 Spring 团队已经发现,控制反转的正确使用确实使单元测试和集成测试变得更容易(在在类上存在 setter 方法和适当的构造函数,使得它们更容易在测试中连接在一起,而无需设置服务定位器注册中心和类似的结构)。 ## 1. Spring 测试简介 测试是 Enterprise 软件开发中不可缺少的一部分。本章重点讨论了 IOC 原则对[单元测试](#unit-testing)的增值,以及 Spring 框架支持[集成测试](#integration-testing)的好处。(在 Enterprise 中对测试的彻底处理超出了本参考手册的范围。) ## 2. 单元测试 与传统的 爪哇 EE 开发相比,依赖注入应该使你的代码更少地依赖于容器。组成应用程序的 POJO 应该在 JUnit 或 TestNG 测试中是可测试的,使用`new`操作符实例化对象,而不需要 Spring 或任何其他容器。你可以使用[模拟对象](#mock-objects)(与其他有价值的测试技术一起使用)来孤立地测试你的代码。如果你遵循 Spring 的体系结构建议,那么你的代码库的干净分层和组件化将促进更容易的单元测试。例如,你可以通过截断或模拟 DAO 或存储库接口来测试服务层对象,而不需要在运行单元测试时访问持久性数据。 真正的单元测试通常运行得非常快,因为没有要设置的运行时基础设施。强调真正的单元测试作为开发方法的一部分,可以提高你的工作效率。你可能不需要测试章节的这一部分来帮助你为基于 IOC 的应用程序编写有效的单元测试。然而,对于某些单元测试场景, Spring 框架提供了模拟对象和测试支持类,这在本章中进行了描述。 ### 2.1.模拟对象 Spring 包括一些专门用于嘲弄的软件包: * [环境](#mock-objects-env) * [JNDI](#mock-objects-jndi) * [Servlet API](#mock-objects-servlet) * [Spring Web Reactive](#mock-objects-web-reactive) #### 2.1.1.环境 `org.springframework.mock.env`包包含`Environment`和`PropertySource`抽象的模拟实现(参见[Bean Definition Profiles](core.html#beans-definition-profiles)和[`PropertySource`抽象](core.html#beans-property-source-abstraction))。`MockEnvironment`和`MockPropertySource`对于开发依赖于环境特定属性的代码的容器外测试非常有用。 #### 2.1.2.JNDI `org.springframework.mock.jndi`包包含 JNDI SPI 的部分实现,你可以使用它为测试套件或独立应用程序设置一个简单的 JNDI 环境。例如,如果 JDBC`DataSource`实例在测试代码中与在 爪哇 EE 容器中绑定到相同的 JNDI 名称,则可以在测试场景中重用应用程序代码和配置,而无需进行修改。 | |在`org.springframework.mock.jndi`包中的模拟 JNDI 支持是
在 Spring Framework5.2 中正式反对的,以支持来自第三方
的完整解决方案,例如[SIMPLE-JNDI](https://github.com/h-thurow/Simple-JNDI)。| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 2.1.3. Servlet 空气污染指数 `org.springframework.mock.web`包包含一组完整的 API 模拟对象,这些对象对于测试 Web 上下文、控制器和过滤器非常有用。这些模拟对象针对 Spring 的 Web MVC 框架的使用,并且通常比动态模拟对象(例如[EasyMock](http://easymock.org/))或可选的 Servlet API 模拟对象(例如[模拟对象](http://www.mockobjects.com))更方便地使用。 | |自 Spring Framework5.0 以来,基于 Servlet 4.0API,`org.springframework.mock.web`中的模拟对象是
。| |---|--------------------------------------------------------------------------------------------------------------------| Spring MVC 测试框架构建在模拟 Servlet API 对象上,以提供 Spring MVC 的集成测试框架。见[MockMvc](#spring-mvc-test-framework)。 #### 2.1.4. Spring 网络反应 `org.springframework.mock.http.server.reactive`包包含用于 WebFlux 应用程序的`ServerHttpRequest`和`ServerHttpResponse`的模拟实现。`org.springframework.mock.web.server`包包含一个 mock`ServerWebExchange`,它依赖于这些 mock 请求和响应对象。 `MockServerHttpRequest`和`MockServerHttpResponse`都从相同的抽象基类扩展为特定于服务器的实现,并与它们共享行为。例如,一旦创建了一个模拟请求,它是不可变的,但是你可以使用`ServerHttpRequest`中的`mutate()`方法来创建一个修改过的实例。 为了让模拟响应正确地实现写契约并返回一个写完成句柄(即`Mono`),它默认情况下使用`Flux`和`cache().then()`,这将缓冲数据并使其可用于测试中的断言。应用程序可以设置一个自定义的写函数(例如,测试一个无限的流)。 [WebTestClient](#webtestclient)构建在模拟请求和响应的基础上,为在没有 HTTP 服务器的情况下测试 WebFlux 应用程序提供支持。客户机还可以用于与正在运行的服务器进行端到端测试。 ### 2.2.单元测试支持类 Spring 包括许多可以帮助单元测试的类。它们可分为两类: * [通用测试工具](#unit-testing-utilities) * [Spring MVC Testing Utilities](#unit-testing-spring-mvc) #### 2.2.1.通用测试工具 `org.springframework.test.util`包包含几个用于单元和集成测试的通用实用程序。 `ReflectionTestUtils`是一组基于反射的实用方法。你可以在测试场景中使用这些方法,在这些场景中,你需要更改一个常量的值,设置一个非`public`字段,调用一个非`public`setter 方法,或者在测试应用程序代码时调用一个非`public`配置或生命周期回调方法,例如以下情况: * 允许`private`或`protected`字段访问的 ORM 框架(例如 JPA 和 Hibernate),而不是用于域实体中属性的`public`setter 方法。 * Spring 对注释的支持(例如`@Autowired`、`@Inject`和`@Resource`),它们为`private`或`protected`字段、setter 方法和配置方法提供依赖注入。 * 对于生命周期回调方法,使用`@PostConstruct`和`@PreDestroy`之类的注释。 [`AopTestUtils`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/test/util/aoptestutils.html)是与 AOP 相关的实用方法的集合。可以使用这些方法获得隐藏在一个或多个 Spring 代理后面的底层目标对象的引用。例如,如果你已经通过使用 EasyMock 或 Mockito 之类的库将 Bean 配置为动态模拟,并且该模拟被包装在 Spring 代理中,那么你可能需要直接访问底层模拟以在其上配置期望并执行验证。关于 Spring 的核心 AOP 实用程序,请参见[`AopUtils`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/ AOP/support/aoputils.html)和[`AopProxyUtils`(https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/ AOP/aopyutils.html)。 #### 2.2.2. Spring MVC 测试工具 `org.springframework.test.web`包包含[`ModelAndViewAssert`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/test/web/modelandviewassert.html),你可以将其与 JUnit、TestNG 或处理 Spring MVC对象的任何其他测试框架结合使用。 | |单元测试 Spring MVC 控制器

以单元测试你的 Spring MVC`Controller`类作为 POJO,使用`ModelAndViewAssert`与`MockHttpServletRequest`结合,`MockHttpSession`,从 Spring 的[Servlet API mocks](#mock-objects-servlet)以此类推。对于 Spring MVC 的
Spring MVC 和 REST`Controller`类以及`WebApplicationContext`配置,要进行彻底的集成测试,请使用[Spring MVC Test Framework](#spring-mvc-test-framework)代替。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ## 3. 集成测试 本节(本章其余部分的大部分内容)涵盖了 Spring 应用程序的集成测试。它包括以下主题: * [Overview](#integration-testing-overview) * [集成测试的目标](#integration-testing-goals) * [JDBC 测试支持](#integration-testing-support-jdbc) * [注解](#integration-testing-annotations) * [Spring TestContext Framework](#testcontext-framework) * [MockMvc](#spring-mvc-test-framework) ### 3.1.概述 重要的是能够执行一些集成测试,而不需要部署到应用程序服务器或连接到其他 Enterprise 基础设施。这样做可以让你测试以下内容: * 你的 Spring IOC 容器上下文的正确接线。 * 使用 JDBC 或 ORM 工具进行数据访问。这可以包括诸如 SQL 语句的正确性、 Hibernate 查询、 JPA 实体映射等等。 Spring 框架为`spring-test`模块中的集成测试提供了一流的支持。实际 jar 文件的名称可能包括发布版本,也可能是长`org.springframework.test`格式,这取决于你从哪里获得它(有关解释,请参见[抚养管理一节](core.html#dependency-management))。这个库包括`org.springframework.test`包,其中包含用于使用 Spring 容器进行集成测试的有价值的类。此测试不依赖于应用程序服务器或其他部署环境。这样的测试比单元测试运行得慢,但比同等的 Selenium 测试或依赖于部署到应用程序服务器的远程测试快得多。 单元和集成测试支持是以注释驱动的[Spring TestContext Framework](#testcontext-framework)的形式提供的。TestContext 框架与实际使用的测试框架无关,该框架允许在各种环境中测试,包括 JUnit、TestNG 和其他环境。 ### 3.2.集成测试的目标 Spring 的集成测试支持具有以下主要目标: * 管理测试之间的[Spring IoC container caching](#testing-ctx-management)。 * 提供[测试夹具实例的依赖注入](#testing-fixture-di)。 * 提供适合于集成测试的[事务管理](#testing-tx)。 * 提供[Spring-specific base classes](#testing-support-classes),以帮助开发人员编写集成测试。 接下来的几节描述了每个目标,并提供了实现和配置细节的链接。 #### 3.2.1.上下文管理和缓存 Spring TestContext 框架提供 Spring `ApplicationContext`实例和`WebApplicationContext`实例的一致加载以及这些上下文的缓存。对加载上下文的缓存的支持很重要,因为启动时间可能会成为一个问题——这不是因为 Spring 本身的开销,而是因为 Spring 容器实例化的对象需要时间来实例化。例如,一个包含 50 到 100 个 Hibernate 映射文件的项目可能需要 10 到 20 秒的时间来加载映射文件,而在每个测试装置中运行每个测试之前产生的成本会导致总体测试运行速度较慢,从而降低开发人员的工作效率。 测试类通常声明 XML 的资源位置数组或 Groovy 配置元数据(通常在 Classpath 中),或者用于配置应用程序的组件类数组。这些位置或类与`web.xml`或用于生产部署的其他配置文件中指定的位置或类相同或相似。 默认情况下,一旦加载,配置的`ApplicationContext`将在每个测试中重用。因此,每个测试套件只产生一次安装成本,并且随后的测试执行要快得多。在这种情况下,术语“测试套件”是指在相同的 JVM 中运行的所有测试——例如,针对给定项目或模块的 Ant、 Maven 或 Gradle 构建的所有测试。在不太可能的情况下,测试会破坏应用程序上下文并需要重新加载(例如,通过修改 Bean 定义或应用程序对象的状态),可以将 TestContext 框架配置为重新加载配置并在执行下一个测试之前重新构建应用程序上下文。 请参阅使用 TestContext 框架的[上下文管理](#testcontext-ctx-management)和[上下文缓存](#testcontext-ctx-management-caching)。 #### 3.2.2.测试夹具的依赖注入 当 TestContext 框架加载应用程序上下文时,它可以通过使用依赖项注入来配置测试类的实例。这提供了一种方便的机制,可以通过使用应用程序上下文中预先配置的 bean 来设置测试装置。这里的一个很大的好处是,你可以跨各种测试场景重用应用程序上下文(例如,用于配置 Spring-托管对象图、事务代理、`DataSource`实例和其他),从而避免了为单个测试用例重复复杂的测试固定设置的需要。 例如,考虑一个场景,其中我们有一个类(`HibernateTitleRepository`),它实现了`Title`域实体的数据访问逻辑。我们希望编写测试以下领域的集成测试: * Spring 配置:基本上,与`HibernateTitleRepository` Bean 的配置相关的一切都是正确的和存在的吗? * Hibernate 映射文件配置:是否所有映射都正确,以及是否存在正确的延迟加载设置? * `HibernateTitleRepository`的逻辑:这个类的配置实例是否如预期的那样执行? 参见使用[TestContext 框架](#testcontext-fixture-di)的测试固定件的依赖注入。 #### 3.2.3.事务管理 在访问真实数据库的测试中,一个常见的问题是它们对持久性存储状态的影响。即使在使用开发数据库时,对状态的更改也可能会影响将来的测试。此外,许多操作(例如插入或修改持久数据)无法在事务之外执行(或验证)。 TestContext 框架解决了这个问题。默认情况下,框架为每个测试创建并回滚一个事务。你可以编写可以假设存在事务的代码。如果你在测试中调用事务性代理对象,那么根据它们配置的事务语义,它们的行为是正确的。此外,如果一个测试方法在为测试而管理的事务中运行时删除了选定的表的内容,则默认情况下事务会回滚,并且数据库会返回到执行测试之前的状态。通过使用在测试的应用程序上下文中定义的`PlatformTransactionManager` Bean 向测试提供事务支持。 如果你想要提交一个事务(这是不寻常的,但在你想要填充或修改数据库的特定测试时偶尔会有用),那么你可以通过使用[`@Commit`](#Integration-Testing-Annotations)注释,告诉 TestContext 框架使事务提交,而不是回滚。 参见事务管理[TestContext 框架](#testcontext-tx)。 #### 3.2.4.集成测试的支持类 Spring TestContext 框架提供了几个`abstract`支持类,这些类简化了集成测试的编写。这些基本测试类为测试框架提供了定义良好的挂钩,以及方便的实例变量和方法,使你能够访问: * `ApplicationContext`,用于执行显式 Bean 查找或测试整个上下文的状态。 * a`JdbcTemplate`,用于执行查询数据库的 SQL 语句。可以使用这样的查询来确认与数据库相关的应用程序代码执行之前和之后的数据库状态,并且 Spring 确保这样的查询在与应用程序代码相同的事务范围内运行。当与 ORM 工具一起使用时,请务必避免[误报](#testcontext-tx-false-positives)。 此外,你可能希望创建你自己的定制的、应用程序范围的超类,其中包含特定于你的项目的实例变量和方法。 参见[TestContext 框架](#testcontext-support-classes)的支持类。 ### 3.3.JDBC 测试支持 `org.springframework.test.jdbc`包包含`JdbcTestUtils`,这是与 JDBC 相关的实用程序函数的集合,旨在简化标准数据库测试场景。具体地说,`JdbcTestUtils`提供了以下静态实用方法。 * `countRowsInTable(..)`:计算给定表中的行数。 * `countRowsInTableWhere(..)`:使用提供的`WHERE`子句计算给定表中的行数。 * `deleteFromTables(..)`:从指定的表中删除所有行。 * `deleteFromTableWhere(..)`:使用提供的`WHERE`子句从给定表中删除行。 * `dropTables(..)`:删除指定的表。 | |[`AbstractTransactionalJUnit4SpringContextTests`](#testcontext-support-classes-junit4)和[`AbstractTransactionalTestNGSpringContextTests`](#testcontext-support-classes-testng)提供了方便的方法,这些方法在`JdbcTestUtils`中委托给上述方法。
`spring-jdbc`模块提供了对配置和启动嵌入式
数据库的支持,你可以在与数据库交互的集成测试中使用该数据库。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ### 3.4.注解 本节介绍了在测试 Spring 应用程序时可以使用的注释。它包括以下主题: * [Spring Testing Annotations](#integration-testing-annotations-spring) * [标准注释支持](#integration-testing-annotations-standard) * [Spring JUnit 4 Testing Annotations](#integration-testing-annotations-junit4) * [Spring JUnit Jupiter Testing Annotations](#integration-testing-annotations-junit-jupiter) * [测试的元注释支持](#integration-testing-annotations-meta) #### 3.4.1. Spring 测试注释 Spring 框架提供了以下一组 Spring 特定的注释,你可以在与 TestContext 框架结合的单元和集成测试中使用这些注释。有关更多信息,请参见相应的 爪哇doc,包括默认属性值、属性别名和其他详细信息。 Spring 的测试注释包括以下内容: * [`@BootstrapWith`](# Spring-testing-annotation-bootstrapwith) * [`@ContextConfiguration`](# Spring-testing-annotation-contextconfiguration) * [`@WebAppConfiguration`](# Spring-testing-annotation-webappconfiguration) * [`@ContextHierarchy`](# Spring-testing-annotation-contexthierarchy) * [`@ActiveProfiles`](# Spring-testing-annotation-ActiveProfiles) * [`@TestPropertySource`](# Spring-testing-annotation-testPropertySource) * [`@DynamicPropertySource`](# Spring-testing-annotation-dynamicpropertysource) * [`@DirtiesContext`](# Spring-testing-annotation-dirtiescontext) * [`@TestExecutionListeners`](# Spring-testing-annotation-testexecutionlisteners) * [`@RecordApplicationEvents`](# Spring-testing-annotation-recordapplicationEvents) * [`@Commit`](# Spring-testing-annotation-commit) * [`@Rollback`](# Spring-testing-annotation-rollback) * [`@BeforeTransaction`](# Spring-testing-annotation-beforeTransaction) * [`@AfterTransaction`](# Spring-testing-annotation-aftertransaction) * [`@Sql`](# Spring-testing-annotation-sql) * [`@SqlConfig`](# Spring-testing-annotation-sqlconfig) * [`@SqlMergeMode`](# Spring-testing-annotation-sqlmergemode) * [`@SqlGroup`](# Spring-testing-annotation-sqlgroup) ##### `@BootstrapWith` `@BootstrapWith`是一个类级注释,你可以使用它来配置如何引导 Spring TestContext 框架。具体地说,你可以使用`@BootstrapWith`来指定自定义的`TestContextBootstrapper`。有关更多详细信息,请参见[引导 TestContext 框架](#testcontext-bootstrapping)一节。 ##### `@ContextConfiguration` `@ContextConfiguration`定义了类级元数据,用于确定如何加载和配置用于集成测试的`ApplicationContext`。具体地说,`@ContextConfiguration`声明用于加载上下文的应用程序上下文资源`locations`或组件`classes`。 资源位置通常是位于 Classpath 中的 XML 配置文件或 Groovy 脚本,而组件类通常是`@Configuration`类。然而,资源位置也可以引用文件系统中的文件和脚本,并且组件类可以是`@Component`类、`@Service`类等等。有关更多详细信息,请参见[组件类](#testcontext-ctx-management-javaconfig-component-classes)。 下面的示例显示了引用 XML 文件的`@ContextConfiguration`注释: 爪哇 ``` @ContextConfiguration("/test-config.xml") (1) class XmlApplicationContextTests { // class body... } ``` |**1**|指一个 XML 文件。| |-----|-------------------------| Kotlin ``` @ContextConfiguration("/test-config.xml") (1) class XmlApplicationContextTests { // class body... } ``` |**1**|指一个 XML 文件。| |-----|-------------------------| 下面的示例显示了引用类的`@ContextConfiguration`注释: 爪哇 ``` @ContextConfiguration(classes = TestConfig.class) (1) class ConfigClassApplicationContextTests { // class body... } ``` |**1**|指的是一门课。| |-----|---------------------| Kotlin ``` @ContextConfiguration(classes = [TestConfig::class]) (1) class ConfigClassApplicationContextTests { // class body... } ``` |**1**|指的是一门课。| |-----|---------------------| 作为一种替代方法,或者除了声明资源位置或组件类之外,还可以使用`@ContextConfiguration`声明`ApplicationContextInitializer`类。下面的示例展示了这样一个案例: 爪哇 ``` @ContextConfiguration(initializers = CustomContextIntializer.class) (1) class ContextInitializerTests { // class body... } ``` |**1**|声明初始化程序类。| |-----|-------------------------------| Kotlin ``` @ContextConfiguration(initializers = [CustomContextIntializer::class]) (1) class ContextInitializerTests { // class body... } ``` |**1**|声明初始化程序类。| |-----|-------------------------------| 你也可以选择使用`@ContextConfiguration`来声明`ContextLoader`策略。但是,请注意,你通常不需要显式地配置加载器,因为默认加载器支持`initializers`和资源`locations`或组件`classes`。 下面的示例既使用了位置,也使用了加载器: 爪哇 ``` @ContextConfiguration(locations = "/test-context.xml", loader = CustomContextLoader.class) (1) class CustomLoaderXmlApplicationContextTests { // class body... } ``` |**1**|配置位置和自定义加载器。| |-----|------------------------------------------------| Kotlin ``` @ContextConfiguration("/test-context.xml", loader = CustomContextLoader::class) (1) class CustomLoaderXmlApplicationContextTests { // class body... } ``` |**1**|配置位置和自定义加载器。| |-----|------------------------------------------------| | |`@ContextConfiguration`提供了对继承资源位置或
配置类的支持,以及由超类
声明的上下文初始化器或包含类。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 请参阅[上下文管理](#testcontext-ctx-management),[`@Nested`测试类配置](#testcontext-junit-jupiter-nested-test-configuration),以及`@ContextConfiguration`爪哇docs 以获取更多详细信息。 ##### `@WebAppConfiguration` `@WebAppConfiguration`是一个类级注释,你可以使用它来声明为集成测试加载的`ApplicationContext`应该是`WebApplicationContext`。在测试类上仅存在`@WebAppConfiguration`就可以确保为测试加载`WebApplicationContext`,使用缺省值`"file:src/main/webapp"`作为 Web 应用程序的根路径(即资源基路径)。后台使用资源库路径创建`MockServletContext`,它是测试的`ServletContext`的`ServletContext`。 下面的示例展示了如何使用`@WebAppConfiguration`注释: 爪哇 ``` @ContextConfiguration @WebAppConfiguration (1) class WebAppTests { // class body... } ``` Kotlin ``` @ContextConfiguration @WebAppConfiguration (1) class WebAppTests { // class body... } ``` |**1**|`@WebAppConfiguration`注释。| |-----|--------------------------------------| 要覆盖默认值,可以使用隐式`value`属性指定不同的基本资源路径。同时支持`classpath:`和`file:`资源前缀。如果没有提供资源前缀,则假定路径是一个文件系统资源。下面的示例展示了如何指定 Classpath 资源: 爪哇 ``` @ContextConfiguration @WebAppConfiguration("classpath:test-web-resources") (1) class WebAppTests { // class body... } ``` |**1**|指定 Classpath 资源。| |-----|--------------------------------| Kotlin ``` @ContextConfiguration @WebAppConfiguration("classpath:test-web-resources") (1) class WebAppTests { // class body... } ``` |**1**|指定 Classpath 资源。| |-----|--------------------------------| 请注意,`@WebAppConfiguration`必须与`@ContextConfiguration`一起使用,无论是在单个测试类中还是在测试类层次结构中。有关更多详细信息,请参见[`@WebAppConfiguration`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/test/context/web/webappconfiguration.html)爪哇doc。 ##### `@ContextHierarchy` `@ContextHierarchy`是一种类级注释,用于为集成测试定义`ApplicationContext`实例的层次结构。`@ContextHierarchy`应该使用一个或多个`@ContextConfiguration`实例的列表来声明,每个实例在上下文层次结构中定义一个级别。下面的示例演示了在单个测试类中使用`@ContextHierarchy`(`@ContextHierarchy`也可以在测试类层次结构中使用): 爪哇 ``` @ContextHierarchy({ @ContextConfiguration("/parent-config.xml"), @ContextConfiguration("/child-config.xml") }) class ContextHierarchyTests { // class body... } ``` Kotlin ``` @ContextHierarchy( ContextConfiguration("/parent-config.xml"), ContextConfiguration("/child-config.xml")) class ContextHierarchyTests { // class body... } ``` 爪哇 ``` @WebAppConfiguration @ContextHierarchy({ @ContextConfiguration(classes = AppConfig.class), @ContextConfiguration(classes = WebConfig.class) }) class WebIntegrationTests { // class body... } ``` Kotlin ``` @WebAppConfiguration @ContextHierarchy( ContextConfiguration(classes = [AppConfig::class]), ContextConfiguration(classes = [WebConfig::class])) class WebIntegrationTests { // class body... } ``` 如果需要合并或覆盖测试类层次结构中上下文层次结构的给定级别的配置,则必须在类层次结构中的每个对应级别上,通过向`@ContextConfiguration`中的`name`属性提供相同的值,显式地命名该级别。参见[上下文层次结构](#testcontext-ctx-management-ctx-hierarchies)和[`@ContextHierarchy`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/test/contexthierarchy.html)爪哇doc 以获取更多示例。 ##### `@ActiveProfiles` `@ActiveProfiles`是一种类级注释,用于声明在为集成测试加载`ApplicationContext`时哪些 Bean 定义配置文件应该处于活动状态。 以下示例表明`dev`配置文件应该处于活动状态: 爪哇 ``` @ContextConfiguration @ActiveProfiles("dev") (1) class DeveloperTests { // class body... } ``` |**1**|指示`dev`配置文件应该处于活动状态。| |-----|-------------------------------------------------| Kotlin ``` @ContextConfiguration @ActiveProfiles("dev") (1) class DeveloperTests { // class body... } ``` |**1**|表示`dev`配置文件应该处于活动状态。| |-----|-------------------------------------------------| 下面的示例表明,`dev`和`integration`配置文件都应该处于活动状态: 爪哇 ``` @ContextConfiguration @ActiveProfiles({"dev", "integration"}) (1) class DeveloperIntegrationTests { // class body... } ``` |**1**|指示`dev`和`integration`配置文件应该处于活动状态。| |-----|--------------------------------------------------------------------| Kotlin ``` @ContextConfiguration @ActiveProfiles(["dev", "integration"]) (1) class DeveloperIntegrationTests { // class body... } ``` |**1**|指示`dev`和`integration`配置文件应该处于活动状态。| |-----|--------------------------------------------------------------------| | |`@ActiveProfiles`提供了对继承活动 Bean 定义配置文件
的支持,该配置文件由超类声明并默认包含类。还可以通过实现自定义[`ActiveProfilesResolver`](#TestContext-CTX-Management-ENV-Profiles-ActiveProfilesResolver)并使用`resolver``@ActiveProfiles`的属性来以编程方式解析活动的
Bean 定义配置文件。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 参见[具有环境配置文件的上下文配置](#testcontext-ctx-management-env-profiles),[`@Nested`测试类配置](#TestContext-JUnit-Jupiter-Nested-Test-Configuration),以及[`@ActiveProfiles`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/org/springframework/test/context/actevepries.html)的示例和进一步详细信息。 ##### `@TestPropertySource` `@TestPropertySource`是一种类级注释,你可以使用它来配置要添加到`PropertySources`中的`Environment`集合中的属性文件和内联属性的位置,用于为集成测试加载`ApplicationContext`。 下面的示例演示了如何从 Classpath 声明属性文件: 爪哇 ``` @ContextConfiguration @TestPropertySource("/test.properties") (1) class MyIntegrationTests { // class body... } ``` |**1**|从 Classpath 根中的`test.properties`获取属性。| |-----|-------------------------------------------------------------------| Kotlin ``` @ContextConfiguration @TestPropertySource("/test.properties") (1) class MyIntegrationTests { // class body... } ``` |**1**|从 Classpath 根中的`test.properties`获取属性。| |-----|-------------------------------------------------------------------| 下面的示例演示了如何声明内联属性: 爪哇 ``` @ContextConfiguration @TestPropertySource(properties = { "timezone = GMT", "port: 4242" }) (1) class MyIntegrationTests { // class body... } ``` |**1**|声明`timezone`和`port`属性。| |-----|-----------------------------------------| Kotlin ``` @ContextConfiguration @TestPropertySource(properties = ["timezone = GMT", "port: 4242"]) (1) class MyIntegrationTests { // class body... } ``` |**1**|声明`timezone`和`port`属性。| |-----|-----------------------------------------| 有关示例和更多详细信息,请参见[具有测试属性源的上下文配置](#testcontext-ctx-management-property-sources)。 ##### `@DynamicPropertySource` `@DynamicPropertySource`是一种方法级别的注释,你可以使用它来注册要添加到`PropertySources`中的`ApplicationContext`集合中的 *dynamic*properties,用于为集成测试加载`ApplicationContext`。当你不知道属性的初始值时,动态属性是有用的——例如,如果属性是由外部资源管理的,比如由[测试容器](https://www.testcontainers.org/)项目管理的容器。 下面的示例演示了如何注册动态属性: 爪哇 ``` @ContextConfiguration class MyIntegrationTests { static MyExternalServer server = // ... @DynamicPropertySource (1) static void dynamicProperties(DynamicPropertyRegistry registry) { (2) registry.add("server.port", server::getPort); (3) } // tests ... } ``` |**1**|用`@DynamicPropertySource`注释`static`方法。| |-----|---------------------------------------------------------------------------------| |**2**|接受`DynamicPropertyRegistry`作为参数。| |**3**|注册一个动态`server.port`属性,以便从服务器上懒洋洋地检索。| Kotlin ``` @ContextConfiguration class MyIntegrationTests { companion object { @JvmStatic val server: MyExternalServer = // ... @DynamicPropertySource (1) @JvmStatic fun dynamicProperties(registry: DynamicPropertyRegistry) { (2) registry.add("server.port", server::getPort) (3) } } // tests ... } ``` |**1**|用`@DynamicPropertySource`注释`static`方法。| |-----|---------------------------------------------------------------------------------| |**2**|接受`DynamicPropertyRegistry`作为参数。| |**3**|注册一个动态`server.port`属性,以便从服务器上懒洋洋地检索。| 有关更多详细信息,请参见[具有动态属性源的上下文配置](#testcontext-ctx-management-dynamic-property-sources)。 ##### `@DirtiesContext` `@DirtiesContext`表示底层 Spring `ApplicationContext`在测试的执行过程中被弄脏了(也就是说,测试以某种方式修改或损坏了它——例如,通过更改单例 Bean 的状态),并且应该关闭。当一个应用程序上下文被标记为 dirty 时,它将从测试框架的缓存中删除并关闭。因此,对于需要具有相同配置元数据的上下文的任何后续测试,都会重新构建基础 Spring 容器。 可以在同一个类或类层次结构中同时使用`@DirtiesContext`作为类级和方法级的注释。在这种情况下,根据配置的`methodMode`和`classMode`,在任何此类注释方法之前或之后以及在当前测试类之前或之后将`ApplicationContext`标记为 dirty。 下面的示例解释了各种配置场景的上下文何时会被弄脏: * 在当前测试类之前,当在类上声明时,将类的模式设置为`BEFORE_CLASS`。 爪哇 ``` @DirtiesContext(classMode = BEFORE_CLASS) (1) class FreshContextTests { // some tests that require a new Spring container } ``` |**1**|在当前测试类之前弄脏上下文。| |-----|------------------------------------------------| Kotlin ``` @DirtiesContext(classMode = BEFORE_CLASS) (1) class FreshContextTests { // some tests that require a new Spring container } ``` |**1**|在当前测试类之前弄脏上下文。| |-----|------------------------------------------------| * 在当前的测试类之后,当在一个类上声明具有设置为`AFTER_CLASS`的类模式时(即默认的类模式)。 爪哇 ``` @DirtiesContext (1) class ContextDirtyingTests { // some tests that result in the Spring container being dirtied } ``` |**1**|在当前测试类之后,将上下文弄脏。| |-----|-----------------------------------------------| Kotlin ``` @DirtiesContext (1) class ContextDirtyingTests { // some tests that result in the Spring container being dirtied } ``` |**1**|在当前测试类之后,将上下文弄脏。| |-----|-----------------------------------------------| * 在当前测试类中的每个测试方法之前,当在类上声明时,将类模式设置为`BEFORE_EACH_TEST_METHOD.` 爪哇 ``` @DirtiesContext(classMode = BEFORE_EACH_TEST_METHOD) (1) class FreshContextTests { // some tests that require a new Spring container } ``` |**1**|在每个测试方法之前弄脏上下文。| |-----|------------------------------------------| Kotlin ``` @DirtiesContext(classMode = BEFORE_EACH_TEST_METHOD) (1) class FreshContextTests { // some tests that require a new Spring container } ``` |**1**|在每个测试方法之前弄脏上下文。| |-----|------------------------------------------| * 在当前测试类中的每个测试方法之后,当在类上声明时,将类模式设置为`AFTER_EACH_TEST_METHOD.` 爪哇 ``` @DirtiesContext(classMode = AFTER_EACH_TEST_METHOD) (1) class ContextDirtyingTests { // some tests that result in the Spring container being dirtied } ``` |**1**|在每种测试方法之后都要弄脏上下文。| |-----|-----------------------------------------| Kotlin ``` @DirtiesContext(classMode = AFTER_EACH_TEST_METHOD) (1) class ContextDirtyingTests { // some tests that result in the Spring container being dirtied } ``` |**1**|在每种测试方法之后都要弄脏上下文。| |-----|-----------------------------------------| * 在当前测试之前,当对方法进行声明时,方法模式设置为`BEFORE_METHOD`。 爪哇 ``` @DirtiesContext(methodMode = BEFORE_METHOD) (1) @Test void testProcessWhichRequiresFreshAppCtx() { // some logic that requires a new Spring container } ``` |**1**|在当前测试方法之前弄脏上下文。| |-----|-------------------------------------------------| Kotlin ``` @DirtiesContext(methodMode = BEFORE_METHOD) (1) @Test fun testProcessWhichRequiresFreshAppCtx() { // some logic that requires a new Spring container } ``` |**1**|在当前测试方法之前弄脏上下文。| |-----|-------------------------------------------------| * 在当前测试之后,当对方法进行声明时,方法模式设置为`AFTER_METHOD`(即默认的方法模式)。 爪哇 ``` @DirtiesContext (1) @Test void testProcessWhichDirtiesAppCtx() { // some logic that results in the Spring container being dirtied } ``` |**1**|在当前测试方法之后,将上下文弄脏。| |-----|------------------------------------------------| Kotlin ``` @DirtiesContext (1) @Test fun testProcessWhichDirtiesAppCtx() { // some logic that results in the Spring container being dirtied } ``` |**1**|在当前测试方法之后,将上下文弄脏。| |-----|------------------------------------------------| 如果你在一个测试中使用`@DirtiesContext`,该测试的上下文被配置为带有`@ContextHierarchy`的上下文层次结构的一部分,那么你可以使用`hierarchyMode`标志来控制如何清除上下文缓存。默认情况下,使用穷举算法来清除上下文缓存,不仅包括当前级别,还包括共享当前测试共有的祖先上下文的所有其他上下文层次结构。所有位于公共祖先上下文的子层次结构中的`ApplicationContext`实例都将从上下文缓存中删除并关闭。如果穷举算法对特定的用例来说过于强大,那么你可以指定更简单的当前级别算法,如下例所示。 爪哇 ``` @ContextHierarchy({ @ContextConfiguration("/parent-config.xml"), @ContextConfiguration("/child-config.xml") }) class BaseTests { // class body... } class ExtendedTests extends BaseTests { @Test @DirtiesContext(hierarchyMode = CURRENT_LEVEL) (1) void test() { // some logic that results in the child context being dirtied } } ``` |**1**|使用当前级别的算法。| |-----|--------------------------------| Kotlin ``` @ContextHierarchy( ContextConfiguration("/parent-config.xml"), ContextConfiguration("/child-config.xml")) open class BaseTests { // class body... } class ExtendedTests : BaseTests() { @Test @DirtiesContext(hierarchyMode = CURRENT_LEVEL) (1) fun test() { // some logic that results in the child context being dirtied } } ``` |**1**|使用当前级别的算法。| |-----|--------------------------------| 有关`EXHAUSTIVE`和`CURRENT_LEVEL`算法的更多详细信息,请参见[`DirtiesContext.HierarchyMode`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/test/annotation/dirtiescontext.therymode.html)爪哇doc。 ##### `@TestExecutionListeners` `@TestExecutionListeners`定义了用于配置应该在`TestContextManager`中注册的`TestExecutionListener`实现的类级元数据。通常,`@TestExecutionListeners`与`@ContextConfiguration`一起使用。 下面的示例显示了如何注册两个`TestExecutionListener`实现: 爪哇 ``` @ContextConfiguration @TestExecutionListeners({CustomTestExecutionListener.class, AnotherTestExecutionListener.class}) (1) class CustomTestExecutionListenerTests { // class body... } ``` |**1**|注册两个`TestExecutionListener`实现。| |-----|-----------------------------------------------------| Kotlin ``` @ContextConfiguration @TestExecutionListeners(CustomTestExecutionListener::class, AnotherTestExecutionListener::class) (1) class CustomTestExecutionListenerTests { // class body... } ``` |**1**|注册两个`TestExecutionListener`实现。| |-----|-----------------------------------------------------| 默认情况下,`@TestExecutionListeners`支持从超类或封闭类继承侦听器。参见[`@Nested`测试类配置](#TestContext-JUnit-Jupiter-Nested-Test-Configuration)和[`@TestExecutionListeners`爪哇doc](https://DOCS. Spring.io/ Spring-Framework/DOCS/5.3.16/javadoc-api/org/SpringFramework/test/contexecutionListeners.html)中的示例和更多详细信息。 ##### `@RecordApplicationEvents` `@RecordApplicationEvents`是一种类级注释,用于指示 * Spring TestContext Framework* 记录在执行单个测试期间在`ApplicationContext`中发布的所有应用程序事件。 可以通过测试中的`ApplicationEvents`API 访问记录的事件。 参见[应用程序事件](#testcontext-application-events)和[`@RecordApplicationEvents`爪哇doc](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/test/context/event/recordapplicationevents.html)以获取示例和更多详细信息。 ##### `@Commit` `@Commit`表示事务性测试方法的事务应在测试方法完成后提交。你可以使用`@Commit`作为`@Rollback(false)`的直接替换,以更明确地传达代码的意图。类似于`@Rollback`,`@Commit`也可以声明为类级或方法级注释。 下面的示例展示了如何使用`@Commit`注释: 爪哇 ``` @Commit (1) @Test void testProcessWithoutRollback() { // ... } ``` |**1**|将测试结果提交给数据库。| |-----|----------------------------------------------| Kotlin ``` @Commit (1) @Test fun testProcessWithoutRollback() { // ... } ``` |**1**|将测试结果提交给数据库。| |-----|----------------------------------------------| ##### `@Rollback` `@Rollback`指示在测试方法完成后是否应该回滚事务测试方法的事务。如果`true`,则回滚事务。否则,事务将被提交(参见[`@Commit`](# Spring-testing-annotation-commit))。 Spring TestContext 框架中集成测试的回滚默认为`true`,即使没有显式声明`@Rollback`。 当声明为类级注释时,`@Rollback`为测试类层次结构中的所有测试方法定义了默认的回滚语义。当声明为方法级别的注释时,`@Rollback`为特定的测试方法定义了回滚语义,可能会覆盖类级别的`@Rollback`或`@Commit`语义。 下面的示例导致测试方法的结果不会被回滚(即,结果被提交到数据库中): 爪哇 ``` @Rollback(false) (1) @Test void testProcessWithoutRollback() { // ... } ``` |**1**|不要回滚结果。| |-----|----------------------------| Kotlin ``` @Rollback(false) (1) @Test fun testProcessWithoutRollback() { // ... } ``` |**1**|不要回滚结果。| |-----|----------------------------| ##### `@BeforeTransaction` `@BeforeTransaction`表示对于已通过使用 Spring 的`@Transactional`注释配置为在事务中运行的测试方法,在事务启动之前应该运行带注释的`void`方法。`@BeforeTransaction`方法不需要是`public`,并且可以在基于 爪哇8 的接口默认方法上声明。 下面的示例展示了如何使用`@BeforeTransaction`注释: 爪哇 ``` @BeforeTransaction (1) void beforeTransaction() { // logic to be run before a transaction is started } ``` |**1**|在事务之前运行此方法。| |-----|-------------------------------------| Kotlin ``` @BeforeTransaction (1) fun beforeTransaction() { // logic to be run before a transaction is started } ``` |**1**|在事务之前运行此方法。| |-----|-------------------------------------| ##### `@AfterTransaction` `@AfterTransaction`表示在事务结束后应该运行带注释的`void`方法,用于通过使用 Spring 的`@Transactional`注释配置为在事务中运行的测试方法。`@AfterTransaction`方法不需要是`public`,并且可以在基于 爪哇8 的接口默认方法上声明。 爪哇 ``` @AfterTransaction (1) void afterTransaction() { // logic to be run after a transaction has ended } ``` |**1**|在事务之后运行此方法。| |-----|------------------------------------| Kotlin ``` @AfterTransaction (1) fun afterTransaction() { // logic to be run after a transaction has ended } ``` |**1**|在事务之后运行此方法。| |-----|------------------------------------| ##### `@Sql` `@Sql`用于对测试类或测试方法进行注释,以配置在集成测试期间针对给定数据库运行的 SQL 脚本。下面的示例展示了如何使用它: 爪哇 ``` @Test @Sql({"/test-schema.sql", "/test-user-data.sql"}) (1) void userTest() { // run code that relies on the test schema and test data } ``` |**1**|为此测试运行两个脚本。| |-----|------------------------------| Kotlin ``` @Test @Sql("/test-schema.sql", "/test-user-data.sql") (1) fun userTest() { // run code that relies on the test schema and test data } ``` |**1**|为此测试运行两个脚本。| |-----|------------------------------| 有关更多详细信息,请参见[使用 @sql 声明式执行 SQL 脚本](#testcontext-executing-sql-declaratively)。 ##### `@SqlConfig` `@SqlConfig`定义了元数据,用于确定如何解析和运行配置有`@Sql`注释的 SQL 脚本。下面的示例展示了如何使用它: 爪哇 ``` @Test @Sql( scripts = "/test-user-data.sql", config = @SqlConfig(commentPrefix = "`", separator = "@@") (1) ) void userTest() { // run code that relies on the test data } ``` |**1**|在 SQL 脚本中设置注释前缀和分隔符。| |-----|--------------------------------------------------------| Kotlin ``` @Test @Sql("/test-user-data.sql", config = SqlConfig(commentPrefix = "`", separator = "@@")) (1) fun userTest() { // run code that relies on the test data } ``` |**1**|在 SQL 脚本中设置注释前缀和分隔符。| |-----|--------------------------------------------------------| ##### `@SqlMergeMode` `@SqlMergeMode`用于对测试类或测试方法进行注释,以配置方法级`@Sql`声明是否与类级`@Sql`声明合并。如果`@SqlMergeMode`未在测试类或测试方法上声明,则默认情况下将使用`OVERRIDE`合并模式。在`OVERRIDE`模式下,方法级别`@Sql`声明将有效地覆盖类级别`@Sql`声明。 请注意,方法级别`@SqlMergeMode`声明覆盖了类级别声明。 下面的示例展示了如何在类级别上使用`@SqlMergeMode`。 爪哇 ``` @SpringJUnitConfig(TestConfig.class) @Sql("/test-schema.sql") @SqlMergeMode(MERGE) (1) class UserTests { @Test @Sql("/user-test-data-001.sql") void standardUserProfile() { // run code that relies on test data set 001 } } ``` |**1**|对于类中的所有测试方法,将`@Sql`合并模式设置为`MERGE`。| |-----|-----------------------------------------------------------------------| Kotlin ``` @SpringJUnitConfig(TestConfig::class) @Sql("/test-schema.sql") @SqlMergeMode(MERGE) (1) class UserTests { @Test @Sql("/user-test-data-001.sql") fun standardUserProfile() { // run code that relies on test data set 001 } } ``` |**1**|对于类中的所有测试方法,将`@Sql`合并模式设置为`MERGE`。| |-----|-----------------------------------------------------------------------| 下面的示例展示了如何在方法级别上使用`@SqlMergeMode`。 Java ``` @SpringJUnitConfig(TestConfig.class) @Sql("/test-schema.sql") class UserTests { @Test @Sql("/user-test-data-001.sql") @SqlMergeMode(MERGE) (1) void standardUserProfile() { // run code that relies on test data set 001 } } ``` |**1**|为特定的测试方法将`@Sql`合并模式设置为`MERGE`。| |-----|----------------------------------------------------------------| Kotlin ``` @SpringJUnitConfig(TestConfig::class) @Sql("/test-schema.sql") class UserTests { @Test @Sql("/user-test-data-001.sql") @SqlMergeMode(MERGE) (1) fun standardUserProfile() { // run code that relies on test data set 001 } } ``` |**1**|为特定的测试方法将`@Sql`合并模式设置为`MERGE`。| |-----|----------------------------------------------------------------| ##### `@SqlGroup` `@SqlGroup`是一个容器注释,它聚合了几个`@Sql`注释。你可以本地使用`@SqlGroup`来声明几个嵌套的`@Sql`注释,或者可以将其与 Java8 对可重复注释的支持结合使用,其中`@Sql`可以在同一个类或方法上声明几次,隐式地生成这个容器注释。下面的示例展示了如何声明 SQL 组: Java ``` @Test @SqlGroup({ (1) @Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`")), @Sql("/test-user-data.sql") )} void userTest() { // run code that uses the test schema and test data } ``` |**1**|声明一组 SQL 脚本。| |-----|-------------------------------| Kotlin ``` @Test @SqlGroup( (1) Sql("/test-schema.sql", config = SqlConfig(commentPrefix = "`")), Sql("/test-user-data.sql")) fun userTest() { // run code that uses the test schema and test data } ``` |**1**|声明一组 SQL 脚本。| |-----|-------------------------------| #### 3.4.2.标准注释支持 Spring TestContext 框架的所有配置都使用标准语义支持以下注释。请注意,这些注释不是特定于测试的,并且可以在 Spring 框架中的任何地方使用。 * `@Autowired` * `@Qualifier` * `@Value` * `@Resource`(javax.annotation)如果存在 JSR-250 * `@ManagedBean`(javax.annotation)如果存在 JSR-250 * `@Inject`如果存在 JSR-330 * `@Named`如果存在 JSR-330 * `@PersistenceContext`(javax.persistence)如果存在 JPA * `@PersistenceUnit`(javax.persistence)如果存在 JPA * `@Required` * `@Transactional`*with[有限的属性支持](#testcontext-tx-attribute-support)* | |JSR-250 生命周期注释

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

如果测试类中的方法使用`@PostConstruct`进行注释,则该方法在底层测试框架的任何方法之前运行
(例如,方法
使用 JUnit Jupiter 的`@BeforeEach`进行注释),这适用于
测试类中的每个测试方法。另一方面,如果测试类中的方法被注释为`@PreDestroy`,则该方法永远不会运行。因此,在测试类中,我们建议
使用来自底层测试框架的测试生命周期回调,而不是`@PostConstruct`和`@PreDestroy`。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 3.4.3. Spring JUnit4 测试注释 以下注释仅在与[Springrunner](#testcontext-junit4-runner)、[Spring’s JUnit 4 rules](#testcontext-junit4-rules)或[Spring’s JUnit 4 support classes](#testcontext-support-classes-junit4)结合使用时才受支持: * [`@IfProfileValue`](#Integration-Testing-Annotations-JUnit4-IFProfileValue) * [`@ProfileValueSourceConfiguration`](#Integration-Testing-Annotations-JUnit4-ProfilEvaluesourceConfiguration) * [`@Timed`](#Integration-Testing-Annotations-JUnit4-Timed) * [`@Repeat`](#Integration-Testing-Annotations-JUnit4-Repeat) ##### `@IfProfileValue` `@IfProfileValue`表示针对特定的测试环境启用了带注释的测试。如果配置的`ProfileValueSource`返回所提供的`value`的匹配`value`,则启用测试。否则,该测试将被禁用,并实际上被忽略。 你可以在类级别、方法级别或两者都应用`@IfProfileValue`。对于该类或其子类中的任何方法,`@IfProfileValue`的类级用法优先于方法级用法。具体地说,如果在类级别和方法级别都启用了测试,则将启用该测试。缺少`@IfProfileValue`意味着隐式启用了测试。这类似于 JUnit4 的`@Ignore`注释的语义,只是`@Ignore`的存在总是禁用测试。 下面的示例显示了一个带有`@IfProfileValue`注释的测试: Java ``` @IfProfileValue(name="java.vendor", value="Oracle Corporation") (1) @Test public void testProcessWhichRunsOnlyOnOracleJvm() { // some logic that should run only on Java VMs from Oracle Corporation } ``` |**1**|仅当 Java 供应商“甲骨文股份有限公司”时才运行此测试。| |-----|----------------------------------------------------------------| Kotlin ``` @IfProfileValue(name="java.vendor", value="Oracle Corporation") (1) @Test fun testProcessWhichRunsOnlyOnOracleJvm() { // some logic that should run only on Java VMs from Oracle Corporation } ``` |**1**|仅当 Java 供应商“甲骨文股份有限公司”时才运行此测试。| |-----|----------------------------------------------------------------| 或者,你可以使用`@IfProfileValue`的列表配置`values`(带有`OR`语义),以在 JUnit4 环境中实现对测试组的类似 TestNG 的支持。考虑以下示例: Java ``` @IfProfileValue(name="test-groups", values={"unit-tests", "integration-tests"}) (1) @Test public void testProcessWhichRunsForUnitOrIntegrationTestGroups() { // some logic that should run only for unit and integration test groups } ``` |**1**|为单元测试和集成测试运行此测试。| |-----|---------------------------------------------------| Kotlin ``` @IfProfileValue(name="test-groups", values=["unit-tests", "integration-tests"]) (1) @Test fun testProcessWhichRunsForUnitOrIntegrationTestGroups() { // some logic that should run only for unit and integration test groups } ``` |**1**|为单元测试和集成测试运行此测试。| |-----|---------------------------------------------------| ##### `@ProfileValueSourceConfiguration` `@ProfileValueSourceConfiguration`是一个类级注释,它指定在检索通过`@IfProfileValue`注释配置的配置文件值时要使用的`ProfileValueSource`类型。如果`@ProfileValueSourceConfiguration`未声明用于测试,则默认情况下使用`SystemProfileValueSource`。下面的示例展示了如何使用`@ProfileValueSourceConfiguration`: Java ``` @ProfileValueSourceConfiguration(CustomProfileValueSource.class) (1) public class CustomProfileValueSourceTests { // class body... } ``` |**1**|使用自定义配置文件的值源。| |-----|----------------------------------| Kotlin ``` @ProfileValueSourceConfiguration(CustomProfileValueSource::class) (1) class CustomProfileValueSourceTests { // class body... } ``` |**1**|使用自定义配置文件的值源。| |-----|----------------------------------| ##### `@Timed` `@Timed`表示带注释的测试方法必须在指定的时间内(以毫秒为单位)完成执行。如果文本执行时间超过了指定的时间段,则测试失败。 该时间段包括测试方法本身的运行、测试的任何重复(参见`@Repeat`),以及测试夹具的任何设置或拆除。下面的示例展示了如何使用它: Java ``` @Timed(millis = 1000) (1) public void testProcessWithOneSecondTimeout() { // some logic that should not take longer than 1 second to run } ``` |**1**|将测试的时间段设置为一秒。| |-----|-----------------------------------------------| Kotlin ``` @Timed(millis = 1000) (1) fun testProcessWithOneSecondTimeout() { // some logic that should not take longer than 1 second to run } ``` |**1**|将测试的时间段设置为一秒。| |-----|-----------------------------------------------| Spring 的`@Timed`注释与 JUnit4 的`@Test(timeout=…​)`支持具有不同的语义。具体地说,由于 JUnit4 处理测试执行超时的方式(即通过在单独的`Thread`中执行测试方法),如果测试时间过长,`@Test(timeout=…​)`会抢先失败测试。 Spring 的`@Timed`,在另一方面,不会先发制人地使测试失败,而是等待测试完成后再失败。 ##### `@Repeat` `@Repeat`表示带注释的测试方法必须重复运行。在注释中指定了要运行测试方法的次数。 要重复的执行范围包括测试方法本身的执行以及测试夹具的任何设置或拆除。当与[`SpringMethodRule`](#TestContext-JUnit4-Rules)一起使用时,范围还包括通过`TestExecutionListener`实现准备测试实例。下面的示例展示了如何使用`@Repeat`注释: Java ``` @Repeat(10) (1) @Test public void testProcessRepeatedly() { // ... } ``` |**1**|把这个测验重复十次。| |-----|---------------------------| Kotlin ``` @Repeat(10) (1) @Test fun testProcessRepeatedly() { // ... } ``` |**1**|把这个测验重复十次。| |-----|---------------------------| #### 3.4.4. Spring Junit Jupiter 测试注释 当与[`SpringExtension`](#TestContext-JUnit-Jupiter-Extension)和 JUnit Jupiter(即 JUnit5 中的编程模型)结合使用时,支持以下注释: * [`@SpringJUnitConfig`](#Integration-Testing-Annotations-JUnit-Jupiter-SpringJunitConfig) * [`@SpringJUnitWebConfig`](#Integration-Testing-Annotations-JUnit-Jupiter-SpringJunitWebConfig) * [`@TestConstructor`](#Integration-Testing-Annotations-TestConstructor) * [`@NestedTestConfiguration`](#Integration-Testing-Annotations-NestedTestConfiguration) * [`@EnabledIf`](#Integration-Testing-Annotations-JUnit-Jupiter-EnableDIF) * [`@DisabledIf`](#Integration-Testing-Annotations-JUnit-Jupiter-DisableDIF) ##### `@SpringJUnitConfig` `@SpringJUnitConfig`是一个组合注释,它结合了来自 JUnit Jupiter 的`@ExtendWith(SpringExtension.class)`和来自 Spring TestContext 框架的`@ContextConfiguration`。它可以在类级别上用作`@ContextConfiguration`的插入替换。关于配置选项,`@ContextConfiguration`和`@SpringJUnitConfig`之间的唯一区别是,组件类可以在`@SpringJUnitConfig`中使用`value`属性声明。 下面的示例展示了如何使用`@SpringJUnitConfig`注释来指定一个配置类: Java ``` @SpringJUnitConfig(TestConfig.class) (1) class ConfigurationClassJUnitJupiterSpringTests { // class body... } ``` |**1**|指定配置类。| |-----|--------------------------------| Kotlin ``` @SpringJUnitConfig(TestConfig::class) (1) class ConfigurationClassJUnitJupiterSpringTests { // class body... } ``` |**1**|指定配置类。| |-----|--------------------------------| 下面的示例展示了如何使用`@SpringJUnitConfig`注释来指定配置文件的位置: Java ``` @SpringJUnitConfig(locations = "/test-config.xml") (1) class XmlJUnitJupiterSpringTests { // class body... } ``` |**1**|指定配置文件的位置。| |-----|---------------------------------------------| Kotlin ``` @SpringJUnitConfig(locations = ["/test-config.xml"]) (1) class XmlJUnitJupiterSpringTests { // class body... } ``` |**1**|指定配置文件的位置。| |-----|---------------------------------------------| 参见[上下文管理](#testcontext-ctx-management)以及[`@SpringJUnitConfig`]的 Javadoc(https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/test/context/junit/junitconfig.html)和`@ContextConfiguration`以获取更多详细信息。 ##### `@SpringJUnitWebConfig` `@SpringJUnitWebConfig`是一个组合注释,它结合了来自 JUnit Jupiter 的`@ExtendWith(SpringExtension.class)`和来自 Spring TestContext 框架的`@WebAppConfiguration`。你可以在类级别上使用它作为`@ContextConfiguration`和`@WebAppConfiguration`的插入替换。关于配置选项,`@ContextConfiguration`和`@SpringJUnitWebConfig`之间的唯一区别是,你可以使用`value`中的`value`属性来声明组件类。此外,只需在`@SpringJUnitWebConfig`中使用`resourcePath`属性,就可以覆盖`value`中的`value`属性。 下面的示例展示了如何使用`@SpringJUnitWebConfig`注释来指定一个配置类: Java ``` @SpringJUnitWebConfig(TestConfig.class) (1) class ConfigurationClassJUnitJupiterSpringWebTests { // class body... } ``` |**1**|指定配置类。| |-----|--------------------------------| Kotlin ``` @SpringJUnitWebConfig(TestConfig::class) (1) class ConfigurationClassJUnitJupiterSpringWebTests { // class body... } ``` |**1**|指定配置类。| |-----|--------------------------------| 下面的示例展示了如何使用`@SpringJUnitWebConfig`注释来指定配置文件的位置: Java ``` @SpringJUnitWebConfig(locations = "/test-config.xml") (1) class XmlJUnitJupiterSpringWebTests { // class body... } ``` |**1**|指定配置文件的位置。| |-----|---------------------------------------------| Kotlin ``` @SpringJUnitWebConfig(locations = ["/test-config.xml"]) (1) class XmlJUnitJupiterSpringWebTests { // class body... } ``` |**1**|指定配置文件的位置。| |-----|---------------------------------------------| 参见[上下文管理](#testcontext-ctx-management)以及[`@SpringJUnitWebConfig`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/SpringFramework/test/context/junit/junitwebconfig.html)的 Javadoc,[<`@ContextConfiguration`](https:///DOCS. Spring.io/ Spring-framework/ Spring-framework/javadoc/javadoc/juntextframework/org/juntextframework/cr/ ##### `@TestConstructor` `@TestConstructor`是一种类型级别的注释,用于配置如何从测试的`ApplicationContext`中的组件自动连接测试类构造函数的参数。 如果`@TestConstructor`在测试类上不存在或元存在,则将使用默认的*测试构造函数 AutoWire 模式*。有关如何更改默认模式的详细信息,请参见下面的技巧。但是,请注意,构造函数上的`@Autowired`的本地声明优先于`@TestConstructor`和默认模式。 | |更改默认的测试构造函数 AutoWire 模式

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

自 Spring Framework5.3 起,默认模式也可以配置为[JUnit 平台配置参数](https://junit.org/junit5/docs/current/user-guide/#running-tests-config-params)。

如果不设置`spring.test.constructor.autowire.mode`属性,则不会自动连接构造函数。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | |在 Spring Framework5.2 中,`@TestConstructor`只支持与
结合使用的`SpringExtension`。请注意,`SpringExtension`是
通常会自动为你注册–例如,当使用诸如`@SpringJUnitConfig`和`@SpringJUnitWebConfig`之类的注释或来自
Spring 引导测试的各种与测试相关的注释时。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ##### `@NestedTestConfiguration` `@NestedTestConfiguration`是一种类型级别的注释,用于配置 Spring 测试配置注释在为内部测试类封装的类层次结构中的处理方式。 如果`@NestedTestConfiguration`在测试类上不存在或不存在元存在,则在其超级类型层次结构中或在其封闭的类层次结构中,将使用默认的*封闭配置继承模式*。有关如何更改默认模式的详细信息,请参见下面的技巧。 | |更改默认的封闭配置继承模式

默认的*封闭配置继承模式*是`INHERIT`,但是可以通过将`@ContextConfiguration`JVM 系统属性设置为`OVERRIDE`来更改。或者,可以通过[`SpringProperties`](Appendix.html#Appendix- Spring-Properties)机制设置默认模式。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| [Spring TestContext Framework](#testcontext-framework)为下面的注释提供了`@NestedTestConfiguration`语义。 * [`@BootstrapWith`](# Spring-testing-annotation-bootstrapwith) * [`@ContextConfiguration`](# Spring-testing-annotation-contextconfiguration) * [`@WebAppConfiguration`](# Spring-testing-annotation-webappconfiguration) * [`@ContextHierarchy`](# Spring-testing-annotation-contexthierarchy) * [`@ActiveProfiles`](# Spring-测试-注释-ActiveProfiles) * [`@TestPropertySource`](# Spring-testing-annotation-testPropertySource) * [`@DynamicPropertySource`](# Spring-testing-annotation-dynamicpropertysource) * [`@DirtiesContext`](# Spring-testing-annotation-dirtiescontext) * [`@TestExecutionListeners`](# Spring-testing-annotation-testexecutionlisteners) * [`@RecordApplicationEvents`](# Spring-testing-annotation-recordapplicationEvents) * [`@Transactional`] * [`@Commit`](# Spring-testing-annotation-commit) * [`@Rollback`](# Spring-testing-annotation-rollback) * [`@Sql`](# Spring-testing-annotation-sql) * [`@SqlConfig`](# Spring-testing-annotation-sqlconfig) * [`@SqlMergeMode`](# Spring-testing-annotation-sqlmergemode) * [`@TestConstructor`](#Integration-Testing-Annotations-TestConstructor) | |在 JUnit Jupiter 中,使用`@NestedTestConfiguration`通常仅在
与`@Nested`测试类结合时才有意义;但是,可能存在支持 Spring 的其他测试
框架和使用这种
注释的嵌套测试类。| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 参见[`@Nested`测试类配置](#testcontext-junit-jupiter-nested-test-configuration)以获得示例和更多详细信息。 ##### `@EnabledIf` `@EnabledIf`用于表示启用了注释的 JUnit Jupiter 测试类或测试方法,并且如果提供的`expression`计算为`true`,则应该运行该方法。具体地说,如果表达式的求值为`Boolean.TRUE`或`String`等于`true`(忽略情况),则启用测试。当应用于类级别时,该类中的所有测试方法在默认情况下也会自动启用。 表达式可以是以下任何一种: * [Spring Expression Language](core.html#expressions)表达式。例如:`@EnabledIf("#{systemProperties['os.name'].toLowerCase().contains('mac')}")` * 在 Spring[`Environment`](core.html#beans-environment)中可用的属性的占位符。例如:`@EnabledIf("${smoke.tests.enabled}")` * 文字文字。例如:`@EnabledIf("true")` 然而,请注意,不是动态解析属性占位符的结果的文本文字是零实用价值的,因为`@EnabledIf("false")`等价于`@Disabled`和`@EnabledIf("true")`在逻辑上是没有意义的。 你可以使用`@EnabledIf`作为元注释来创建自定义组合注释。例如,你可以创建一个自定义`@EnabledOnMac`注释,如下所示: 爪哇 ``` @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @EnabledIf( expression = "#{systemProperties['os.name'].toLowerCase().contains('mac')}", reason = "Enabled on Mac OS" ) public @interface EnabledOnMac {} ``` Kotlin ``` @Target(AnnotationTarget.TYPE, AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.RUNTIME) @EnabledIf( expression = "#{systemProperties['os.name'].toLowerCase().contains('mac')}", reason = "Enabled on Mac OS" ) annotation class EnabledOnMac {} ``` ##### `@DisabledIf` `@DisabledIf`用于表示注释的 JUnit Jupiter 测试类或测试方法已禁用,并且如果提供的`expression`计算为`true`,则不应运行该测试方法。具体地说,如果表达式的求值为`Boolean.TRUE`或`String`等于`true`(忽略情况),则禁用测试。当应用于类级别时,该类中的所有测试方法也会自动禁用。 表达式可以是以下任何一种: * [Spring Expression Language](core.html#expressions)表达式。例如:`@DisabledIf("#{systemProperties['os.name'].toLowerCase().contains('mac')}")` * 在 Spring[`Environment`](core.html#beans-environment)中可用的属性的占位符。例如:`@DisabledIf("${smoke.tests.disabled}")` * 文字文字。例如:`@DisabledIf("true")` 然而,请注意,不是动态解析属性占位符的结果的文本文字是零实用价值的,因为`@DisabledIf("true")`等价于`@Disabled`和`@DisabledIf("false")`在逻辑上是没有意义的。 你可以使用`@DisabledIf`作为元注释来创建自定义组合注释。例如,你可以创建一个自定义`@DisabledOnMac`注释,如下所示: 爪哇 ``` @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @DisabledIf( expression = "#{systemProperties['os.name'].toLowerCase().contains('mac')}", reason = "Disabled on Mac OS" ) public @interface DisabledOnMac {} ``` Kotlin ``` @Target(AnnotationTarget.TYPE, AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.RUNTIME) @DisabledIf( expression = "#{systemProperties['os.name'].toLowerCase().contains('mac')}", reason = "Disabled on Mac OS" ) annotation class DisabledOnMac {} ``` #### 3.4.5.测试的元注释支持 你可以使用大多数与测试相关的注释[元注释](core.html#beans-meta-annotations)来创建定制的组合注释,并减少跨测试套件的配置重复。 你可以结合[TestContext 框架](#testcontext-framework)使用以下每一项作为元注释。 * `@BootstrapWith` * `@ContextConfiguration` * `@ContextHierarchy` * `@ActiveProfiles` * `@TestPropertySource` * `@DirtiesContext` * `@WebAppConfiguration` * `@TestExecutionListeners` * `@Transactional` * `@BeforeTransaction` * `@AfterTransaction` * `@Commit` * `@Rollback` * `@Sql` * `@SqlConfig` * `@SqlMergeMode` * `@SqlGroup` * `@Repeat` *(仅在 JUnit4 上支持)* * `@Timed` *(仅在 JUnit4 上支持)* * `@IfProfileValue` *(仅在 JUnit4 上支持)* * `@ProfileValueSourceConfiguration` *(仅在 JUnit4 上支持)* * `@SpringJUnitConfig` *(仅在 Junit Jupiter 上支持)* * `@SpringJUnitWebConfig` *(仅在 Junit Jupiter 上支持)* * `@TestConstructor` *(仅在 Junit Jupiter 上支持)* * `@NestedTestConfiguration` *(仅在 Junit Jupiter 上支持)* * `@EnabledIf` *(仅在 Junit Jupiter 上支持)* * `@DisabledIf` *(仅在 Junit Jupiter 上支持)* 考虑以下示例: 爪哇 ``` @RunWith(SpringRunner.class) @ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"}) @ActiveProfiles("dev") @Transactional public class OrderRepositoryTests { } @RunWith(SpringRunner.class) @ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"}) @ActiveProfiles("dev") @Transactional public class UserRepositoryTests { } ``` Kotlin ``` @RunWith(SpringRunner::class) @ContextConfiguration("/app-config.xml", "/test-data-access-config.xml") @ActiveProfiles("dev") @Transactional class OrderRepositoryTests { } @RunWith(SpringRunner::class) @ContextConfiguration("/app-config.xml", "/test-data-access-config.xml") @ActiveProfiles("dev") @Transactional class UserRepositoryTests { } ``` 如果我们发现我们在基于 JUnit4 的测试套件中重复了前面的配置,那么我们可以通过引入自定义组合注释来减少重复,该注释集中了 Spring 的公共测试配置,如下所示: 爪哇 ``` @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"}) @ActiveProfiles("dev") @Transactional public @interface TransactionalDevTestConfig { } ``` Kotlin ``` @Target(AnnotationTarget.TYPE) @Retention(AnnotationRetention.RUNTIME) @ContextConfiguration("/app-config.xml", "/test-data-access-config.xml") @ActiveProfiles("dev") @Transactional annotation class TransactionalDevTestConfig { } ``` 然后,我们可以使用自定义的`@TransactionalDevTestConfig`注释来简化基于 JUnit4 的测试类的配置,如下所示: 爪哇 ``` @RunWith(SpringRunner.class) @TransactionalDevTestConfig public class OrderRepositoryTests { } @RunWith(SpringRunner.class) @TransactionalDevTestConfig public class UserRepositoryTests { } ``` Kotlin ``` @RunWith(SpringRunner::class) @TransactionalDevTestConfig class OrderRepositoryTests @RunWith(SpringRunner::class) @TransactionalDevTestConfig class UserRepositoryTests ``` 如果我们编写使用 JUnit Jupiter 的测试,我们可以进一步减少代码重复,因为 JUnit5 中的注释也可以用作元注释。考虑以下示例: 爪哇 ``` @ExtendWith(SpringExtension.class) @ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"}) @ActiveProfiles("dev") @Transactional class OrderRepositoryTests { } @ExtendWith(SpringExtension.class) @ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"}) @ActiveProfiles("dev") @Transactional class UserRepositoryTests { } ``` Kotlin ``` @ExtendWith(SpringExtension::class) @ContextConfiguration("/app-config.xml", "/test-data-access-config.xml") @ActiveProfiles("dev") @Transactional class OrderRepositoryTests { } @ExtendWith(SpringExtension::class) @ContextConfiguration("/app-config.xml", "/test-data-access-config.xml") @ActiveProfiles("dev") @Transactional class UserRepositoryTests { } ``` 如果我们发现我们在基于 JUnit Jupiter 的测试套件中重复了前面的配置,那么我们可以通过引入一个自定义组合注释来减少重复,该注释集中了 Spring 和 JUnit Jupiter 的公共测试配置,如下所示: 爪哇 ``` @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @ExtendWith(SpringExtension.class) @ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"}) @ActiveProfiles("dev") @Transactional public @interface TransactionalDevTestConfig { } ``` Kotlin ``` @Target(AnnotationTarget.TYPE) @Retention(AnnotationRetention.RUNTIME) @ExtendWith(SpringExtension::class) @ContextConfiguration("/app-config.xml", "/test-data-access-config.xml") @ActiveProfiles("dev") @Transactional annotation class TransactionalDevTestConfig { } ``` 然后,我们可以使用自定义的`@TransactionalDevTestConfig`注释来简化基于 JUnit Jupiter 的各个测试类的配置,如下所示: 爪哇 ``` @TransactionalDevTestConfig class OrderRepositoryTests { } @TransactionalDevTestConfig class UserRepositoryTests { } ``` Kotlin ``` @TransactionalDevTestConfig class OrderRepositoryTests { } @TransactionalDevTestConfig class UserRepositoryTests { } ``` 由于 JUnit Jupiter 支持使用`@Test`、`@RepeatedTest`、`ParameterizedTest`等作为元注释,因此你还可以在测试方法级别上创建自定义的组合注释。例如,如果我们希望创建一个组合注释,将来自 Junit Jupiter 的`@Test`和`@Tag`注释与来自 Spring 的`@Transactional`注释结合在一起,那么我们可以创建一个`@TransactionalIntegrationTest`注释,如下所示: 爪哇 ``` @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Transactional @Tag("integration-test") // org.junit.jupiter.api.Tag @Test // org.junit.jupiter.api.Test public @interface TransactionalIntegrationTest { } ``` Kotlin ``` @Target(AnnotationTarget.TYPE) @Retention(AnnotationRetention.RUNTIME) @Transactional @Tag("integration-test") // org.junit.jupiter.api.Tag @Test // org.junit.jupiter.api.Test annotation class TransactionalIntegrationTest { } ``` 然后我们可以使用我们的自定义`@TransactionalIntegrationTest`注释来简化基于 JUnit Jupiter 的单个测试方法的配置,如下所示: 爪哇 ``` @TransactionalIntegrationTest void saveOrder() { } @TransactionalIntegrationTest void deleteOrder() { } ``` Kotlin ``` @TransactionalIntegrationTest fun saveOrder() { } @TransactionalIntegrationTest fun deleteOrder() { } ``` 有关更多详细信息,请参见[Spring Annotation Programming Model](https://github.com/spring-projects/spring-framework/wiki/Spring-Annotation-Programming-Model)维基页面。 ### 3.5. Spring TestContext 框架 Spring TestContext 框架(位于`org.springframework.test.context`包中)提供了通用的、注释驱动的单元和集成测试支持,该支持与正在使用的测试框架无关。TestContext 框架还非常重视约定而不是配置,使用合理的默认值,你可以通过基于注释的配置来覆盖这些默认值。 除了通用的测试基础设施之外,TestContext 框架还为 JUnit4、JUnit Jupiter(AKA 为 JUnit5)和 TestNG 提供了明确的支持。对于 JUnit4 和 TestNG, Spring 提供`abstract`支持类。此外, Spring 为 JUnit4 提供了自定义的 JUnit`Runner`和自定义的 JUnit`Rules`,为 JUnit Jupiter 提供了自定义的`Extension`,允许你编写所谓的 POJO 测试类。扩展特定的类层次结构不需要 POJO 测试类,例如`abstract`支持类。 下一节将概述 TestContext 框架的内部内容。如果你只对使用框架感兴趣,而对使用自己的自定义侦听器或自定义加载器扩展框架不感兴趣,可以直接访问配置([上下文管理](#testcontext-ctx-management)、[依赖注入](#testcontext-fixture-di)、[事务管理](#testcontext-tx))、[支持类](#testcontext-support-classes)和[注释支持](#integration-testing-annotations)部分。 #### 3.5.1.关键抽象 框架的核心包括[依赖注入](#testcontext-fixture-di)类和`TestContext`、`TestExecutionListener`和`SmartContextLoader`接口。为每个测试类创建`TestContextManager`(例如,用于在 JUnit Jupiter 中的单个测试类中执行所有测试方法)。而`TestContextManager`则管理保存当前测试上下文的`TestContext`。随着测试的进行,`TestContextManager`还会更新`TestContext`的状态,并将其委托给`TestExecutionListener`实现,该实现通过提供依赖注入、管理事务等来实现实际的测试执行。a`SmartContextLoader`负责为给定的测试类加载`ApplicationContext`。有关更多信息和各种实现的示例,请参见[javadoc](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/test/context/package-summary.html)和 Spring 测试套件。 ##### `TestContext` `TestContext`封装了运行测试的上下文(与实际使用的测试框架无关),并为它负责的测试实例提供了上下文管理和缓存支持。如果请求,`TestContext`还将委托给`SmartContextLoader`以加载`ApplicationContext`。 ##### `TestContextManager` `TestContextManager`是 Spring TestContext 框架的主要入口点,负责管理单个`TestContext`,并在定义良好的测试执行点向每个注册的`TestExecutionListener`发送事件信号: * 在特定测试框架的任何“类之前”或“所有之前”方法之前。 * 测试实例后处理。 * 在特定测试框架的任何“before”或“before each”方法之前。 * 在测试方法执行之前,但在测试设置之后. * 在测试方法执行后但在测试前立即拆除。 * 在特定测试框架的任何“之后”或“之后”方法之后。 * 在特定测试框架的任何“课后”或“毕竟”方法之后。 ##### `TestExecutionListener` `TestExecutionListener`定义了用于对注册侦听器的`TestContextManager`发布的测试执行事件做出反应的 API。参见[`TestExecutionListener`配置]。 ##### 上下文加载程序 `ContextLoader`是一个策略接口,用于为 Spring TestContext 框架管理的集成测试加载`ApplicationContext`。你应该实现`WebApplicationContext`而不是这个接口,以提供对组件类、活动 Bean 定义配置文件、测试属性源、上下文层次结构和`WebApplicationContext`支持的支持。 `SmartContextLoader`是`ContextLoader`接口的扩展,它取代了原始的极小值`ContextLoader`SPI。具体地说,`SmartContextLoader`可以选择处理资源位置、组件类或上下文初始化器。此外,`SmartContextLoader`可以在其加载的上下文中设置活动 Bean 定义配置文件和测试属性源。 Spring 提供了以下实现方式: * `TestContext`:两个默认加载器之一,它在内部委托给一个`AnnotationConfigContextLoader`、一个`GenericXmlContextLoader`或一个`GenericGroovyXmlContextLoader`,这取决于为测试类声明的配置,或者取决于是否存在默认位置或默认配置类。仅当 Groovy 位于 Classpath 上时,才启用 Groovy 支持。 * `WebDelegatingSmartContextLoader`:两个默认加载器之一,它在内部委托给一个`AnnotationConfigWebContextLoader`、一个`GenericXmlWebContextLoader`或一个`GenericGroovyXmlWebContextLoader`,这取决于为测试类声明的配置,或者取决于缺省位置或缺省配置类的存在。只有当测试类上存在`@WebAppConfiguration`时,才使用 Web`ContextLoader`。仅当 Groovy 位于 Classpath 上时,才启用 Groovy 支持。 * `AnnotationConfigContextLoader`:从组件类加载标准的`ApplicationContext`。 * `AnnotationConfigWebContextLoader`:从组件类加载`WebApplicationContext`。 * `GenericGroovyXmlContextLoader`:从 Groovy 脚本或 XML 配置文件的资源位置加载标准的`ApplicationContext`。 * `GenericGroovyXmlWebContextLoader`:从 Groovy 脚本或 XML 配置文件的资源位置加载`WebApplicationContext`。 * `GenericXmlContextLoader`:从 XML 资源位置加载标准的`ApplicationContext`。 * `GenericXmlWebContextLoader`:从 XML 资源位置加载`WebApplicationContext`。 #### 3.5.2.引导 TestContext 框架 Spring TestContext 框架内部的默认配置对于所有常见的用例都足够了。但是,有时开发团队或第三方框架希望更改默认的`ContextLoader`,实现自定义的`GenericXmlWebContextLoader`或,增加`ContextCustomizerFactory`和`TestExecutionListener`实现的默认集,以此类推。对于这种对 TestContext 框架如何操作的低级控制, Spring 提供了一种引导策略。 `ApplicationEventsTestExecutionListener`定义了引导 TestContext 框架的 SPI。`TestContextBootstrapper`由`TestContextManager`用于加载用于当前测试的`TestExecutionListener`实现,并构建其管理的`TestContext`。你可以直接使用`@BootstrapWith`或作为元注释,为测试类(或测试类层次结构)配置自定义引导策略。如果没有使用`@BootstrapWith`显式配置引导程序,则使用`DefaultTestContextBootstrapper`或`WebTestContextBootstrapper`,这取决于`@WebAppConfiguration`的存在。 由于`TestContextBootstrapper`SPI 在未来可能会发生变化(以适应新的需求),因此我们强烈鼓励实现者不要直接实现这个接口,而是扩展`AbstractTestContextBootstrapper`或它的一个具体子类。 #### 3.5.3.`TestExecutionListener`配置 Spring 提供了以下`TestExecutionListener`默认情况下注册的实现,其顺序完全如下: * `ServletTestExecutionListener`:为`WebApplicationContext`配置 Servlet API 模拟。 * `GenericGroovyXmlWebContextLoader`:处理“before”模式的`@DirtiesContext`注释。 * `ApplicationEventsTestExecutionListener`:提供对[`ApplicationEvents`](#TestContext-Application-Events)的支持。 * `DependencyInjectionTestExecutionListener`:为测试实例提供依赖注入。 * `DirtiesContextTestExecutionListener`:处理“after”模式的`@DirtiesContext`注释。 * `TransactionalTestExecutionListener`:提供带有默认回滚语义的事务测试执行。 * `SqlScriptsTestExecutionListener`:运行使用`TransactionalTestExecutionListener`注释配置的 SQL 脚本。 * `EventPublishingTestExecutionListener`:将测试执行事件发布到测试的`ApplicationContext`(参见[测试执行事件](#testcontext-test-execution-events))。 ##### 注册`TestExecutionListener`实现 你可以使用`@TestExecutionListeners`注释来注册测试类及其子类的`TestExecutionListener`实现。有关详细信息和示例,请参见[注释支持](#integration-testing-annotations)和[`@TestExecutionListeners`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/test/contexectionlisteners.html)的 爪哇doc。 ##### 默认`TestExecutionListener`实现的自动发现 通过使用`@TestExecutionListeners`注册`TestExecutionListener`实现适合于在有限的测试场景中使用的自定义侦听器。但是,如果需要在整个测试套件中使用自定义侦听器,那么它可能会变得很麻烦。该问题通过支持通过`SpringFactoriesLoader`机制自动发现默认`TestExecutionListener`实现来解决。 具体地说,`ApplicationEvents`模块在其`META-INF/spring.factories`属性文件中的`org.springframework.test.context.TestExecutionListener`键下声明所有核心默认`TestExecutionListener`实现。第三方框架和开发人员可以通过他们自己的`META-INF/spring.factories`属性文件以同样的方式向默认侦听器列表贡献他们自己的`TestExecutionListener`实现。 ##### 排序`TestExecutionListener`实现 当 TestContext 框架通过[前述](#testcontext-tel-config-automatic-discovery)`SpringFactoriesLoader`机制发现默认的`TestExecutionListener`实现时,实例化的侦听器将通过使用 Spring 的`SpringFactoriesLoader`进行排序,该命令使用 Spring 的`Ordered`接口和`@Order`注释进行排序。`AbstractTestExecutionListener`和 Spring 提供的所有默认`TestExecutionListener`实现用适当的值实现`Ordered`。因此,第三方框架和开发人员应该通过实现`TestExecutionListener`或声明`@Order`来确保其默认的`TestExecutionListener`实现是按正确的顺序注册的。关于分配给每个核心侦听器的值的详细信息,请参见 爪哇doc 获取核心默认`getOrder()`实现的`TestExecutionListener`方法。 ##### 合并`TestExecutionListener`实现 如果通过`@TestExecutionListeners`注册了自定义`TestExecutionListener`,则不会注册默认侦听器。在大多数常见的测试场景中,这有效地迫使开发人员手动声明除任何自定义侦听器之外的所有默认侦听器。下面的清单演示了这种配置风格: 爪哇 ``` @ContextConfiguration @TestExecutionListeners({ MyCustomTestExecutionListener.class, ServletTestExecutionListener.class, DirtiesContextBeforeModesTestExecutionListener.class, DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class, TransactionalTestExecutionListener.class, SqlScriptsTestExecutionListener.class }) class MyTest { // class body... } ``` Kotlin ``` @ContextConfiguration @TestExecutionListeners( MyCustomTestExecutionListener::class, ServletTestExecutionListener::class, DirtiesContextBeforeModesTestExecutionListener::class, DependencyInjectionTestExecutionListener::class, DirtiesContextTestExecutionListener::class, TransactionalTestExecutionListener::class, SqlScriptsTestExecutionListener::class ) class MyTest { // class body... } ``` 这种方法的挑战在于,它要求开发人员准确地知道默认注册了哪些侦听器。此外,缺省侦听器集可以从一个版本更改到另一个版本——例如,`SqlScriptsTestExecutionListener`在 Spring Framework4.1 中引入,`DirtiesContextBeforeModesTestExecutionListener`在 Spring Framework4.2 中引入。此外,像 Spring 引导和 Spring 安全之类的第三方框架通过使用前述的[自动发现机制](#testcontext-tel-config-automatic-discovery)来注册它们自己的默认`TestExecutionListener`实现。 为了避免必须意识到并重新声明所有默认侦听器,你可以将`@TestExecutionListeners`的`mergeMode`属性设置为`MergeMode.MERGE_WITH_DEFAULTS`。`MERGE_WITH_DEFAULTS`表示本地声明的侦听器应该与默认侦听器合并。合并算法确保从列表中删除重复项,并确保合并后的侦听器集合根据`AnnotationAwareOrderComparator`的语义进行排序,如[Ordering`TestExecutionListener`实现](#TestContext-TEL-Config-Ordering)中所述。如果侦听器实现了`Ordered`,或者用`@Order`进行了注释,那么它可能会影响与默认值合并的位置。否则,在合并时,本地声明的侦听器将被追加到默认侦听器列表中。 例如,如果上一个示例中的`MyCustomTestExecutionListener`类将其`order`值(例如,`500`)配置为小于`ServletTestExecutionListener`(恰好是`1000`)的顺序,然后可以将`MyCustomTestExecutionListener`与`ServletTestExecutionListener`前面的默认值列表自动合并,并且可以用以下示例替换前面的示例: 爪哇 ``` @ContextConfiguration @TestExecutionListeners( listeners = MyCustomTestExecutionListener.class, mergeMode = MERGE_WITH_DEFAULTS ) class MyTest { // class body... } ``` Kotlin ``` @ContextConfiguration @TestExecutionListeners( listeners = [MyCustomTestExecutionListener::class], mergeMode = MERGE_WITH_DEFAULTS ) class MyTest { // class body... } ``` #### 3.5.4.应用程序事件 Spring Framework5.3.3 以来,TestContext Framework 提供了对记录[应用程序事件](core.html#context-functionality-events)中发布的`ApplicationContext`的支持,以便可以针对测试中的那些事件执行断言。在执行单个测试期间发布的所有事件都可以通过`ApplicationEvents`API 获得,该 API 允许你以`java.util.Stream`的形式处理事件。 要在测试中使用`ApplicationEvents`,请执行以下操作。 * 确保你的测试类是用[`@RecordApplicationEvents`](# Spring-testing-annotation-recordapplicationEvents)进行注释或元注释的。 * 确保`ApplicationEventsTestExecutionListener`已注册。但是,请注意,`ApplicationEventsTestExecutionListener`是默认注册的,并且只有在你通过`@TestExecutionListeners`进行了自定义配置且不包括默认侦听器的情况下,才需要手动注册。 * 用`@Autowired`注释类型`ApplicationEvents`的字段,并在你的测试和生命周期方法(例如 JUnit Jupiter 中的`@BeforeEach`和`@AfterEach`方法)中使用`ApplicationEvents`的实例。 * 当使用[朱尼特木星的 SpringExtension](#testcontext-junit-jupiter-extension)时,可以在测试或生命周期方法中声明类型为`ApplicationEvents`的方法参数,作为测试类中`@Autowired`字段的替代。 下面的测试类使用`SpringExtension`for JUnit Jupiter 和[AssertJ](https://assertj.github.io/doc/)来断言在 Spring 管理的组件中调用方法时发布的应用程序事件的类型: 爪哇 ``` @SpringJUnitConfig(/* ... */) @RecordApplicationEvents (1) class OrderServiceTests { @Autowired OrderService orderService; @Autowired ApplicationEvents events; (2) @Test void submitOrder() { // Invoke method in OrderService that publishes an event orderService.submitOrder(new Order(/* ... */)); // Verify that an OrderSubmitted event was published long numEvents = events.stream(OrderSubmitted.class).count(); (3) assertThat(numEvents).isEqualTo(1); } } ``` |**1**|用`@RecordApplicationEvents`注释测试类。| |-----|-----------------------------------------------------------------------------------------| |**2**|为当前测试注入`ApplicationEvents`实例。| |**3**|使用`ApplicationEvents`API 来计算发布了多少`OrderSubmitted`事件。| Kotlin ``` @SpringJUnitConfig(/* ... */) @RecordApplicationEvents (1) class OrderServiceTests { @Autowired lateinit var orderService: OrderService @Autowired lateinit var events: ApplicationEvents (2) @Test fun submitOrder() { // Invoke method in OrderService that publishes an event orderService.submitOrder(Order(/* ... */)) // Verify that an OrderSubmitted event was published val numEvents = events.stream(OrderSubmitted::class).count() (3) assertThat(numEvents).isEqualTo(1) } } ``` |**1**|用`@RecordApplicationEvents`注释测试类。| |-----|-----------------------------------------------------------------------------------------| |**2**|为当前测试注入`ApplicationEvents`实例。| |**3**|使用`ApplicationEvents`API 来计算发布了多少`OrderSubmitted`事件。| 有关`ApplicationEvents`爪哇doc](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/test/context/event/applicationevents.html)的更多详细信息,请参见[`ApplicationEvents`api。 #### 3.5.5.测试执行事件 Spring Framework5.2 中引入的`EventPublishingTestExecutionListener`提供了一种实现自定义`TestExecutionListener`的替代方法。测试的`ApplicationContext`中的组件可以侦听`EventPublishingTestExecutionListener`发布的以下事件,每个事件对应于`TestExecutionListener`API 中的一个方法。 * `BeforeTestClassEvent` * `PrepareTestInstanceEvent` * `BeforeTestMethodEvent` * `BeforeTestExecutionEvent` * `AfterTestExecutionEvent` * `AfterTestMethodEvent` * `AfterTestClassEvent` | |只有在`ApplicationContext`已经加载的情况下,这些事件才会发布。| |---|------------------------------------------------------------------------------------| 这些事件可能由于各种原因而被使用,例如重置模拟 bean 或跟踪测试执行。使用测试执行事件而不是实现自定义`TestExecutionListener`的一个优点是,测试执行事件可以被注册在测试`ApplicationContext`中的任何 Spring Bean 消耗,并且这样的 bean 可以直接受益于依赖注入和`ApplicationContext`的其他特性。相反,在`ApplicationContext`中,a`AfterTestClassEvent`不是 Bean。 为了侦听测试执行事件, Spring Bean 可以选择实现`org.springframework.context.ApplicationListener`接口。或者,侦听器方法可以使用`AfterTestClassEvent`进行注释,并配置为侦听上面列出的特定事件类型之一(参见[基于注释的事件监听器](core.html#context-functionality-events-annotation))。由于这种方法的流行, Spring 提供了以下专用的`@EventListener`注释,以简化测试执行事件侦听器的注册。这些注释驻留在`org.springframework.test.context.event.annotation`包中。 * `@BeforeTestClass` * `@PrepareTestInstance` * `@BeforeTestMethod` * `@BeforeTestExecution` * `@AfterTestExecution` * `@AfterTestMethod` * `@AfterTestClass` ##### 异常处理 默认情况下,如果测试执行事件侦听器在使用事件时抛出异常,则该异常将传播到使用中的底层测试框架(例如 JUnit 或 TestNG)。例如,如果消耗`BeforeTestMethodEvent`导致异常,则相应的测试方法将作为异常的结果而失败。相反,如果异步测试执行事件侦听器抛出异常,则异常将不会传播到底层测试框架。有关异步异常处理的更多详细信息,请参阅类级 爪哇doc for`@EventListener`。 ##### 异步侦听器 如果希望特定的测试执行事件侦听器异步处理事件,可以使用 Spring 的[regular`@Async`support](integration.html#schooling-annotation-support-async)。有关更多详细信息,请参见类级 爪哇doc`@EventListener`。 #### 3.5.6.上下文管理 每个`TestContext`都为其负责的测试实例提供了上下文管理和缓存支持。测试实例不会自动接收对配置的`ApplicationContext`的访问。但是,如果测试类实现了`ApplicationContextAware`接口,则将向测试实例提供对`ApplicationContext`的引用。注意`AbstractJUnit4SpringContextTests`和`AbstractTestNGSpringContextTests`实现`ApplicationContextAware`,因此,自动提供对`ApplicationContext`的访问。 | |@AutoWired ApplicationContext

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

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

Kotlin

```
@SpringJUnitConfig
class MyTest {

@Autowired (1)
lateinit var applicationContext: ApplicationContext

// class body...
}
```

|**1**|Injecting the `ApplicationContext`.|
|-----|-----------------------------------|

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

爪哇

```
@SpringJUnitWebConfig (1)
class MyWebAppTest {

@Autowired (2)
WebApplicationContext wac;

// class body...
}
```

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

Kotlin

```
@SpringJUnitWebConfig (1)
class MyWebAppTest {

@Autowired (2)
lateinit var wac: WebApplicationContext
// class body...
}
```

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

Dependency injection by using `@Autowired` is provided by the`DependencyInjectionTestExecutionListener`, which is configured by default
(see [Dependency Injection of Test Fixtures](#testcontext-fixture-di)).| |-----|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |**1**|注入`ApplicationContext`。| |**1**|注入`ApplicationContext`。| |**1**|配置`WebApplicationContext`。| |**2**|注入`WebApplicationContext`。| |**1**|配置`WebApplicationContext`。| |**2**|注入`WebApplicationContext`。| 使用 TestContext 框架的测试类不需要扩展任何特定的类或实现特定的接口来配置它们的应用程序上下文。相反,配置是通过在类级别声明`@ContextConfiguration`注释来实现的。如果你的测试类没有显式声明应用程序上下文资源位置或组件类,那么配置的`ContextLoader`将决定如何从默认位置或默认配置类加载上下文。除了上下文资源位置和组件类之外,还可以通过应用程序上下文初始化器来配置应用程序上下文。 下面的部分解释了如何使用 Spring 的`@ContextConfiguration`注释来通过使用 XML 配置文件、Groovy 脚本、组件类(通常是`@Configuration`类)或上下文初始化器来配置测试`ApplicationContext`。或者,你可以为高级用例实现和配置你自己的自定义`SmartContextLoader`。 * [使用 XML 资源的上下文配置](#testcontext-ctx-management-xml) * [使用 Groovy 脚本的上下文配置](#testcontext-ctx-management-groovy) * [具有组件类的上下文配置](#testcontext-ctx-management-javaconfig) * [混合 XML、Groovy 脚本和组件类](#testcontext-ctx-management-mixed-config) * [带有上下文初始化器的上下文配置](#testcontext-ctx-management-initializers) * [上下文配置继承](#testcontext-ctx-management-inheritance) * [具有环境配置文件的上下文配置](#testcontext-ctx-management-env-profiles) * [具有测试属性源的上下文配置](#testcontext-ctx-management-property-sources) * [具有动态属性源的上下文配置](#testcontext-ctx-management-dynamic-property-sources) * [加载`WebApplicationContext`](#testcontext-ctx-management-web) * [上下文缓存](#testcontext-ctx-management-caching) * [上下文层次结构](#testcontext-ctx-management-ctx-hierarchies) ##### 使用 XML 资源的上下文配置 要通过使用 XML 配置文件为测试加载`ApplicationContext`,请用`@ContextConfiguration`注释测试类,并使用包含 XML 配置元数据资源位置的数组配置`locations`属性。普通或相对路径(例如,`context.xml`)被视为相对于定义测试类的包的 Classpath 资源。以斜杠开头的路径被视为绝对 Classpath 位置(例如,`/org/example/config.xml`)。表示资源 URL 的路径(即,带有`classpath:`、`file:`、`http:`等前缀的路径)被使用*就像现在一样*。 爪哇 ``` @ExtendWith(SpringExtension.class) // ApplicationContext will be loaded from "/app-config.xml" and // "/test-config.xml" in the root of the classpath @ContextConfiguration(locations={"/app-config.xml", "/test-config.xml"}) (1) class MyTest { // class body... } ``` |**1**|将 locations 属性设置为 XML 文件列表。| |-----|-------------------------------------------------------| Kotlin ``` @ExtendWith(SpringExtension::class) // ApplicationContext will be loaded from "/app-config.xml" and // "/test-config.xml" in the root of the classpath @ContextConfiguration("/app-config.xml", "/test-config.xml") (1) class MyTest { // class body... } ``` |**1**|将 locations 属性设置为 XML 文件列表。| |-----|-------------------------------------------------------| `@ContextConfiguration`通过标准的 爪哇`value`属性支持`locations`属性的别名。因此,如果不需要在`@ContextConfiguration`中声明其他属性,则可以省略`locations`属性名的声明,并使用以下示例中演示的速记格式声明资源位置: 爪哇 ``` @ExtendWith(SpringExtension.class) @ContextConfiguration({"/app-config.xml", "/test-config.xml"}) (1) class MyTest { // class body... } ``` |**1**|指定 XML 文件而不使用`location`属性。| |-----|------------------------------------------------------------| Kotlin ``` @ExtendWith(SpringExtension::class) @ContextConfiguration("/app-config.xml", "/test-config.xml") (1) class MyTest { // class body... } ``` |**1**|指定 XML 文件而不使用`location`属性。| |-----|------------------------------------------------------------| 如果从`@ContextConfiguration`注释中省略`locations`和`value`属性,TestContext 框架将尝试检测默认的 XML 资源位置。具体地说,`GenericXmlContextLoader`和`GenericXmlWebContextLoader`根据测试类的名称检测缺省位置。如果你的类名为`com.example.MyTest`,则`GenericXmlContextLoader`从`"classpath:com/example/MyTest-context.xml"`加载应用程序上下文。下面的示例展示了如何做到这一点: 爪哇 ``` @ExtendWith(SpringExtension.class) // ApplicationContext will be loaded from // "classpath:com/example/MyTest-context.xml" @ContextConfiguration (1) class MyTest { // class body... } ``` |**1**|从默认位置加载配置。| |-----|------------------------------------------------| Kotlin ``` @ExtendWith(SpringExtension::class) // ApplicationContext will be loaded from // "classpath:com/example/MyTest-context.xml" @ContextConfiguration (1) class MyTest { // class body... } ``` |**1**|从默认位置加载配置。| |-----|------------------------------------------------| ##### 使用 Groovy 脚本的上下文配置 要使用使用使用[Groovy Bean Definition DSL](core.html#groovy-bean-definition-dsl)的 Groovy 脚本为你的测试加载`ApplicationContext`,你可以使用`@ContextConfiguration`注释测试类,并配置`locations`或`value`属性,并使用一个包含 Groovy 脚本资源位置的数组。Groovy 脚本的资源查找语义与[XML 配置文件](#testcontext-ctx-management-xml)的资源查找语义相同。 | |启用 Groovy 脚本支持`value`
在 Spring
testContext 框架中使用 Groovy 脚本加载`ApplicationContext`的支持是自动启用的,如果 Groovy 在 Classpath 上。| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 下面的示例展示了如何指定 Groovy 配置文件: 爪哇 ``` @ExtendWith(SpringExtension.class) // ApplicationContext will be loaded from "/AppConfig.groovy" and // "/TestConfig.groovy" in the root of the classpath @ContextConfiguration({"/AppConfig.groovy", "/TestConfig.Groovy"}) (1) class MyTest { // class body... } ``` Kotlin ``` @ExtendWith(SpringExtension::class) // ApplicationContext will be loaded from "/AppConfig.groovy" and // "/TestConfig.groovy" in the root of the classpath @ContextConfiguration("/AppConfig.groovy", "/TestConfig.Groovy") (1) class MyTest { // class body... } ``` |**1**|指定 Groovy 配置文件的位置。| |-----|------------------------------------------------------| 如果从`@ContextConfiguration`注释中省略`locations`和`value`属性,TestContext 框架将尝试检测默认的 Groovy 脚本。具体地说,`GenericGroovyXmlContextLoader`和`GenericGroovyXmlWebContextLoader`基于测试类的名称检测缺省位置。如果你的类名为`com.example.MyTest`,那么 Groovy 上下文加载程序将从`"classpath:com/example/MyTestContext.groovy"`加载应用程序上下文。下面的示例展示了如何使用默认值: 爪哇 ``` @ExtendWith(SpringExtension.class) // ApplicationContext will be loaded from // "classpath:com/example/MyTestContext.groovy" @ContextConfiguration (1) class MyTest { // class body... } ``` |**1**|从默认位置加载配置。| |-----|------------------------------------------------| Kotlin ``` @ExtendWith(SpringExtension::class) // ApplicationContext will be loaded from // "classpath:com/example/MyTestContext.groovy" @ContextConfiguration (1) class MyTest { // class body... } ``` |**1**|从默认位置加载配置。| |-----|------------------------------------------------| | |同时声明 XML 配置和 Groovy 脚本

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

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






<<<>r=“1319”>r=“>”>r=”1319“/>r=”r=“>”>“>
术语“组件类”可以指以下任何一种:

* 用
注释的类
* 一个组件(即用`@Component`、`@Service`、`WebApplicationContext`注释的类,或其他原型注释)。

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

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

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


参见[`@Configuration`](https://DOCS. Spring.io/ Spring-framework/5.3.16/javadog/javadoc-api-api/org/5.3.16/javadoc-api-api-api/org/org/javadoc-api-api/org/org/spramework/Springframework/contem/context/context/context/context/cont 特别注意
对`@Bean`精简模式的讨论。| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 如果省略`@ContextConfiguration`注释中的`classes`属性,TestContext 框架将尝试检测缺省配置类的存在。具体地说,`AnnotationConfigContextLoader`和`AnnotationConfigWebContextLoader`检测满足配置类实现要求的测试类的所有`static`嵌套类,如在[`@Configuration`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/context/context/annation/configuration.html)中指定的。请注意,配置类的名称是任意的。此外,如果需要,一个测试类可以包含多个`static`嵌套配置类。在下面的示例中,`OrderServiceTest`类声明了一个名为`static`的嵌套配置类,该配置类自动用于为测试类加载`ApplicationContext`: 爪哇 ``` @SpringJUnitConfig (1) // ApplicationContext will be loaded from the // static nested Config class class OrderServiceTest { @Configuration static class Config { // this bean will be injected into the OrderServiceTest class @Bean OrderService orderService() { OrderService orderService = new OrderServiceImpl(); // set properties, etc. return orderService; } } @Autowired OrderService orderService; @Test void testOrderService() { // test the orderService } } ``` |**1**|从嵌套的`Config`类加载配置信息。| |-----|-----------------------------------------------------------------| Kotlin ``` @SpringJUnitConfig (1) // ApplicationContext will be loaded from the nested Config class class OrderServiceTest { @Autowired lateinit var orderService: OrderService @Configuration class Config { // this bean will be injected into the OrderServiceTest class @Bean fun orderService(): OrderService { // set properties, etc. return OrderServiceImpl() } } @Test fun testOrderService() { // test the orderService } } ``` |**1**|从嵌套的`Config`类加载配置信息。| |-----|-----------------------------------------------------------------| ##### 混合 XML、Groovy 脚本和组件类 有时可能需要混合 XML 配置文件、Groovy 脚本和组件类(通常是`@Configuration`类)来为测试配置`ApplicationContext`。例如,如果你在生产中使用 XML 配置,那么你可能会决定使用`@Configuration`类来为你的测试配置特定的由 Spring 管理的组件,反之亦然。 此外,一些第三方框架(例如 Spring Boot)提供了用于同时从不同类型的资源(例如,XML 配置文件、Groovy 脚本和类)加载的一流支持。 Spring 框架在历史上并不支持这一标准部署。因此, Spring 框架在`spring-test`模块中交付的大多数`SmartContextLoader`实现只为每个测试上下文支持一种资源类型。然而,这并不意味着你不能同时使用这两种方法。一般规则的一个例外是`GenericGroovyXmlContextLoader`和`GenericGroovyXmlWebContextLoader`同时支持 XML 配置文件和 Groovy 脚本。此外,第三方框架可以选择通过`@ContextConfiguration`来支持`locations`和`classes`的声明,并且,在 TestContext 框架中的标准测试支持下,你有以下选项。 如果希望使用资源位置(例如,XML 或 Groovy)和`@Configuration`类来配置测试,则必须选择一个作为入口点,其中一个必须包含或导入另一个。例如,在 XML 或 Groovy 脚本中,可以通过使用组件扫描或将它们定义为正常的 Spring bean 来包含`@Configuration`类,而在`@Configuration`类中,可以使用`@ImportResource`来导入 XML 配置文件或 Groovy 脚本。请注意,这种行为在语义上等同于在生产中配置应用程序的方式:在生产配置中,你可以定义一组 XML 或 Groovy 资源位置,也可以定义一组`@Configuration`类,从这些类加载你的生产`ApplicationContext`,但是,你仍然可以自由地包含或导入其他类型的配置。 ##### 带有上下文初始化器的上下文配置 要使用上下文初始化器为测试配置`ApplicationContext`,请用`@ContextConfiguration`注释测试类,并使用一个数组配置`initializers`属性,该数组包含对实现`ApplicationContextInitializer`的类的引用。然后使用声明的上下文初始化器初始化为测试加载的`ConfigurableApplicationContext`。请注意,每个声明的初始化器支持的具体`ConfigurableApplicationContext`类型必须与使用中的`SmartContextLoader`创建的`ApplicationContext`类型兼容(通常是`GenericApplicationContext`)。此外,初始化器被调用的顺序取决于它们是实现 Spring 的`Ordered`接口,还是使用 Spring 的`@Order`注释或标准的`@Priority`注释。下面的示例展示了如何使用初始化器: 爪哇 ``` @ExtendWith(SpringExtension.class) // ApplicationContext will be loaded from TestConfig // and initialized by TestAppCtxInitializer @ContextConfiguration( classes = TestConfig.class, initializers = TestAppCtxInitializer.class) (1) class MyTest { // class body... } ``` |**1**|通过使用配置类和初始化器指定配置。| |-----|---------------------------------------------------------------------------| Kotlin ``` @ExtendWith(SpringExtension::class) // ApplicationContext will be loaded from TestConfig // and initialized by TestAppCtxInitializer @ContextConfiguration( classes = [TestConfig::class], initializers = [TestAppCtxInitializer::class]) (1) class MyTest { // class body... } ``` |**1**|通过使用配置类和初始化器指定配置。| |-----|---------------------------------------------------------------------------| 你还可以完全省略`@ContextConfiguration`中的 XML 配置文件、Groovy 脚本或组件类的声明,而只声明`ApplicationContextInitializer`类,这些类随后负责在上下文中注册 bean——例如,通过编程方式从 XML 文件或配置类中加载 Bean 定义。下面的示例展示了如何做到这一点: 爪哇 ``` @ExtendWith(SpringExtension.class) // ApplicationContext will be initialized by EntireAppInitializer // which presumably registers beans in the context @ContextConfiguration(initializers = EntireAppInitializer.class) (1) class MyTest { // class body... } ``` |**1**|仅使用初始化器指定配置。| |-----|------------------------------------------------------| Kotlin ``` @ExtendWith(SpringExtension::class) // ApplicationContext will be initialized by EntireAppInitializer // which presumably registers beans in the context @ContextConfiguration(initializers = [EntireAppInitializer::class]) (1) class MyTest { // class body... } ``` |**1**|仅使用初始化器指定配置。| |-----|------------------------------------------------------| ##### 上下文配置继承 `@ContextConfiguration`支持布尔`inheritLocations`和`inheritInitializers`属性,这些属性表示是否应该继承由超类声明的资源位置或组件类和上下文初始化器。这两个标志的默认值都是`true`。这意味着测试类继承了资源位置或组件类,以及由任何超类声明的上下文初始化器。具体地说,测试类的资源位置或组件类被追加到由超类声明的资源位置或注释类的列表中。类似地,给定测试类的初始化器被添加到由测试超类定义的初始化器集合中。因此,子类可以选择扩展资源位置、组件类或上下文初始化器。 如果`inheritLocations`或`inheritInitializers`中的`inheritInitializers`属性被设置为`false`,则资源位置或组件类和上下文初始化器分别用于测试类的影子和有效替换由超类定义的配置。 | |在 Spring Framework5.3 中,测试配置也可以从包含
类中继承。有关详细信息,请参见[`@Nested`测试类配置]。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 在下一个使用 XML 资源位置的示例中,`ApplicationContext`的`ExtendedTest`的`ApplicationContext`按照这个顺序从`base-config.xml`和`extended-config.xml`加载。因此,在`extended-config.xml`中定义的 bean 可以覆盖(即替换)在`base-config.xml`中定义的 bean。下面的示例展示了一个类如何扩展另一个类,并同时使用自己的配置文件和超类的配置文件: 爪哇 ``` @ExtendWith(SpringExtension.class) // ApplicationContext will be loaded from "/base-config.xml" // in the root of the classpath @ContextConfiguration("/base-config.xml") (1) class BaseTest { // class body... } // ApplicationContext will be loaded from "/base-config.xml" and // "/extended-config.xml" in the root of the classpath @ContextConfiguration("/extended-config.xml") (2) class ExtendedTest extends BaseTest { // class body... } ``` |**1**|在超类中定义的配置文件。| |-----|---------------------------------------------| |**2**|在子类中定义的配置文件。| Kotlin ``` @ExtendWith(SpringExtension::class) // ApplicationContext will be loaded from "/base-config.xml" // in the root of the classpath @ContextConfiguration("/base-config.xml") (1) open class BaseTest { // class body... } // ApplicationContext will be loaded from "/base-config.xml" and // "/extended-config.xml" in the root of the classpath @ContextConfiguration("/extended-config.xml") (2) class ExtendedTest : BaseTest() { // class body... } ``` |**1**|在超类中定义的配置文件。| |-----|---------------------------------------------| |**2**|在子类中定义的配置文件。| 类似地,在下一个使用组件类的示例中,`ApplicationContext`的`ExtendedTest`是从`BaseConfig`和`ExtendedConfig`类按此顺序加载的。因此,在`ExtendedConfig`中定义的 bean 可以覆盖(即替换)在`BaseConfig`中定义的 bean。下面的示例展示了一个类如何扩展另一个类,并同时使用自己的配置类和超类的配置类: 爪哇 ``` // ApplicationContext will be loaded from BaseConfig @SpringJUnitConfig(BaseConfig.class) (1) class BaseTest { // class body... } // ApplicationContext will be loaded from BaseConfig and ExtendedConfig @SpringJUnitConfig(ExtendedConfig.class) (2) class ExtendedTest extends BaseTest { // class body... } ``` |**1**|在超类中定义的配置类。| |-----|----------------------------------------------| |**2**|在子类中定义的配置类。| Kotlin ``` // ApplicationContext will be loaded from BaseConfig @SpringJUnitConfig(BaseConfig::class) (1) open class BaseTest { // class body... } // ApplicationContext will be loaded from BaseConfig and ExtendedConfig @SpringJUnitConfig(ExtendedConfig::class) (2) class ExtendedTest : BaseTest() { // class body... } ``` |**1**|在超类中定义的配置类。| |-----|----------------------------------------------| |**2**|在子类中定义的配置类。| 在下一个使用上下文初始化器的示例中,`ApplicationContext`的`ExtendedTest`通过使用`BaseInitializer`和`ExtendedInitializer`进行初始化。然而,请注意,初始化器被调用的顺序取决于它们是实现 Spring 的`Ordered`接口,还是使用 Spring 的`@Order`注释或标准的`@Priority`注释。下面的示例展示了一个类如何扩展另一个类,并同时使用自己的初始化器和超类的初始化器: 爪哇 ``` // ApplicationContext will be initialized by BaseInitializer @SpringJUnitConfig(initializers = BaseInitializer.class) (1) class BaseTest { // class body... } // ApplicationContext will be initialized by BaseInitializer // and ExtendedInitializer @SpringJUnitConfig(initializers = ExtendedInitializer.class) (2) class ExtendedTest extends BaseTest { // class body... } ``` |**1**|在超类中定义的初始化器。| |-----|--------------------------------------| |**2**|在子类中定义的初始化器。| Kotlin ``` // ApplicationContext will be initialized by BaseInitializer @SpringJUnitConfig(initializers = [BaseInitializer::class]) (1) open class BaseTest { // class body... } // ApplicationContext will be initialized by BaseInitializer // and ExtendedInitializer @SpringJUnitConfig(initializers = [ExtendedInitializer::class]) (2) class ExtendedTest : BaseTest() { // class body... } ``` |**1**|在超类中定义的初始化器。| |-----|--------------------------------------| |**2**|在子类中定义的初始化器。| ##### 具有环境配置文件的上下文配置 Spring 框架具有对环境和配置文件(AKA“ Bean 定义配置文件”)的概念的一流支持,并且集成测试可以被配置为针对各种测试场景激活特定的 Bean 定义配置文件。这是通过用`@ActiveProfiles`注释一个测试类并提供一个配置文件列表来实现的,这些配置文件在为测试加载`ApplicationContext`时应该被激活。 | |你可以将`@ActiveProfiles`用于`SmartContextLoader`SPI 的任何实现,但是`@ActiveProfiles`不支持较早的`ContextLoader`SPI 的实现。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 考虑使用 XML 配置和`@Configuration`类的两个示例: ``` ``` 爪哇 ``` @ExtendWith(SpringExtension.class) // ApplicationContext will be loaded from "classpath:/app-config.xml" @ContextConfiguration("/app-config.xml") @ActiveProfiles("dev") class TransferServiceTest { @Autowired TransferService transferService; @Test void testTransferService() { // test the transferService } } ``` Kotlin ``` @ExtendWith(SpringExtension::class) // ApplicationContext will be loaded from "classpath:/app-config.xml" @ContextConfiguration("/app-config.xml") @ActiveProfiles("dev") class TransferServiceTest { @Autowired lateinit var transferService: TransferService @Test fun testTransferService() { // test the transferService } } ``` 当运行`TransferServiceTest`时,其`ApplicationContext`将从 Classpath 的根目录中的`app-config.xml`配置文件加载。如果检查`app-config.xml`,可以看到`accountRepository` Bean 对`dataSource` Bean 具有依赖关系。然而,`dataSource`并未被定义为顶层 Bean。相反,`dataSource`被定义了三次:在`production`配置文件中,在`dev`配置文件中,以及在`default`配置文件中。 通过用`TransferServiceTest`注释`@ActiveProfiles("dev")`,我们指示 Spring TestContext 框架加载`ApplicationContext`,并将活动配置文件设置为`{"dev"}`。结果,嵌入式数据库被创建并填充了测试数据,并且`accountRepository` Bean 与开发`DataSource`的引用连接。这很可能就是我们在集成测试中想要的。 有时,将 bean 分配到`default`配置文件中是有用的。只有当没有其他配置文件被特别激活时,默认配置文件中的 bean 才会被包含。你可以使用它来定义要在应用程序的默认状态下使用的“fallback”bean。例如,你可以显式地为`dev`和`production`配置文件提供数据源,但在这两个配置文件都不是活动的情况下,将内存中数据源定义为默认值。 下面的代码清单演示了如何使用`@Configuration`类而不是 XML 来实现相同的配置和集成测试: 爪哇 ``` @Configuration @Profile("dev") public class StandaloneDataConfig { @Bean public DataSource dataSource() { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.HSQL) .addScript("classpath:com/bank/config/sql/schema.sql") .addScript("classpath:com/bank/config/sql/test-data.sql") .build(); } } ``` Kotlin ``` @Configuration @Profile("dev") class StandaloneDataConfig { @Bean fun dataSource(): DataSource { return EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.HSQL) .addScript("classpath:com/bank/config/sql/schema.sql") .addScript("classpath:com/bank/config/sql/test-data.sql") .build() } } ``` 爪哇 ``` @Configuration @Profile("production") public class JndiDataConfig { @Bean(destroyMethod="") public DataSource dataSource() throws Exception { Context ctx = new InitialContext(); return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource"); } } ``` Kotlin ``` @Configuration @Profile("production") class JndiDataConfig { @Bean(destroyMethod = "") fun dataSource(): DataSource { val ctx = InitialContext() return ctx.lookup("java:comp/env/jdbc/datasource") as DataSource } } ``` 爪哇 ``` @Configuration @Profile("default") public class DefaultDataConfig { @Bean public DataSource dataSource() { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.HSQL) .addScript("classpath:com/bank/config/sql/schema.sql") .build(); } } ``` Kotlin ``` @Configuration @Profile("default") class DefaultDataConfig { @Bean fun dataSource(): DataSource { return EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.HSQL) .addScript("classpath:com/bank/config/sql/schema.sql") .build() } } ``` 爪哇 ``` @Configuration public class TransferServiceConfig { @Autowired DataSource dataSource; @Bean public TransferService transferService() { return new DefaultTransferService(accountRepository(), feePolicy()); } @Bean public AccountRepository accountRepository() { return new JdbcAccountRepository(dataSource); } @Bean public FeePolicy feePolicy() { return new ZeroFeePolicy(); } } ``` Kotlin ``` @Configuration class TransferServiceConfig { @Autowired lateinit var dataSource: DataSource @Bean fun transferService(): TransferService { return DefaultTransferService(accountRepository(), feePolicy()) } @Bean fun accountRepository(): AccountRepository { return JdbcAccountRepository(dataSource) } @Bean fun feePolicy(): FeePolicy { return ZeroFeePolicy() } } ``` 爪哇 ``` @SpringJUnitConfig({ TransferServiceConfig.class, StandaloneDataConfig.class, JndiDataConfig.class, DefaultDataConfig.class}) @ActiveProfiles("dev") class TransferServiceTest { @Autowired TransferService transferService; @Test void testTransferService() { // test the transferService } } ``` Kotlin ``` @SpringJUnitConfig( TransferServiceConfig::class, StandaloneDataConfig::class, JndiDataConfig::class, DefaultDataConfig::class) @ActiveProfiles("dev") class TransferServiceTest { @Autowired lateinit var transferService: TransferService @Test fun testTransferService() { // test the transferService } } ``` 在这个变体中,我们将 XML 配置拆分为四个独立的`@Configuration`类: * `TransferServiceConfig`:通过使用`@Autowired`通过依赖注入获得`dataSource`。 * `StandaloneDataConfig`:为适合于开发人员测试的嵌入式数据库定义`dataSource`。 * `JndiDataConfig`:定义在生产环境中从 JNDI 检索的`dataSource`。 * `DefaultDataConfig`:为默认的嵌入式数据库定义一个`dataSource`,以防没有配置文件处于活动状态。 与基于 XML 的配置示例一样,我们仍然使用`TransferServiceTest`注释`@ActiveProfiles("dev")`,但这次我们使用`@ContextConfiguration`注释来指定所有四个配置类。测试类本身的主体完全保持不变。 通常的情况是,在一个给定的项目中,跨多个测试类使用一组配置文件。因此,为了避免重复`@ActiveProfiles`注释的声明,可以在基类上声明`@ActiveProfiles`一次,子类自动从基类继承`@ActiveProfiles`配置。在下面的示例中,`@ActiveProfiles`的声明(以及其他注释)已移动到一个抽象超类`AbstractIntegrationTest`: | |在 Spring Framework5.3 中,测试配置也可以从包含
类中继承。有关详细信息,请参见[`@Nested`测试类配置]。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 爪哇 ``` @SpringJUnitConfig({ TransferServiceConfig.class, StandaloneDataConfig.class, JndiDataConfig.class, DefaultDataConfig.class}) @ActiveProfiles("dev") abstract class AbstractIntegrationTest { } ``` Kotlin ``` @SpringJUnitConfig( TransferServiceConfig::class, StandaloneDataConfig::class, JndiDataConfig::class, DefaultDataConfig::class) @ActiveProfiles("dev") abstract class AbstractIntegrationTest { } ``` 爪哇 ``` // "dev" profile inherited from superclass class TransferServiceTest extends AbstractIntegrationTest { @Autowired TransferService transferService; @Test void testTransferService() { // test the transferService } } ``` Kotlin ``` // "dev" profile inherited from superclass class TransferServiceTest : AbstractIntegrationTest() { @Autowired lateinit var transferService: TransferService @Test fun testTransferService() { // test the transferService } } ``` `@ActiveProfiles`还支持一个`inheritProfiles`属性,该属性可用于禁用活动配置文件的继承,如下例所示: 爪哇 ``` // "dev" profile overridden with "production" @ActiveProfiles(profiles = "production", inheritProfiles = false) class ProductionTransferServiceTest extends AbstractIntegrationTest { // test body } ``` Kotlin ``` // "dev" profile overridden with "production" @ActiveProfiles("production", inheritProfiles = false) class ProductionTransferServiceTest : AbstractIntegrationTest() { // test body } ``` 此外,有时需要以编程方式而不是声明式地解决测试的活动配置文件——例如,基于: * 当前的操作系统。 * 测试是否在持续集成构建服务器上运行。 * 某些环境变量的存在。 * 自定义类级注释的存在。 * 其他问题。 要以编程方式解析活动 Bean 定义配置文件,你可以实现一个自定义的`ActiveProfilesResolver`,并使用`resolver`的`resolver`属性对其进行注册。有关更多信息,请参见相应的[javadoc](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/test/context/ActiveProfilesResolver.html)。下面的示例演示如何实现和注册自定义`OperatingSystemActiveProfilesResolver`: 爪哇 ``` // "dev" profile overridden programmatically via a custom resolver @ActiveProfiles( resolver = OperatingSystemActiveProfilesResolver.class, inheritProfiles = false) class TransferServiceTest extends AbstractIntegrationTest { // test body } ``` Kotlin ``` // "dev" profile overridden programmatically via a custom resolver @ActiveProfiles( resolver = OperatingSystemActiveProfilesResolver::class, inheritProfiles = false) class TransferServiceTest : AbstractIntegrationTest() { // test body } ``` 爪哇 ``` public class OperatingSystemActiveProfilesResolver implements ActiveProfilesResolver { @Override public String[] resolve(Class testClass) { String profile = ...; // determine the value of profile based on the operating system return new String[] {profile}; } } ``` Kotlin ``` class OperatingSystemActiveProfilesResolver : ActiveProfilesResolver { override fun resolve(testClass: Class<*>): Array { val profile: String = ... // determine the value of profile based on the operating system return arrayOf(profile) } } ``` ##### 具有测试属性源的上下文配置 Spring 框架对具有属性源层次结构的环境的概念具有一流的支持,并且你可以使用特定于测试的属性源来配置集成测试。与`@Configuration`类上使用的`@PropertySource`注释相反,你可以在测试类上声明`@TestPropertySource`注释,以声明测试属性文件或内联属性的资源位置。这些测试属性源被添加到`PropertySources`中的`Environment`中的`ApplicationContext`集合中,用于为带注释的集成测试加载`ApplicationContext`。 | |你可以使用`@TestPropertySource`与`SmartContextLoader`SPI 的任何实现一起使用`@TestPropertySource`,但是`@TestPropertySource`不支持与较早的`ContextLoader`SPI 的实现一起使用
`SmartContextLoader`通过`getPropertySourceLocations()`中的方法访问合并的测试属性源值
。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ###### 声明测试属性源 可以使用`locations`或`value`的属性配置测试属性文件。 支持传统的和基于 XML 的属性文件格式——例如,`"classpath:/com/example/test.properties"`或`"file:///path/to/file.xml"`。 每个路径被解释为 Spring `Resource`。普通路径(例如,`"test.properties"`)被视为与定义测试类的包相关的 Classpath 资源。以斜杠开头的路径被视为绝对 Classpath 资源(例如:`"/org/example/test.xml"`)。通过使用指定的资源协议加载引用 URL 的路径(例如,带有`classpath:`、`file:`或`http:`前缀的路径)。不允许使用资源位置通配符(例如`***/**.properties`):每个位置必须精确地计算一个`.properties`或`.xml`资源。 下面的示例使用了一个测试属性文件: 爪哇 ``` @ContextConfiguration @TestPropertySource("/test.properties") (1) class MyIntegrationTests { // class body... } ``` |**1**|指定具有绝对路径的属性文件。| |-----|---------------------------------------------------| Kotlin ``` @ContextConfiguration @TestPropertySource("/test.properties") (1) class MyIntegrationTests { // class body... } ``` |**1**|指定具有绝对路径的属性文件。| |-----|---------------------------------------------------| 你可以使用`@TestPropertySource`的`properties`属性,以键-值对的形式配置内联属性,如下一个示例所示。将所有键值对添加到附件`Environment`中,作为具有最高优先级的单个测试`PropertySource`。 所支持的键-值对语法与为 爪哇 属性文件中的条目定义的语法相同: * `key=value` * `key:value` * `key value` 下面的示例设置了两个内联属性: 爪哇 ``` @ContextConfiguration @TestPropertySource(properties = {"timezone = GMT", "port: 4242"}) (1) class MyIntegrationTests { // class body... } ``` |**1**|使用键值语法的两种变体设置两个属性。| |-----|-----------------------------------------------------------------------| Kotlin ``` @ContextConfiguration @TestPropertySource(properties = ["timezone = GMT", "port: 4242"]) (1) class MyIntegrationTests { // class body... } ``` |**1**|使用键值语法的两种变体设置两个属性。| |-----|-----------------------------------------------------------------------| | |在 Spring Framework5.2 中,`@TestPropertySource`可以用作*可重复注释*。
这意味着可以在单个
测试类上有多个`@TestPropertySource`的声明,随着`locations`和`properties`后面的`@TestPropertySource`注释覆盖了前面的`@TestPropertySource`注释,
此外,你可以在一个测试类上声明多个组合注释,每个组合注释
meta 注释为`@TestPropertySource`,并且所有那些`@TestPropertySource`声明都将贡献给你的测试属性源。

直接呈现`@TestPropertySource`注释总是优先于
meta-present`@TestPropertySource`注释。换句话说,`locations`和`properties`来自直接存在的`@TestPropertySource`注释将覆盖`locations`和`properties`来自`@TestPropertySource`用作元注释的注释。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ###### 默认属性文件检测 如果`@TestPropertySource`被声明为空注释(即,对于`locations`或`properties`属性没有显式的值),则尝试检测相对于声明该注释的类的默认属性文件。例如,如果带注释的测试类是`com.example.MyTest`,那么对应的默认属性文件是`classpath:com/example/MyTest.properties`。如果无法检测到默认值,则抛出一个`IllegalStateException`。 ###### 优先权 测试属性的优先级高于在操作系统环境、Java 系统属性或应用程序通过使用`@PropertySource`或编程方式声明性地添加的属性源中定义的属性。因此,可以使用测试属性来选择性地覆盖从系统和应用程序属性源加载的属性。此外,与从资源位置加载的属性相比,内联属性具有更高的优先级。但是,请注意,通过[`@DynamicPropertySource`]注册的属性(#testcontext-ctx-management-dynamic-property-sources)比通过`@TestPropertySource`加载的属性具有更高的优先级。 在下一个示例中,`timezone`和`port`属性以及`"/test.properties"`中定义的任何属性覆盖了在系统和应用程序属性源中定义的同名属性。此外,如果`"/test.properties"`文件为`timezone`和`port`属性定义条目,则这些条目将被使用`properties`属性声明的内联属性覆盖。下面的示例展示了如何在文件和内联中指定属性: Java ``` @ContextConfiguration @TestPropertySource( locations = "/test.properties", properties = {"timezone = GMT", "port: 4242"} ) class MyIntegrationTests { // class body... } ``` Kotlin ``` @ContextConfiguration @TestPropertySource("/test.properties", properties = ["timezone = GMT", "port: 4242"] ) class MyIntegrationTests { // class body... } ``` ###### 继承和重写测试属性源 `@TestPropertySource`支持布尔`inheritLocations`和`inheritProperties`属性,这些属性表示超类声明的属性文件和内联属性的资源位置是否应该被继承。这两个标志的默认值都是`true`。这意味着测试类继承了由任何超类声明的位置和内联属性。具体地说,测试类的位置和内联属性被追加到超类声明的位置和内联属性之后。因此,子类可以选择扩展位置和内联属性。请注意,后面出现的属性是与前面出现的相同名称的 shadow(即覆盖)属性。此外,前面提到的优先规则也适用于继承的测试属性源。 如果`inheritLocations`或`inheritProperties`中的`inheritProperties`属性设置为`false`,则位置或内联属性分别用于测试类的影子和有效地替换由超类定义的配置。 | |在 Spring Framework5.3 中,测试配置也可以从包含
类中继承。有关详细信息,请参见[`@Nested`测试类配置]。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 在下一个示例中,只使用`base.properties`文件作为测试属性源加载`ApplicationContext`for`BaseTest`。相比之下,`ApplicationContext`的`ExtendedTest`是通过使用`base.properties`和`extended.properties`文件作为测试属性源位置加载的。下面的示例展示了如何通过使用`properties`文件在子类和其超类中定义属性: Java ``` @TestPropertySource("base.properties") @ContextConfiguration class BaseTest { // ... } @TestPropertySource("extended.properties") @ContextConfiguration class ExtendedTest extends BaseTest { // ... } ``` Kotlin ``` @TestPropertySource("base.properties") @ContextConfiguration open class BaseTest { // ... } @TestPropertySource("extended.properties") @ContextConfiguration class ExtendedTest : BaseTest() { // ... } ``` 在下一个示例中,只使用内联的`key1`属性加载`ApplicationContext`for`BaseTest`。与此相反,`ApplicationContext`的`ExtendedTest`是通过使用内联的`key1`和`key2`属性加载的。下面的示例展示了如何通过使用内联属性在子类和其超类中定义属性: Java ``` @TestPropertySource(properties = "key1 = value1") @ContextConfiguration class BaseTest { // ... } @TestPropertySource(properties = "key2 = value2") @ContextConfiguration class ExtendedTest extends BaseTest { // ... } ``` Kotlin ``` @TestPropertySource(properties = ["key1 = value1"]) @ContextConfiguration open class BaseTest { // ... } @TestPropertySource(properties = ["key2 = value2"]) @ContextConfiguration class ExtendedTest : BaseTest() { // ... } ``` ##### 具有动态属性源的上下文配置 从 Spring Framework5.2.5 开始,TestContext 框架通过`@DynamicPropertySource`注释为*动态*属性提供支持。此注释可用于集成测试,这些测试需要在`Environment`中的`PropertySources`集合中添加具有动态值的属性,用于为集成测试加载`ApplicationContext`。 | |`@DynamicPropertySource`注释及其支持的基础结构是
,最初的设计目的是允许基于[测试容器](https://www.testcontainers.org/)的测试中的属性很容易地暴露在
Spring 集成测试中。然而,这个特性也可以用于任何形式的
外部资源,其生命周期被维护在测试的`ApplicationContext`之外。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 与在类级别应用的[`@TestPropertySource`](#testcontext-ctx-management-property-sources)注释不同,`@DynamicPropertySource`必须应用到一个`static`方法,该方法接受一个`DynamicPropertyRegistry`参数,该参数用于将*名称-值*对添加到`Environment`。值是动态的,并通过`Supplier`提供,只有在解析属性时才调用该属性。通常,方法引用用于提供值,如下面的示例所示,该示例使用 TestContainers 项目来管理 Spring `ApplicationContext`之外的 Redis 容器。托管 Redis 容器的 IP 地址和端口可通过`redis.host`和`redis.port`属性提供给测试`ApplicationContext`中的组件。这些属性可以通过 Spring 的`Environment`抽象进行访问,也可以直接注入到 Spring 管理的组件中——例如,分别通过`@Value("${redis.host}")`和`@Value("${redis.port}")`。 | |如果在基类中使用`@DynamicPropertySource`,并发现子类
中的测试失败,因为子类之间的动态属性发生了变化,那么你可能需要用[
](# Spring-testing-annotation-dirtiescontext)对基类进行注释,以
确保每个子类都具有正确的动态`ApplicationContext`属性。| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| Java ``` @SpringJUnitConfig(/* ... */) @Testcontainers class ExampleIntegrationTests { @Container static RedisContainer redis = new RedisContainer(); @DynamicPropertySource static void redisProperties(DynamicPropertyRegistry registry) { registry.add("redis.host", redis::getContainerIpAddress); registry.add("redis.port", redis::getMappedPort); } // tests ... } ``` Kotlin ``` @SpringJUnitConfig(/* ... */) @Testcontainers class ExampleIntegrationTests { companion object { @Container @JvmStatic val redis: RedisContainer = RedisContainer() @DynamicPropertySource @JvmStatic fun redisProperties(registry: DynamicPropertyRegistry) { registry.add("redis.host", redis::getContainerIpAddress) registry.add("redis.port", redis::getMappedPort) } } // tests ... } ``` ###### 优先权 动态属性的优先级高于从`@TestPropertySource`、操作系统的环境、Java 系统属性或应用程序通过使用`@PropertySource`或编程方式声明性地添加的属性源加载的属性。因此,动态属性可以用于选择性地覆盖通过`@TestPropertySource`、系统属性源和应用程序属性源加载的属性。 ##### 装入`WebApplicationContext` 要指示 TestContext 框架加载`WebApplicationContext`而不是标准的`ApplicationContext`,你可以用`@WebAppConfiguration`注释相应的测试类。 测试类上的`@WebAppConfiguration`指示 TestContext 框架应该为集成测试加载`WebApplicationContext`。在后台,TCF 确保创建了`MockServletContext`并将其提供给测试的 WAC。默认情况下,`MockServletContext`的基本资源路径设置为`src/main/webapp`。这被解释为与你的 JVM 的根相关的路径(通常是你的项目的路径)。如果你熟悉 Maven 项目中 Web 应用程序的目录结构,那么你就知道`src/main/webapp`是你的 WAR 的根目录的默认位置。如果需要重写此默认值,则可以提供`@WebAppConfiguration`注释的替代路径(例如,`@WebAppConfiguration("src/test/webapp")`)。如果希望从 Classpath 而不是文件系统引用基本资源路径,则可以使用 Spring 的`classpath:`前缀。 请注意, Spring 对`WebApplicationContext`实现的测试支持与其对标准`ApplicationContext`实现的支持是相同的。当使用`WebApplicationContext`进行测试时,你可以使用`@ContextConfiguration`声明 XML 配置文件、Groovy 脚本或`@Configuration`类。你还可以自由地使用任何其他测试注释,例如`@ActiveProfiles`、`@TestExecutionListeners`、`@Sql`、`@Rollback`和其他注释。 本节中的其余示例显示了用于加载`WebApplicationContext`的各种配置选项中的一些。下面的示例展示了 TestContext 框架对配置之上的约定的支持: Java ``` @ExtendWith(SpringExtension.class) // defaults to "file:src/main/webapp" @WebAppConfiguration // detects "WacTests-context.xml" in the same package // or static nested @Configuration classes @ContextConfiguration class WacTests { //... } ``` Kotlin ``` @ExtendWith(SpringExtension::class) // defaults to "file:src/main/webapp" @WebAppConfiguration // detects "WacTests-context.xml" in the same package // or static nested @Configuration classes @ContextConfiguration class WacTests { //... } ``` 如果使用`@WebAppConfiguration`注释测试类而不指定资源基路径,则资源路径实际上默认为`file:src/main/webapp`。类似地,如果在不指定资源`locations`的情况下声明`@ContextConfiguration`,组件`classes`,或上下文`initializers`, Spring 试图通过使用约定(即`WacTests-context.xml`在与`WacTests`类相同的包中或静态嵌套`@Configuration`类)来检测配置的存在。 下面的示例显示了如何显式地声明具有`@WebAppConfiguration`的资源库路径和具有`@ContextConfiguration`的 XML 资源位置: Java ``` @ExtendWith(SpringExtension.class) // file system resource @WebAppConfiguration("webapp") // classpath resource @ContextConfiguration("/spring/test-servlet-config.xml") class WacTests { //... } ``` Kotlin ``` @ExtendWith(SpringExtension::class) // file system resource @WebAppConfiguration("webapp") // classpath resource @ContextConfiguration("/spring/test-servlet-config.xml") class WacTests { //... } ``` 这里需要注意的重要一点是,这两种注释的路径具有不同的语义。默认情况下,`@WebAppConfiguration`资源路径是基于文件系统的,而`@ContextConfiguration`资源位置是基于 Classpath 的。 下面的示例表明,我们可以通过指定 Spring 资源前缀来覆盖这两种注释的默认资源语义: Java ``` @ExtendWith(SpringExtension.class) // classpath resource @WebAppConfiguration("classpath:test-web-resources") // file system resource @ContextConfiguration("file:src/main/webapp/WEB-INF/servlet-config.xml") class WacTests { //... } ``` Kotlin ``` @ExtendWith(SpringExtension::class) // classpath resource @WebAppConfiguration("classpath:test-web-resources") // file system resource @ContextConfiguration("file:src/main/webapp/WEB-INF/servlet-config.xml") class WacTests { //... } ``` 将本例中的注释与前面的示例进行对比。 []()使用 Web 模拟 为了提供全面的 Web 测试支持,TestContext 框架有一个`ServletTestExecutionListener`,默认情况下启用。当针对`WebApplicationContext`进行测试时,此[`TestExecutionListener`](#testcontext-key-abstractions)通过在每个测试方法之前使用 Spring Web 的`RequestContextHolder`设置默认的线程-本地状态,并基于配置有`@WebAppConfiguration`的基本资源路径创建`MockHttpServletResponse`、`ServletWebRequest`。`ServletTestExecutionListener`还确保`MockHttpServletResponse`和`ServletWebRequest`可以被注入到测试实例中,并且,一旦测试完成,它将清理线程本地状态。 一旦为测试加载了`WebApplicationContext`,你可能会发现需要与 Web 模拟交互——例如,在调用 Web 组件后设置测试 fixture 或执行断言。下面的示例展示了哪些模拟可以自动连接到你的测试实例中。请注意,`WebApplicationContext`和`MockServletContext`都是跨测试套件缓存的,而其他模拟是由`ServletTestExecutionListener`按每个测试方法管理的。 Java ``` @SpringJUnitWebConfig class WacTests { @Autowired WebApplicationContext wac; // cached @Autowired MockServletContext servletContext; // cached @Autowired MockHttpSession session; @Autowired MockHttpServletRequest request; @Autowired MockHttpServletResponse response; @Autowired ServletWebRequest webRequest; //... } ``` Kotlin ``` @SpringJUnitWebConfig class WacTests { @Autowired lateinit var wac: WebApplicationContext // cached @Autowired lateinit var servletContext: MockServletContext // cached @Autowired lateinit var session: MockHttpSession @Autowired lateinit var request: MockHttpServletRequest @Autowired lateinit var response: MockHttpServletResponse @Autowired lateinit var webRequest: ServletWebRequest //... } ``` ##### 上下文缓存 一旦 TestContext Framework 为测试加载`ApplicationContext`(或`WebApplicationContext`),该上下文将被缓存,并在所有随后的测试中重用,这些测试在相同的测试套件中声明相同的唯一上下文配置。要理解缓存的工作原理,重要的是要理解“唯一”和“测试套件”的含义。 `ApplicationContext`可以通过用于加载它的配置参数的组合来唯一标识。因此,配置参数的唯一组合被用来生成一个关键字,在该关键字下缓存上下文。TestContext 框架使用以下配置参数来构建上下文缓存键: * `locations`(来自`@ContextConfiguration`) * `classes`(来自`@ContextConfiguration`) * `contextInitializerClasses`(来自`@ContextConfiguration`) * `contextCustomizers`(来自`ContextCustomizerFactory`)–这包括`@DynamicPropertySource`方法以及来自 Spring Boot 的测试支持的各种功能,例如`@MockBean`和`@SpyBean`。 * `contextLoader`(来自`@ContextConfiguration`) * `parent`(来自`@ContextHierarchy`) * `activeProfiles`(来自`@ActiveProfiles`) * `propertySourceLocations`(来自`@TestPropertySource`) * `propertySourceProperties`(来自`@TestPropertySource`) * `resourceBasePath`(来自`@WebAppConfiguration`) 例如,如果`TestClassA`为`locations`(或`value`)属性的`{"app-config.xml", "test-config.xml"}`指定`{"app-config.xml", "test-config.xml"}`,则 TestContext 框架加载相应的`ApplicationContext`,并将其存储在`static`上下文缓存中,该密钥仅基于这些位置。因此,如果`TestClassB`也为其位置定义了`{"app-config.xml", "test-config.xml"}`(通过继承显式或隐式地),但没有定义`@WebAppConfiguration`,则不同的`ContextLoader`、不同的活动配置文件、不同的上下文初始化器、不同的测试属性源或不同的父上下文,那么相同的`ApplicationContext`由两个测试类共享。这意味着加载应用程序上下文的设置成本仅发生一次(每个测试套件),并且随后的测试执行速度要快得多。 | |测试套件和分叉进程

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

要受益于缓存机制,所有测试都必须在相同的进程或测试
套件中运行。这可以通过在 IDE 中作为一个组执行所有测试来实现。类似地,
在使用 Ant、 Maven 或 Gradle 之类的构建框架执行测试时,重要的是要确保构建框架不会在测试之间分叉。例如,
如果将 Maven surefire 插件的[`forkMode`](https:// Maven.apache.org/plugins/ Maven-surefire-plugin/test-mojo.html#forkmode)设置为`always`或`pertest`,则 TestContext 框架
不能在测试类之间运行
当需要调试使用 Spring TestContext 框架执行的测试时,可以对
控制台输出进行有用的分析(即输出到`SYSOUT`和`SYSERR`流)。一些构建工具和 IDE 能够将控制台输出与给定的
测试相关联;但是,某些控制台输出不能很容易地与给定的测试相关联。

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

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

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

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

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

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

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

当 Spring `ApplicationContext`通过 JVM 关机钩子关闭时,在关机阶段执行的回调
在名为`SpringContextShutdownHook`的线程上执行。因此,
如果你希望禁用在`ApplicationContext`关闭
时触发的控制台日志记录,则可以通过 JVM 关闭钩子,在你的日志
框架中注册一个自定义过滤器,该框架允许你忽略由该线程发起的任何日志记录。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ##### 上下文层次结构 在编写依赖加载的 Spring `ApplicationContext`的集成测试时,通常只需针对单个上下文进行测试。然而,有时对`ApplicationContext`实例的层次结构进行测试是有益的,甚至是必要的。例如,如果你正在开发 Spring MVC Web 应用程序,则通常有一个根`WebApplicationContext`由 Spring 的`ContextLoaderListener`加载,而一个子`WebApplicationContext`由 Spring 的`DispatcherServlet`加载。这将导致一个父子上下文层次结构,其中共享组件和基础设施配置在根上下文中声明,并由特定于 Web 的组件在子上下文中使用。另一个用例可以在 Spring 批处理应用程序中找到,在该应用程序中,通常有一个父上下文为共享批处理基础设施提供配置,以及一个子上下文为特定批处理作业的配置提供配置。 可以在单个测试类上或在测试类层次结构中,通过使用`@ContextHierarchy`注释声明上下文配置来编写使用上下文层次结构的集成测试。如果在测试类层次结构中的多个类上声明了上下文层次结构,则还可以合并或覆盖上下文层次结构中特定的命名级别的上下文配置。当合并层次结构中给定级别的配置时,配置资源类型(即 XML 配置文件或组件类)必须是一致的。否则,在上下文层次结构中使用不同的资源类型配置不同的级别是完全可以接受的。 本节中剩余的基于 JUnit Jupiter 的示例展示了需要使用上下文层次结构的集成测试的常见配置场景。 具有上下文层次结构的单个测试类 `ControllerIntegrationTests`通过声明由两个级别组成的上下文层次结构,表示 Spring MVC Web 应用程序的典型集成测试场景,一个用于根`WebApplicationContext`(通过使用`TestAppConfig``@Configuration`类加载),一个用于调度器 Servlet `WebApplicationContext`(通过使用`WebConfig`类加载)。自动连接到测试实例的`WebApplicationContext`是子上下文(即层次结构中最低的上下文)。下面的清单展示了这个配置场景: Java ``` @ExtendWith(SpringExtension.class) @WebAppConfiguration @ContextHierarchy({ @ContextConfiguration(classes = TestAppConfig.class), @ContextConfiguration(classes = WebConfig.class) }) class ControllerIntegrationTests { @Autowired WebApplicationContext wac; // ... } ``` Kotlin ``` @ExtendWith(SpringExtension::class) @WebAppConfiguration @ContextHierarchy( ContextConfiguration(classes = [TestAppConfig::class]), ContextConfiguration(classes = [WebConfig::class])) class ControllerIntegrationTests { @Autowired lateinit var wac: WebApplicationContext // ... } ``` 具有隐式父上下文的类层次结构 本例中的测试类在测试类层次结构中定义了上下文层次结构。`AbstractWebTests`在 Spring 驱动的 Web 应用程序中声明根`WebApplicationContext`的配置。但请注意,`AbstractWebTests`并不声明`@ContextHierarchy`。因此,`AbstractWebTests`的子类可以选择性地参与上下文层次结构或遵循`@ContextConfiguration`的标准语义。`SoapWebServiceTests`和`RestWebServiceTests`都扩展了`AbstractWebTests`,并通过使用`@ContextHierarchy`定义了上下文层次结构。结果是加载了三个应用程序上下文(每个`@ContextConfiguration`的声明有一个),并且基于`AbstractWebTests`中的配置加载的应用程序上下文被设置为为为具体子类加载的每个上下文的父上下文。下面的清单展示了这个配置场景: 爪哇 ``` @ExtendWith(SpringExtension.class) @WebAppConfiguration @ContextConfiguration("file:src/main/webapp/WEB-INF/applicationContext.xml") public abstract class AbstractWebTests {} @ContextHierarchy(@ContextConfiguration("/spring/soap-ws-config.xml")) public class SoapWebServiceTests extends AbstractWebTests {} @ContextHierarchy(@ContextConfiguration("/spring/rest-ws-config.xml")) public class RestWebServiceTests extends AbstractWebTests {} ``` Kotlin ``` @ExtendWith(SpringExtension::class) @WebAppConfiguration @ContextConfiguration("file:src/main/webapp/WEB-INF/applicationContext.xml") abstract class AbstractWebTests @ContextHierarchy(ContextConfiguration("/spring/soap-ws-config.xml")) class SoapWebServiceTests : AbstractWebTests() @ContextHierarchy(ContextConfiguration("/spring/rest-ws-config.xml")) class RestWebServiceTests : AbstractWebTests() ``` 具有合并上下文层次结构配置的类层次结构 本例中的类展示了命名层次结构级别的使用,以便合并上下文层次结构中特定级别的配置。`BaseTests`定义了层次结构中的两个级别,`parent`和`child`。`ExtendedTests`扩展了`BaseTests`,并指示 Spring TestContext 框架合并`child`层次结构级别的上下文配置,方法是确保`@ContextConfiguration`中的`name`属性中声明的名称都是`child`。结果是加载了三个应用程序上下文:一个用于`/app-config.xml`,一个用于`/user-config.xml`,一个用于`{"/user-config.xml", "/order-config.xml"}`。与前面的示例一样,从`/app-config.xml`加载的应用程序上下文被设置为从`/user-config.xml`和`{"/user-config.xml", "/order-config.xml"}`加载的上下文的父上下文。下面的清单展示了这个配置场景: 爪哇 ``` @ExtendWith(SpringExtension.class) @ContextHierarchy({ @ContextConfiguration(name = "parent", locations = "/app-config.xml"), @ContextConfiguration(name = "child", locations = "/user-config.xml") }) class BaseTests {} @ContextHierarchy( @ContextConfiguration(name = "child", locations = "/order-config.xml") ) class ExtendedTests extends BaseTests {} ``` Kotlin ``` @ExtendWith(SpringExtension::class) @ContextHierarchy( ContextConfiguration(name = "parent", locations = ["/app-config.xml"]), ContextConfiguration(name = "child", locations = ["/user-config.xml"])) open class BaseTests {} @ContextHierarchy( ContextConfiguration(name = "child", locations = ["/order-config.xml"]) ) class ExtendedTests : BaseTests() {} ``` 具有重写的上下文层次结构配置的类层次结构 与前面的示例相反,这个示例演示了如何通过将`@ContextConfiguration`中的`inheritLocations`标志设置为`false`来覆盖上下文层次结构中给定命名级别的配置。因此,`ExtendedTests`的应用程序上下文仅从`/test-user-config.xml`加载,并将其父集设置为从`/app-config.xml`加载的上下文。下面的清单展示了这个配置场景: 爪哇 ``` @ExtendWith(SpringExtension.class) @ContextHierarchy({ @ContextConfiguration(name = "parent", locations = "/app-config.xml"), @ContextConfiguration(name = "child", locations = "/user-config.xml") }) class BaseTests {} @ContextHierarchy( @ContextConfiguration( name = "child", locations = "/test-user-config.xml", inheritLocations = false )) class ExtendedTests extends BaseTests {} ``` Kotlin ``` @ExtendWith(SpringExtension::class) @ContextHierarchy( ContextConfiguration(name = "parent", locations = ["/app-config.xml"]), ContextConfiguration(name = "child", locations = ["/user-config.xml"])) open class BaseTests {} @ContextHierarchy( ContextConfiguration( name = "child", locations = ["/test-user-config.xml"], inheritLocations = false )) class ExtendedTests : BaseTests() {} ``` | |在上下文层次结构

中玷污上下文如果你在一个测试中使用`@DirtiesContext`,该测试的上下文被配置为
上下文层次结构的一部分,那么你可以使用`hierarchyMode`标志来控制如何清除上下文缓存
。有关更多详细信息,请参见`@DirtiesContext`中对[Spring Testing Annotations](#spring-testing-annotation-dirtiescontext)的讨论和[`@DirtiesContext`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/test/annotation/dirtiescontext.html)的讨论。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 3.5.7.测试夹具的依赖注入 当你使用`DependencyInjectionTestExecutionListener`(它是默认配置的)时,你的测试实例的依赖项是从你配置了`@ContextConfiguration`或相关注释的应用程序上下文中的 bean 注入的。你可以使用 setter 注入、字段注入,或者两者都使用,这取决于你选择了哪些注释,以及是否将它们放置在 setter 方法或字段上。如果你正在使用 JUnit Jupiter,你也可以选择使用构造函数注入(参见[Dependency Injection with`SpringExtension`](#TestContext-JUnit-Jupiter-DI))。为了与 Spring 的基于注释的注入支持保持一致,还可以使用 Spring 的`@Autowired`注释或来自 JSR-330 的`@Inject`注释进行字段和 setter 注入。 | |对于 JUnit Jupiter 以外的测试框架,TestContext 框架不
参与测试类的实例化。因此,对于构造函数使用`@Autowired`或`@Inject`对测试类没有影响。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | |尽管在生产代码中不鼓励现场注入,但在测试代码中,现场注入实际上是
非常自然的。这种差异的基本原理是,你将永远不会直接实例化你的测试类
。因此,不需要能够在测试类上调用
构造函数或 setter 方法。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 因为`@Autowired`用于执行[按类型自动布线](core.html#beans-factory-autowire),所以如果你有多个相同类型的 Bean 定义,那么对于那些特定的 bean,你不能依赖这种方法。在这种情况下,可以将`@Autowired`与`@Qualifier`结合使用。你还可以选择将`@Inject`与`@Named`结合使用。或者,如果你的测试类具有对其`ApplicationContext`的访问权限,则可以使用(例如)对`applicationContext.getBean("titleRepository", TitleRepository.class)`的调用来执行显式查找。 如果不希望将依赖注入应用于测试实例,请不要使用`@Autowired`或`@Inject`注释域或 setter 方法。或者,可以通过显式地使用`@TestExecutionListeners`配置类并从侦听器列表中省略`DependencyInjectionTestExecutionListener.class`来完全禁用依赖注入。 考虑测试`HibernateTitleRepository`类的场景,如[Goals](#integration-testing-goals)部分所概述的那样。接下来的两个代码清单演示了`@Autowired`在字段和 setter 方法上的使用。在所有示例代码列表之后,将介绍应用程序上下文配置。 | |以下代码列表中的依赖注入行为不是 JUnit
Jupiter 特有的。相同的 DI 技术可以与任何支持的测试
框架结合使用。

以下示例调用静态断言方法,例如`assertNotNull()`,
,但不预先处理与`Assertions`的调用。在这种情况下,假设
方法是通过`import static`声明正确导入的,该声明在
示例中未显示。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 第一个代码清单显示了一个基于 JUnit Jupiter 的测试类实现,它使用`@Autowired`进行字段注入: 爪哇 ``` @ExtendWith(SpringExtension.class) // specifies the Spring configuration to load for this test fixture @ContextConfiguration("repository-config.xml") class HibernateTitleRepositoryTests { // this instance will be dependency injected by type @Autowired HibernateTitleRepository titleRepository; @Test void findById() { Title title = titleRepository.findById(new Long(10)); assertNotNull(title); } } ``` Kotlin ``` @ExtendWith(SpringExtension::class) // specifies the Spring configuration to load for this test fixture @ContextConfiguration("repository-config.xml") class HibernateTitleRepositoryTests { // this instance will be dependency injected by type @Autowired lateinit var titleRepository: HibernateTitleRepository @Test fun findById() { val title = titleRepository.findById(10) assertNotNull(title) } } ``` 或者,你可以将类配置为使用`@Autowired`进行 setter 注入,如下所示: 爪哇 ``` @ExtendWith(SpringExtension.class) // specifies the Spring configuration to load for this test fixture @ContextConfiguration("repository-config.xml") class HibernateTitleRepositoryTests { // this instance will be dependency injected by type HibernateTitleRepository titleRepository; @Autowired void setTitleRepository(HibernateTitleRepository titleRepository) { this.titleRepository = titleRepository; } @Test void findById() { Title title = titleRepository.findById(new Long(10)); assertNotNull(title); } } ``` Kotlin ``` @ExtendWith(SpringExtension::class) // specifies the Spring configuration to load for this test fixture @ContextConfiguration("repository-config.xml") class HibernateTitleRepositoryTests { // this instance will be dependency injected by type lateinit var titleRepository: HibernateTitleRepository @Autowired fun setTitleRepository(titleRepository: HibernateTitleRepository) { this.titleRepository = titleRepository } @Test fun findById() { val title = titleRepository.findById(10) assertNotNull(title) } } ``` 前面的代码列表使用由`@ContextConfiguration`注释(即`repository-config.xml`)引用的相同 XML 上下文文件。以下显示了此配置: ``` ``` | |如果你是从 Spring 提供的测试基类进行扩展的,而测试基类恰好在它的一个 setter 方法上使用`@Autowired`,那么你可能在应用程序上下文中定义了多个受影响的
类型的 bean(例如,多个`DataSource`bean)。在
这样的情况下,可以重写 setter 方法,并使用`@Qualifier`注释来表示特定的目标 Bean,如下所示(但也要确保将其委托给超类中重写的
方法):

java





<><1893"r="gt="1914"/>指定的限定词表示指定的限定符[lt=”lt=“lt=”lt=“lt=”lt=“lt17”/>`定义中的``声明相匹配。 Bean name
被用作回退限定符值,因此你也可以有效地通过这里的名称指向特定的
Bean(如前面所示,假设`myDataSource`是 Bean `id`)。| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 3.5.8.测试请求和会话范围的 bean Spring 从早期开始就支持[请求和会话范围的 bean](core.html#beans-factory-scopes-other),你可以通过以下步骤来测试你的请求范围和会话范围的 bean: * 通过使用`@WebAppConfiguration`注释测试类,确保为测试加载了`WebApplicationContext`。 * 将模拟请求或会话注入到你的测试实例中,并根据需要准备测试装置。 * 调用从配置的`WebApplicationContext`中检索到的 Web 组件(带有依赖项注入)。 * 对模拟执行断言。 下一个代码片段显示了登录用例的 XML 配置。请注意,`userService` Bean 对请求作用域`loginAction` Bean 具有依赖关系。而且,`LoginAction`是通过使用[Spel 表达式](core.html#expressions)实例化的,后者从当前的 HTTP 请求中检索用户名和密码。在测试中,我们希望通过 TestContext 框架管理的模拟来配置这些请求参数。下面的清单显示了这个用例的配置: 请求范围 Bean 配置 ``` ``` 在`RequestScopedBeanTests`中,我们将`UserService`(即被测试的对象)和`MockHttpServletRequest`注入到我们的测试实例中。在我们的`requestScope()`测试方法中,我们通过在提供的`MockHttpServletRequest`中设置请求参数来设置测试夹具。当在我们的`userService`上调用`loginUser()`方法时,我们确信用户服务可以访问当前`MockHttpServletRequest`的请求范围`loginAction`(即我们只设置参数的那个)。然后,我们可以根据用户名和密码的已知输入,针对结果执行断言。下面的清单展示了如何做到这一点: 爪哇 ``` @SpringJUnitWebConfig class RequestScopedBeanTests { @Autowired UserService userService; @Autowired MockHttpServletRequest request; @Test void requestScope() { request.setParameter("user", "enigma"); request.setParameter("pswd", "$pr!ng"); LoginResults results = userService.loginUser(); // assert results } } ``` Kotlin ``` @SpringJUnitWebConfig class RequestScopedBeanTests { @Autowired lateinit var userService: UserService @Autowired lateinit var request: MockHttpServletRequest @Test fun requestScope() { request.setParameter("user", "enigma") request.setParameter("pswd", "\$pr!ng") val results = userService.loginUser() // assert results } } ``` 下面的代码片段与我们前面看到的请求范围的代码片段类似 Bean。然而,这一次,`userService` Bean 对会话范围`userPreferences` Bean 具有依赖关系。请注意,`UserPreferences` Bean 是通过使用 SPEL 表达式实例化的,该表达式从当前 HTTP 会话中检索主题。在测试中,我们需要在由 TestContext 框架管理的模拟会话中配置一个主题。下面的示例展示了如何做到这一点: 会话范围 Bean 配置 ``` ``` 在`SessionScopedBeanTests`中,我们将`UserService`和`MockHttpSession`注入到我们的测试实例中。在我们的`sessionScope()`测试方法中,我们通过在提供的`MockHttpSession`中设置预期的`theme`属性来设置测试夹具。当在我们的`userService`上调用`processUserPreferences()`方法时,我们确信用户服务可以访问当前`MockHttpSession`的会话范围`userPreferences`,并且我们可以根据配置的主题对结果执行断言。下面的示例展示了如何做到这一点: 爪哇 ``` @SpringJUnitWebConfig class SessionScopedBeanTests { @Autowired UserService userService; @Autowired MockHttpSession session; @Test void sessionScope() throws Exception { session.setAttribute("theme", "blue"); Results results = userService.processUserPreferences(); // assert results } } ``` Kotlin ``` @SpringJUnitWebConfig class SessionScopedBeanTests { @Autowired lateinit var userService: UserService @Autowired lateinit var session: MockHttpSession @Test fun sessionScope() { session.setAttribute("theme", "blue") val results = userService.processUserPreferences() // assert results } } ``` #### 3.5.9.事务管理 在 TestContext 框架中,事务由`TransactionalTestExecutionListener`管理,这是默认配置的,即使你没有在测试类上显式声明`@TestExecutionListeners`。但是,要启用对事务的支持,你必须在`ApplicationContext`中配置一个`PlatformTransactionManager` Bean,它加载了`@ContextConfiguration`语义(更多详细信息将在后面提供)。此外,你必须在测试的类级别或方法级别声明 Spring 的`@Transactional`注释。 ##### 测试管理事务 测试管理事务是通过使用`TransactionalTestExecutionListener`或通过使用`TestTransaction`(在后面描述)以声明式方式管理的事务。你不应该将这样的事务与 Spring 管理的事务(那些直接由 Spring 在`ApplicationContext`中为测试加载的事务中管理的事务)或应用程序管理的事务(那些在由测试调用的应用程序代码中以编程方式管理的事务)混淆。 Spring-管理事务和应用程序管理事务通常参与测试管理事务。但是,如果 Spring-managed 或 application-managed 事务配置为除`REQUIRED`或`SUPPORTS`以外的任何传播类型,则应谨慎使用(有关详细信息,请参见[事务传播](data-access.html#tx-propagation)的讨论)。 | |在结合 Spring 的测试管理事务使用来自测试框架
的任何形式的抢占超时时时时时
时,必须谨慎。

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

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

*Junit4 的`@Test(timeout = …​)`支持而`TimeOut`rule

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

*testng 的`@Test(timeOut = …​)`支持| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ##### 启用和禁用事务 用`@Transactional`注释测试方法会导致测试在事务中运行,默认情况下,该事务在测试完成后会自动回滚。如果一个测试类使用`@Transactional`进行注释,那么这个类层次结构中的每个测试方法都在一个事务中运行。未使用`@Transactional`(在类或方法级别)进行注释的测试方法不在事务中运行。注意,`@Transactional`在测试生命周期方法上不受支持——例如,用 JUnit Jupiter 的`@BeforeAll`、`@BeforeEach`注释的方法,等等。此外,用`@Transactional`注释但将`propagation`属性设置为`NOT_SUPPORTED`或`NEVER`的测试不会在事务中运行。 | Attribute |支持测试管理的事务| |--------------------------------------------|----------------------------------------------------------------------| | `value` and `transactionManager` |是的| | `propagation` |只支持`Propagation.NOT_SUPPORTED`和`Propagation.NEVER`| | `isolation` |无| | `timeout` |无| | `readOnly` |无| | `rollbackFor` and `rollbackForClassName` |否:用`TestTransaction.flagForRollback()`代替| |`noRollbackFor` and `noRollbackForClassName`|否:用`TestTransaction.flagForCommit()`代替| | |方法级别的生命周期方法——例如,用 JUnit Jupiter 的`@BeforeEach`或`@AfterEach`注释的方法——在测试管理事务中运行。在另一种
方法上,使用组件级和类级生命周期方法——例如,使用
Junit Jupiter 的`@BeforeAll`或`@AfterAll`注释的方法,以及使用 Testng 的`@BeforeSuite`、`@AfterSuite`注释的方法,或者`@AfterClass`—是*不是*在
测试管理事务中运行。

如果你需要在
事务中以组件级或类级生命周期方法运行代码,你可能希望将相应的`PlatformTransactionManager`注入到
你的测试类中,然后将其与`TransactionTemplate`用于程序化的
事务管理。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 请注意,[`AbstractTransactionalJUnit4SpringContextTests`](#TestContext-Support-Classes-JUnit4)和[](#TestContext-Support-Classes-TestNG)是为类级别的事务支持而预先配置的。 下面的示例演示了为基于 Hibernate 的`UserRepository`编写集成测试的常见场景: 爪哇 ``` @SpringJUnitConfig(TestConfig.class) @Transactional class HibernateUserRepositoryTests { @Autowired HibernateUserRepository repository; @Autowired SessionFactory sessionFactory; JdbcTemplate jdbcTemplate; @Autowired void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } @Test void createUser() { // track initial state in test database: final int count = countRowsInTable("user"); User user = new User(...); repository.save(user); // Manual flush is required to avoid false positive in test sessionFactory.getCurrentSession().flush(); assertNumUsers(count + 1); } private int countRowsInTable(String tableName) { return JdbcTestUtils.countRowsInTable(this.jdbcTemplate, tableName); } private void assertNumUsers(int expected) { assertEquals("Number of rows in the [user] table.", expected, countRowsInTable("user")); } } ``` Kotlin ``` @SpringJUnitConfig(TestConfig::class) @Transactional class HibernateUserRepositoryTests { @Autowired lateinit var repository: HibernateUserRepository @Autowired lateinit var sessionFactory: SessionFactory lateinit var jdbcTemplate: JdbcTemplate @Autowired fun setDataSource(dataSource: DataSource) { this.jdbcTemplate = JdbcTemplate(dataSource) } @Test fun createUser() { // track initial state in test database: val count = countRowsInTable("user") val user = User() repository.save(user) // Manual flush is required to avoid false positive in test sessionFactory.getCurrentSession().flush() assertNumUsers(count + 1) } private fun countRowsInTable(tableName: String): Int { return JdbcTestUtils.countRowsInTable(jdbcTemplate, tableName) } private fun assertNumUsers(expected: Int) { assertEquals("Number of rows in the [user] table.", expected, countRowsInTable("user")) } } ``` 正如在[事务回滚和提交行为](#testcontext-tx-rollback-and-commit-behavior)中所解释的那样,在`createUser()`方法运行后不需要清理数据库,因为对数据库所做的任何更改都会由`TransactionalTestExecutionListener`自动回滚。 ##### 事务回滚和提交行为 默认情况下,测试事务将在测试完成后自动回滚;但是,事务提交和回滚行为可以通过`@Commit`和`@Rollback`注释进行声明性配置。有关更多详细信息,请参见[注释支持](#integration-testing-annotations)部分中的相应条目。 ##### 程序化事务管理 可以通过使用`TestTransaction`中的静态方法以编程方式与测试管理的事务交互。例如,可以在测试方法中、方法之前和方法之后使用`TestTransaction`来启动或结束当前的测试管理事务,或者为回滚或提交配置当前的测试管理事务。只要启用`TransactionalTestExecutionListener`,对`TestTransaction`的支持就会自动可用。 下面的示例演示了`TestTransaction`的一些特性。关于[`TestTransaction`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/test/context/transaction/testtransaction.html)的更多详细信息,请参见 爪哇doc。 爪哇 ``` @ContextConfiguration(classes = TestConfig.class) public class ProgrammaticTransactionManagementTests extends AbstractTransactionalJUnit4SpringContextTests { @Test public void transactionalTest() { // assert initial state in test database: assertNumUsers(2); deleteFromTables("user"); // changes to the database will be committed! TestTransaction.flagForCommit(); TestTransaction.end(); assertFalse(TestTransaction.isActive()); assertNumUsers(0); TestTransaction.start(); // perform other actions against the database that will // be automatically rolled back after the test completes... } protected void assertNumUsers(int expected) { assertEquals("Number of rows in the [user] table.", expected, countRowsInTable("user")); } } ``` Kotlin ``` @ContextConfiguration(classes = [TestConfig::class]) class ProgrammaticTransactionManagementTests : AbstractTransactionalJUnit4SpringContextTests() { @Test fun transactionalTest() { // assert initial state in test database: assertNumUsers(2) deleteFromTables("user") // changes to the database will be committed! TestTransaction.flagForCommit() TestTransaction.end() assertFalse(TestTransaction.isActive()) assertNumUsers(0) TestTransaction.start() // perform other actions against the database that will // be automatically rolled back after the test completes... } protected fun assertNumUsers(expected: Int) { assertEquals("Number of rows in the [user] table.", expected, countRowsInTable("user")) } } ``` ##### 在事务之外运行代码 有时,你可能需要在事务性测试方法之前或之后运行特定的代码,但要在事务性上下文之外运行,例如,要在运行测试之前验证初始数据库状态,或者在测试运行之后验证预期的事务提交行为(如果测试被配置为提交事务)。`TransactionalTestExecutionListener`支持针对此类场景的`@BeforeTransaction`和`@AfterTransaction`注释。你可以使用这些注释之一对测试类中的任何`void`方法或测试接口中的任何`void`默认方法进行注释,并且`TransactionalTestExecutionListener`确保你的 before transaction 方法或 after transaction 方法在适当的时间运行。 | |任何之前的方法(例如用 JUnit Jupiter 的`@BeforeEach`注释的方法)
和任何之后的方法(例如用 JUnit Jupiter 的`@AfterEach`注释的方法)都是
在事务中运行的。此外,对于未配置为在
事务中运行的测试方法,不运行带有`@BeforeTransaction`或`@AfterTransaction`注释的方法。| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ##### 配置事务管理器 `TransactionalTestExecutionListener`期望在 Spring `ApplicationContext`中定义一个`PlatformTransactionManager`用于测试。如果在测试的`ApplicationContext`中有多个`PlatformTransactionManager`实例,则可以使用`@Transactional("myTxMgr")`或`@Transactional(transactionManager = "myTxMgr")`声明限定符,或者`TransactionManagementConfigurer`可以通过`@Configuration`类实现。查询[爪哇doc for`TestContextTransactionUtils.retrieveTransactionManager()`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/test/contexttransactionutils.html#retrivetransactionmanager-org.spramework.test.context.tcontext.tcontext-tcontext-justcontext-java.lang.string-)以获取有关在测试中查找事务管理器的算法的详细信息,该算法用于在测试的<< ##### 演示所有与事务相关的注释 下面的基于 JUnit Jupiter 的示例显示了一个虚拟的集成测试场景,该场景突出显示了所有与事务相关的注释。该示例的目的不是演示最佳实践,而是演示如何使用这些注释。有关更多信息和配置示例,请参见[注释支持](#integration-testing-annotations)小节。[用于`@Sql`的事务管理](#TestContext-Executing-SQL-Declarationly-TX)包含一个附加示例,该示例使用`@Sql`执行带有默认事务回滚语义的声明性 SQL 脚本。下面的示例显示了相关的注释: 爪哇 ``` @SpringJUnitConfig @Transactional(transactionManager = "txMgr") @Commit class FictitiousTransactionalTest { @BeforeTransaction void verifyInitialDatabaseState() { // logic to verify the initial state before a transaction is started } @BeforeEach void setUpTestDataWithinTransaction() { // set up test data within the transaction } @Test // overrides the class-level @Commit setting @Rollback void modifyDatabaseWithinTransaction() { // logic which uses the test data and modifies database state } @AfterEach void tearDownWithinTransaction() { // run "tear down" logic within the transaction } @AfterTransaction void verifyFinalDatabaseState() { // logic to verify the final state after transaction has rolled back } } ``` Kotlin ``` @SpringJUnitConfig @Transactional(transactionManager = "txMgr") @Commit class FictitiousTransactionalTest { @BeforeTransaction fun verifyInitialDatabaseState() { // logic to verify the initial state before a transaction is started } @BeforeEach fun setUpTestDataWithinTransaction() { // set up test data within the transaction } @Test // overrides the class-level @Commit setting @Rollback fun modifyDatabaseWithinTransaction() { // logic which uses the test data and modifies database state } @AfterEach fun tearDownWithinTransaction() { // run "tear down" logic within the transaction } @AfterTransaction fun verifyFinalDatabaseState() { // logic to verify the final state after transaction has rolled back } } ``` | |在测试 ORM 代码

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

爪哇
```
// ...

@Autowired
SessionFactory sessionFactory;

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

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

// ...
```


```
// ...

@Autowired
lateinit var sessionFactory: SessionFactory

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

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

// ...
```<>
<"><"><"><"gt="gt="2085">>><">>><<<"gt=2085">>>>>>>><<<<<"gt=| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 3.5.10.执行 SQL 脚本 在针对关系数据库编写集成测试时,运行 SQL 脚本来修改数据库模式或将测试数据插入到表中通常是有益的。`spring-jdbc`模块通过在 Spring `ApplicationContext`加载时执行 SQL 脚本,为*初始化*嵌入式或现有数据库提供支持。详见[嵌入式数据库支持](data-access.html#jdbc-embedded-database-support)和[用嵌入式数据库测试数据访问逻辑](data-access.html#jdbc-embedded-database-dao-testing)。 虽然在`ApplicationContext`加载时初始化一个用于测试*曾经*的数据库是非常有用的,但有时能够修改数据库*期间*集成测试是必不可少的。下面的部分解释了如何在集成测试期间以编程方式和声明式方式运行 SQL 脚本。 ##### 以编程方式执行 SQL 脚本 Spring 提供了以下用于在集成测试方法中以编程方式执行 SQL 脚本的选项。 * `org.springframework.jdbc.datasource.init.ScriptUtils` * `org.springframework.jdbc.datasource.init.ResourceDatabasePopulator` * `org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests` * `org.springframework.test.context.testng.AbstractTransactionalTestNGSpringContextTests` `ScriptUtils`提供了一组用于处理 SQL 脚本的静态实用程序方法,主要用于框架内的内部使用。但是,如果你需要完全控制 SQL 脚本的解析和运行方式,`ScriptUtils`可能比后面介绍的其他一些替代方案更适合你的需要。有关更多详细信息,请参见[javadoc](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/jdbc/datasource/init/ScriptUtils.html)中的单个方法。 `ResourceDatabasePopulator`提供了一个基于对象的 API,用于通过使用在外部资源中定义的 SQL 脚本以编程方式填充、初始化或清理数据库。`ResourceDatabasePopulator`提供了用于配置字符编码、语句分隔符、注释分隔符和解析和运行脚本时使用的错误处理标志的选项。每个配置选项都有一个合理的默认值。有关默认值的详细信息,请参见[javadoc](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/jdbc/datasource/init/ResourceDatabasePopulator.html)。要运行在`ResourceDatabasePopulator`中配置的脚本,你可以调用`populate(Connection)`方法来针对`java.sql.Connection`运行填充器,也可以调用`execute(DataSource)`方法来针对`javax.sql.DataSource`运行填充器。下面的示例为测试模式和测试数据指定 SQL 脚本,将语句分隔符设置为`@@`,并针对`DataSource`运行脚本: 爪哇 ``` @Test void databaseTest() { ResourceDatabasePopulator populator = new ResourceDatabasePopulator(); populator.addScripts( new ClassPathResource("test-schema.sql"), new ClassPathResource("test-data.sql")); populator.setSeparator("@@"); populator.execute(this.dataSource); // run code that uses the test schema and data } ``` Kotlin ``` @Test fun databaseTest() { val populator = ResourceDatabasePopulator() populator.addScripts( ClassPathResource("test-schema.sql"), ClassPathResource("test-data.sql")) populator.setSeparator("@@") populator.execute(dataSource) // run code that uses the test schema and data } ``` 请注意,`ResourceDatabasePopulator`内部委托给`ScriptUtils`用于解析和运行 SQL 脚本。类似地,[`executeSqlScript(..)`](#testcontext-support-classes-junit4)和[`AbstractTransactionalTestNGSpringContextTests`](#testcontext-support-classes-testng)中的`executeSqlScript(..)`方法在内部使用`ResourceDatabasePopulator`来运行 SQL 脚本。有关更多详细信息,请参见 爪哇doc 获取各种`executeSqlScript(..)`方法。 ##### 使用 @sql 声明式执行 SQL 脚本 除了上述以编程方式运行 SQL 脚本的机制外,还可以在 Spring TestContext 框架中声明性地配置 SQL 脚本。具体地说,你可以在测试类或测试方法上声明`@Sql`注释,以配置应在集成测试方法之前或之后针对给定数据库运行的 SQL 脚本的各个 SQL 语句或资源路径。对`@Sql`的支持由`SqlScriptsTestExecutionListener`提供,默认情况下启用。 | |方法级别`@Sql`声明默认重写类级别声明。然而,作为 Spring Framework5.2 的
,该行为可以通过`@SqlMergeMode`配置为每个测试类或每个
测试方法。有关更多详细信息,请参见[使用`@SqlMergeMode`合并和覆盖配置]。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ###### 路径资源语义 每个路径被解释为 Spring `Resource`。普通路径(例如,`"schema.sql"`)被视为与定义测试类的包相关的 Classpath 资源。以斜杠开头的路径被视为绝对 Classpath 资源(例如,`"/org/example/schema.sql"`)。通过使用指定的资源协议加载引用 URL 的路径(例如,带有`classpath:`、`file:`、`http:`前缀的路径)。 下面的示例展示了如何在类级别和基于 JUnit Jupiter 的集成测试类中的方法级别上使用`@Sql`: 爪哇 ``` @SpringJUnitConfig @Sql("/test-schema.sql") class DatabaseTests { @Test void emptySchemaTest() { // run code that uses the test schema without any test data } @Test @Sql({"/test-schema.sql", "/test-user-data.sql"}) void userTest() { // run code that uses the test schema and test data } } ``` Kotlin ``` @SpringJUnitConfig @Sql("/test-schema.sql") class DatabaseTests { @Test fun emptySchemaTest() { // run code that uses the test schema without any test data } @Test @Sql("/test-schema.sql", "/test-user-data.sql") fun userTest() { // run code that uses the test schema and test data } } ``` ###### 默认脚本检测 如果没有指定 SQL 脚本或语句,则尝试检测`default`脚本,这取决于声明`@Sql`的位置。如果无法检测到默认值,则抛出`IllegalStateException`。 * 类级声明:如果带注释的测试类是`com.example.MyTest`,则对应的默认脚本是`classpath:com/example/MyTest.sql`。 * 方法级别声明:如果带注释的测试方法名为`testMethod()`,并且在类`com.example.MyTest`中定义,那么对应的默认脚本是`classpath:com/example/MyTest.testMethod.sql`。 ###### 声明多个`@Sql`集 如果需要为给定的测试类或测试方法配置多组 SQL 脚本,但语法配置不同,错误处理规则不同,或者每组执行阶段不同,则可以声明`@Sql`的多个实例。对于 爪哇8,你可以使用`@Sql`作为可重复的注释。否则,你可以使用`@SqlGroup`注释作为显式容器来声明`@Sql`的多个实例。 下面的示例展示了如何使用`@Sql`作为 爪哇8 的可重复注释: 爪哇 ``` @Test @Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`")) @Sql("/test-user-data.sql") void userTest() { // run code that uses the test schema and test data } ``` Kotlin ``` // Repeatable annotations with non-SOURCE retention are not yet supported by Kotlin ``` 在前面示例中介绍的场景中,`test-schema.sql`脚本对单行注释使用了不同的语法。 下面的示例与前面的示例相同,只是`@Sql`声明被组合在`@SqlGroup`中。对于 爪哇8 及以上版本,`@SqlGroup`的使用是可选的,但是你可能需要使用`@SqlGroup`来与其他 JVM 语言(如 Kotlin)兼容。 爪哇 ``` @Test @SqlGroup({ @Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`")), @Sql("/test-user-data.sql") )} void userTest() { // run code that uses the test schema and test data } ``` Kotlin ``` @Test @SqlGroup( Sql("/test-schema.sql", config = SqlConfig(commentPrefix = "`")), Sql("/test-user-data.sql")) fun userTest() { // Run code that uses the test schema and test data } ``` ###### 脚本执行阶段 默认情况下,SQL 脚本在相应的测试方法之前运行。但是,如果需要在测试方法之后运行一组特定的脚本(例如,清理数据库状态),则可以在`executionPhase`中使用`@Sql`属性,如下例所示: 爪哇 ``` @Test @Sql( scripts = "create-test-data.sql", config = @SqlConfig(transactionMode = ISOLATED) ) @Sql( scripts = "delete-test-data.sql", config = @SqlConfig(transactionMode = ISOLATED), executionPhase = AFTER_TEST_METHOD ) void userTest() { // run code that needs the test data to be committed // to the database outside of the test's transaction } ``` Kotlin ``` @Test @SqlGroup( Sql("create-test-data.sql", config = SqlConfig(transactionMode = ISOLATED)), Sql("delete-test-data.sql", config = SqlConfig(transactionMode = ISOLATED), executionPhase = AFTER_TEST_METHOD)) fun userTest() { // run code that needs the test data to be committed // to the database outside of the test's transaction } ``` 注意,`ISOLATED`和`AFTER_TEST_METHOD`分别从`Sql.TransactionMode`和`Sql.ExecutionPhase`静态导入。 ###### 带有`@SqlConfig`的脚本配置 你可以使用`@SqlConfig`注释来配置脚本解析和错误处理。当声明为集成测试类上的类级注释时,`@SqlConfig`充当测试类层次结构中所有 SQL 脚本的全局配置。当通过使用`@Sql`注释的`config`属性直接声明时,`@SqlConfig`充当在附件`@Sql`注释中声明的 SQL 脚本的本地配置。`@SqlConfig`中的每个属性都有一个隐含的默认值,该默认值在相应属性的 爪哇doc 中有文档说明。由于 爪哇 语言规范中为注释属性定义的规则,遗憾的是,不可能为注释属性赋值`null`。因此,为了支持对继承的全局配置的重写,`@SqlConfig`属性具有显式默认值`""`(用于字符串),`{}`(用于数组),或`DEFAULT`(用于枚举)。这种方法允许`@SqlConfig`的本地声明通过提供`""`、`{}`或`DEFAULT`以外的值,选择性地覆盖`@SqlConfig`的全局声明中的单个属性。当局部`@SqlConfig`属性不提供除`""`、`{}`或`DEFAULT`以外的显式值时,全局`@SqlConfig`属性将被继承。因此,显式的局部配置重写全局配置。 由`@Sql`和`@SqlConfig`提供的配置选项与`ScriptUtils`和`ResourceDatabasePopulator`支持的配置选项等价,但它们是由``XML 名称空间元素提供的那些选项的超集。参见[`@Sql`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/test/context/jdbc/sql.html)和[`@SqlConfig`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api-api/org/context/jdbc/jdbc/sqlconframework.html)中的各个属性的 爪哇doc,以获取详细信息。 **`@Sql`的事务管理 ** 默认情况下,`SqlScriptsTestExecutionListener`为使用`@Sql`配置的脚本推断所需的事务语义。具体地说,SQL 脚本在没有事务的情况下运行,在现有 Spring 管理的事务(例如,由`TransactionalTestExecutionListener`管理的事务,用于用`@Transactional`注释的测试)中运行,或者在独立的事务中运行,取决于`@SqlConfig`中`transactionMode`属性的配置值,以及在测试的`PlatformTransactionManager`中是否存在`ApplicationContext`。然而,作为最低要求,测试的`javax.sql.DataSource`中必须存在`ApplicationContext`。 如果`SqlScriptsTestExecutionListener`用于检测`DataSource`和`PlatformTransactionManager`并推断事务语义的算法不适合你的需要,则可以通过设置`dataSource`和`transactionManager`的`@SqlConfig`属性来指定显式名称。此外,你可以通过设置`@SqlConfig`的`transactionMode`属性来控制事务传播行为(例如,是否应该在独立的事务中运行脚本)。尽管对`@Sql`的事务管理所支持的所有选项的彻底讨论超出了本参考手册的范围,但对于[`@SqlConfig`](https://DOCS. Spring.io/ Spring-Framework/DOCS/5.3.16/javadoc-api/org/SpringFramework/tFramework/tFramework/tform/springframework/test/test/contextbc/jdbc/sqlconfig.html)和[`SqlScriptsTestExecutionListener`](DOCS.[ps:/ Spring.io/[[framework. Spring-fram 下面的示例展示了一个典型的测试场景,该场景使用 JUnit Jupiter 和事务测试`@Sql`: 爪哇 ``` @SpringJUnitConfig(TestDatabaseConfig.class) @Transactional class TransactionalSqlScriptsTests { final JdbcTemplate jdbcTemplate; @Autowired TransactionalSqlScriptsTests(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } @Test @Sql("/test-data.sql") void usersTest() { // verify state in test database: assertNumUsers(2); // run code that uses the test data... } int countRowsInTable(String tableName) { return JdbcTestUtils.countRowsInTable(this.jdbcTemplate, tableName); } void assertNumUsers(int expected) { assertEquals(expected, countRowsInTable("user"), "Number of rows in the [user] table."); } } ``` Kotlin ``` @SpringJUnitConfig(TestDatabaseConfig::class) @Transactional class TransactionalSqlScriptsTests @Autowired constructor(dataSource: DataSource) { val jdbcTemplate: JdbcTemplate = JdbcTemplate(dataSource) @Test @Sql("/test-data.sql") fun usersTest() { // verify state in test database: assertNumUsers(2) // run code that uses the test data... } fun countRowsInTable(tableName: String): Int { return JdbcTestUtils.countRowsInTable(jdbcTemplate, tableName) } fun assertNumUsers(expected: Int) { assertEquals(expected, countRowsInTable("user"), "Number of rows in the [user] table.") } } ``` 请注意,在`usersTest()`方法运行后,不需要清理数据库,因为对数据库所做的任何更改(在测试方法内或`/test-data.sql`脚本内)都会由`TransactionalTestExecutionListener`自动回滚(有关详细信息,请参见[事务管理](#testcontext-tx))。 ###### 用`@SqlMergeMode`合并和覆盖配置 在 Spring Framework5.2 中,可以将方法级`@Sql`声明与类级声明合并。例如,这允许你为每个测试类提供一次数据库模式或一些公共测试数据的配置,然后为每个测试方法提供额外的、特定于用例的测试数据。要启用`@Sql`合并,请用`@SqlMergeMode(MERGE)`注释你的测试类或测试方法。要禁用特定测试方法(或特定测试子类)的合并,可以通过`@SqlMergeMode(OVERRIDE)`切换回默认模式。有关示例和更多详细信息,请参阅[`@SqlMergeMode`注释文档部分](# Spring-testing-annotation-sqlmergemode)。 #### 3.5.11.并行测试执行 Spring Framework5.0 引入了在使用 Spring TestContext 框架时在单个 JVM 内并行执行测试的基本支持。通常,这意味着大多数测试类或测试方法可以并行运行,而不需要对测试代码或配置进行任何更改。 | |有关如何设置并行测试执行的详细信息,请参阅
测试框架、构建工具或 IDE 的文档。| |---|-------------------------------------------------------------------------------------------------------------------------------| 请记住,在测试套件中引入并发可能会导致意外的副作用、奇怪的运行时行为,以及间歇性或似乎随机地失败的测试。 Spring 因此,团队为何时不并行运行测试提供了以下一般准则。 如果测试: * 使用 Spring Framework 的`@DirtiesContext`支持。 * 使用 Spring Boot 的`@MockBean`或`@SpyBean`支持。 * 使用 JUnit4 的`@FixMethodOrder`支持或任何旨在确保测试方法以特定顺序运行的测试框架功能。但是,请注意,如果整个测试类并行运行,则不适用于此。 * 更改共享服务或系统(如数据库、消息代理、文件系统等)的状态。这既适用于嵌入式系统,也适用于外部系统。 | |如果并行测试执行失败,并且异常地声明当前测试的`ApplicationContext`不再活动,这通常意味着`ApplicationContext`在不同的线程中从`ContextCache`中被删除。

这可能是由于使用`@DirtiesContext`或由于从`ContextCache`中自动驱逐所致。如果`@DirtiesContext`是罪魁祸首,则需要找到一种方法来
避免使用`@DirtiesContext`,或者将此类测试排除在并行执行之外。如果
已超过`ContextCache`的最大大小,则可以增加缓存的最大大小
。详见[上下文缓存](#testcontext-ctx-management-caching)讨论。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | |Spring TestContext 框架中的并行测试执行只有在以下情况下才是可能的:
底层`TestContext`实现提供了一个复制构造函数,如
中所解释的[`TestContext`]的 爪哇doc(https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/test/text/context.html)。 Spring 中使用的`DefaultTestContext`提供了这样的构造函数。但是,如果使用提供自定义
实现的`TestContext`第三方库,则需要
验证它是否适合并行测试执行。| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 3.5.12.TestContext 框架支持类 本节描述了支持 Spring TestContext 框架的各种类。 ##### Spring JUnit4Runner Spring TestContext 框架通过自定义运行器(在 JUnit4.12 或更高版本上支持)提供与 JUnit4 的完全集成。通过使用`@RunWith(SpringJUnit4ClassRunner.class)`或更短的`@RunWith(SpringRunner.class)`变体注释测试类,开发人员可以实现基于标准 JUnit4 的单元和集成测试,并同时获得 TestContext 框架的好处,例如对加载应用程序上下文、测试实例的依赖注入、事务性测试方法执行的支持,等等。如果希望将 Spring TestContext 框架与可选的运行器(例如 JUnit4 的Runner)或第三方运行器(例如)一起使用,则可以选择使用。 下面的代码清单显示了配置使用自定义 Spring `Runner`运行的测试类的最低要求: 爪哇 ``` @RunWith(SpringRunner.class) @TestExecutionListeners({}) public class SimpleTest { @Test public void testMethod() { // test logic... } } ``` Kotlin ``` @RunWith(SpringRunner::class) @TestExecutionListeners class SimpleTest { @Test fun testMethod() { // test logic... } } ``` 在前面的示例中,`@TestExecutionListeners`被配置为空列表,以禁用默认侦听器,否则将需要通过`ApplicationContext`配置`@ContextConfiguration`。 ##### Spring JUnit4 规则 `org.springframework.test.context.junit4.rules`包提供了以下 JUnit4 规则(JUnit4.12 或更高版本支持): * `SpringClassRule` * `SpringMethodRule` `SpringClassRule`是支持 Spring TestContext 框架的类级特性的 JUnit`TestRule`,而`SpringMethodRule`是支持 Spring TestContext 框架的实例级和方法级特性的 JUnit`MethodRule`。 与`SpringRunner`相反, Spring 的基于规则的 JUnit 支持具有独立于任何`org.junit.runner.Runner`实现的优点,因此可以与现有的替代运行器(例如 JUnit4 的`Parameterized`)或第三方运行器(例如`MockitoJUnitRunner`)组合。 要支持 TestContext 框架的全部功能,你必须将`SpringClassRule`与`SpringMethodRule`合并。下面的示例展示了在集成测试中声明这些规则的正确方法: 爪哇 ``` // Optionally specify a non-Spring Runner via @RunWith(...) @ContextConfiguration public class IntegrationTest { @ClassRule public static final SpringClassRule springClassRule = new SpringClassRule(); @Rule public final SpringMethodRule springMethodRule = new SpringMethodRule(); @Test public void testMethod() { // test logic... } } ``` Kotlin ``` // Optionally specify a non-Spring Runner via @RunWith(...) @ContextConfiguration class IntegrationTest { @Rule val springMethodRule = SpringMethodRule() @Test fun testMethod() { // test logic... } companion object { @ClassRule val springClassRule = SpringClassRule() } } ``` ##### JUnit4 支持类 `org.springframework.test.context.junit4`包为基于 JUnit4 的测试用例(JUnit4.12 或更高版本支持)提供了以下支持类: * `AbstractJUnit4SpringContextTests` * `AbstractTransactionalJUnit4SpringContextTests` `AbstractJUnit4SpringContextTests`是一个抽象基测试类,它将 Spring TestContext 框架与 JUnit4 环境中显式的`ApplicationContext`测试支持集成在一起。扩展`AbstractJUnit4SpringContextTests`时,可以访问`protected``applicationContext`实例变量,可以使用该变量执行显式 Bean 查找或测试整个上下文的状态。 `AbstractTransactionalJUnit4SpringContextTests`是`AbstractJUnit4SpringContextTests`的抽象事务扩展,它为 JDBC 访问添加了一些方便的功能。该类期望在`javax.sql.DataSource` Bean 和`PlatformTransactionManager` Bean 中定义`ApplicationContext`。扩展`AbstractTransactionalJUnit4SpringContextTests`时,可以访问`protected``jdbcTemplate`实例变量,你可以使用该变量运行 SQL 语句来查询数据库。可以使用这样的查询来确认在运行数据库相关的应用程序代码之前和之后的数据库状态,并且 Spring 确保这样的查询在与应用程序代码相同的事务的范围内运行。当与 ORM 工具一起使用时,请务必避免[误报](#testcontext-tx-false-positives)。正如在[JDBC 测试支持](#integration-testing-support-jdbc)中提到的,`AbstractTransactionalJUnit4SpringContextTests`还提供了方便的方法,通过使用前面提到的`JdbcTestUtils`将方法委托给`jdbcTemplate`中的方法。此外,`AbstractTransactionalJUnit4SpringContextTests`提供了一个`executeSqlScript(..)`方法,用于针对配置的`DataSource`运行 SQL 脚本。 | |这些类为扩展提供了方便。如果不希望你的测试类
绑定到 Spring 特定的类层次结构,则可以使用
或[Spring’s
JUnit rules](#testcontext-junit4-rules)配置你自己的自定义测试类。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ##### 朱尼特木星的 SpringExtension Spring TestContext 框架提供了与 JUnit Jupiter 测试框架的完全集成,JUnit5 中介绍了该测试框架。通过使用`@ExtendWith(SpringExtension.class)`对测试类进行注释,你可以实现标准的基于 JUnit Jupiter 的单元和集成测试,并同时获得 TestContext 框架的好处,例如对加载应用程序上下文的支持、测试实例的依赖注入、事务测试方法的执行,等等。 此外,由于 JUnit Jupiter 中丰富的扩展 API, Spring 在 Spring 支持 JUnit4 和 TestNG 的功能集之上和之上提供了以下功能: * 用于测试构造函数、测试方法和测试生命周期回调方法的依赖注入。有关更多详细信息,请参见[dependency injection with`SpringExtension`]。 * 基于 SPEL 表达式、环境变量、系统属性等对[条件测试执行](https://junit.org/junit5/docs/current/user-guide/#extensions-conditions)的强大支持。有关更多详细信息和示例,请参见`@EnabledIf`和`@DisabledIf`中的文档。 * 自定义合成的注释结合了来自 Spring 和 JUnit Jupiter 的注释。有关更多详细信息,请参见`@TransactionalDevTestConfig`和`@TransactionalIntegrationTest`中的示例。 下面的代码清单显示了如何配置一个测试类,以便将`SpringExtension`与`@ContextConfiguration`结合使用: 爪哇 ``` // Instructs JUnit Jupiter to extend the test with Spring support. @ExtendWith(SpringExtension.class) // Instructs Spring to load an ApplicationContext from TestConfig.class @ContextConfiguration(classes = TestConfig.class) class SimpleTests { @Test void testMethod() { // test logic... } } ``` Kotlin ``` // Instructs JUnit Jupiter to extend the test with Spring support. @ExtendWith(SpringExtension::class) // Instructs Spring to load an ApplicationContext from TestConfig::class @ContextConfiguration(classes = [TestConfig::class]) class SimpleTests { @Test fun testMethod() { // test logic... } } ``` 由于还可以使用 JUnit5 中的注释作为元注释, Spring 提供了`@SpringJUnitConfig`和`@SpringJUnitWebConfig`组合注释,以简化测试`ApplicationContext`和 JUnit Jupiter 的配置。 下面的示例使用`@SpringJUnitConfig`来减少前面示例中使用的配置量: 爪哇 ``` // Instructs Spring to register the SpringExtension with JUnit // Jupiter and load an ApplicationContext from TestConfig.class @SpringJUnitConfig(TestConfig.class) class SimpleTests { @Test void testMethod() { // test logic... } } ``` Kotlin ``` // Instructs Spring to register the SpringExtension with JUnit // Jupiter and load an ApplicationContext from TestConfig.class @SpringJUnitConfig(TestConfig::class) class SimpleTests { @Test fun testMethod() { // test logic... } } ``` 类似地,下面的示例使用`@SpringJUnitWebConfig`创建一个`WebApplicationContext`,用于 JUnit Jupiter: 爪哇 ``` // Instructs Spring to register the SpringExtension with JUnit // Jupiter and load a WebApplicationContext from TestWebConfig.class @SpringJUnitWebConfig(TestWebConfig.class) class SimpleWebTests { @Test void testMethod() { // test logic... } } ``` Kotlin ``` // Instructs Spring to register the SpringExtension with JUnit // Jupiter and load a WebApplicationContext from TestWebConfig::class @SpringJUnitWebConfig(TestWebConfig::class) class SimpleWebTests { @Test fun testMethod() { // test logic... } } ``` 有关更多详细信息,请参见`@SpringJUnitConfig`和`@SpringJUnitWebConfig`中的文档。 ##### 依赖注入与`SpringExtension` `SpringExtension`实现了来自 JUnit Jupiter 的[`ParameterResolver`](https://junit.org/junit5/DOCS/current/user-guide/#extensions-parameter-resolution)扩展 API,它允许 Spring 为测试构造函数、测试方法和测试生命周期回调方法提供依赖注入。 具体地说,`SpringExtension`可以将来自测试的`ApplicationContext`的依赖关系注入到使用`@BeforeAll`、`@AfterAll`、`@BeforeEach`、`@AfterEach`、`@RepeatedTest`、`@RepeatedTest`、`@ParameterizedTest`等注释的测试构造函数和方法中。 ###### 构造函数注入 如果 JUnit Jupiter 测试类的构造函数中的特定参数类型为`ApplicationContext`(或其子类型),或者被注释或 meta 注释为`@Autowired`,`@Qualifier`,或`@Value`, Spring 将该特定参数的值注入相应的 Bean 或来自测试的`ApplicationContext`的值。 Spring 还可以被配置为在一个测试类构造函数被认为是*可自动连接*的情况下自动连接用于该构造函数的所有参数。如果满足以下条件之一(按优先顺序),则构造函数被认为是可自动连接的。 * 构造函数用`@Autowired`注释。 * `@TestConstructor`在测试类上存在或元存在,并且`autowireMode`属性设置为`ALL`。 * 默认的*测试构造函数 AutoWire 模式*已更改为`ALL`。 有关`@TestConstructor`的使用以及如何更改全局*测试构造函数 AutoWire 模式*的详细信息,请参见[`@TestConstructor`](#Integration-Testing-Annotations-TestConstructor)。 | |如果一个测试类的构造函数被认为是*可自动连接*,则 Spring
承担解决构造函数中所有参数的参数的责任。
因此,在 JUnit Jupiter 注册的其他`ParameterResolver`都不能为这样的构造函数解析
参数。| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | |测试类的构造函数注入不能与 JUnit
Jupiter 的`@TestInstance(PER_CLASS)`支持一起使用如果`@DirtiesContext`是用来关闭
测试的`ApplicationContext`测试方法之前或之后的
的,原因是`@TestInstance(PER_CLASS)`指示 JUnit Jupiter 在测试方法调用之间缓存测试
实例。因此,测试实例将保留
对 bean 的引用,这些 bean 最初是从`ApplicationContext`注入的,而
随后被关闭。由于在这种情况下测试类的构造函数只会被调用
一次,因此依赖项注入将不会再次发生,而随后的测试
将与来自闭合的`ApplicationContext`的 bean 进行交互,这可能会导致错误。

将`@DirtiesContext`与“测试方法之前”或“测试方法之后”的模式在
中与`@TestInstance(PER_CLASS)`结合,必须将 Spring
中的依赖项配置为通过字段或 setter 注入提供,以便它们可以在测试
方法调用之间重新注入。| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 在下面的示例中, Spring 将从`OrderService` Bean 加载的`ApplicationContext`中的`TestConfig.class`注入到`OrderServiceIntegrationTests`构造函数中。 爪哇 ``` @SpringJUnitConfig(TestConfig.class) class OrderServiceIntegrationTests { private final OrderService orderService; @Autowired OrderServiceIntegrationTests(OrderService orderService) { this.orderService = orderService; } // tests that use the injected OrderService } ``` Kotlin ``` @SpringJUnitConfig(TestConfig::class) class OrderServiceIntegrationTests @Autowired constructor(private val orderService: OrderService){ // tests that use the injected OrderService } ``` 请注意,这个特性允许测试依赖项`final`,因此是不可变的。 如果`spring.test.constructor.autowire.mode`属性是`all`(参见[`@TestConstructor`](#Integration-Testing-Annotations-TestConstructor)),我们可以在前面的示例中省略构造函数上`@Autowired`的声明,从而产生以下结果。 爪哇 ``` @SpringJUnitConfig(TestConfig.class) class OrderServiceIntegrationTests { private final OrderService orderService; OrderServiceIntegrationTests(OrderService orderService) { this.orderService = orderService; } // tests that use the injected OrderService } ``` Kotlin ``` @SpringJUnitConfig(TestConfig::class) class OrderServiceIntegrationTests(val orderService:OrderService) { // tests that use the injected OrderService } ``` ###### 方法注入 如果 JUnit Jupiter 测试方法或测试生命周期回调方法中的参数类型为`ApplicationContext`(或其子类型),或被注释或 meta 注释为`@Autowired`,`@Qualifier`,或`@Value`, Spring 从测试的`ApplicationContext`中将该特定参数的值注入相应的 Bean。 在下面的示例中, Spring 将从`OrderService`加载的`ApplicationContext`中的`TestConfig.class`注入到`deleteOrder()`测试方法中: 爪哇 ``` @SpringJUnitConfig(TestConfig.class) class OrderServiceIntegrationTests { @Test void deleteOrder(@Autowired OrderService orderService) { // use orderService from the test's ApplicationContext } } ``` Kotlin ``` @SpringJUnitConfig(TestConfig::class) class OrderServiceIntegrationTests { @Test fun deleteOrder(@Autowired orderService: OrderService) { // use orderService from the test's ApplicationContext } } ``` 由于 JUnit Jupiter 中`ParameterResolver`支持的健壮性,你还可以将多个依赖注入到单个方法中,不仅来自 Spring,还来自 JUnit Jupiter 本身或其他第三方扩展。 下面的示例展示了如何让 Spring 和 JUnit Jupiter 同时向`placeOrderRepeatedly()`测试方法中注入依赖项。 爪哇 ``` @SpringJUnitConfig(TestConfig.class) class OrderServiceIntegrationTests { @RepeatedTest(10) void placeOrderRepeatedly(RepetitionInfo repetitionInfo, @Autowired OrderService orderService) { // use orderService from the test's ApplicationContext // and repetitionInfo from JUnit Jupiter } } ``` Kotlin ``` @SpringJUnitConfig(TestConfig::class) class OrderServiceIntegrationTests { @RepeatedTest(10) fun placeOrderRepeatedly(repetitionInfo:RepetitionInfo, @Autowired orderService:OrderService) { // use orderService from the test's ApplicationContext // and repetitionInfo from JUnit Jupiter } } ``` 注意,使用来自 Junit Jupiter 的`@RepeatedTest`可以让测试方法访问`RepetitionInfo`。 ##### `@Nested`测试类配置 自 Spring Framework5.0 以来,*Spring TestContext Framework*一直支持在 JUnit Jupiter 中的`@Nested`测试类上使用与测试相关的注释;然而,在 Spring Framework5.3 之前,类级测试配置注释并不像来自超类那样来自封闭类的*继承*。 Spring Framework5.3 引入了用于从封闭类继承测试类配置的第一类支持,并且这样的配置将在默认情况下被继承。要从默认的`INHERIT`模式更改为`OVERRIDE`模式,你可以用`@Nested`注释单个`@Nested`测试类。显式的`@NestedTestConfiguration`声明将应用于带注释的测试类及其任何子类和嵌套的类。因此,你可以用`@NestedTestConfiguration`注释顶级测试类,这将递归地应用于它的所有嵌套测试类。 为了允许开发团队将缺省模式更改为`OVERRIDE`——例如,为了与 Spring Framework5.0 到 5.2 兼容——缺省模式可以通过 JVM 系统属性或 Classpath 根中的`spring.properties`文件进行全局更改。有关详细信息,请参见[“更改默认的封闭配置继承模式”](#integration-testing-annotations-nestedtestconfiguration)注释。 尽管下面的“Hello World”示例非常简单,但它展示了如何在顶级类上声明公共配置,该类由其`@Nested`测试类继承。在这个特定的示例中,只继承了`TestConfig`配置类。每个嵌套测试类提供自己的一组活动配置文件,从而为每个嵌套测试类提供一个不同的`ApplicationContext`(有关详细信息,请参见[上下文缓存](#testcontext-ctx-management-caching))。请参阅[支持的注释](#integration-testing-annotations-nestedtestconfiguration)列表,以查看哪些注释可以在`@Nested`测试类中继承。 爪哇 ``` @SpringJUnitConfig(TestConfig.class) class GreetingServiceTests { @Nested @ActiveProfiles("lang_en") class EnglishGreetings { @Test void hello(@Autowired GreetingService service) { assertThat(service.greetWorld()).isEqualTo("Hello World"); } } @Nested @ActiveProfiles("lang_de") class GermanGreetings { @Test void hello(@Autowired GreetingService service) { assertThat(service.greetWorld()).isEqualTo("Hallo Welt"); } } } ``` Kotlin ``` @SpringJUnitConfig(TestConfig::class) class GreetingServiceTests { @Nested @ActiveProfiles("lang_en") inner class EnglishGreetings { @Test fun hello(@Autowired service:GreetingService) { assertThat(service.greetWorld()).isEqualTo("Hello World") } } @Nested @ActiveProfiles("lang_de") inner class GermanGreetings { @Test fun hello(@Autowired service:GreetingService) { assertThat(service.greetWorld()).isEqualTo("Hallo Welt") } } } ``` ##### TestNG 支持类 `org.springframework.test.context.testng`包为基于 TestNG 的测试用例提供了以下支持类: * `AbstractTestNGSpringContextTests` * `AbstractTransactionalTestNGSpringContextTests` `AbstractTestNGSpringContextTests`是一个抽象基测试类,它将 Spring TestContext 框架与 TestNG 环境中显式的`ApplicationContext`测试支持集成在一起。扩展`AbstractTestNGSpringContextTests`时,可以访问`protected``applicationContext`实例变量,你可以使用该变量执行显式 Bean 查找或测试整个上下文的状态。 `AbstractTransactionalTestNGSpringContextTests`是`AbstractTestNGSpringContextTests`的抽象事务扩展,它为 JDBC 访问添加了一些方便的功能。该类期望在`javax.sql.DataSource` Bean 和`PlatformTransactionManager` Bean 中定义`ApplicationContext`。扩展`AbstractTransactionalTestNGSpringContextTests`时,可以访问`protected``jdbcTemplate`实例变量,你可以使用该变量运行 SQL 语句来查询数据库。可以使用这样的查询来确认在运行数据库相关的应用程序代码之前和之后的数据库状态,并且 Spring 确保这样的查询在与应用程序代码相同的事务范围内运行。当与 ORM 工具一起使用时,请务必避免[误报](#testcontext-tx-false-positives)。正如在[JDBC 测试支持](#integration-testing-support-jdbc)中提到的,`AbstractTransactionalTestNGSpringContextTests`还提供了方便的方法,通过使用前面提到的`jdbcTemplate`将方法委托给`JdbcTestUtils`中的方法。此外,`AbstractTransactionalTestNGSpringContextTests`提供了一个`executeSqlScript(..)`方法,用于针对配置的`DataSource`运行 SQL 脚本。 | |这些类为扩展提供了方便。如果你不希望你的测试类
被绑定到 Spring 特定的类层次结构,那么你可以通过使用`@ContextConfiguration`、`@TestExecutionListeners`以此类推,并通过
手动使用`TestContextManager`来配置你自己的自定义测试类。请参阅源代码
的`AbstractTestNGSpringContextTests`,以获得如何测试类的示例。| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ### 3.6.WebTestClient `WebTestClient`是一个用于测试服务器应用程序的 HTTP 客户机。它封装了 Spring 的[WebClient](web-reactive.html#webflux-client),并使用它来执行请求,但公开了一个用于验证响应的测试 facade。`WebTestClient`可用于执行端到端的 HTTP 测试。 Spring MVC 和 Spring WebFlux 应用程序也可用于在没有运行服务器的情况下通过模拟服务器来测试请求和响应对象。 | |Kotlin 用户:参见[本节](languages.html#kotlin-webtestclient-issue)相关的`WebTestClient`的使用。| |---|-----------------------------------------------------------------------------------------------------------------| #### 3.6.1.设置 要设置`WebTestClient`,你需要选择要绑定到的服务器设置。这可以是几个模拟服务器设置选项中的一个,也可以是与实时服务器的连接。 ##### 绑定到控制器 此设置允许你通过模拟请求和响应对象测试特定的控制器,而无需运行服务器。 对于 WebFlux 应用程序,使用以下方法加载与[WebFlux 爪哇 配置](web-reactive.html#webflux-config)等价的基础设施,注册给定的控制器,并创建[Webhandler 链](web-reactive.html#webflux-web-handler-api)来处理请求: 爪哇 ``` WebTestClient client = WebTestClient.bindToController(new TestController()).build(); ``` Kotlin ``` val client = WebTestClient.bindToController(TestController()).build() ``` 对于 Spring MVC,使用以下方法委托给[StandalOneMockmvcBuilder](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/test/web/servlet/setup/StandaloneMockMvcBuilder.html)以加载与[WebMVC 爪哇 配置](web.html#mvc-config)等价的基础设施,注册给定的控制器,并创建[MockMvc](#spring-mvc-test-framework)的实例来处理请求: 爪哇 ``` WebTestClient client = MockMvcWebTestClient.bindToController(new TestController()).build(); ``` Kotlin ``` val client = MockMvcWebTestClient.bindToController(TestController()).build() ``` ##### 绑定到`ApplicationContext` 这种设置允许你使用 Spring MVC 或 Spring WebFlux 基础设施和控制器声明加载 Spring 配置,并使用它通过模拟请求和响应对象来处理请求,而无需运行服务器。 对于 WebFlux,使用以下方法将 Spring `ApplicationContext`传递到[WebHttphandlerBuilder](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/server/adapter/WebHttpHandlerBuilder.html#applicationContext-org.springframework.context.ApplicationContext-),以创建[Webhandler 链](web-reactive.html#webflux-web-handler-api)来处理请求: 爪哇 ``` @SpringJUnitConfig(WebConfig.class) (1) class MyTests { WebTestClient client; @BeforeEach void setUp(ApplicationContext context) { (2) client = WebTestClient.bindToApplicationContext(context).build(); (3) } } ``` |**1**|指定要加载的配置| |-----|---------------------------------| |**2**|注入配置| |**3**|创建`WebTestClient`| Kotlin ``` @SpringJUnitConfig(WebConfig::class) (1) class MyTests { lateinit var client: WebTestClient @BeforeEach fun setUp(context: ApplicationContext) { (2) client = WebTestClient.bindToApplicationContext(context).build() (3) } } ``` |**1**|指定要加载的配置| |-----|---------------------------------| |**2**|注入配置| |**3**|创建`WebTestClient`| 对于 Spring MVC,使用以下方法,其中将 Spring `ApplicationContext`传递到[mockmvcbuilders.webappcontextsetup](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/test/web/servlet/setup/MockMvcBuilders.html#webAppContextSetup-org.springframework.web.context.WebApplicationContext-),以创建[MockMvc](#spring-mvc-test-framework)实例来处理请求: 爪哇 ``` @ExtendWith(SpringExtension.class) @WebAppConfiguration("classpath:META-INF/web-resources") (1) @ContextHierarchy({ @ContextConfiguration(classes = RootConfig.class), @ContextConfiguration(classes = WebConfig.class) }) class MyTests { @Autowired WebApplicationContext wac; (2) WebTestClient client; @BeforeEach void setUp() { client = MockMvcWebTestClient.bindToApplicationContext(this.wac).build(); (3) } } ``` |**1**|指定要加载的配置| |-----|---------------------------------| |**2**|注入配置| |**3**|创建`WebTestClient`| Kotlin ``` @ExtendWith(SpringExtension.class) @WebAppConfiguration("classpath:META-INF/web-resources") (1) @ContextHierarchy({ @ContextConfiguration(classes = RootConfig.class), @ContextConfiguration(classes = WebConfig.class) }) class MyTests { @Autowired lateinit var wac: WebApplicationContext; (2) lateinit var client: WebTestClient @BeforeEach fun setUp() { (2) client = MockMvcWebTestClient.bindToApplicationContext(wac).build() (3) } } ``` |**1**|指定要加载的配置| |-----|---------------------------------| |**2**|注入配置| |**3**|创建`WebTestClient`| ##### 绑定到路由器功能 这种设置允许你通过模拟请求和响应对象来测试[功能端点](web-reactive.html#webflux-fn),而不需要运行服务器。 对于 WebFlux,使用以下方法委托`RouterFunctions.toWebHandler`来创建服务器设置以处理请求: 爪哇 ``` RouterFunction route = ... client = WebTestClient.bindToRouterFunction(route).build(); ``` Kotlin ``` val route: RouterFunction<*> = ... val client = WebTestClient.bindToRouterFunction(route).build() ``` 对于 Spring MVC,目前没有测试[WebMVC 功能端点](web.html#webmvc-fn)的选项。 ##### 绑定到服务器 此设置连接到正在运行的服务器,以执行完整的端到端 HTTP 测试: 爪哇 ``` client = WebTestClient.bindToServer().baseUrl("http://localhost:8080").build(); ``` Kotlin ``` client = WebTestClient.bindToServer().baseUrl("http://localhost:8080").build() ``` ##### 客户端配置 除了前面描述的服务器设置选项外,还可以配置客户端选项,包括基本 URL、默认标头、客户端过滤器和其他选项。这些选项在`bindToServer()`之后很容易获得。对于所有其他配置选项,你需要使用`configureClient()`来从服务器配置转换到客户机配置,如下所示: 爪哇 ``` client = WebTestClient.bindToController(new TestController()) .configureClient() .baseUrl("/test") .build(); ``` Kotlin ``` client = WebTestClient.bindToController(TestController()) .configureClient() .baseUrl("/test") .build() ``` #### 3.6.2.写作测试 `WebTestClient`提供与[WebClient](web-reactive.html#webflux-client)相同的 API,直到使用`exchange()`执行请求为止。请参阅[WebClient](web-reactive.html#webflux-client-body)文档中的示例,以了解如何准备包含表单数据、多部分数据等任何内容的请求。 在调用`exchange()`之后,`WebTestClient`偏离了`WebClient`,而是继续使用工作流来验证响应。 要断言响应状态和头,请使用以下方法: 爪哇 ``` client.get().uri("/persons/1") .accept(MediaType.APPLICATION_JSON) .exchange() .expectStatus().isOk() .expectHeader().contentType(MediaType.APPLICATION_JSON); ``` Kotlin ``` client.get().uri("/persons/1") .accept(MediaType.APPLICATION_JSON) .exchange() .expectStatus().isOk() .expectHeader().contentType(MediaType.APPLICATION_JSON) ``` 如果你希望所有的期望都被断言,即使其中一个失败了,你可以使用`expectAll(..)`,而不是使用多个链接的`expect*(..)`调用。此功能类似于 AssertJ 中的*软断言*支持和 JUnit Jupiter 中的`assertAll()`支持。 爪哇 ``` client.get().uri("/persons/1") .accept(MediaType.APPLICATION_JSON) .exchange() .expectAll( spec -> spec.expectStatus().isOk(), spec -> spec.expectHeader().contentType(MediaType.APPLICATION_JSON) ); ``` 然后,你可以选择通过以下方式之一对响应体进行解码: * `expectBody(Class)`:解码为单个对象。 * `expectBodyList(Class)`:将对象解码并收集到`List`。 * `expectBody()`:将`byte[]`或空体解码为[JSON 内容](#webtestclient-json)。 并在生成的较高级别对象上执行断言: 爪哇 ``` client.get().uri("/persons") .exchange() .expectStatus().isOk() .expectBodyList(Person.class).hasSize(3).contains(person); ``` Kotlin ``` import org.springframework.test.web.reactive.server.expectBodyList client.get().uri("/persons") .exchange() .expectStatus().isOk() .expectBodyList().hasSize(3).contains(person) ``` 如果内置断言不足,则可以使用该对象并执行任何其他断言: 爪哇 ``` import org.springframework.test.web.reactive.server.expectBody client.get().uri("/persons/1") .exchange() .expectStatus().isOk() .expectBody(Person.class) .consumeWith(result -> { // custom assertions (e.g. AssertJ)... }); ``` Kotlin ``` client.get().uri("/persons/1") .exchange() .expectStatus().isOk() .expectBody() .consumeWith { // custom assertions (e.g. AssertJ)... } ``` 或者,你可以退出工作流并获得`EntityExchangeResult`: 爪哇 ``` EntityExchangeResult result = client.get().uri("/persons/1") .exchange() .expectStatus().isOk() .expectBody(Person.class) .returnResult(); ``` Kotlin ``` import org.springframework.test.web.reactive.server.expectBody val result = client.get().uri("/persons/1") .exchange() .expectStatus().isOk .expectBody() .returnResult() ``` | |当你需要用泛型解码到目标类型时,请查找接受[
](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/core/core/parameterizedtyreference.html)而不是)的重载方法() ``` ##### JSON 内容 你可以在没有目标类型的情况下使用`expectBody()`对原始内容执行断言,而不是通过更高级别的对象。 要用[JSONAssert](https://jsonassert.skyscreamer.org)验证完整的 JSON 内容: 爪哇 ``` client.get().uri("/persons/1") .exchange() .expectStatus().isOk() .expectBody() .json("{\"name\":\"Jane\"}") ``` Kotlin ``` client.get().uri("/persons/1") .exchange() .expectStatus().isOk() .expectBody() .json("{\"name\":\"Jane\"}") ``` 要用[JSONPath](https://github.com/jayway/JsonPath)验证 JSON 内容: 爪哇 ``` client.get().uri("/persons") .exchange() .expectStatus().isOk() .expectBody() .jsonPath("$[0].name").isEqualTo("Jane") .jsonPath("$[1].name").isEqualTo("Jason"); ``` Kotlin ``` client.get().uri("/persons") .exchange() .expectStatus().isOk() .expectBody() .jsonPath("$[0].name").isEqualTo("Jane") .jsonPath("$[1].name").isEqualTo("Jason") ``` ##### 流式响应 要测试可能无限的流,例如`"text/event-stream"`或`"application/x-ndjson"`,首先要验证响应状态和头,然后获得`FluxExchangeResult`: 爪哇 ``` FluxExchangeResult result = client.get().uri("/events") .accept(TEXT_EVENT_STREAM) .exchange() .expectStatus().isOk() .returnResult(MyEvent.class); ``` Kotlin ``` import org.springframework.test.web.reactive.server.returnResult val result = client.get().uri("/events") .accept(TEXT_EVENT_STREAM) .exchange() .expectStatus().isOk() .returnResult() ``` 现在,你可以使用`StepVerifier`中的`reactor-test`来使用响应流了: 爪哇 ``` Flux eventFlux = result.getResponseBody(); StepVerifier.create(eventFlux) .expectNext(person) .expectNextCount(4) .consumeNextWith(p -> ...) .thenCancel() .verify(); ``` Kotlin ``` val eventFlux = result.getResponseBody() StepVerifier.create(eventFlux) .expectNext(person) .expectNextCount(4) .consumeNextWith { p -> ... } .thenCancel() .verify() ``` ##### MockMVC 断言 `WebTestClient`是一个 HTTP 客户机,因此它只能验证客户机响应中的内容,包括状态、头和主体。 在使用 mockMVC 服务器设置测试 Spring MVC 应用程序时,你有额外的选择来对服务器响应执行进一步的断言。要做到这一点,首先要在断言主体之后获得`ExchangeResult`: 爪哇 ``` // For a response with a body EntityExchangeResult result = client.get().uri("/persons/1") .exchange() .expectStatus().isOk() .expectBody(Person.class) .returnResult(); // For a response without a body EntityExchangeResult result = client.get().uri("/path") .exchange() .expectBody().isEmpty(); ``` Kotlin ``` // For a response with a body val result = client.get().uri("/persons/1") .exchange() .expectStatus().isOk() .expectBody(Person.class) .returnResult(); // For a response without a body val result = client.get().uri("/path") .exchange() .expectBody().isEmpty(); ``` 然后切换到 MockMVC 服务器响应断言: 爪哇 ``` MockMvcWebTestClient.resultActionsFor(result) .andExpect(model().attribute("integer", 3)) .andExpect(model().attribute("string", "a string value")); ``` Kotlin ``` MockMvcWebTestClient.resultActionsFor(result) .andExpect(model().attribute("integer", 3)) .andExpect(model().attribute("string", "a string value")); ``` ### 3.7.MockMVC Spring MVC 测试框架,也称为 MockMVC,为测试 Spring MVC 应用程序提供了支持。 Spring 它执行完整的 MVC 请求处理,但通过模拟请求和响应对象,而不是运行中的服务器。 MockMVC 可以单独用于执行请求和验证响应。它也可以通过[WebTestClient](#webtestclient)使用,其中 MockMVC 作为服务器插入以处理请求。`WebTestClient`的优点是可以选择使用更高级别的对象而不是原始数据,并且可以针对实时服务器切换到完整的端到端 HTTP 测试,并使用相同的测试 API。 #### 3.7.1.概述 你可以通过实例化控制器、向其注入依赖项并调用其方法来编写 Spring MVC 的普通单元测试。然而,这样的测试不会验证请求映射、数据绑定、消息转换、类型转换、验证,也不涉及任何支持`@InitBinder`、`@ModelAttribute`或`@ExceptionHandler`的方法。 Spring MVC 测试框架,也称为`MockMvc`,旨在为 Spring MVC 控制器提供更完整的测试,而无需运行服务器。它通过调用`DispacherServlet`并从`spring-test`模块传递[“mock” implementations of the Servlet API](#mock-objects-servlet)来实现这一点,该模块在不运行服务器的情况下复制完整的 Spring MVC 请求处理。 MockMVC 是一个服务器端测试框架,它允许你使用轻量级和有针对性的测试来验证 Spring MVC 应用程序的大部分功能。你可以单独使用它来执行请求和验证响应,也可以通过[WebTestClient](#webtestclient)API 使用它,并将 MockMVC 插入为服务器来处理请求。 ##### 静态导入 当直接使用 MockMVC 来执行请求时,你将需要用于以下方面的静态导入: * `MockMvcBuilders.*` * `MockMvcRequestBuilders.*` * `MockMvcResultMatchers.*` * `MockMvcResultHandlers.*` 记住这一点的一个简单方法是搜索`MockMvc*`。如果使用 Eclipse,请确保在 Eclipse 首选项中也添加上面的“最喜欢的静态成员”。 当通过[WebTestClient](#webtestclient)使用 mockmvc 时,不需要静态导入。`WebTestClient`提供了一个没有静态导入的 Fluent API。 ##### 设置选项 MockMVC 可以通过以下两种方式之一设置。一种方法是直接指向你想要测试的控制器,并以编程方式配置 Spring MVC 基础设施。第二个是指向具有 Spring MVC 和控制器基础设施的 Spring 配置。 要设置用于测试特定控制器的 mockmvc,请使用以下方法: 爪哇 ``` class MyWebTests { MockMvc mockMvc; @BeforeEach void setup() { this.mockMvc = MockMvcBuilders.standaloneSetup(new AccountController()).build(); } // ... } ``` Kotlin ``` class MyWebTests { lateinit var mockMvc : MockMvc @BeforeEach fun setup() { mockMvc = MockMvcBuilders.standaloneSetup(AccountController()).build() } // ... } ``` 或者,你也可以在通过[WebTestClient](#webtestclient-controller-config)进行测试时使用此设置,该设置将委托给与上面所示相同的构建器。 要通过 Spring 配置设置 mockmvc,请使用以下方法: 爪哇 ``` @SpringJUnitWebConfig(locations = "my-servlet-context.xml") class MyWebTests { MockMvc mockMvc; @BeforeEach void setup(WebApplicationContext wac) { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); } // ... } ``` Kotlin ``` @SpringJUnitWebConfig(locations = ["my-servlet-context.xml"]) class MyWebTests { lateinit var mockMvc: MockMvc @BeforeEach fun setup(wac: WebApplicationContext) { mockMvc = MockMvcBuilders.webAppContextSetup(wac).build() } // ... } ``` 或者,你也可以在通过[WebTestClient](#webtestclient-context-config)进行测试时使用此设置,该设置将委托给与上面所示相同的构建器。 你应该使用哪个设置选项? `webAppContextSetup`加载你实际的 Spring MVC 配置,从而产生一个更完整的集成测试。由于 TestContext 框架缓存了加载的 Spring 配置,因此它有助于保持测试快速运行,即使你在测试套件中引入了更多的测试。此外,还可以通过 Spring 配置将模拟服务注入控制器,以保持对 Web 层的重点测试。下面的示例使用 Mockito 声明了一个模拟服务: ``` ``` 然后,你可以将模拟服务注入到测试中,以设置和验证你的期望,如下例所示: 爪哇 ``` @SpringJUnitWebConfig(locations = "test-servlet-context.xml") class AccountTests { @Autowired AccountService accountService; MockMvc mockMvc; @BeforeEach void setup(WebApplicationContext wac) { this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); } // ... } ``` Kotlin ``` @SpringJUnitWebConfig(locations = ["test-servlet-context.xml"]) class AccountTests { @Autowired lateinit var accountService: AccountService lateinit mockMvc: MockMvc @BeforeEach fun setup(wac: WebApplicationContext) { mockMvc = MockMvcBuilders.webAppContextSetup(wac).build() } // ... } ``` 另一方面,`standaloneSetup`更接近于单位检验。它一次测试一个控制器。你可以手动为控制器注入模拟依赖项,并且它不涉及加载 Spring 配置。这样的测试更多地关注于样式,并使其更容易地看到正在测试的控制器,是否需要任何特定的 MVC 配置来工作,等等。`standaloneSetup`也是编写临时测试以验证特定行为或调试问题的一种非常方便的方法。 与大多数“集成与单元测试”的辩论一样,没有正确或错误的答案。然而,使用`standaloneSetup`确实意味着需要额外的`webAppContextSetup`测试,以便验证你的 Spring MVC 配置。或者,你可以使用`webAppContextSetup`编写所有测试,以便始终根据实际的 Spring MVC 配置进行测试。 ##### 设置功能 无论你使用哪个 MockMVC Builder,所有`MockMvcBuilder`实现都提供了一些常见且非常有用的特性。例如,你可以为所有请求声明一个`Accept`头,并期望在所有响应中的状态为 200,以及`Content-Type`头,如下所示: 爪哇 ``` // static import of MockMvcBuilders.standaloneSetup MockMvc mockMvc = standaloneSetup(new MusicController()) .defaultRequest(get("/").accept(MediaType.APPLICATION_JSON)) .alwaysExpect(status().isOk()) .alwaysExpect(content().contentType("application/json;charset=UTF-8")) .build(); ``` Kotlin ``` // Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed ``` 此外,第三方框架(和应用程序)可以预先打包设置指令,例如`MockMvcConfigurer`中的设置指令。 Spring 框架有一个这样的内置实现,该实现有助于跨请求保存和重用 HTTP 会话。你可以按以下方式使用它: 爪哇 ``` // static import of SharedHttpSessionConfigurer.sharedHttpSession MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new TestController()) .apply(sharedHttpSession()) .build(); // Use mockMvc to perform requests... ``` Kotlin ``` // Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed ``` 参见 爪哇doc 的[`ConfigurableMockMvcBuilder`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/test/web/ Servlet/setup/confirablemcmvcbuilder.html),以获得所有 MockMVC Builder 特性的列表,或使用 IDE 探索可用的选项。 ##### 执行请求 本节展示了如何单独使用 MockMVC 来执行请求和验证响应。如果通过`WebTestClient`使用 mockmvc,请查看[写作测试](#webtestclient-tests)上的相应部分。 要执行使用任何 HTTP 方法的请求,如以下示例所示: 爪哇 ``` // static import of MockMvcRequestBuilders.* mockMvc.perform(post("/hotels/{id}", 42).accept(MediaType.APPLICATION_JSON)); ``` Kotlin ``` import org.springframework.test.web.servlet.post mockMvc.post("/hotels/{id}", 42) { accept = MediaType.APPLICATION_JSON } ``` 你还可以执行内部使用`MockMultipartHttpServletRequest`的文件上传请求,这样就不会实际解析多个部分的请求。相反,你必须将其设置为类似于以下示例: Java ``` mockMvc.perform(multipart("/doc").file("a1", "ABC".getBytes("UTF-8"))); ``` Kotlin ``` import org.springframework.test.web.servlet.multipart mockMvc.multipart("/doc") { file("a1", "ABC".toByteArray(charset("UTF8"))) } ``` 你可以用 URI 模板样式指定查询参数,如下例所示: Java ``` mockMvc.perform(get("/hotels?thing={thing}", "somewhere")); ``` Kotlin ``` mockMvc.get("/hotels?thing={thing}", "somewhere") ``` 还可以添加表示查询或表单参数的 Servlet 请求参数,如下例所示: Java ``` mockMvc.perform(get("/hotels").param("thing", "somewhere")); ``` Kotlin ``` import org.springframework.test.web.servlet.get mockMvc.get("/hotels") { param("thing", "somewhere") } ``` 如果应用程序代码依赖于 Servlet 请求参数,并且没有显式地检查查询字符串(通常是这种情况),那么使用哪个选项并不重要。但是,请记住,与 URI 模板一起提供的查询参数已经被解码,而通过`param(…​)`方法提供的请求参数预计已经被解码。 在大多数情况下,最好是将上下文路径和 Servlet 路径留在请求 URI 之外。如果必须使用完整的请求 URI 进行测试,请确保相应地设置`contextPath`和`servletPath`,以使请求映射工作,如下例所示: Java ``` mockMvc.perform(get("/app/main/hotels/{id}").contextPath("/app").servletPath("/main")) ``` Kotlin ``` import org.springframework.test.web.servlet.get mockMvc.get("/app/main/hotels/{id}") { contextPath = "/app" servletPath = "/main" } ``` 在前面的示例中,每次执行请求时都要设置`contextPath`和`servletPath`,这会很麻烦。相反,你可以设置默认的请求属性,如下例所示: Java ``` class MyWebTests { MockMvc mockMvc; @BeforeEach void setup() { mockMvc = standaloneSetup(new AccountController()) .defaultRequest(get("/") .contextPath("/app").servletPath("/main") .accept(MediaType.APPLICATION_JSON)).build(); } } ``` Kotlin ``` // Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed ``` 前面的属性影响通过`MockMvc`实例执行的每个请求。如果在给定的请求中也指定了相同的属性,那么它将重写默认值。这就是为什么默认请求中的 HTTP 方法和 URI 无关紧要的原因,因为它们必须在每个请求中指定。 ##### 定义期望 可以通过在执行请求后追加一个或多个`andExpect(..)`调用来定义期望,如下例所示。一旦一种预期落空,就不会再有其他预期了。 Java ``` // static import of MockMvcRequestBuilders.* and MockMvcResultMatchers.* mockMvc.perform(get("/accounts/1")).andExpect(status().isOk()); ``` Kotlin ``` import org.springframework.test.web.servlet.get mockMvc.get("/accounts/1").andExpect { status().isOk() } ``` 可以通过在执行请求后追加`andExpectAll(..)`来定义多个期望,如下例所示。与`andExpect(..)`相反,`andExpectAll(..)`保证将断言所有提供的期望,并将跟踪和报告所有故障。 Java ``` // static import of MockMvcRequestBuilders.* and MockMvcResultMatchers.* mockMvc.perform(get("/accounts/1")).andExpectAll( status().isOk(), content().contentType("application/json;charset=UTF-8")); ``` `MockMvcResultMatchers.*`提供了一些期望,其中一些期望进一步嵌套了更详细的期望。 预期分为两大类。第一类断言验证响应的属性(例如,响应状态、标题和内容)。这些是可以断言的最重要的结果。 第二类断言超出了响应范围。这些断言允许你检查 Spring MVC 的特定方面,例如哪个控制器方法处理了请求,是否引发并处理了异常,模型的内容是什么,选择了什么视图,添加了什么 flash 属性,等等。它们还允许你检查 Servlet 特定的方面,例如请求和会话属性。 以下测试断言绑定或验证失败: Java ``` mockMvc.perform(post("/persons")) .andExpect(status().isOk()) .andExpect(model().attributeHasErrors("person")); ``` Kotlin ``` import org.springframework.test.web.servlet.post mockMvc.post("/persons").andExpect { status().isOk() model { attributeHasErrors("person") } } ``` 很多时候,在编写测试时,转储执行的请求的结果是有用的。你可以这样做,其中`print()`是来自`MockMvcResultHandlers`的静态导入: Java ``` mockMvc.perform(post("/persons")) .andDo(print()) .andExpect(status().isOk()) .andExpect(model().attributeHasErrors("person")); ``` Kotlin ``` import org.springframework.test.web.servlet.post mockMvc.post("/persons").andDo { print() }.andExpect { status().isOk() model { attributeHasErrors("person") } } ``` 只要请求处理不会导致未处理的异常,`print()`方法就会将所有可用的结果数据打印到`System.out`。还有一个`log()`方法和`print()`方法的两个附加变体,一个接受`OutputStream`,另一个接受`Writer`。例如,调用`print(System.err)`将结果数据打印到`System.err`,而调用`print(myWriter)`将结果数据打印到自定义写入器。如果希望记录结果数据而不是打印结果,则可以调用`log()`方法,该方法将结果数据记录为`DEBUG`日志类别下的单个`DEBUG`消息。 在某些情况下,你可能希望获得对结果的直接访问,并验证某些在其他情况下无法验证的内容。这可以通过在所有其他期望之后附加`.andReturn()`来实现,如下例所示: Java ``` MvcResult mvcResult = mockMvc.perform(post("/persons")).andExpect(status().isOk()).andReturn(); // ... ``` Kotlin ``` var mvcResult = mockMvc.post("/persons").andExpect { status().isOk() }.andReturn() // ... ``` 如果所有测试都重复相同的期望,那么在构建`MockMvc`实例时,可以设置一次公共期望,如下例所示: Java ``` standaloneSetup(new SimpleController()) .alwaysExpect(status().isOk()) .alwaysExpect(content().contentType("application/json;charset=UTF-8")) .build() ``` Kotlin ``` // Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed ``` 注意,通常的期望总是被应用的,并且在不创建单独的`MockMvc`实例的情况下不能被重写。 当 JSON 响应内容包含用[Spring HATEOAS](https://github.com/spring-projects/spring-hateoas)创建的超媒体链接时,可以使用 JSONPath 表达式来验证结果链接,如下例所示: Java ``` mockMvc.perform(get("/people").accept(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.links[?(@.rel == 'self')].href").value("http://localhost:8080/people")); ``` Kotlin ``` mockMvc.get("/people") { accept(MediaType.APPLICATION_JSON) }.andExpect { jsonPath("$.links[?(@.rel == 'self')].href") { value("http://localhost:8080/people") } } ``` 当 XML 响应内容包含用[Spring HATEOAS](https://github.com/spring-projects/spring-hateoas)创建的超媒体链接时,可以使用 XPath 表达式来验证生成的链接: Java ``` Map ns = Collections.singletonMap("ns", "http://www.w3.org/2005/Atom"); mockMvc.perform(get("/handle").accept(MediaType.APPLICATION_XML)) .andExpect(xpath("/person/ns:link[@rel='self']/@href", ns).string("http://localhost:8080/people")); ``` Kotlin ``` val ns = mapOf("ns" to "http://www.w3.org/2005/Atom") mockMvc.get("/handle") { accept(MediaType.APPLICATION_XML) }.andExpect { xpath("/person/ns:link[@rel='self']/@href", ns) { string("http://localhost:8080/people") } } ``` ##### 异步请求 本节展示了如何单独使用 MockMVC 来测试异步请求处理。如果通过[WebTestClient](#webtestclient)使用 MockMVC,则没有什么特别的事情可以使异步请求工作,因为`WebTestClient`会自动执行本节中所描述的操作。 Servlet 3.0 异步请求[supported in Spring MVC](web.html#mvc-ann-async)通过退出 Servlet 容器线程并允许应用程序异步计算响应来工作,在此之后进行异步分派以在 Servlet 容器线程上完成处理。 Spring 在 MVC 测试中,异步请求可以通过首先断言产生的异步值,然后手动执行异步调度,最后验证响应来进行测试。下面是返回`DeferredResult`、`Callable`或反应类型(例如反应器`Mono`)的控制器方法的示例测试: Java ``` // static import of MockMvcRequestBuilders.* and MockMvcResultMatchers.* @Test void test() throws Exception { MvcResult mvcResult = this.mockMvc.perform(get("/path")) .andExpect(status().isOk()) (1) .andExpect(request().asyncStarted()) (2) .andExpect(request().asyncResult("body")) (3) .andReturn(); this.mockMvc.perform(asyncDispatch(mvcResult)) (4) .andExpect(status().isOk()) (5) .andExpect(content().string("body")); } ``` |**1**|检查响应状态仍未更改| |-----|---------------------------------------------------------------------| |**2**|异步处理肯定已经启动了。| |**3**|等待并断言异步结果| |**4**|手动执行异步分派(因为没有正在运行的容器)| |**5**|验证最终响应| Kotlin ``` @Test fun test() { var mvcResult = mockMvc.get("/path").andExpect { status().isOk() (1) request { asyncStarted() } (2) // TODO Remove unused generic parameter request { asyncResult("body") } (3) }.andReturn() mockMvc.perform(asyncDispatch(mvcResult)) (4) .andExpect { status().isOk() (5) content().string("body") } } ``` |**1**|检查响应状态仍未更改| |-----|---------------------------------------------------------------------| |**2**|异步处理肯定已经启动了。| |**3**|等待并断言异步结果| |**4**|手动执行异步分派(因为没有正在运行的容器)| |**5**|验证最终响应| ##### 流式响应 测试流响应(例如服务器发送的事件)的最佳方法是通过[WebTestClient](#webtestclient),它可以用作测试客户端,以连接到`MockMvc`实例,从而在 Spring MVC 控制器上执行测试,而无需运行服务器。例如: 爪哇 ``` WebTestClient client = MockMvcWebTestClient.bindToController(new SseController()).build(); FluxExchangeResult 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 中也是支持的,在这里你可以[测试正在运行的服务器](https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-testing-spring-boot-applications-testing-with-running-server)。 ##### 过滤注册 在设置`MockMvc`实例时,可以注册一个或多个 Servlet `Filter`实例,如下例所示: 爪哇 ``` mockMvc = standaloneSetup(new PersonController()).addFilters(new CharacterEncodingFilter()).build(); ``` Kotlin ``` // Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed ``` 通过`spring-test`中的`MockFilterChain`调用注册过滤器,最后一个过滤器将委托给`DispatcherServlet`。 ##### MockMVC 与端到端测试 MockMVC 是基于来自`spring-test`模块的 Servlet API 模拟实现构建的,并且不依赖于正在运行的容器。因此,与实际运行客户机和实时服务器的完整端到端集成测试相比,存在一些差异。 思考这个问题最简单的方法是从空白`MockHttpServletRequest`开始。无论你向它添加什么,请求都会变成什么。可能会让你感到惊讶的是,默认情况下没有上下文路径;没有`jsessionid`cookie;没有转发、错误或异步分派;因此,没有实际的 JSP 呈现。相反,“转发”和“重定向”URL 保存在`MockHttpServletResponse`中,并且可以在期望的情况下断言。 这意味着,如果使用 JSP,你可以验证请求被转发到的 JSP 页面,但不呈现 HTML。换句话说,不会调用 JSP。但是,请注意,所有不依赖于转发的其他呈现技术,例如 ThymeLeaf 和 FreeMarker,都会按照预期的那样将 HTML 呈现给响应主体。对于通过`@ResponseBody`方法呈现 JSON、XML 和其他格式,也是如此。 或者,你可以考虑使用`@SpringBootTest`从 Spring 启动的完整的端到端集成测试支持。见[Spring Boot Reference Guide](https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-testing)。 每种方法都有优点和缺点。 Spring MVC 测试中提供的选项是从经典单元测试到完全集成测试的不同规模的停止。可以肯定的是, Spring MVC 测试中的所有选项都不属于经典单元测试的范畴,但它们更接近于经典单元测试。例如,你可以通过将模拟的服务注入控制器来隔离 Web 层,在这种情况下,你只能通过`DispatcherServlet`来测试 Web 层,但要使用实际的 Spring 配置,因为你可能会在与上面的层隔离的情况下测试数据访问层。此外,你还可以使用独立设置,一次只关注一个控制器,并手动提供使其工作所需的配置。 使用 Spring MVC 测试时的另一个重要区别是,从概念上讲,这样的测试是服务器端的,因此你可以检查使用了什么处理程序,如果异常是用 HandleRexCeptionResolver 处理的,模型的内容是什么,有哪些绑定错误,以及其他细节。这意味着更容易编写预期值,因为服务器不是一个不透明的框,就像通过实际的 HTTP 客户机进行测试时一样。这通常是经典单元测试的一个优势:它更容易编写、推理和调试,但不会取代对完全集成测试的需求。同时,重要的是不要忽视这样一个事实,即反应是最重要的检查事项。简而言之,即使在同一个项目中,这里也有多种测试风格和策略的空间。 ##### 进一步的例子 该框架自己的测试包括[许多样本测试](https://github.com/spring-projects/spring-framework/tree/main/spring-test/src/test/java/org/springframework/test/web/servlet/samples),旨在展示如何单独或通过[WebTestClient](https://github.com/spring-projects/spring-framework/tree/main/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client)使用 MockMVC。浏览这些示例以获得更多的想法。 #### 3.7.2.htmlunit 集成 Spring 提供[MockMvc](#spring-mvc-test-server)和[HtmlUnit](http://htmlunit.sourceforge.net/)之间的集成。这简化了在使用基于 HTML 的视图时执行端到端测试的过程。这种集成使你能够: * 通过使用[HtmlUnit](http://htmlunit.sourceforge.net/)、[WebDriver](https://www.seleniumhq.org)和[Geb](http://www.gebish.org/manual/current/#spock-junit-testng)等工具轻松地测试 HTML 页面,而无需部署到 Servlet 容器。 * 在页面中测试 爪哇Script。 * 还可以选择使用模拟服务进行测试,以加快测试速度。 * 在容器内端到端测试和容器外集成测试之间共享逻辑。 | |MockMVC 使用不依赖于 Servlet 容器
的模板化技术(例如,ThymeLeaf、FreeMarker 和其他),但不适用于 JSP,因为
它们依赖于 Servlet 容器。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ##### 为什么要进行 HTMLUnit 集成? 我脑海中浮现的最明显的问题是“我为什么需要这个?”最好通过探索一个非常基本的示例应用程序来找到答案。假设你有一个 Spring MVC Web 应用程序,该应用程序在`Message`对象上支持增删改查操作。该应用程序还支持对所有消息进行分页。你会如何去测试它呢? 通过 Spring MVC 测试,我们可以很容易地测试是否能够创建`Message`,如下所示: 爪哇 ``` MockHttpServletRequestBuilder createMessage = post("/messages/") .param("summary", "Spring Rocks") .param("text", "In case you didn't know, Spring Rocks!"); mockMvc.perform(createMessage) .andExpect(status().is3xxRedirection()) .andExpect(redirectedUrl("/messages/123")); ``` Kotlin ``` @Test fun test() { mockMvc.post("/messages/") { param("summary", "Spring Rocks") param("text", "In case you didn't know, Spring Rocks!") }.andExpect { status().is3xxRedirection() redirectedUrl("/messages/123") } } ``` 如果我们想测试允许我们创建消息的窗体视图,该怎么办?例如,假设我们的表单看起来像以下片段: ```
``` 我们如何确保我们的表单产生正确的请求来创建新消息?一次幼稚的尝试可能类似于以下几点: 爪哇 ``` mockMvc.perform(get("/messages/form")) .andExpect(xpath("//input[@name='summary']").exists()) .andExpect(xpath("//textarea[@name='text']").exists()); ``` Kotlin ``` mockMvc.get("/messages/form").andExpect { xpath("//input[@name='summary']") { exists() } xpath("//textarea[@name='text']") { exists() } } ``` 这种测试有一些明显的缺点。如果我们更新控制器以使用参数`message`而不是`text`,那么我们的表单测试将继续通过,即使 HTML 表单与控制器不同步。为了解决这个问题,我们可以将两个测试结合起来,如下所示: 爪哇 ``` String summaryParamName = "summary"; String textParamName = "text"; mockMvc.perform(get("/messages/form")) .andExpect(xpath("//input[@name='" + summaryParamName + "']").exists()) .andExpect(xpath("//textarea[@name='" + textParamName + "']").exists()); MockHttpServletRequestBuilder createMessage = post("/messages/") .param(summaryParamName, "Spring Rocks") .param(textParamName, "In case you didn't know, Spring Rocks!"); mockMvc.perform(createMessage) .andExpect(status().is3xxRedirection()) .andExpect(redirectedUrl("/messages/123")); ``` Kotlin ``` val summaryParamName = "summary"; val textParamName = "text"; mockMvc.get("/messages/form").andExpect { xpath("//input[@name='$summaryParamName']") { exists() } xpath("//textarea[@name='$textParamName']") { exists() } } mockMvc.post("/messages/") { param(summaryParamName, "Spring Rocks") param(textParamName, "In case you didn't know, Spring Rocks!") }.andExpect { status().is3xxRedirection() redirectedUrl("/messages/123") } ``` 这将减少我们的测试不正确通过的风险,但仍然存在一些问题: * 如果我们的页面上有多个表单怎么办?诚然,我们可以更新 XPath 表达式,但是随着我们考虑更多因素,它们变得更加复杂:字段是正确的类型吗?字段启用了吗?以此类推。 * 另一个问题是,我们正在做的工作是预期的两倍。我们必须首先验证视图,然后使用刚刚验证过的相同参数提交视图。理想情况下,这可以一次完成。 * 最后,我们仍然无法解释某些事情。例如,如果表单也有我们希望测试的 爪哇Script 验证呢? 总的问题是,测试一个 Web 页面并不涉及一个单独的交互。相反,它是用户如何与 Web 页面交互以及该 Web 页面如何与其他资源交互的组合。例如,表单视图的结果被用作用户创建消息的输入。此外,我们的表单视图可能会使用影响页面行为的额外资源,例如 爪哇Script 验证。 ###### 整合测试的拯救? 为了解决前面提到的问题,我们可以执行端到端集成测试,但这有一些缺点。考虑测试这个视图,它可以让我们在消息中进行页面浏览。我们可能需要进行以下测试: * 我们的页面是否向用户显示通知,以表明当消息为空时没有可用的结果? * 我们的页面是否正确地显示了一条消息? * 我们的页面是否适当地支持分页? 要设置这些测试,我们需要确保我们的数据库包含正确的消息。这导致了一些额外的挑战: * 确保数据库中有正确的消息是很乏味的。(考虑一下国外的关键限制。 * 测试可能会变得很慢,因为每个测试都需要确保数据库处于正确的状态。 * 由于我们的数据库需要处于特定的状态,因此我们不能并行运行测试。 * 对自动生成的 ID、时间戳等项执行断言可能很困难。 这些挑战并不意味着我们应该完全放弃端到端集成测试。相反,我们可以通过重构详细的测试,使用运行速度更快、更可靠且没有副作用的模拟服务,来减少端到端集成测试的数量。然后,我们可以实现少量真正的端到端集成测试,这些测试验证简单的工作流,以确保所有工作都正确地结合在一起。 ###### 进入 HTMLUnit 集成 那么,我们如何在测试页面的交互和在测试套件中保持良好性能之间实现平衡呢?答案是:“通过整合 MockMVC 和 HTMLUnit。” ###### htmlunit 集成选项 当你想要将 mockmvc 与 htmlunit 集成在一起时,你有许多选项: * [mockmvc 和 htmlunit](#spring-mvc-test-server-htmlunit-mah):如果你想使用原始的 htmlUnit 库,请使用此选项。 * [MockMVC 和 WebDriver](#spring-mvc-test-server-htmlunit-webdriver):使用此选项可以简化开发,并在集成和端到端测试之间重用代码。 * [MOCKMVC 和 GEB](#spring-mvc-test-server-htmlunit-geb):如果你想使用 Groovy 进行测试、简化开发以及在集成和端到端测试之间重用代码,请使用此选项。 ##### mockmvc 和 htmlunit 本节介绍如何集成 MockMVC 和 HTMLUnit。如果你想使用原始的 HTMLUnit 库,请使用此选项。 ###### mockmvc 和 htmlunit 设置 首先,确保包含了对`net.sourceforge.htmlunit:htmlunit`的测试依赖项。为了在 Apache HttpComponents4.5+ 中使用 HTMLUnit,你需要使用 HTMLUnit2.18 或更高版本。 通过使用`MockMvcWebClientBuilder`,我们可以轻松地创建一个与 MockMVC 集成的 htmlUnit`WebClient`,如下所示: 爪哇 ``` WebClient webClient; @BeforeEach void setup(WebApplicationContext context) { webClient = MockMvcWebClientBuilder .webAppContextSetup(context) .build(); } ``` Kotlin ``` lateinit var webClient: WebClient @BeforeEach fun setup(context: WebApplicationContext) { webClient = MockMvcWebClientBuilder .webAppContextSetup(context) .build() } ``` | |这是使用`MockMvcWebClientBuilder`的一个简单示例。关于高级用法,
参见[advanced`MockMvcWebClientBuilder`](# Spring-mvc-test-server-htmlunit-mah-advanced-builder)。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 这确保了将`localhost`作为服务器引用的任何 URL 都指向我们的`MockMvc`实例,而不需要真正的 HTTP 连接。正常情况下,通过使用网络连接来请求任何其他 URL。这让我们很容易测试 CDNS 的使用情况。 ###### mockmvc 和 htmlunit 的使用 现在,我们可以像通常那样使用 HTMLUnit,但不需要将我们的应用程序部署到 Servlet 容器中。例如,我们可以请求该视图创建带有以下内容的消息: 爪哇 ``` HtmlPage createMsgFormPage = webClient.getPage("http://localhost/messages/form"); ``` Kotlin ``` val createMsgFormPage = webClient.getPage("http://localhost/messages/form") ``` | |默认的上下文路径是`""`。或者,我们可以指定上下文路径,
,如[advanced`MockMvcWebClientBuilder`](# Spring-mvc-test-server-htmlunit-mah-advanced-builder)中所述。| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 一旦我们有了对`HtmlPage`的引用,我们就可以填写表单并提交它来创建消息,如下例所示: 爪哇 ``` HtmlForm form = createMsgFormPage.getHtmlElementById("messageForm"); HtmlTextInput summaryInput = createMsgFormPage.getHtmlElementById("summary"); summaryInput.setValueAttribute("Spring Rocks"); HtmlTextArea textInput = createMsgFormPage.getHtmlElementById("text"); textInput.setText("In case you didn't know, Spring Rocks!"); HtmlSubmitInput submit = form.getOneHtmlElementByAttribute("input", "type", "submit"); HtmlPage newMessagePage = submit.click(); ``` Kotlin ``` val form = createMsgFormPage.getHtmlElementById("messageForm") val summaryInput = createMsgFormPage.getHtmlElementById("summary") summaryInput.setValueAttribute("Spring Rocks") val textInput = createMsgFormPage.getHtmlElementById("text") textInput.setText("In case you didn't know, Spring Rocks!") val submit = form.getOneHtmlElementByAttribute("input", "type", "submit") val newMessagePage = submit.click() ``` 最后,我们可以验证新消息是否已成功创建。以下断言使用[AssertJ](https://assertj.github.io/doc/)库: 爪哇 ``` assertThat(newMessagePage.getUrl().toString()).endsWith("/messages/123"); String id = newMessagePage.getHtmlElementById("id").getTextContent(); assertThat(id).isEqualTo("123"); String summary = newMessagePage.getHtmlElementById("summary").getTextContent(); assertThat(summary).isEqualTo("Spring Rocks"); String text = newMessagePage.getHtmlElementById("text").getTextContent(); assertThat(text).isEqualTo("In case you didn't know, Spring Rocks!"); ``` Kotlin ``` assertThat(newMessagePage.getUrl().toString()).endsWith("/messages/123") val id = newMessagePage.getHtmlElementById("id").getTextContent() assertThat(id).isEqualTo("123") val summary = newMessagePage.getHtmlElementById("summary").getTextContent() assertThat(summary).isEqualTo("Spring Rocks") val text = newMessagePage.getHtmlElementById("text").getTextContent() assertThat(text).isEqualTo("In case you didn't know, Spring Rocks!") ``` 前面的代码在许多方面改进了我们的[MockMVC 测试](#spring-mvc-test-server-htmlunit-mock-mvc-test)。首先,我们不再需要显式地验证表单,然后创建一个看起来像表单的请求。相反,我们请求表单,填写它,然后提交它,从而大大减少了开销。 另一个重要的因素是[HTMLUnit 使用 Mozilla Rhino 引擎](http://htmlunit.sourceforge.net/javascript.html)来评估 爪哇Script。这意味着我们还可以在页面中测试 爪哇Script 的行为。 有关使用 htmlunit 的更多信息,请参见[htmlunit 文档](http://htmlunit.sourceforge.net/gettingStarted.html)。 ###### 高级`MockMvcWebClientBuilder` 在到目前为止的示例中,我们已经以尽可能简单的方式使用`MockMvcWebClientBuilder`,通过基于 Spring TestContext 框架为我们加载的`WebApplicationContext`构建`WebClient`。下面的示例重复了这种方法: 爪哇 ``` WebClient webClient; @BeforeEach void setup(WebApplicationContext context) { webClient = MockMvcWebClientBuilder .webAppContextSetup(context) .build(); } ``` Kotlin ``` lateinit var webClient: WebClient @BeforeEach fun setup(context: WebApplicationContext) { webClient = MockMvcWebClientBuilder .webAppContextSetup(context) .build() } ``` 我们还可以指定其他配置选项,如下例所示: 爪哇 ``` WebClient webClient; @BeforeEach void setup() { webClient = MockMvcWebClientBuilder // demonstrates applying a MockMvcConfigurer (Spring Security) .webAppContextSetup(context, springSecurity()) // for illustration only - defaults to "" .contextPath("") // By default MockMvc is used for localhost only; // the following will use MockMvc for example.com and example.org as well .useMockMvcForHosts("example.com","example.org") .build(); } ``` Kotlin ``` lateinit var webClient: WebClient @BeforeEach fun setup() { webClient = MockMvcWebClientBuilder // demonstrates applying a MockMvcConfigurer (Spring Security) .webAppContextSetup(context, springSecurity()) // for illustration only - defaults to "" .contextPath("") // By default MockMvc is used for localhost only; // the following will use MockMvc for example.com and example.org as well .useMockMvcForHosts("example.com","example.org") .build() } ``` 作为一种选择,我们可以通过分别配置`MockMvc`实例并将其提供给`MockMvcWebClientBuilder`来执行完全相同的设置,如下所示: 爪哇 ``` MockMvc mockMvc = MockMvcBuilders .webAppContextSetup(context) .apply(springSecurity()) .build(); webClient = MockMvcWebClientBuilder .mockMvcSetup(mockMvc) // for illustration only - defaults to "" .contextPath("") // By default MockMvc is used for localhost only; // the following will use MockMvc for example.com and example.org as well .useMockMvcForHosts("example.com","example.org") .build(); ``` Kotlin ``` // Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed ``` 这更详细,但是,通过使用`MockMvc`实例构建`WebClient`实例,我们可以在指尖获得 MockMVC 的全部功能。 | |有关创建`MockMvc`实例的更多信息,请参见[设置选项](#spring-mvc-test-server-setup-options)。| |---|-----------------------------------------------------------------------------------------------------------------------| ##### MockMVC 和 WebDriver 在前面的部分中,我们已经了解了如何将 MockMVC 与原始的 HTMLUnitAPI 结合使用。在本节中,我们在 Selenium[WebDriver](https://docs.seleniumhq.org/projects/webdriver/)中使用了额外的抽象,以使事情变得更简单。 ###### 为什么是 Webdriver 和 MockMVC? 我们已经可以使用 HTMLUnit 和 MockMVC 了,那么为什么我们要使用 WebDriver 呢?Selenium WebDriver 提供了一个非常优雅的 API,可以让我们轻松地组织代码。为了更好地展示它是如何工作的,我们将在本节中探讨一个示例。 | |尽管是[Selenium](https://docs.seleniumhq.org/)的一部分,WebDriver 并不需要
的 Selenium 服务器来运行测试。| |---|-------------------------------------------------------------------------------------------------------------------------------------| 假设我们需要确保消息是正确创建的。测试包括查找 HTML 表单输入元素,填写它们,并做出各种断言。 这种方法会导致许多单独的测试,因为我们也希望测试错误条件。例如,如果我们只填写表单的一部分,我们希望确保得到一个错误。如果我们填写了整个表单,那么新创建的消息将在之后显示。 如果其中一个字段被命名为“Summary”,那么我们可能会在测试中的多个地方重复类似于以下内容的内容: 爪哇 ``` HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary"); summaryInput.setValueAttribute(summary); ``` Kotlin ``` val summaryInput = currentPage.getHtmlElementById("summary") summaryInput.setValueAttribute(summary) ``` 那么,如果我们将`id`更改为`smmry`会发生什么呢?这样做将迫使我们更新所有的测试,以纳入这一变化。这违反了 dry 原则,因此我们最好将该代码提取到它自己的方法中,如下所示: 爪哇 ``` public HtmlPage createMessage(HtmlPage currentPage, String summary, String text) { setSummary(currentPage, summary); // ... } public void setSummary(HtmlPage currentPage, String summary) { HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary"); summaryInput.setValueAttribute(summary); } ``` Kotlin ``` fun createMessage(currentPage: HtmlPage, summary:String, text:String) :HtmlPage{ setSummary(currentPage, summary); // ... } fun setSummary(currentPage:HtmlPage , summary: String) { val summaryInput = currentPage.getHtmlElementById("summary") summaryInput.setValueAttribute(summary) } ``` 这样做可以确保在更改 UI 时不需要更新所有的测试。 我们甚至可以更进一步,将这个逻辑放在一个`Object`中,该逻辑表示我们当前所在的`HtmlPage`,如下例所示: 爪哇 ``` public class CreateMessagePage { final HtmlPage currentPage; final HtmlTextInput summaryInput; final HtmlSubmitInput submit; public CreateMessagePage(HtmlPage currentPage) { this.currentPage = currentPage; this.summaryInput = currentPage.getHtmlElementById("summary"); this.submit = currentPage.getHtmlElementById("submit"); } public T createMessage(String summary, String text) throws Exception { setSummary(summary); HtmlPage result = submit.click(); boolean error = CreateMessagePage.at(result); return (T) (error ? new CreateMessagePage(result) : new ViewMessagePage(result)); } public void setSummary(String summary) throws Exception { summaryInput.setValueAttribute(summary); } public static boolean at(HtmlPage page) { return "Create Message".equals(page.getTitleText()); } } ``` Kotlin ``` class CreateMessagePage(private val currentPage: HtmlPage) { val summaryInput: HtmlTextInput = currentPage.getHtmlElementById("summary") val submit: HtmlSubmitInput = currentPage.getHtmlElementById("submit") fun createMessage(summary: String, text: String): T { setSummary(summary) val result = submit.click() val error = at(result) return (if (error) CreateMessagePage(result) else ViewMessagePage(result)) as T } fun setSummary(summary: String) { summaryInput.setValueAttribute(summary) } fun at(page: HtmlPage): Boolean { return "Create Message" == page.getTitleText() } } } ``` 以前,这种模式被称为[页面对象模式](https://github.com/SeleniumHQ/selenium/wiki/PageObjects)。虽然我们可以通过 HTMLUnit 实现这一点,但 WebDriver 提供了一些工具,我们将在下面的小节中对这些工具进行探讨,以使这种模式更容易实现。 ###### MockMVC 和 WebDriver 设置 要在 Spring MVC 测试框架中使用 Selenium WebDriver,请确保你的项目包含对`org.seleniumhq.selenium:selenium-htmlunit-driver`的测试依赖关系。 我们可以通过使用`MockMvcHtmlUnitDriverBuilder`很容易地创建与 MockMVC 集成的 Selenium WebDriver,如下例所示: 爪哇 ``` WebDriver driver; @BeforeEach void setup(WebApplicationContext context) { driver = MockMvcHtmlUnitDriverBuilder .webAppContextSetup(context) .build(); } ``` Kotlin ``` lateinit var driver: WebDriver @BeforeEach fun setup(context: WebApplicationContext) { driver = MockMvcHtmlUnitDriverBuilder .webAppContextSetup(context) .build() } ``` | |这是使用`MockMvcHtmlUnitDriverBuilder`的一个简单示例。有关更高级的
用法,请参见[advanced`MockMvcHtmlUnitDriverBuilder`](# Spring-mvc-test-server-htmlunit-webdriver-advanced-builder)。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 前面的示例确保将引用`localhost`作为服务器的任何 URL 都指向我们的`MockMvc`实例,而不需要真正的 HTTP 连接。正常情况下,通过使用网络连接来请求任何其他 URL。这让我们很容易测试 CDNS 的使用情况。 ###### MockMVC 和 WebDriver 的使用 现在,我们可以像通常那样使用 WebDriver,但不需要将我们的应用程序部署到 Servlet 容器中。例如,我们可以请求该视图创建带有以下内容的消息: 爪哇 ``` CreateMessagePage page = CreateMessagePage.to(driver); ``` Kotlin ``` val page = CreateMessagePage.to(driver) ``` 然后,我们可以填写表单并提交它来创建一条消息,如下所示: 爪哇 ``` ViewMessagePage viewMessagePage = page.createMessage(ViewMessagePage.class, expectedSummary, expectedText); ``` Kotlin ``` val viewMessagePage = page.createMessage(ViewMessagePage::class, expectedSummary, expectedText) ``` 通过利用页面对象模式,这改进了[htmlunit 测试](#spring-mvc-test-server-htmlunit-mah-usage)的设计。正如我们在[为什么是 Webdriver 和 MockMVC?](#spring-mvc-test-server-htmlunit-webdriver-why)中提到的,我们可以在 HTMLUnit 中使用 Page 对象模式,但是使用 WebDriver 要容易得多。考虑以下`CreateMessagePage`实现: 爪哇 ``` public class CreateMessagePage extends AbstractPage { (1) (2) private WebElement summary; private WebElement text; (3) @FindBy(css = "input[type=submit]") private WebElement submit; public CreateMessagePage(WebDriver driver) { super(driver); } public T createMessage(Class resultPage, String summary, String details) { this.summary.sendKeys(summary); this.text.sendKeys(details); this.submit.click(); return PageFactory.initElements(driver, resultPage); } public static CreateMessagePage to(WebDriver driver) { driver.get("http://localhost:9990/mail/messages/form"); return PageFactory.initElements(driver, CreateMessagePage.class); } } ``` |**1**|`CreateMessagePage`扩展了`AbstractPage`。我们没有讨论`AbstractPage`的详细信息,但是,总而言之,它包含了我们所有页面的通用功能。
例如,如果我们的应用程序具有导航栏、全局错误消息和其他
功能,我们可以将此逻辑放置在共享位置。| |-----|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |**2**|对于 HTML 页面中我们感兴趣的每个部分,我们都有一个成员变量
。这些是`WebElement`型。Webdriver 的[`PageFactory`](https://github.com/seleniumhq/selenium/wiki/pageFactory)让我们通过自动解析`CreateMessagePage`每个`WebElement`,从 htmlunit 版本的`CreateMessagePage`中删除大量代码。[`PageFactory#initElements(WebDriver,Class)`](https://seleniumhq.github.io/selenium/DOCS/api/java/org/openqa/selenium/support/pageFactory.html#initelements-org.openqa.selenium.webdriver-java.lang.class-)方法通过使用字段名并查找
由页面中元素的`id`或`name`自动解析每个`WebElement`。| |**3**|我们可以使用[`@FindBy`注释](https://github.com/seleniumhq/selenium/wiki/pageFactory#making-the-example-work-using-annotations)来覆盖默认的查找行为。我们的示例展示了如何使用`@FindBy`注释来使用`css`选择器(**input[type=submit]**)查找我们的 Submit 按钮。| Kotlin ``` class CreateMessagePage(private val driver: WebDriver) : AbstractPage(driver) { (1) (2) private lateinit var summary: WebElement private lateinit var text: WebElement (3) @FindBy(css = "input[type=submit]") private lateinit var submit: WebElement fun createMessage(resultPage: Class, summary: String, details: String): T { this.summary.sendKeys(summary) text.sendKeys(details) submit.click() return PageFactory.initElements(driver, resultPage) } companion object { fun to(driver: WebDriver): CreateMessagePage { driver.get("http://localhost:9990/mail/messages/form") return PageFactory.initElements(driver, CreateMessagePage::class.java) } } } ``` |**1**|`CreateMessagePage`扩展了`AbstractPage`。我们没有讨论`AbstractPage`的详细信息,但是,总而言之,它包含了我们所有页面的通用功能。
例如,如果我们的应用程序具有导航栏、全局错误消息和其他
功能,我们可以将此逻辑放置在共享位置。| |-----|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |**2**|对于 HTML 页面中我们感兴趣的每个部分,我们都有一个成员变量
。它们是`WebElement`型。Webdriver 的[`PageFactory`](https://github.com/seleniumhq/selenium/wiki/pageFactory)让我们通过自动解析`WebElement`每个`WebElement`,从 htmlunit 版本的`CreateMessagePage`中删除大量代码。[`PageFactory#initElements(WebDriver,Class)`](https://seleniumhq.github.io/selenium/DOCS/api/java/org/openqa/selenium/support/pageFactory.html#initelements-org.openqa.selenium.webdriver-java.lang.class-)方法通过使用字段名并查找
由页面中元素的`id`或`name`自动解析每个用法,请参见[advanced`MockMvcHtmlUnitDriverBuilder`](# Spring-mvc-test-server-htmlunit-webdriver-advanced-builder)。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 这确保将引用`localhost`作为服务器的任何 URL 指向我们的`MockMvc`实例,而不需要真正的 HTTP 连接。正常情况下,通过使用网络连接来请求任何其他 URL。这让我们可以很容易地测试 CDNS 的使用情况。 ###### MockMVC 和 GEB 的使用 现在,我们可以像通常那样使用 GEB,但不需要将我们的应用程序部署到 Servlet 容器中。例如,我们可以请求该视图创建带有以下内容的消息: ``` to CreateMessagePage ``` 然后,我们可以填写表单并提交它来创建一条消息,如下所示: ``` when: form.summary = expectedSummary form.text = expectedMessage submit.click(ViewMessagePage) ``` 未找到的任何未识别的方法调用或属性访问或引用都将转发到当前页对象。这删除了我们在直接使用 WebDriver 时所需的大量样板代码。 与直接使用 WebDriver 一样,通过使用页面对象模式,这改进了[htmlunit 测试](#spring-mvc-test-server-htmlunit-mah-usage)的设计。正如前面提到的,我们可以在 HTMLUnit 和 WebDriver 中使用页面对象模式,但是使用 GEB 则更容易。考虑一下我们新的基于 Groovy 的`CreateMessagePage`实现: ``` class CreateMessagePage extends Page { static url = 'messages/form' static at = { assert title == 'Messages : Create'; true } static content = { submit { $('input[type=submit]') } form { $('form') } errors(required:false) { $('label.error, .alert-error')?.text() } } } ``` 我们的`CreateMessagePage`扩展了`Page`。我们不讨论`Page`的详细信息,但总而言之,它包含了我们所有页面的通用功能。我们定义了一个可以在其中找到此页面的 URL。这让我们可以导航到该页面,如下所示: ``` to CreateMessagePage ``` 我们还有一个`at`闭包,它确定我们是否在指定的页面上。如果我们在正确的页面上,它应该返回`true`。这就是为什么我们可以断言我们在正确的页面上,如下所示: ``` then: at CreateMessagePage errors.contains('This field is required.') ``` | |我们在闭包中使用断言,这样我们就可以确定问题出在哪里
,如果我们在错误的页面上。| |---|---------------------------------------------------------------------------------------------------------------------| 接下来,我们创建一个`content`闭包,该闭包指定页面中所有感兴趣的区域。我们可以使用[jQuery-ish Navigator API](http://www.gebish.org/manual/current/#the-jquery-ish-navigator-api)来选择我们感兴趣的内容。 最后,我们可以验证新消息是否已成功创建,如下所示: ``` then: at ViewMessagePage success == 'Successfully created a new message' id date summary == expectedSummary message == expectedMessage ``` 有关如何最大限度地利用 GEB 的更多详细信息,请参见[GEB 之书](http://www.gebish.org/manual/current/)用户手册。 ### 3.8.测试客户端应用程序 你可以使用客户端测试来测试内部使用`RestTemplate`的代码。其思想是声明预期的请求并提供“存根”响应,这样你就可以专注于孤立地测试代码(也就是说,在不运行服务器的情况下)。下面的示例展示了如何做到这一点: Java ``` RestTemplate restTemplate = new RestTemplate(); MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build(); mockServer.expect(requestTo("/greeting")).andRespond(withSuccess()); // Test code that uses the above RestTemplate ... mockServer.verify(); ``` Kotlin ``` val restTemplate = RestTemplate() val mockServer = MockRestServiceServer.bindTo(restTemplate).build() mockServer.expect(requestTo("/greeting")).andRespond(withSuccess()) // Test code that uses the above RestTemplate ... mockServer.verify() ``` 在前面的示例中,`MockRestServiceServer`(客户端 REST 测试的中心类)使用自定义的`RestTemplate`配置`ClientHttpRequestFactory`,该自定义配置根据预期断言实际请求并返回“存根”响应。在这种情况下,我们期望请求`/greeting`,并希望返回带有`text/plain`内容的 200 响应。我们可以根据需要定义额外的期望请求和存根响应。当我们定义期望的请求和存根响应时,`RestTemplate`可以像往常一样在客户端代码中使用。在测试结束时,可以使用`mockServer.verify()`来验证所有的期望都已满足。 默认情况下,请求的预期顺序是声明期望的顺序。在构建服务器时,可以设置`ignoreExpectOrder`选项,在这种情况下,将检查所有期望(按顺序)以找到给定请求的匹配项。这意味着请求可以按任何顺序提出。下面的示例使用`ignoreExpectOrder`: Java ``` server = MockRestServiceServer.bindTo(restTemplate).ignoreExpectOrder(true).build(); ``` Kotlin ``` server = MockRestServiceServer.bindTo(restTemplate).ignoreExpectOrder(true).build() ``` 即使默认情况下是无序的请求,每个请求也只允许运行一次。`expect`方法提供了一个重载变量,该变量接受一个指定计数范围的`ExpectedCount`参数(例如,`once`,`manyTimes`,`max`,`min`,`between`,等等)。下面的示例使用`times`: Java ``` RestTemplate restTemplate = new RestTemplate(); MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build(); mockServer.expect(times(2), requestTo("/something")).andRespond(withSuccess()); mockServer.expect(times(3), requestTo("/somewhere")).andRespond(withSuccess()); // ... mockServer.verify(); ``` Kotlin ``` val restTemplate = RestTemplate() val mockServer = MockRestServiceServer.bindTo(restTemplate).build() mockServer.expect(times(2), requestTo("/something")).andRespond(withSuccess()) mockServer.expect(times(3), requestTo("/somewhere")).andRespond(withSuccess()) // ... mockServer.verify() ``` 请注意,当`ignoreExpectOrder`未设置(默认值),因此,请求是按照声明的顺序进行的,那么该顺序仅适用于任何预期请求中的第一个。例如,如果“/something”预期两次,然后是“/somewhere”三次,那么在向“/somewhere”发出请求之前,应该有一个对“/something”的请求,但是,除了随后的“/something”和“/somewhere”之外,请求可以随时出现。 作为上述所有方法的替代方案,客户端测试支持还提供了`ClientHttpRequestFactory`实现,你可以将其配置为`RestTemplate`,以将其绑定到`MockMvc`实例。这允许使用实际的服务器端逻辑处理请求,但不需要运行服务器。下面的示例展示了如何做到这一点: Java ``` MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); this.restTemplate = new RestTemplate(new MockMvcClientHttpRequestFactory(mockMvc)); // Test code that uses the above RestTemplate ... ``` Kotlin ``` val mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build() restTemplate = RestTemplate(MockMvcClientHttpRequestFactory(mockMvc)) // Test code that uses the above RestTemplate ... ``` #### 3.8.1.静态导入 与服务器端测试一样,客户端测试的 Fluent API 需要一些静态导入。搜索`MockRest*`很容易找到。Eclipse 用户应该在 Java Editor Content Assist Favorites 下的 Eclipse 首选项中添加`MockRestRequestMatchers.*`和`MockRestResponseCreators.*`作为“最喜欢的静态成员”。这允许在输入静态方法名的第一个字符后使用内容辅助。其他 IDE(例如 IntelliJ)可能不需要任何额外的配置。检查对静态成员的代码完成的支持。 #### 3.8.2.客户端 REST 测试的进一步示例 Spring MVC 测试自己的测试包括[示例测试](https://github.com/spring-projects/spring-framework/tree/main/spring-test/src/test/java/org/springframework/test/web/client/samples)客户端 REST 测试。 ## 4. 更多资源 有关测试的更多信息,请参见以下参考资料: * [JUnit](https://www.junit.org/):“一个程序员友好的 Java 测试框架”。 Spring 框架在其测试套件中使用,并在[Spring TestContext Framework](#testcontext-framework)中支持。 * [TestNG](https://testng.org/):受 JUnit 启发的测试框架,增加了对测试组、数据驱动测试、分布式测试和其他特性的支持。在[Spring TestContext Framework](#testcontext-framework)中支持 * [AssertJ](https://assertj.github.io/doc/):“Fluent Assertions for Java”,包括对 Java8Lambdas、Streams 和其他功能的支持。 * [模拟对象](https://en.wikipedia.org/wiki/Mock_Object):维基百科条目。 * [MockObjects.com](http://www.mockobjects.com/):专门用于模拟对象的网站,这是一种在测试驱动开发中改进代码设计的技术。 * [Mockito](https://mockito.github.io):基于[Test Spy](http://xunitpatterns.com/Test%20Spy.html)模式的 Java 模拟库。 Spring 框架在其测试套件中使用。 * [EasyMock](https://easymock.org/):Java 库“通过使用 Java 的代理机制动态生成接口的模拟对象(以及通过类扩展的对象),为接口提供模拟对象。” * [JMock](https://jmock.org/):支持使用模拟对象对 Java 代码进行测试驱动开发的库。 * [DbUnit](https://www.dbunit.org/):JUnit 扩展(也可用于 Ant 和 Maven),它的目标是数据库驱动的项目,其中包括在测试运行之间使数据库处于已知状态。 * [测试容器](https://www.testcontainers.org/):支持 JUnit 测试的 Java 库,提供通用数据库、Selenium Web 浏览器或任何其他可以在 Docker 容器中运行的轻量级一次性实例。 * [研磨机](https://sourceforge.net/projects/grinder/):Java 负载测试框架。 * [斯普林莫克](https://github.com/Ninja-Squad/springmockk):支持使用[MockK](https://mockk.io/)代替 mockito 编写的 Spring 引导集成测试。