# 测试
本章介绍 Spring 对集成测试的支持和单元测试的最佳实践。 Spring 团队提倡测试驱动开发。 Spring 团队已经发现,控制反转的正确使用确实使单元测试和集成测试变得更容易(在在类上存在 setter 方法和适当的构造函数,使得它们更容易在测试中连接在一起,而无需设置服务定位器注册中心和类似的结构)。
# 1. Spring 测试简介
测试是 Enterprise 软件开发中不可缺少的一部分。本章重点讨论了 IOC 原则对单元测试的增值,以及 Spring 框架支持集成测试的好处。(在 Enterprise 中对测试的彻底处理超出了本参考手册的范围。)
# 2. 单元测试
与传统的 爪哇 EE 开发相比,依赖注入应该使你的代码更少地依赖于容器。组成应用程序的 POJO 应该在 JUnit 或 TestNG 测试中是可测试的,使用new
操作符实例化对象,而不需要 Spring 或任何其他容器。你可以使用模拟对象(与其他有价值的测试技术一起使用)来孤立地测试你的代码。如果你遵循 Spring 的体系结构建议,那么你的代码库的干净分层和组件化将促进更容易的单元测试。例如,你可以通过截断或模拟 DAO 或存储库接口来测试服务层对象,而不需要在运行单元测试时访问持久性数据。
真正的单元测试通常运行得非常快,因为没有要设置的运行时基础设施。强调真正的单元测试作为开发方法的一部分,可以提高你的工作效率。你可能不需要测试章节的这一部分来帮助你为基于 IOC 的应用程序编写有效的单元测试。然而,对于某些单元测试场景, Spring 框架提供了模拟对象和测试支持类,这在本章中进行了描述。
# 2.1.模拟对象
Spring 包括一些专门用于嘲弄的软件包:
# 2.1.1.环境
org.springframework.mock.env
包包含Environment
和PropertySource
抽象的模拟实现(参见Bean Definition Profiles和[PropertySource
抽象](core.html#beans-property-source-abstraction))。MockEnvironment
和MockPropertySource
对于开发依赖于环境特定属性的代码的容器外测试非常有用。
# 2.1.2.JNDI
org.springframework.mock.jndi
包包含 JNDI SPI 的部分实现,你可以使用它为测试套件或独立应用程序设置一个简单的 JNDI 环境。例如,如果 JDBCDataSource
实例在测试代码中与在 爪哇 EE 容器中绑定到相同的 JNDI 名称,则可以在测试场景中重用应用程序代码和配置,而无需进行修改。
在org.springframework.mock.jndi 包中的模拟 JNDI 支持是在 Spring Framework5.2 中正式反对的,以支持来自第三方 的完整解决方案,例如SIMPLE-JNDI (opens new window)。 |
---|
# 2.1.3. Servlet 空气污染指数
org.springframework.mock.web
包包含一组完整的 API 模拟对象,这些对象对于测试 Web 上下文、控制器和过滤器非常有用。这些模拟对象针对 Spring 的 Web MVC 框架的使用,并且通常比动态模拟对象(例如EasyMock (opens new window))或可选的 Servlet API 模拟对象(例如模拟对象 (opens new window))更方便地使用。
自 Spring Framework5.0 以来,基于 Servlet 4.0API,org.springframework.mock.web 中的模拟对象是。 |
---|
Spring MVC 测试框架构建在模拟 Servlet API 对象上,以提供 Spring MVC 的集成测试框架。见MockMvc。
# 2.1.4. Spring 网络反应
org.springframework.mock.http.server.reactive
包包含用于 WebFlux 应用程序的ServerHttpRequest
和ServerHttpResponse
的模拟实现。org.springframework.mock.web.server
包包含一个 mockServerWebExchange
,它依赖于这些 mock 请求和响应对象。
MockServerHttpRequest
和MockServerHttpResponse
都从相同的抽象基类扩展为特定于服务器的实现,并与它们共享行为。例如,一旦创建了一个模拟请求,它是不可变的,但是你可以使用ServerHttpRequest
中的mutate()
方法来创建一个修改过的实例。
为了让模拟响应正确地实现写契约并返回一个写完成句柄(即Mono<Void>
),它默认情况下使用Flux
和cache().then()
,这将缓冲数据并使其可用于测试中的断言。应用程序可以设置一个自定义的写函数(例如,测试一个无限的流)。
WebTestClient构建在模拟请求和响应的基础上,为在没有 HTTP 服务器的情况下测试 WebFlux 应用程序提供支持。客户机还可以用于与正在运行的服务器进行端到端测试。
# 2.2.单元测试支持类
Spring 包括许多可以帮助单元测试的类。它们可分为两类:
# 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<gtr="435"/>对象的任何其他测试框架结合使用。
单元测试 Spring MVC 控制器 以单元测试你的 Spring MVC Controller 类作为 POJO,使用ModelAndViewAssert 与MockHttpServletRequest 结合,MockHttpSession ,从 Spring 的Servlet API mocks以此类推。对于 Spring MVC 的Spring MVC 和 REST Controller 类以及WebApplicationContext 配置,要进行彻底的集成测试,请使用Spring MVC Test Framework代替。 |
---|
# 3. 集成测试
本节(本章其余部分的大部分内容)涵盖了 Spring 应用程序的集成测试。它包括以下主题:
# 3.1.概述
重要的是能够执行一些集成测试,而不需要部署到应用程序服务器或连接到其他 Enterprise 基础设施。这样做可以让你测试以下内容:
你的 Spring IOC 容器上下文的正确接线。
使用 JDBC 或 ORM 工具进行数据访问。这可以包括诸如 SQL 语句的正确性、 Hibernate 查询、 JPA 实体映射等等。
Spring 框架为spring-test
模块中的集成测试提供了一流的支持。实际 jar 文件的名称可能包括发布版本,也可能是长org.springframework.test
格式,这取决于你从哪里获得它(有关解释,请参见抚养管理一节)。这个库包括org.springframework.test
包,其中包含用于使用 Spring 容器进行集成测试的有价值的类。此测试不依赖于应用程序服务器或其他部署环境。这样的测试比单元测试运行得慢,但比同等的 Selenium 测试或依赖于部署到应用程序服务器的远程测试快得多。
单元和集成测试支持是以注释驱动的Spring TestContext Framework的形式提供的。TestContext 框架与实际使用的测试框架无关,该框架允许在各种环境中测试,包括 JUnit、TestNG 和其他环境。
# 3.2.集成测试的目标
Spring 的集成测试支持具有以下主要目标:
管理测试之间的Spring IoC container caching。
提供测试夹具实例的依赖注入。
提供适合于集成测试的事务管理。
提供Spring-specific base 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 框架的上下文管理和上下文缓存。
# 3.2.2.测试夹具的依赖注入
当 TestContext 框架加载应用程序上下文时,它可以通过使用依赖项注入来配置测试类的实例。这提供了一种方便的机制,可以通过使用应用程序上下文中预先配置的 bean 来设置测试装置。这里的一个很大的好处是,你可以跨各种测试场景重用应用程序上下文(例如,用于配置 Spring-托管对象图、事务代理、DataSource
实例和其他),从而避免了为单个测试用例重复复杂的测试固定设置的需要。
例如,考虑一个场景,其中我们有一个类(HibernateTitleRepository
),它实现了Title
域实体的数据访问逻辑。我们希望编写测试以下领域的集成测试:
Spring 配置:基本上,与
HibernateTitleRepository
Bean 的配置相关的一切都是正确的和存在的吗?Hibernate 映射文件配置:是否所有映射都正确,以及是否存在正确的延迟加载设置?
HibernateTitleRepository
的逻辑:这个类的配置实例是否如预期的那样执行?
参见使用TestContext 框架的测试固定件的依赖注入。
# 3.2.3.事务管理
在访问真实数据库的测试中,一个常见的问题是它们对持久性存储状态的影响。即使在使用开发数据库时,对状态的更改也可能会影响将来的测试。此外,许多操作(例如插入或修改持久数据)无法在事务之外执行(或验证)。
TestContext 框架解决了这个问题。默认情况下,框架为每个测试创建并回滚一个事务。你可以编写可以假设存在事务的代码。如果你在测试中调用事务性代理对象,那么根据它们配置的事务语义,它们的行为是正确的。此外,如果一个测试方法在为测试而管理的事务中运行时删除了选定的表的内容,则默认情况下事务会回滚,并且数据库会返回到执行测试之前的状态。通过使用在测试的应用程序上下文中定义的PlatformTransactionManager
Bean 向测试提供事务支持。
如果你想要提交一个事务(这是不寻常的,但在你想要填充或修改数据库的特定测试时偶尔会有用),那么你可以通过使用[@Commit
](#Integration-Testing-Annotations)注释,告诉 TestContext 框架使事务提交,而不是回滚。
参见事务管理TestContext 框架。
# 3.2.4.集成测试的支持类
Spring TestContext 框架提供了几个abstract
支持类,这些类简化了集成测试的编写。这些基本测试类为测试框架提供了定义良好的挂钩,以及方便的实例变量和方法,使你能够访问:
ApplicationContext
,用于执行显式 Bean 查找或测试整个上下文的状态。a
JdbcTemplate
,用于执行查询数据库的 SQL 语句。可以使用这样的查询来确认与数据库相关的应用程序代码执行之前和之后的数据库状态,并且 Spring 确保这样的查询在与应用程序代码相同的事务范围内运行。当与 ORM 工具一起使用时,请务必避免误报。
此外,你可能希望创建你自己的定制的、应用程序范围的超类,其中包含特定于你的项目的实例变量和方法。
参见TestContext 框架的支持类。
# 3.3.JDBC 测试支持
org.springframework.test.jdbc
包包含JdbcTestUtils
,这是与 JDBC 相关的实用程序函数的集合,旨在简化标准数据库测试场景。具体地说,JdbcTestUtils
提供了以下静态实用方法。
countRowsInTable(..)
:计算给定表中的行数。countRowsInTableWhere(..)
:使用提供的WHERE
子句计算给定表中的行数。deleteFromTables(..)
:从指定的表中删除所有行。deleteFromTableWhere(..)
:使用提供的WHERE
子句从给定表中删除行。dropTables(..)
:删除指定的表。
[AbstractTransactionalJUnit4SpringContextTests ](#testcontext-support-classes-junit4)和[AbstractTransactionalTestNGSpringContextTests ](#testcontext-support-classes-testng)提供了方便的方法,这些方法在JdbcTestUtils 中委托给上述方法。spring-jdbc 模块提供了对配置和启动嵌入式数据库的支持,你可以在与数据库交互的集成测试中使用该数据库。 |
---|
# 3.4.注解
本节介绍了在测试 Spring 应用程序时可以使用的注释。它包括以下主题:
# 3.4.1. Spring 测试注释
Spring 框架提供了以下一组 Spring 特定的注释,你可以在与 TestContext 框架结合的单元和集成测试中使用这些注释。有关更多信息,请参见相应的 爪哇doc,包括默认属性值、属性别名和其他详细信息。
Spring 的测试注释包括以下内容:
[
@BootstrapWith
](# Spring-testing-annotation-bootstrapwith)[
@ContextConfiguration
](# Spring-testing-annotation-contextconfiguration)[
@WebAppConfiguration
](# Spring-testing-annotation-webappconfiguration)[
@ContextHierarchy
](# Spring-testing-annotation-contexthierarchy)[
@ActiveProfiles
](# Spring-testing-annotation-ActiveProfiles)[
@TestPropertySource
](# Spring-testing-annotation-testPropertySource)[
@DynamicPropertySource
](# Spring-testing-annotation-dynamicpropertysource)[
@DirtiesContext
](# Spring-testing-annotation-dirtiescontext)[
@TestExecutionListeners
](# Spring-testing-annotation-testexecutionlisteners)[
@RecordApplicationEvents
](# Spring-testing-annotation-recordapplicationEvents)[
@Commit
](# Spring-testing-annotation-commit)[
@Rollback
](# Spring-testing-annotation-rollback)[
@BeforeTransaction
](# Spring-testing-annotation-beforeTransaction)[
@AfterTransaction
](# Spring-testing-annotation-aftertransaction)[
@Sql
](# Spring-testing-annotation-sql)[
@SqlConfig
](# Spring-testing-annotation-sqlconfig)[
@SqlMergeMode
](# Spring-testing-annotation-sqlmergemode)[
@SqlGroup
](# Spring-testing-annotation-sqlgroup)
# @BootstrapWith
@BootstrapWith
是一个类级注释,你可以使用它来配置如何引导 Spring TestContext 框架。具体地说,你可以使用@BootstrapWith
来指定自定义的TestContextBootstrapper
。有关更多详细信息,请参见引导 TestContext 框架一节。
# @ContextConfiguration
@ContextConfiguration
定义了类级元数据,用于确定如何加载和配置用于集成测试的ApplicationContext
。具体地说,@ContextConfiguration
声明用于加载上下文的应用程序上下文资源locations
或组件classes
。
资源位置通常是位于 Classpath 中的 XML 配置文件或 Groovy 脚本,而组件类通常是@Configuration
类。然而,资源位置也可以引用文件系统中的文件和脚本,并且组件类可以是@Component
类、@Service
类等等。有关更多详细信息,请参见组件类。
下面的示例显示了引用 XML 文件的@ContextConfiguration
注释:
爪哇
@ContextConfiguration("/test-config.xml") (1)
class XmlApplicationContextTests {
// class body...
}
1 | 指一个 XML 文件。 |
---|
Kotlin
@ContextConfiguration("/test-config.xml") (1)
class XmlApplicationContextTests {
// class body...
}
1 | 指一个 XML 文件。 |
---|
下面的示例显示了引用类的@ContextConfiguration
注释:
爪哇
@ContextConfiguration(classes = TestConfig.class) (1)
class ConfigClassApplicationContextTests {
// class body...
}
1 | 指的是一门课。 |
---|
Kotlin
@ContextConfiguration(classes = [TestConfig::class]) (1)
class ConfigClassApplicationContextTests {
// class body...
}
1 | 指的是一门课。 |
---|
作为一种替代方法,或者除了声明资源位置或组件类之外,还可以使用@ContextConfiguration
声明ApplicationContextInitializer
类。下面的示例展示了这样一个案例:
爪哇
@ContextConfiguration(initializers = CustomContextIntializer.class) (1)
class ContextInitializerTests {
// class body...
}
1 | 声明初始化程序类。 |
---|
Kotlin
@ContextConfiguration(initializers = [CustomContextIntializer::class]) (1)
class ContextInitializerTests {
// class body...
}
1 | 声明初始化程序类。 |
---|
你也可以选择使用@ContextConfiguration
来声明ContextLoader
策略。但是,请注意,你通常不需要显式地配置加载器,因为默认加载器支持initializers
和资源locations
或组件classes
。
下面的示例既使用了位置,也使用了加载器:
爪哇
@ContextConfiguration(locations = "/test-context.xml", loader = CustomContextLoader.class) (1)
class CustomLoaderXmlApplicationContextTests {
// class body...
}
1 | 配置位置和自定义加载器。 |
---|
Kotlin
@ContextConfiguration("/test-context.xml", loader = CustomContextLoader::class) (1)
class CustomLoaderXmlApplicationContextTests {
// class body...
}
1 | 配置位置和自定义加载器。 |
---|
@ContextConfiguration 提供了对继承资源位置或配置类的支持,以及由超类 声明的上下文初始化器或包含类。 |
---|
请参阅上下文管理,[@Nested
测试类配置](#testcontext-junit-jupiter-nested-test-configuration),以及@ContextConfiguration
爪哇docs 以获取更多详细信息。
# @WebAppConfiguration
@WebAppConfiguration
是一个类级注释,你可以使用它来声明为集成测试加载的ApplicationContext
应该是WebApplicationContext
。在测试类上仅存在@WebAppConfiguration
就可以确保为测试加载WebApplicationContext
,使用缺省值"file:src/main/webapp"
作为 Web 应用程序的根路径(即资源基路径)。后台使用资源库路径创建MockServletContext
,它是测试的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
属性提供相同的值,显式地命名该级别。参见上下文层次结构和[@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 定义配置文件。 |
---|
参见具有环境配置文件的上下文配置,[@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 属性。 |
---|
有关示例和更多详细信息,请参见具有测试属性源的上下文配置。
# @DynamicPropertySource
@DynamicPropertySource
是一种方法级别的注释,你可以使用它来注册要添加到PropertySources
中的ApplicationContext
集合中的 dynamicproperties,用于为集成测试加载ApplicationContext
。当你不知道属性的初始值时,动态属性是有用的——例如,如果属性是由外部资源管理的,比如由测试容器 (opens new window)项目管理的容器。
下面的示例演示了如何注册动态属性:
爪哇
@ContextConfiguration
class MyIntegrationTests {
static MyExternalServer server = // ...
@DynamicPropertySource (1)
static void dynamicProperties(DynamicPropertyRegistry registry) { (2)
registry.add("server.port", server::getPort); (3)
}
// tests ...
}
1 | 用@DynamicPropertySource 注释static 方法。 |
---|---|
2 | 接受DynamicPropertyRegistry 作为参数。 |
3 | 注册一个动态server.port 属性,以便从服务器上懒洋洋地检索。 |
Kotlin
@ContextConfiguration
class MyIntegrationTests {
companion object {
@JvmStatic
val server: MyExternalServer = // ...
@DynamicPropertySource (1)
@JvmStatic
fun dynamicProperties(registry: DynamicPropertyRegistry) { (2)
registry.add("server.port", server::getPort) (3)
}
}
// tests ...
}
1 | 用@DynamicPropertySource 注释static 方法。 |
---|---|
2 | 接受DynamicPropertyRegistry 作为参数。 |
3 | 注册一个动态server.port 属性,以便从服务器上懒洋洋地检索。 |
有关更多详细信息,请参见具有动态属性源的上下文配置。
# @DirtiesContext
@DirtiesContext
表示底层 Spring ApplicationContext
在测试的执行过程中被弄脏了(也就是说,测试以某种方式修改或损坏了它——例如,通过更改单例 Bean 的状态),并且应该关闭。当一个应用程序上下文被标记为 dirty 时,它将从测试框架的缓存中删除并关闭。因此,对于需要具有相同配置元数据的上下文的任何后续测试,都会重新构建基础 Spring 容器。
可以在同一个类或类层次结构中同时使用@DirtiesContext
作为类级和方法级的注释。在这种情况下,根据配置的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 访问记录的事件。
参见应用程序事件和[@RecordApplicationEvents
爪哇doc](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/test/context/event/recordapplicationevents.html)以获取示例和更多详细信息。
# @Commit
@Commit
表示事务性测试方法的事务应在测试方法完成后提交。你可以使用@Commit
作为@Rollback(false)
的直接替换,以更明确地传达代码的意图。类似于@Rollback
,@Commit
也可以声明为类级或方法级注释。
下面的示例展示了如何使用@Commit
注释:
爪哇
@Commit (1)
@Test
void testProcessWithoutRollback() {
// ...
}
1 | 将测试结果提交给数据库。 |
---|
Kotlin
@Commit (1)
@Test
fun testProcessWithoutRollback() {
// ...
}
1 | 将测试结果提交给数据库。 |
---|
# @Rollback
@Rollback
指示在测试方法完成后是否应该回滚事务测试方法的事务。如果true
,则回滚事务。否则,事务将被提交(参见[@Commit
](# Spring-testing-annotation-commit))。 Spring TestContext 框架中集成测试的回滚默认为true
,即使没有显式声明@Rollback
。
当声明为类级注释时,@Rollback
为测试类层次结构中的所有测试方法定义了默认的回滚语义。当声明为方法级别的注释时,@Rollback
为特定的测试方法定义了回滚语义,可能会覆盖类级别的@Rollback
或@Commit
语义。
下面的示例导致测试方法的结果不会被回滚(即,结果被提交到数据库中):
爪哇
@Rollback(false) (1)
@Test
void testProcessWithoutRollback() {
// ...
}
1 | 不要回滚结果。 |
---|
Kotlin
@Rollback(false) (1)
@Test
fun testProcessWithoutRollback() {
// ...
}
1 | 不要回滚结果。 |
---|
# @BeforeTransaction
@BeforeTransaction
表示对于已通过使用 Spring 的@Transactional
注释配置为在事务中运行的测试方法,在事务启动之前应该运行带注释的void
方法。@BeforeTransaction
方法不需要是public
,并且可以在基于 爪哇8 的接口默认方法上声明。
下面的示例展示了如何使用@BeforeTransaction
注释:
爪哇
@BeforeTransaction (1)
void beforeTransaction() {
// logic to be run before a transaction is started
}
1 | 在事务之前运行此方法。 |
---|
Kotlin
@BeforeTransaction (1)
fun beforeTransaction() {
// logic to be run before a transaction is started
}
1 | 在事务之前运行此方法。 |
---|
# @AfterTransaction
@AfterTransaction
表示在事务结束后应该运行带注释的void
方法,用于通过使用 Spring 的@Transactional
注释配置为在事务中运行的测试方法。@AfterTransaction
方法不需要是public
,并且可以在基于 爪哇8 的接口默认方法上声明。
爪哇
@AfterTransaction (1)
void afterTransaction() {
// logic to be run after a transaction has ended
}
1 | 在事务之后运行此方法。 |
---|
Kotlin
@AfterTransaction (1)
fun afterTransaction() {
// logic to be run after a transaction has ended
}
1 | 在事务之后运行此方法。 |
---|
# @Sql
@Sql
用于对测试类或测试方法进行注释,以配置在集成测试期间针对给定数据库运行的 SQL 脚本。下面的示例展示了如何使用它:
爪哇
@Test
@Sql({"/test-schema.sql", "/test-user-data.sql"}) (1)
void userTest() {
// run code that relies on the test schema and test data
}
1 | 为此测试运行两个脚本。 |
---|
Kotlin
@Test
@Sql("/test-schema.sql", "/test-user-data.sql") (1)
fun userTest() {
// run code that relies on the test schema and test data
}
1 | 为此测试运行两个脚本。 |
---|
有关更多详细信息,请参见使用 @sql 声明式执行 SQL 脚本。
# @SqlConfig
@SqlConfig
定义了元数据,用于确定如何解析和运行配置有@Sql
注释的 SQL 脚本。下面的示例展示了如何使用它:
爪哇
@Test
@Sql(
scripts = "/test-user-data.sql",
config = @SqlConfig(commentPrefix = "`", separator = "@@") (1)
)
void userTest() {
// run code that relies on the test data
}
1 | 在 SQL 脚本中设置注释前缀和分隔符。 |
---|
Kotlin
@Test
@Sql("/test-user-data.sql", config = SqlConfig(commentPrefix = "`", separator = "@@")) (1)
fun userTest() {
// run code that relies on the test data
}
1 | 在 SQL 脚本中设置注释前缀和分隔符。 |
---|
# @SqlMergeMode
@SqlMergeMode
用于对测试类或测试方法进行注释,以配置方法级@Sql
声明是否与类级@Sql
声明合并。如果@SqlMergeMode
未在测试类或测试方法上声明,则默认情况下将使用OVERRIDE
合并模式。在OVERRIDE
模式下,方法级别@Sql
声明将有效地覆盖类级别@Sql
声明。
请注意,方法级别@SqlMergeMode
声明覆盖了类级别声明。
下面的示例展示了如何在类级别上使用@SqlMergeMode
。
爪哇
@SpringJUnitConfig(TestConfig.class)
@Sql("/test-schema.sql")
@SqlMergeMode(MERGE) (1)
class UserTests {
@Test
@Sql("/user-test-data-001.sql")
void standardUserProfile() {
// run code that relies on test data set 001
}
}
1 | 对于类中的所有测试方法,将@Sql 合并模式设置为MERGE 。 |
---|
Kotlin
@SpringJUnitConfig(TestConfig::class)
@Sql("/test-schema.sql")
@SqlMergeMode(MERGE) (1)
class UserTests {
@Test
@Sql("/user-test-data-001.sql")
fun standardUserProfile() {
// run code that relies on test data set 001
}
}
1 | 对于类中的所有测试方法,将@Sql 合并模式设置为MERGE 。 |
---|
下面的示例展示了如何在方法级别上使用@SqlMergeMode
。
Java
@SpringJUnitConfig(TestConfig.class)
@Sql("/test-schema.sql")
class UserTests {
@Test
@Sql("/user-test-data-001.sql")
@SqlMergeMode(MERGE) (1)
void standardUserProfile() {
// run code that relies on test data set 001
}
}
1 | 为特定的测试方法将@Sql 合并模式设置为MERGE 。 |
---|
Kotlin
@SpringJUnitConfig(TestConfig::class)
@Sql("/test-schema.sql")
class UserTests {
@Test
@Sql("/user-test-data-001.sql")
@SqlMergeMode(MERGE) (1)
fun standardUserProfile() {
// run code that relies on test data set 001
}
}
1 | 为特定的测试方法将@Sql 合并模式设置为MERGE 。 |
---|
# @SqlGroup
@SqlGroup
是一个容器注释,它聚合了几个@Sql
注释。你可以本地使用@SqlGroup
来声明几个嵌套的@Sql
注释,或者可以将其与 Java8 对可重复注释的支持结合使用,其中@Sql
可以在同一个类或方法上声明几次,隐式地生成这个容器注释。下面的示例展示了如何声明 SQL 组:
Java
@Test
@SqlGroup({ (1)
@Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`")),
@Sql("/test-user-data.sql")
)}
void userTest() {
// run code that uses the test schema and test data
}
1 | 声明一组 SQL 脚本。 |
---|
Kotlin
@Test
@SqlGroup( (1)
Sql("/test-schema.sql", config = SqlConfig(commentPrefix = "`")),
Sql("/test-user-data.sql"))
fun userTest() {
// run code that uses the test schema and test data
}
1 | 声明一组 SQL 脚本。 |
---|
# 3.4.2.标准注释支持
Spring TestContext 框架的所有配置都使用标准语义支持以下注释。请注意,这些注释不是特定于测试的,并且可以在 Spring 框架中的任何地方使用。
@Autowired
@Qualifier
@Value
@Resource
(javax.annotation)如果存在 JSR-250@ManagedBean
(javax.annotation)如果存在 JSR-250@Inject
如果存在 JSR-330@Named
如果存在 JSR-330@PersistenceContext
(javax.persistence)如果存在 JPA@PersistenceUnit
(javax.persistence)如果存在 JPA@Required
@Transactional
with有限的属性支持
JSR-250 生命周期注释 在 Spring TestContext 框架中,可以在 @PostConstruct 和@PreDestroy 中配置的任何应用程序组件上使用标准语义。 但是,在实际的测试类中,这些生命周期注释的使用是有限的。 如果测试类中的方法使用 @PostConstruct 进行注释,则该方法在底层测试框架的任何方法之前运行(例如,方法 使用 JUnit Jupiter 的 @BeforeEach 进行注释),这适用于测试类中的每个测试方法。另一方面,如果测试类中的方法被注释为 @PreDestroy ,则该方法永远不会运行。因此,在测试类中,我们建议使用来自底层测试框架的测试生命周期回调,而不是 @PostConstruct 和@PreDestroy 。 |
---|
# 3.4.3. Spring JUnit4 测试注释
以下注释仅在与Springrunner、Spring’s JUnit 4 rules或Spring’s JUnit 4 support classes结合使用时才受支持:
[
@IfProfileValue
](#Integration-Testing-Annotations-JUnit4-IFProfileValue)[
@ProfileValueSourceConfiguration
](#Integration-Testing-Annotations-JUnit4-ProfilEvaluesourceConfiguration)[
@Timed
](#Integration-Testing-Annotations-JUnit4-Timed)[
@Repeat
](#Integration-Testing-Annotations-JUnit4-Repeat)
# @IfProfileValue
@IfProfileValue
表示针对特定的测试环境启用了带注释的测试。如果配置的ProfileValueSource
返回所提供的value
的匹配value
,则启用测试。否则,该测试将被禁用,并实际上被忽略。
你可以在类级别、方法级别或两者都应用@IfProfileValue
。对于该类或其子类中的任何方法,@IfProfileValue
的类级用法优先于方法级用法。具体地说,如果在类级别和方法级别都启用了测试,则将启用该测试。缺少@IfProfileValue
意味着隐式启用了测试。这类似于 JUnit4 的@Ignore
注释的语义,只是@Ignore
的存在总是禁用测试。
下面的示例显示了一个带有@IfProfileValue
注释的测试:
Java
@IfProfileValue(name="java.vendor", value="Oracle Corporation") (1)
@Test
public void testProcessWhichRunsOnlyOnOracleJvm() {
// some logic that should run only on Java VMs from Oracle Corporation
}
1 | 仅当 Java 供应商“甲骨文股份有限公司”时才运行此测试。 |
---|
Kotlin
@IfProfileValue(name="java.vendor", value="Oracle Corporation") (1)
@Test
fun testProcessWhichRunsOnlyOnOracleJvm() {
// some logic that should run only on Java VMs from Oracle Corporation
}
1 | 仅当 Java 供应商“甲骨文股份有限公司”时才运行此测试。 |
---|
或者,你可以使用@IfProfileValue
的列表配置values
(带有OR
语义),以在 JUnit4 环境中实现对测试组的类似 TestNG 的支持。考虑以下示例:
Java
@IfProfileValue(name="test-groups", values={"unit-tests", "integration-tests"}) (1)
@Test
public void testProcessWhichRunsForUnitOrIntegrationTestGroups() {
// some logic that should run only for unit and integration test groups
}
1 | 为单元测试和集成测试运行此测试。 |
---|
Kotlin
@IfProfileValue(name="test-groups", values=["unit-tests", "integration-tests"]) (1)
@Test
fun testProcessWhichRunsForUnitOrIntegrationTestGroups() {
// some logic that should run only for unit and integration test groups
}
1 | 为单元测试和集成测试运行此测试。 |
---|
# @ProfileValueSourceConfiguration
@ProfileValueSourceConfiguration
是一个类级注释,它指定在检索通过@IfProfileValue
注释配置的配置文件值时要使用的ProfileValueSource
类型。如果@ProfileValueSourceConfiguration
未声明用于测试,则默认情况下使用SystemProfileValueSource
。下面的示例展示了如何使用@ProfileValueSourceConfiguration
:
Java
@ProfileValueSourceConfiguration(CustomProfileValueSource.class) (1)
public class CustomProfileValueSourceTests {
// class body...
}
1 | 使用自定义配置文件的值源。 |
---|
Kotlin
@ProfileValueSourceConfiguration(CustomProfileValueSource::class) (1)
class CustomProfileValueSourceTests {
// class body...
}
1 | 使用自定义配置文件的值源。 |
---|
# @Timed
@Timed
表示带注释的测试方法必须在指定的时间内(以毫秒为单位)完成执行。如果文本执行时间超过了指定的时间段,则测试失败。
该时间段包括测试方法本身的运行、测试的任何重复(参见@Repeat
),以及测试夹具的任何设置或拆除。下面的示例展示了如何使用它:
Java
@Timed(millis = 1000) (1)
public void testProcessWithOneSecondTimeout() {
// some logic that should not take longer than 1 second to run
}
1 | 将测试的时间段设置为一秒。 |
---|
Kotlin
@Timed(millis = 1000) (1)
fun testProcessWithOneSecondTimeout() {
// some logic that should not take longer than 1 second to run
}
1 | 将测试的时间段设置为一秒。 |
---|
Spring 的@Timed
注释与 JUnit4 的@Test(timeout=…)
支持具有不同的语义。具体地说,由于 JUnit4 处理测试执行超时的方式(即通过在单独的Thread
中执行测试方法),如果测试时间过长,@Test(timeout=…)
会抢先失败测试。 Spring 的@Timed
,在另一方面,不会先发制人地使测试失败,而是等待测试完成后再失败。
# @Repeat
@Repeat
表示带注释的测试方法必须重复运行。在注释中指定了要运行测试方法的次数。
要重复的执行范围包括测试方法本身的执行以及测试夹具的任何设置或拆除。当与[SpringMethodRule
](#TestContext-JUnit4-Rules)一起使用时,范围还包括通过TestExecutionListener
实现准备测试实例。下面的示例展示了如何使用@Repeat
注释:
Java
@Repeat(10) (1)
@Test
public void testProcessRepeatedly() {
// ...
}
1 | 把这个测验重复十次。 |
---|
Kotlin
@Repeat(10) (1)
@Test
fun testProcessRepeatedly() {
// ...
}
1 | 把这个测验重复十次。 |
---|
# 3.4.4. Spring Junit Jupiter 测试注释
当与[SpringExtension
](#TestContext-JUnit-Jupiter-Extension)和 JUnit Jupiter(即 JUnit5 中的编程模型)结合使用时,支持以下注释:
[
@SpringJUnitConfig
](#Integration-Testing-Annotations-JUnit-Jupiter-SpringJunitConfig)[
@SpringJUnitWebConfig
](#Integration-Testing-Annotations-JUnit-Jupiter-SpringJunitWebConfig)[
@TestConstructor
](#Integration-Testing-Annotations-TestConstructor)[
@NestedTestConfiguration
](#Integration-Testing-Annotations-NestedTestConfiguration)[
@EnabledIf
](#Integration-Testing-Annotations-JUnit-Jupiter-EnableDIF)[
@DisabledIf
](#Integration-Testing-Annotations-JUnit-Jupiter-DisableDIF)
# @SpringJUnitConfig
@SpringJUnitConfig
是一个组合注释,它结合了来自 JUnit Jupiter 的@ExtendWith(SpringExtension.class)
和来自 Spring TestContext 框架的@ContextConfiguration
。它可以在类级别上用作@ContextConfiguration
的插入替换。关于配置选项,@ContextConfiguration
和@SpringJUnitConfig
之间的唯一区别是,组件类可以在@SpringJUnitConfig
中使用value
属性声明。
下面的示例展示了如何使用@SpringJUnitConfig
注释来指定一个配置类:
Java
@SpringJUnitConfig(TestConfig.class) (1)
class ConfigurationClassJUnitJupiterSpringTests {
// class body...
}
1 | 指定配置类。 |
---|
Kotlin
@SpringJUnitConfig(TestConfig::class) (1)
class ConfigurationClassJUnitJupiterSpringTests {
// class body...
}
1 | 指定配置类。 |
---|
下面的示例展示了如何使用@SpringJUnitConfig
注释来指定配置文件的位置:
Java
@SpringJUnitConfig(locations = "/test-config.xml") (1)
class XmlJUnitJupiterSpringTests {
// class body...
}
1 | 指定配置文件的位置。 |
---|
Kotlin
@SpringJUnitConfig(locations = ["/test-config.xml"]) (1)
class XmlJUnitJupiterSpringTests {
// class body...
}
1 | 指定配置文件的位置。 |
---|
参见上下文管理以及[@SpringJUnitConfig
]的 Javadoc(https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/test/context/junit/junitconfig.html)和@ContextConfiguration
以获取更多详细信息。
# @SpringJUnitWebConfig
@SpringJUnitWebConfig
是一个组合注释,它结合了来自 JUnit Jupiter 的@ExtendWith(SpringExtension.class)
和来自 Spring TestContext 框架的@WebAppConfiguration
。你可以在类级别上使用它作为@ContextConfiguration
和@WebAppConfiguration
的插入替换。关于配置选项,@ContextConfiguration
和@SpringJUnitWebConfig
之间的唯一区别是,你可以使用value
中的value
属性来声明组件类。此外,只需在@SpringJUnitWebConfig
中使用resourcePath
属性,就可以覆盖value
中的value
属性。
下面的示例展示了如何使用@SpringJUnitWebConfig
注释来指定一个配置类:
Java
@SpringJUnitWebConfig(TestConfig.class) (1)
class ConfigurationClassJUnitJupiterSpringWebTests {
// class body...
}
1 | 指定配置类。 |
---|
Kotlin
@SpringJUnitWebConfig(TestConfig::class) (1)
class ConfigurationClassJUnitJupiterSpringWebTests {
// class body...
}
1 | 指定配置类。 |
---|
下面的示例展示了如何使用@SpringJUnitWebConfig
注释来指定配置文件的位置:
Java
@SpringJUnitWebConfig(locations = "/test-config.xml") (1)
class XmlJUnitJupiterSpringWebTests {
// class body...
}
1 | 指定配置文件的位置。 |
---|
Kotlin
@SpringJUnitWebConfig(locations = ["/test-config.xml"]) (1)
class XmlJUnitJupiterSpringWebTests {
// class body...
}
1 | 指定配置文件的位置。 |
---|
参见上下文管理以及[@SpringJUnitWebConfig
](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/SpringFramework/test/context/junit/junitwebconfig.html)的 Javadoc,[<@ContextConfiguration
](https:///DOCS. Spring.io/ Spring-framework/ Spring-framework/javadoc/javadoc/juntextframework/org/juntextframework/cr/
# @TestConstructor
@TestConstructor
是一种类型级别的注释,用于配置如何从测试的ApplicationContext
中的组件自动连接测试类构造函数的参数。
如果@TestConstructor
在测试类上不存在或元存在,则将使用默认的测试构造函数 AutoWire 模式。有关如何更改默认模式的详细信息,请参见下面的技巧。但是,请注意,构造函数上的@Autowired
的本地声明优先于@TestConstructor
和默认模式。
更改默认的测试构造函数 AutoWire 模式 可以通过将 spring.test.constructor.autowire.mode JVM 系统属性设置为all 来更改默认的测试构造函数 AutoWire 模式。或者,默认模式也可以通过[ SpringProperties ](Appendix.html#Appendix- Spring-Properties)机制设置。自 Spring Framework5.3 起,默认模式也可以配置为JUnit 平台配置参数 (opens new window)。 如果不设置 spring.test.constructor.autowire.mode 属性,则不会自动连接构造函数。 |
---|
在 Spring Framework5.2 中,@TestConstructor 只支持与结合使用的 SpringExtension 。请注意,SpringExtension 是通常会自动为你注册–例如,当使用诸如 @SpringJUnitConfig 和@SpringJUnitWebConfig 之类的注释或来自Spring 引导测试的各种与测试相关的注释时。 |
---|
# @NestedTestConfiguration
@NestedTestConfiguration
是一种类型级别的注释,用于配置 Spring 测试配置注释在为内部测试类封装的类层次结构中的处理方式。
如果@NestedTestConfiguration
在测试类上不存在或不存在元存在,则在其超级类型层次结构中或在其封闭的类层次结构中,将使用默认的封闭配置继承模式。有关如何更改默认模式的详细信息,请参见下面的技巧。
更改默认的封闭配置继承模式 默认的封闭配置继承模式是 INHERIT ,但是可以通过将@ContextConfiguration JVM 系统属性设置为OVERRIDE 来更改。或者,可以通过[SpringProperties ](Appendix.html#Appendix- Spring-Properties)机制设置默认模式。 |
---|
Spring TestContext Framework为下面的注释提供了@NestedTestConfiguration
语义。
[
@BootstrapWith
](# Spring-testing-annotation-bootstrapwith)[
@ContextConfiguration
](# Spring-testing-annotation-contextconfiguration)[
@WebAppConfiguration
](# Spring-testing-annotation-webappconfiguration)[
@ContextHierarchy
](# Spring-testing-annotation-contexthierarchy)[
@ActiveProfiles
](# Spring-测试-注释-ActiveProfiles)[
@TestPropertySource
](# Spring-testing-annotation-testPropertySource)[
@DynamicPropertySource
](# Spring-testing-annotation-dynamicpropertysource)[
@DirtiesContext
](# Spring-testing-annotation-dirtiescontext)[
@TestExecutionListeners
](# Spring-testing-annotation-testexecutionlisteners)[
@RecordApplicationEvents
](# Spring-testing-annotation-recordapplicationEvents)[
@Transactional
][
@Commit
](# Spring-testing-annotation-commit)[
@Rollback
](# Spring-testing-annotation-rollback)[
@Sql
](# Spring-testing-annotation-sql)[
@SqlConfig
](# Spring-testing-annotation-sqlconfig)[
@SqlMergeMode
](# Spring-testing-annotation-sqlmergemode)[
@TestConstructor
](#Integration-Testing-Annotations-TestConstructor)
在 JUnit Jupiter 中,使用@NestedTestConfiguration 通常仅在与 @Nested 测试类结合时才有意义;但是,可能存在支持 Spring 的其他测试框架和使用这种 注释的嵌套测试类。 |
---|
参见[@Nested
测试类配置](#testcontext-junit-jupiter-nested-test-configuration)以获得示例和更多详细信息。
# @EnabledIf
@EnabledIf
用于表示启用了注释的 JUnit Jupiter 测试类或测试方法,并且如果提供的expression
计算为true
,则应该运行该方法。具体地说,如果表达式的求值为Boolean.TRUE
或String
等于true
(忽略情况),则启用测试。当应用于类级别时,该类中的所有测试方法在默认情况下也会自动启用。
表达式可以是以下任何一种:
Spring Expression Language表达式。例如:
@EnabledIf("#{systemProperties['os.name'].toLowerCase().contains('mac')}")
在 Spring[
Environment
](core.html#beans-environment)中可用的属性的占位符。例如:@EnabledIf("${smoke.tests.enabled}")
文字文字。例如:
@EnabledIf("true")
然而,请注意,不是动态解析属性占位符的结果的文本文字是零实用价值的,因为@EnabledIf("false")
等价于@Disabled
和@EnabledIf("true")
在逻辑上是没有意义的。
你可以使用@EnabledIf
作为元注释来创建自定义组合注释。例如,你可以创建一个自定义@EnabledOnMac
注释,如下所示:
爪哇
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@EnabledIf(
expression = "#{systemProperties['os.name'].toLowerCase().contains('mac')}",
reason = "Enabled on Mac OS"
)
public @interface EnabledOnMac {}
Kotlin
@Target(AnnotationTarget.TYPE, AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@EnabledIf(
expression = "#{systemProperties['os.name'].toLowerCase().contains('mac')}",
reason = "Enabled on Mac OS"
)
annotation class EnabledOnMac {}
# @DisabledIf
@DisabledIf
用于表示注释的 JUnit Jupiter 测试类或测试方法已禁用,并且如果提供的expression
计算为true
,则不应运行该测试方法。具体地说,如果表达式的求值为Boolean.TRUE
或String
等于true
(忽略情况),则禁用测试。当应用于类级别时,该类中的所有测试方法也会自动禁用。
表达式可以是以下任何一种:
Spring Expression Language表达式。例如:
@DisabledIf("#{systemProperties['os.name'].toLowerCase().contains('mac')}")
在 Spring[
Environment
](core.html#beans-environment)中可用的属性的占位符。例如:@DisabledIf("${smoke.tests.disabled}")
文字文字。例如:
@DisabledIf("true")
然而,请注意,不是动态解析属性占位符的结果的文本文字是零实用价值的,因为@DisabledIf("true")
等价于@Disabled
和@DisabledIf("false")
在逻辑上是没有意义的。
你可以使用@DisabledIf
作为元注释来创建自定义组合注释。例如,你可以创建一个自定义@DisabledOnMac
注释,如下所示:
爪哇
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@DisabledIf(
expression = "#{systemProperties['os.name'].toLowerCase().contains('mac')}",
reason = "Disabled on Mac OS"
)
public @interface DisabledOnMac {}
Kotlin
@Target(AnnotationTarget.TYPE, AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@DisabledIf(
expression = "#{systemProperties['os.name'].toLowerCase().contains('mac')}",
reason = "Disabled on Mac OS"
)
annotation class DisabledOnMac {}
# 3.4.5.测试的元注释支持
你可以使用大多数与测试相关的注释元注释来创建定制的组合注释,并减少跨测试套件的配置重复。
你可以结合TestContext 框架使用以下每一项作为元注释。
@BootstrapWith
@ContextConfiguration
@ContextHierarchy
@ActiveProfiles
@TestPropertySource
@DirtiesContext
@WebAppConfiguration
@TestExecutionListeners
@Transactional
@BeforeTransaction
@AfterTransaction
@Commit
@Rollback
@Sql
@SqlConfig
@SqlMergeMode
@SqlGroup
@Repeat
(仅在 JUnit4 上支持)@Timed
(仅在 JUnit4 上支持)@IfProfileValue
(仅在 JUnit4 上支持)@ProfileValueSourceConfiguration
(仅在 JUnit4 上支持)@SpringJUnitConfig
(仅在 Junit Jupiter 上支持)@SpringJUnitWebConfig
(仅在 Junit Jupiter 上支持)@TestConstructor
(仅在 Junit Jupiter 上支持)@NestedTestConfiguration
(仅在 Junit Jupiter 上支持)@EnabledIf
(仅在 Junit Jupiter 上支持)@DisabledIf
(仅在 Junit Jupiter 上支持)
考虑以下示例:
爪哇
@RunWith(SpringRunner.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
public class OrderRepositoryTests { }
@RunWith(SpringRunner.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
public class UserRepositoryTests { }
Kotlin
@RunWith(SpringRunner::class)
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
@ActiveProfiles("dev")
@Transactional
class OrderRepositoryTests { }
@RunWith(SpringRunner::class)
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
@ActiveProfiles("dev")
@Transactional
class UserRepositoryTests { }
如果我们发现我们在基于 JUnit4 的测试套件中重复了前面的配置,那么我们可以通过引入自定义组合注释来减少重复,该注释集中了 Spring 的公共测试配置,如下所示:
爪哇
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
public @interface TransactionalDevTestConfig { }
Kotlin
@Target(AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
@ActiveProfiles("dev")
@Transactional
annotation class TransactionalDevTestConfig { }
然后,我们可以使用自定义的@TransactionalDevTestConfig
注释来简化基于 JUnit4 的测试类的配置,如下所示:
爪哇
@RunWith(SpringRunner.class)
@TransactionalDevTestConfig
public class OrderRepositoryTests { }
@RunWith(SpringRunner.class)
@TransactionalDevTestConfig
public class UserRepositoryTests { }
Kotlin
@RunWith(SpringRunner::class)
@TransactionalDevTestConfig
class OrderRepositoryTests
@RunWith(SpringRunner::class)
@TransactionalDevTestConfig
class UserRepositoryTests
如果我们编写使用 JUnit Jupiter 的测试,我们可以进一步减少代码重复,因为 JUnit5 中的注释也可以用作元注释。考虑以下示例:
爪哇
@ExtendWith(SpringExtension.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
class OrderRepositoryTests { }
@ExtendWith(SpringExtension.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
class UserRepositoryTests { }
Kotlin
@ExtendWith(SpringExtension::class)
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
@ActiveProfiles("dev")
@Transactional
class OrderRepositoryTests { }
@ExtendWith(SpringExtension::class)
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
@ActiveProfiles("dev")
@Transactional
class UserRepositoryTests { }
如果我们发现我们在基于 JUnit Jupiter 的测试套件中重复了前面的配置,那么我们可以通过引入一个自定义组合注释来减少重复,该注释集中了 Spring 和 JUnit Jupiter 的公共测试配置,如下所示:
爪哇
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(SpringExtension.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
public @interface TransactionalDevTestConfig { }
Kotlin
@Target(AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@ExtendWith(SpringExtension::class)
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
@ActiveProfiles("dev")
@Transactional
annotation class TransactionalDevTestConfig { }
然后,我们可以使用自定义的@TransactionalDevTestConfig
注释来简化基于 JUnit Jupiter 的各个测试类的配置,如下所示:
爪哇
@TransactionalDevTestConfig
class OrderRepositoryTests { }
@TransactionalDevTestConfig
class UserRepositoryTests { }
Kotlin
@TransactionalDevTestConfig
class OrderRepositoryTests { }
@TransactionalDevTestConfig
class UserRepositoryTests { }
由于 JUnit Jupiter 支持使用@Test
、@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 (opens new window)维基页面。
# 3.5. Spring TestContext 框架
Spring TestContext 框架(位于org.springframework.test.context
包中)提供了通用的、注释驱动的单元和集成测试支持,该支持与正在使用的测试框架无关。TestContext 框架还非常重视约定而不是配置,使用合理的默认值,你可以通过基于注释的配置来覆盖这些默认值。
除了通用的测试基础设施之外,TestContext 框架还为 JUnit4、JUnit Jupiter(AKA 为 JUnit5)和 TestNG 提供了明确的支持。对于 JUnit4 和 TestNG, Spring 提供abstract
支持类。此外, Spring 为 JUnit4 提供了自定义的 JUnitRunner
和自定义的 JUnitRules
,为 JUnit Jupiter 提供了自定义的Extension
,允许你编写所谓的 POJO 测试类。扩展特定的类层次结构不需要 POJO 测试类,例如abstract
支持类。
下一节将概述 TestContext 框架的内部内容。如果你只对使用框架感兴趣,而对使用自己的自定义侦听器或自定义加载器扩展框架不感兴趣,可以直接访问配置(上下文管理、依赖注入、事务管理)、支持类和注释支持部分。
# 3.5.1.关键抽象
框架的核心包括依赖注入类和TestContext
、TestExecutionListener
和SmartContextLoader
接口。为每个测试类创建TestContextManager
(例如,用于在 JUnit Jupiter 中的单个测试类中执行所有测试方法)。而TestContextManager
则管理保存当前测试上下文的TestContext
。随着测试的进行,TestContextManager
还会更新TestContext
的状态,并将其委托给TestExecutionListener
实现,该实现通过提供依赖注入、管理事务等来实现实际的测试执行。aSmartContextLoader
负责为给定的测试类加载ApplicationContext
。有关更多信息和各种实现的示例,请参见javadoc (opens new window)和 Spring 测试套件。
# TestContext
TestContext
封装了运行测试的上下文(与实际使用的测试框架无关),并为它负责的测试实例提供了上下文管理和缓存支持。如果请求,TestContext
还将委托给SmartContextLoader
以加载ApplicationContext
。
# TestContextManager
TestContextManager
是 Spring TestContext 框架的主要入口点,负责管理单个TestContext
,并在定义良好的测试执行点向每个注册的TestExecutionListener
发送事件信号:
在特定测试框架的任何“类之前”或“所有之前”方法之前。
测试实例后处理。
在特定测试框架的任何“before”或“before each”方法之前。
在测试方法执行之前,但在测试设置之后.
在测试方法执行后但在测试前立即拆除。
在特定测试框架的任何“之后”或“之后”方法之后。
在特定测试框架的任何“课后”或“毕竟”方法之后。
# TestExecutionListener
TestExecutionListener
定义了用于对注册侦听器的TestContextManager
发布的测试执行事件做出反应的 API。参见[TestExecutionListener
配置]。
# 上下文加载程序
ContextLoader
是一个策略接口,用于为 Spring TestContext 框架管理的集成测试加载ApplicationContext
。你应该实现WebApplicationContext
而不是这个接口,以提供对组件类、活动 Bean 定义配置文件、测试属性源、上下文层次结构和WebApplicationContext
支持的支持。
SmartContextLoader
是ContextLoader
接口的扩展,它取代了原始的极小值ContextLoader
SPI。具体地说,SmartContextLoader
可以选择处理资源位置、组件类或上下文初始化器。此外,SmartContextLoader
可以在其加载的上下文中设置活动 Bean 定义配置文件和测试属性源。
Spring 提供了以下实现方式:
TestContext
:两个默认加载器之一,它在内部委托给一个AnnotationConfigContextLoader
、一个GenericXmlContextLoader
或一个GenericGroovyXmlContextLoader
,这取决于为测试类声明的配置,或者取决于是否存在默认位置或默认配置类。仅当 Groovy 位于 Classpath 上时,才启用 Groovy 支持。WebDelegatingSmartContextLoader
:两个默认加载器之一,它在内部委托给一个AnnotationConfigWebContextLoader
、一个GenericXmlWebContextLoader
或一个GenericGroovyXmlWebContextLoader
,这取决于为测试类声明的配置,或者取决于缺省位置或缺省配置类的存在。只有当测试类上存在@WebAppConfiguration
时,才使用 WebContextLoader
。仅当 Groovy 位于 Classpath 上时,才启用 Groovy 支持。AnnotationConfigContextLoader
:从组件类加载标准的ApplicationContext
。AnnotationConfigWebContextLoader
:从组件类加载WebApplicationContext
。GenericGroovyXmlContextLoader
:从 Groovy 脚本或 XML 配置文件的资源位置加载标准的ApplicationContext
。GenericGroovyXmlWebContextLoader
:从 Groovy 脚本或 XML 配置文件的资源位置加载WebApplicationContext
。GenericXmlContextLoader
:从 XML 资源位置加载标准的ApplicationContext
。GenericXmlWebContextLoader
:从 XML 资源位置加载WebApplicationContext
。
# 3.5.2.引导 TestContext 框架
Spring TestContext 框架内部的默认配置对于所有常见的用例都足够了。但是,有时开发团队或第三方框架希望更改默认的ContextLoader
,实现自定义的GenericXmlWebContextLoader
或,增加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
(参见测试执行事件)。
# 注册TestExecutionListener
实现
你可以使用@TestExecutionListeners
注释来注册测试类及其子类的TestExecutionListener
实现。有关详细信息和示例,请参见注释支持和[@TestExecutionListeners
](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/test/contexectionlisteners.html)的 爪哇doc。
# 默认TestExecutionListener
实现的自动发现
通过使用@TestExecutionListeners
注册TestExecutionListener
实现适合于在有限的测试场景中使用的自定义侦听器。但是,如果需要在整个测试套件中使用自定义侦听器,那么它可能会变得很麻烦。该问题通过支持通过SpringFactoriesLoader
机制自动发现默认TestExecutionListener
实现来解决。
具体地说,ApplicationEvents
模块在其META-INF/spring.factories
属性文件中的org.springframework.test.context.TestExecutionListener
键下声明所有核心默认TestExecutionListener
实现。第三方框架和开发人员可以通过他们自己的META-INF/spring.factories
属性文件以同样的方式向默认侦听器列表贡献他们自己的TestExecutionListener
实现。
# 排序TestExecutionListener
实现
当 TestContext 框架通过前述SpringFactoriesLoader
机制发现默认的TestExecutionListener
实现时,实例化的侦听器将通过使用 Spring 的SpringFactoriesLoader
进行排序,该命令使用 Spring 的Ordered
接口和@Order
注释进行排序。AbstractTestExecutionListener
和 Spring 提供的所有默认TestExecutionListener
实现用适当的值实现Ordered
。因此,第三方框架和开发人员应该通过实现TestExecutionListener
或声明@Order
来确保其默认的TestExecutionListener
实现是按正确的顺序注册的。关于分配给每个核心侦听器的值的详细信息,请参见 爪哇doc 获取核心默认getOrder()
实现的TestExecutionListener
方法。
# 合并TestExecutionListener
实现
如果通过@TestExecutionListeners
注册了自定义TestExecutionListener
,则不会注册默认侦听器。在大多数常见的测试场景中,这有效地迫使开发人员手动声明除任何自定义侦听器之外的所有默认侦听器。下面的清单演示了这种配置风格:
爪哇
@ContextConfiguration
@TestExecutionListeners({
MyCustomTestExecutionListener.class,
ServletTestExecutionListener.class,
DirtiesContextBeforeModesTestExecutionListener.class,
DependencyInjectionTestExecutionListener.class,
DirtiesContextTestExecutionListener.class,
TransactionalTestExecutionListener.class,
SqlScriptsTestExecutionListener.class
})
class MyTest {
// class body...
}
Kotlin
@ContextConfiguration
@TestExecutionListeners(
MyCustomTestExecutionListener::class,
ServletTestExecutionListener::class,
DirtiesContextBeforeModesTestExecutionListener::class,
DependencyInjectionTestExecutionListener::class,
DirtiesContextTestExecutionListener::class,
TransactionalTestExecutionListener::class,
SqlScriptsTestExecutionListener::class
)
class MyTest {
// class body...
}
这种方法的挑战在于,它要求开发人员准确地知道默认注册了哪些侦听器。此外,缺省侦听器集可以从一个版本更改到另一个版本——例如,SqlScriptsTestExecutionListener
在 Spring Framework4.1 中引入,DirtiesContextBeforeModesTestExecutionListener
在 Spring Framework4.2 中引入。此外,像 Spring 引导和 Spring 安全之类的第三方框架通过使用前述的自动发现机制来注册它们自己的默认TestExecutionListener
实现。
为了避免必须意识到并重新声明所有默认侦听器,你可以将@TestExecutionListeners
的mergeMode
属性设置为MergeMode.MERGE_WITH_DEFAULTS
。MERGE_WITH_DEFAULTS
表示本地声明的侦听器应该与默认侦听器合并。合并算法确保从列表中删除重复项,并确保合并后的侦听器集合根据AnnotationAwareOrderComparator
的语义进行排序,如[OrderingTestExecutionListener
实现](#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 提供了对记录应用程序事件中发布的ApplicationContext
的支持,以便可以针对测试中的那些事件执行断言。在执行单个测试期间发布的所有事件都可以通过ApplicationEvents
API 获得,该 API 允许你以java.util.Stream
的形式处理事件。
要在测试中使用ApplicationEvents
,请执行以下操作。
确保你的测试类是用[
@RecordApplicationEvents
](# Spring-testing-annotation-recordapplicationEvents)进行注释或元注释的。确保
ApplicationEventsTestExecutionListener
已注册。但是,请注意,ApplicationEventsTestExecutionListener
是默认注册的,并且只有在你通过@TestExecutionListeners
进行了自定义配置且不包括默认侦听器的情况下,才需要手动注册。用
@Autowired
注释类型ApplicationEvents
的字段,并在你的测试和生命周期方法(例如 JUnit Jupiter 中的@BeforeEach
和@AfterEach
方法)中使用ApplicationEvents
的实例。- 当使用朱尼特木星的 SpringExtension时,可以在测试或生命周期方法中声明类型为
ApplicationEvents
的方法参数,作为测试类中@Autowired
字段的替代。
- 当使用朱尼特木星的 SpringExtension时,可以在测试或生命周期方法中声明类型为
下面的测试类使用SpringExtension
for JUnit Jupiter 和AssertJ (opens new window)来断言在 Spring 管理的组件中调用方法时发布的应用程序事件的类型:
爪哇
@SpringJUnitConfig(/* ... */)
@RecordApplicationEvents (1)
class OrderServiceTests {
@Autowired
OrderService orderService;
@Autowired
ApplicationEvents events; (2)
@Test
void submitOrder() {
// Invoke method in OrderService that publishes an event
orderService.submitOrder(new Order(/* ... */));
// Verify that an OrderSubmitted event was published
long numEvents = events.stream(OrderSubmitted.class).count(); (3)
assertThat(numEvents).isEqualTo(1);
}
}
1 | 用@RecordApplicationEvents 注释测试类。 |
---|---|
2 | 为当前测试注入ApplicationEvents 实例。 |
3 | 使用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
中,aAfterTestClassEvent
不是 Bean。
为了侦听测试执行事件, Spring Bean 可以选择实现org.springframework.context.ApplicationListener
接口。或者,侦听器方法可以使用AfterTestClassEvent
进行注释,并配置为侦听上面列出的特定事件类型之一(参见基于注释的事件监听器)。由于这种方法的流行, Spring 提供了以下专用的@EventListener
注释,以简化测试执行事件侦听器的注册。这些注释驻留在org.springframework.test.context.event.annotation
包中。
@BeforeTestClass
@PrepareTestInstance
@BeforeTestMethod
@BeforeTestExecution
@AfterTestExecution
@AfterTestMethod
@AfterTestClass
# 异常处理
默认情况下,如果测试执行事件侦听器在使用事件时抛出异常,则该异常将传播到使用中的底层测试框架(例如 JUnit 或 TestNG)。例如,如果消耗BeforeTestMethodEvent
导致异常,则相应的测试方法将作为异常的结果而失败。相反,如果异步测试执行事件侦听器抛出异常,则异常将不会传播到底层测试框架。有关异步异常处理的更多详细信息,请参阅类级 爪哇doc for@EventListener
。
# 异步侦听器
如果希望特定的测试执行事件侦听器异步处理事件,可以使用 Spring 的[regular@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="gt=">gt=”1228"|1|Injecting the ApplicationContext
.|
|-----|-----------------------------------|
Kotlin<br/>@SpringJUnitConfig<br/>class MyTest {<br/><br/> @Autowired (1)<br/> lateinit var applicationContext: ApplicationContext<br/><br/> // class body...<br/>}<br/>
|1|Injecting the ApplicationContext
.|
|-----|-----------------------------------|
Similarly, if your test is configured to load a WebApplicationContext
, you can inject
the web application context into your test, as follows:
爪哇<br/>@SpringJUnitWebConfig (1)<br/>class MyWebAppTest {<br/><br/> @Autowired (2)<br/> WebApplicationContext wac;<br/><br/> // class body...<br/>}<br/>
|1|Configuring the WebApplicationContext
.|
|-----|----------------------------------------|
|2| Injecting the WebApplicationContext
. |
Kotlin<br/>@SpringJUnitWebConfig (1)<br/>class MyWebAppTest {<br/><br/> @Autowired (2)<br/> lateinit var wac: WebApplicationContext<br/> // class body...<br/>}<br/>
|1|Configuring the WebApplicationContext
.|
|-----|----------------------------------------|
|2| Injecting the WebApplicationContext
. |
Dependency injection by using @Autowired
is provided by theDependencyInjectionTestExecutionListener
, which is configured by default
(see Dependency Injection of Test Fixtures).|
|-----|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|1|注入ApplicationContext
。|
|1|注入ApplicationContext
。|
|1|配置WebApplicationContext
。|
|2|注入WebApplicationContext
。|
|1|配置WebApplicationContext
。|
|2|注入WebApplicationContext
。|
使用 TestContext 框架的测试类不需要扩展任何特定的类或实现特定的接口来配置它们的应用程序上下文。相反,配置是通过在类级别声明@ContextConfiguration
注释来实现的。如果你的测试类没有显式声明应用程序上下文资源位置或组件类,那么配置的ContextLoader
将决定如何从默认位置或默认配置类加载上下文。除了上下文资源位置和组件类之外,还可以通过应用程序上下文初始化器来配置应用程序上下文。
下面的部分解释了如何使用 Spring 的@ContextConfiguration
注释来通过使用 XML 配置文件、Groovy 脚本、组件类(通常是@Configuration
类)或上下文初始化器来配置测试ApplicationContext
。或者,你可以为高级用例实现和配置你自己的自定义SmartContextLoader
。
[加载
WebApplicationContext
](#testcontext-ctx-management-web)
# 使用 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的 Groovy 脚本为你的测试加载ApplicationContext
,你可以使用@ContextConfiguration
注释测试类,并配置locations
或value
属性,并使用一个包含 Groovy 脚本资源位置的数组。Groovy 脚本的资源查找语义与XML 配置文件的资源查找语义相同。
启用 Groovy 脚本支持value 在 Spring testContext 框架中使用 Groovy 脚本加载 ApplicationContext 的支持是自动启用的,如果 Groovy 在 Classpath 上。 |
---|
下面的示例展示了如何指定 Groovy 配置文件:
爪哇
@ExtendWith(SpringExtension.class)
// ApplicationContext will be loaded from "/AppConfig.groovy" and
// "/TestConfig.groovy" in the root of the classpath
@ContextConfiguration({"/AppConfig.groovy", "/TestConfig.Groovy"}) (1)
class MyTest {
// class body...
}
Kotlin
@ExtendWith(SpringExtension::class)
// ApplicationContext will be loaded from "/AppConfig.groovy" and
// "/TestConfig.groovy" in the root of the classpath
@ContextConfiguration("/AppConfig.groovy", "/TestConfig.Groovy") (1)
class MyTest {
// class body...
}
1 | 指定 Groovy 配置文件的位置。 |
---|
如果从@ContextConfiguration
注释中省略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=“>”><gt=">r=”1319“/>r=”r=“>”>“><gt=”r=" |
---|
# 具有组件类的上下文配置
要通过使用组件类为你的测试加载ApplicationContext
(参见基于 爪哇 的容器配置),你可以用@ContextConfiguration
注释你的测试类,并用一个包含对组件类的引用的数组配置classes
属性。下面的示例展示了如何做到这一点:
爪哇
@ExtendWith(SpringExtension.class)
// ApplicationContext will be loaded from AppConfig and TestConfig
@ContextConfiguration(classes = {AppConfig.class, TestConfig.class}) (1)
class MyTest {
// class body...
}
1 | 指定组件类。 |
---|
Kotlin
@ExtendWith(SpringExtension::class)
// ApplicationContext will be loaded from AppConfig and TestConfig
@ContextConfiguration(classes = [AppConfig::class, TestConfig::class]) (1)
class MyTest {
// class body...
}
1 | 指定组件类。 |
---|
组件类 术语“组件类”可以指以下任何一种: * 用 注释的类 * 一个组件(即用 @Component 、@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
类的两个示例:
<!-- app-config.xml -->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:jee="http://www.springframework.org/schema/jee"
xsi:schemaLocation="...">
<bean id="transferService"
class="com.bank.service.internal.DefaultTransferService">
<constructor-arg ref="accountRepository"/>
<constructor-arg ref="feePolicy"/>
</bean>
<bean id="accountRepository"
class="com.bank.repository.internal.JdbcAccountRepository">
<constructor-arg ref="dataSource"/>
</bean>
<bean id="feePolicy"
class="com.bank.service.internal.ZeroFeePolicy"/>
<beans profile="dev">
<jdbc:embedded-database id="dataSource">
<jdbc:script
location="classpath:com/bank/config/sql/schema.sql"/>
<jdbc:script
location="classpath:com/bank/config/sql/test-data.sql"/>
</jdbc:embedded-database>
</beans>
<beans profile="production">
<jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
</beans>
<beans profile="default">
<jdbc:embedded-database id="dataSource">
<jdbc:script
location="classpath:com/bank/config/sql/schema.sql"/>
</jdbc:embedded-database>
</beans>
</beans>
爪哇
@ExtendWith(SpringExtension.class)
// ApplicationContext will be loaded from "classpath:/app-config.xml"
@ContextConfiguration("/app-config.xml")
@ActiveProfiles("dev")
class TransferServiceTest {
@Autowired
TransferService transferService;
@Test
void testTransferService() {
// test the transferService
}
}
Kotlin
@ExtendWith(SpringExtension::class)
// ApplicationContext will be loaded from "classpath:/app-config.xml"
@ContextConfiguration("/app-config.xml")
@ActiveProfiles("dev")
class TransferServiceTest {
@Autowired
lateinit var transferService: TransferService
@Test
fun testTransferService() {
// test the transferService
}
}
当运行TransferServiceTest
时,其ApplicationContext
将从 Classpath 的根目录中的app-config.xml
配置文件加载。如果检查app-config.xml
,可以看到accountRepository
Bean 对dataSource
Bean 具有依赖关系。然而,dataSource
并未被定义为顶层 Bean。相反,dataSource
被定义了三次:在production
配置文件中,在dev
配置文件中,以及在default
配置文件中。
通过用TransferServiceTest
注释@ActiveProfiles("dev")
,我们指示 Spring TestContext 框架加载ApplicationContext
,并将活动配置文件设置为{"dev"}
。结果,嵌入式数据库被创建并填充了测试数据,并且accountRepository
Bean 与开发DataSource
的引用连接。这很可能就是我们在集成测试中想要的。
有时,将 bean 分配到default
配置文件中是有用的。只有当没有其他配置文件被特别激活时,默认配置文件中的 bean 才会被包含。你可以使用它来定义要在应用程序的默认状态下使用的“fallback”bean。例如,你可以显式地为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 (opens new window)。下面的示例演示如何实现和注册自定义OperatingSystemActiveProfilesResolver
:
爪哇
// "dev" profile overridden programmatically via a custom resolver
@ActiveProfiles(
resolver = OperatingSystemActiveProfilesResolver.class,
inheritProfiles = false)
class TransferServiceTest extends AbstractIntegrationTest {
// test body
}
Kotlin
// "dev" profile overridden programmatically via a custom resolver
@ActiveProfiles(
resolver = OperatingSystemActiveProfilesResolver::class,
inheritProfiles = false)
class TransferServiceTest : AbstractIntegrationTest() {
// test body
}
爪哇
public class OperatingSystemActiveProfilesResolver implements ActiveProfilesResolver {
@Override
public String[] resolve(Class<?> testClass) {
String profile = ...;
// determine the value of profile based on the operating system
return new String[] {profile};
}
}
Kotlin
class OperatingSystemActiveProfilesResolver : ActiveProfilesResolver {
override fun resolve(testClass: Class<*>): Array<String> {
val profile: String = ...
// determine the value of profile based on the operating system
return arrayOf(profile)
}
}
# 具有测试属性源的上下文配置
Spring 框架对具有属性源层次结构的环境的概念具有一流的支持,并且你可以使用特定于测试的属性源来配置集成测试。与@Configuration
类上使用的@PropertySource
注释相反,你可以在测试类上声明@TestPropertySource
注释,以声明测试属性文件或内联属性的资源位置。这些测试属性源被添加到PropertySources
中的Environment
中的ApplicationContext
集合中,用于为带注释的集成测试加载ApplicationContext
。
你可以使用@TestPropertySource 与SmartContextLoader SPI 的任何实现一起使用@TestPropertySource ,但是@TestPropertySource 不支持与较早的ContextLoader SPI 的实现一起使用SmartContextLoader 通过getPropertySourceLocations() 中的<gt r=“1495”和<gt=“1494”/>方法访问合并的测试属性源值。 |
---|
# 声明测试属性源
可以使用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
forBaseTest
。相比之下,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
forBaseTest
。与此相反,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 注释及其支持的基础结构是,最初的设计目的是允许基于测试容器 (opens new window)的测试中的属性很容易地暴露在 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 测试支持,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 框架不能在测试类之间运行<gtcache 上下文,结果是构建过程的速度明显慢得多。 |
---|
上下文缓存的大小是有界的,默认的最大大小为 32。每当达到最大大小时,就会使用最近使用的驱逐策略来驱逐和关闭陈旧的上下文。通过设置名为spring.test.context.cache.maxSize
的 JVM 系统属性,可以从命令行或构建脚本配置最大大小。作为一种选择,你可以通过[SpringProperties
](acception.html#acception- Spring-properties)机制设置相同的属性。
由于在给定的测试套件中加载大量的应用程序上下文可能会导致该套件花费不必要的长时间运行,因此准确地知道加载和缓存了多少上下文通常是有益的。要查看底层上下文缓存的统计信息,可以将org.springframework.test.context.cache
日志类别的日志级别设置为DEBUG
。
在不太可能的情况下,测试会破坏应用程序上下文并需要重新加载(例如,通过修改 Bean 定义或应用程序对象的状态),你可以使用@DirtiesContext
对测试类或测试方法进行注释(请参见Spring Testing Annotations中对@DirtiesContext
的讨论)。这指示 Spring 在运行需要相同应用程序上下文的下一个测试之前,从缓存中删除上下文并重建应用程序上下文。请注意,对@DirtiesContext
注释的支持是由DirtiesContextBeforeModesTestExecutionListener
和DirtiesContextTestExecutionListener
提供的,这是默认启用的。
ApplicationContext 生命周期和控制台日志 当需要调试使用 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的讨论和[@DirtiesContext ](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/test/annotation/dirtiescontext.html)的讨论。 |
---|
# 3.5.7.测试夹具的依赖注入
当你使用DependencyInjectionTestExecutionListener
(它是默认配置的)时,你的测试实例的依赖项是从你配置了@ContextConfiguration
或相关注释的应用程序上下文中的 bean 注入的。你可以使用 setter 注入、字段注入,或者两者都使用,这取决于你选择了哪些注释,以及是否将它们放置在 setter 方法或字段上。如果你正在使用 JUnit Jupiter,你也可以选择使用构造函数注入(参见[Dependency Injection withSpringExtension
](#TestContext-JUnit-Jupiter-DI))。为了与 Spring 的基于注释的注入支持保持一致,还可以使用 Spring 的@Autowired
注释或来自 JSR-330 的@Inject
注释进行字段和 setter 注入。
对于 JUnit Jupiter 以外的测试框架,TestContext 框架不 参与测试类的实例化。因此,对于构造函数使用 @Autowired 或@Inject 对测试类没有影响。 |
---|
尽管在生产代码中不鼓励现场注入,但在测试代码中,现场注入实际上是 非常自然的。这种差异的基本原理是,你将永远不会直接实例化你的测试类 。因此,不需要能够在测试类上调用 构造函数或 setter 方法。 |
---|
因为@Autowired
用于执行按类型自动布线,所以如果你有多个相同类型的 Bean 定义,那么对于那些特定的 bean,你不能依赖这种方法。在这种情况下,可以将@Autowired
与@Qualifier
结合使用。你还可以选择将@Inject
与@Named
结合使用。或者,如果你的测试类具有对其ApplicationContext
的访问权限,则可以使用(例如)对applicationContext.getBean("titleRepository", TitleRepository.class)
的调用来执行显式查找。
如果不希望将依赖注入应用于测试实例,请不要使用@Autowired
或@Inject
注释域或 setter 方法。或者,可以通过显式地使用@TestExecutionListeners
配置类并从侦听器列表中省略DependencyInjectionTestExecutionListener.class
来完全禁用依赖注入。
考虑测试HibernateTitleRepository
类的场景,如Goals部分所概述的那样。接下来的两个代码清单演示了@Autowired
在字段和 setter 方法上的使用。在所有示例代码列表之后,将介绍应用程序上下文配置。
以下代码列表中的依赖注入行为不是 JUnit Jupiter 特有的。相同的 DI 技术可以与任何支持的测试 框架结合使用。 以下示例调用静态断言方法,例如 assertNotNull() ,,但不预先处理与 Assertions 的调用。在这种情况下,假设方法是通过 import static 声明正确导入的,该声明在示例中未显示。 |
---|
第一个代码清单显示了一个基于 JUnit Jupiter 的测试类实现,它使用@Autowired
进行字段注入:
爪哇
@ExtendWith(SpringExtension.class)
// specifies the Spring configuration to load for this test fixture
@ContextConfiguration("repository-config.xml")
class HibernateTitleRepositoryTests {
// this instance will be dependency injected by type
@Autowired
HibernateTitleRepository titleRepository;
@Test
void findById() {
Title title = titleRepository.findById(new Long(10));
assertNotNull(title);
}
}
Kotlin
@ExtendWith(SpringExtension::class)
// specifies the Spring configuration to load for this test fixture
@ContextConfiguration("repository-config.xml")
class HibernateTitleRepositoryTests {
// this instance will be dependency injected by type
@Autowired
lateinit var titleRepository: HibernateTitleRepository
@Test
fun findById() {
val title = titleRepository.findById(10)
assertNotNull(title)
}
}
或者,你可以将类配置为使用@Autowired
进行 setter 注入,如下所示:
爪哇
@ExtendWith(SpringExtension.class)
// specifies the Spring configuration to load for this test fixture
@ContextConfiguration("repository-config.xml")
class HibernateTitleRepositoryTests {
// this instance will be dependency injected by type
HibernateTitleRepository titleRepository;
@Autowired
void setTitleRepository(HibernateTitleRepository titleRepository) {
this.titleRepository = titleRepository;
}
@Test
void findById() {
Title title = titleRepository.findById(new Long(10));
assertNotNull(title);
}
}
Kotlin
@ExtendWith(SpringExtension::class)
// specifies the Spring configuration to load for this test fixture
@ContextConfiguration("repository-config.xml")
class HibernateTitleRepositoryTests {
// this instance will be dependency injected by type
lateinit var titleRepository: HibernateTitleRepository
@Autowired
fun setTitleRepository(titleRepository: HibernateTitleRepository) {
this.titleRepository = titleRepository
}
@Test
fun findById() {
val title = titleRepository.findById(10)
assertNotNull(title)
}
}
前面的代码列表使用由@ContextConfiguration
注释(即repository-config.xml
)引用的相同 XML 上下文文件。以下显示了此配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- this bean will be injected into the HibernateTitleRepositoryTests class -->
<bean id="titleRepository" class="com.foo.repository.hibernate.HibernateTitleRepository">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>
<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
<!-- configuration elided for brevity -->
</bean>
</beans>
如果你是从 Spring 提供的测试基类进行扩展的,而测试基类恰好在它的一个 setter 方法上使用@Autowired ,那么你可能在应用程序上下文中定义了多个受影响的类型的 bean(例如,多个 DataSource bean)。在这样的情况下,可以重写 setter 方法,并使用 @Qualifier 注释来表示特定的目标 Bean,如下所示(但也要确保将其委托给超类中重写的方法): java <><1893"r="gt="1914"/><gt15"/>指定的限定词表示指定的限定符[lt=”lt=“lt=”lt=“lt=”lt=“lt17”/><lt=“lt=”lt="lt= 它的值与相应的 <bean> 定义中的<qualifier> 声明相匹配。 Bean name被用作回退限定符值,因此你也可以有效地通过这里的名称指向特定的 Bean(如前面所示,假设 myDataSource 是 Bean id )。 |
---|
# 3.5.8.测试请求和会话范围的 bean
Spring 从早期开始就支持请求和会话范围的 bean,你可以通过以下步骤来测试你的请求范围和会话范围的 bean:
通过使用
@WebAppConfiguration
注释测试类,确保为测试加载了WebApplicationContext
。将模拟请求或会话注入到你的测试实例中,并根据需要准备测试装置。
调用从配置的
WebApplicationContext
中检索到的 Web 组件(带有依赖项注入)。对模拟执行断言。
下一个代码片段显示了登录用例的 XML 配置。请注意,userService
Bean 对请求作用域loginAction
Bean 具有依赖关系。而且,LoginAction
是通过使用Spel 表达式实例化的,后者从当前的 HTTP 请求中检索用户名和密码。在测试中,我们希望通过 TestContext 框架管理的模拟来配置这些请求参数。下面的清单显示了这个用例的配置:
请求范围 Bean 配置
<beans>
<bean id="userService" class="com.example.SimpleUserService"
c:loginAction-ref="loginAction"/>
<bean id="loginAction" class="com.example.LoginAction"
c:username="#{request.getParameter('user')}"
c:password="#{request.getParameter('pswd')}"
scope="request">
<aop:scoped-proxy/>
</bean>
</beans>
在RequestScopedBeanTests
中,我们将UserService
(即被测试的对象)和MockHttpServletRequest
注入到我们的测试实例中。在我们的requestScope()
测试方法中,我们通过在提供的MockHttpServletRequest
中设置请求参数来设置测试夹具。当在我们的userService
上调用loginUser()
方法时,我们确信用户服务可以访问当前MockHttpServletRequest
的请求范围loginAction
(即我们只设置参数的那个)。然后,我们可以根据用户名和密码的已知输入,针对结果执行断言。下面的清单展示了如何做到这一点:
爪哇
@SpringJUnitWebConfig
class RequestScopedBeanTests {
@Autowired UserService userService;
@Autowired MockHttpServletRequest request;
@Test
void requestScope() {
request.setParameter("user", "enigma");
request.setParameter("pswd", "$pr!ng");
LoginResults results = userService.loginUser();
// assert results
}
}
Kotlin
@SpringJUnitWebConfig
class RequestScopedBeanTests {
@Autowired lateinit var userService: UserService
@Autowired lateinit var request: MockHttpServletRequest
@Test
fun requestScope() {
request.setParameter("user", "enigma")
request.setParameter("pswd", "\$pr!ng")
val results = userService.loginUser()
// assert results
}
}
下面的代码片段与我们前面看到的请求范围的代码片段类似 Bean。然而,这一次,userService
Bean 对会话范围userPreferences
Bean 具有依赖关系。请注意,UserPreferences
Bean 是通过使用 SPEL 表达式实例化的,该表达式从当前 HTTP 会话中检索主题。在测试中,我们需要在由 TestContext 框架管理的模拟会话中配置一个主题。下面的示例展示了如何做到这一点:
会话范围 Bean 配置
<beans>
<bean id="userService" class="com.example.SimpleUserService"
c:userPreferences-ref="userPreferences" />
<bean id="userPreferences" class="com.example.UserPreferences"
c:theme="#{session.getAttribute('theme')}"
scope="session">
<aop:scoped-proxy/>
</bean>
</beans>
在SessionScopedBeanTests
中,我们将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
以外的任何传播类型,则应谨慎使用(有关详细信息,请参见事务传播的讨论)。
在结合 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)和[<gtr="2023"/>](#TestContext-Support-Classes-TestNG)是为类级别的事务支持而预先配置的。
下面的示例演示了为基于 Hibernate 的UserRepository
编写集成测试的常见场景:
爪哇
@SpringJUnitConfig(TestConfig.class)
@Transactional
class HibernateUserRepositoryTests {
@Autowired
HibernateUserRepository repository;
@Autowired
SessionFactory sessionFactory;
JdbcTemplate jdbcTemplate;
@Autowired
void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
@Test
void createUser() {
// track initial state in test database:
final int count = countRowsInTable("user");
User user = new User(...);
repository.save(user);
// Manual flush is required to avoid false positive in test
sessionFactory.getCurrentSession().flush();
assertNumUsers(count + 1);
}
private int countRowsInTable(String tableName) {
return JdbcTestUtils.countRowsInTable(this.jdbcTemplate, tableName);
}
private void assertNumUsers(int expected) {
assertEquals("Number of rows in the [user] table.", expected, countRowsInTable("user"));
}
}
Kotlin
@SpringJUnitConfig(TestConfig::class)
@Transactional
class HibernateUserRepositoryTests {
@Autowired
lateinit var repository: HibernateUserRepository
@Autowired
lateinit var sessionFactory: SessionFactory
lateinit var jdbcTemplate: JdbcTemplate
@Autowired
fun setDataSource(dataSource: DataSource) {
this.jdbcTemplate = JdbcTemplate(dataSource)
}
@Test
fun createUser() {
// track initial state in test database:
val count = countRowsInTable("user")
val user = User()
repository.save(user)
// Manual flush is required to avoid false positive in test
sessionFactory.getCurrentSession().flush()
assertNumUsers(count + 1)
}
private fun countRowsInTable(tableName: String): Int {
return JdbcTestUtils.countRowsInTable(jdbcTemplate, tableName)
}
private fun assertNumUsers(expected: Int) {
assertEquals("Number of rows in the [user] table.", expected, countRowsInTable("user"))
}
}
正如在事务回滚和提交行为中所解释的那样,在createUser()
方法运行后不需要清理数据库,因为对数据库所做的任何更改都会由TransactionalTestExecutionListener
自动回滚。
# 事务回滚和提交行为
默认情况下,测试事务将在测试完成后自动回滚;但是,事务提交和回滚行为可以通过@Commit
和@Rollback
注释进行声明性配置。有关更多详细信息,请参见注释支持部分中的相应条目。
# 程序化事务管理
可以通过使用TestTransaction
中的静态方法以编程方式与测试管理的事务交互。例如,可以在测试方法中、方法之前和方法之后使用TestTransaction
来启动或结束当前的测试管理事务,或者为回滚或提交配置当前的测试管理事务。只要启用TransactionalTestExecutionListener
,对TestTransaction
的支持就会自动可用。
下面的示例演示了TestTransaction
的一些特性。关于[TestTransaction
](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/test/context/transaction/testtransaction.html)的更多详细信息,请参见 爪哇doc。
爪哇
@ContextConfiguration(classes = TestConfig.class)
public class ProgrammaticTransactionManagementTests extends
AbstractTransactionalJUnit4SpringContextTests {
@Test
public void transactionalTest() {
// assert initial state in test database:
assertNumUsers(2);
deleteFromTables("user");
// changes to the database will be committed!
TestTransaction.flagForCommit();
TestTransaction.end();
assertFalse(TestTransaction.isActive());
assertNumUsers(0);
TestTransaction.start();
// perform other actions against the database that will
// be automatically rolled back after the test completes...
}
protected void assertNumUsers(int expected) {
assertEquals("Number of rows in the [user] table.", expected, countRowsInTable("user"));
}
}
Kotlin
@ContextConfiguration(classes = [TestConfig::class])
class ProgrammaticTransactionManagementTests : AbstractTransactionalJUnit4SpringContextTests() {
@Test
fun transactionalTest() {
// assert initial state in test database:
assertNumUsers(2)
deleteFromTables("user")
// changes to the database will be committed!
TestTransaction.flagForCommit()
TestTransaction.end()
assertFalse(TestTransaction.isActive())
assertNumUsers(0)
TestTransaction.start()
// perform other actions against the database that will
// be automatically rolled back after the test completes...
}
protected fun assertNumUsers(expected: Int) {
assertEquals("Number of rows in the [user] table.", expected, countRowsInTable("user"))
}
}
# 在事务之外运行代码
有时,你可能需要在事务性测试方法之前或之后运行特定的代码,但要在事务性上下文之外运行,例如,要在运行测试之前验证初始数据库状态,或者在测试运行之后验证预期的事务提交行为(如果测试被配置为提交事务)。TransactionalTestExecutionListener
支持针对此类场景的@BeforeTransaction
和@AfterTransaction
注释。你可以使用这些注释之一对测试类中的任何void
方法或测试接口中的任何void
默认方法进行注释,并且TransactionalTestExecutionListener
确保你的 before transaction 方法或 after transaction 方法在适当的时间运行。
任何之前的方法(例如用 JUnit Jupiter 的@BeforeEach 注释的方法)和任何之后的方法(例如用 JUnit Jupiter 的 @AfterEach 注释的方法)都是在事务中运行的。此外,对于未配置为在 事务中运行的测试方法,不运行带有 @BeforeTransaction 或@AfterTransaction 注释的方法。 |
---|
# 配置事务管理器
TransactionalTestExecutionListener
期望在 Spring ApplicationContext
中定义一个PlatformTransactionManager
用于测试。如果在测试的ApplicationContext
中有多个PlatformTransactionManager
实例,则可以使用@Transactional("myTxMgr")
或@Transactional(transactionManager = "myTxMgr")
声明限定符,或者TransactionManagementConfigurer
可以通过@Configuration
类实现。查询[爪哇doc forTestContextTransactionUtils.retrieveTransactionManager()
](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/test/contexttransactionutils.html#retrivetransactionmanager-org.spramework.test.context.tcontext.tcontext-tcontext-justcontext-java.lang.string-)以获取有关在测试中查找事务管理器的算法的详细信息,该算法用于在测试的<<
# 演示所有与事务相关的注释
下面的基于 JUnit Jupiter 的示例显示了一个虚拟的集成测试场景,该场景突出显示了所有与事务相关的注释。该示例的目的不是演示最佳实践,而是演示如何使用这些注释。有关更多信息和配置示例,请参见注释支持小节。[用于@Sql
的事务管理](#TestContext-Executing-SQL-Declarationly-TX)包含一个附加示例,该示例使用@Sql
执行带有默认事务回滚语义的声明性 SQL 脚本。下面的示例显示了相关的注释:
爪哇
@SpringJUnitConfig
@Transactional(transactionManager = "txMgr")
@Commit
class FictitiousTransactionalTest {
@BeforeTransaction
void verifyInitialDatabaseState() {
// logic to verify the initial state before a transaction is started
}
@BeforeEach
void setUpTestDataWithinTransaction() {
// set up test data within the transaction
}
@Test
// overrides the class-level @Commit setting
@Rollback
void modifyDatabaseWithinTransaction() {
// logic which uses the test data and modifies database state
}
@AfterEach
void tearDownWithinTransaction() {
// run "tear down" logic within the transaction
}
@AfterTransaction
void verifyFinalDatabaseState() {
// logic to verify the final state after transaction has rolled back
}
}
Kotlin
@SpringJUnitConfig
@Transactional(transactionManager = "txMgr")
@Commit
class FictitiousTransactionalTest {
@BeforeTransaction
fun verifyInitialDatabaseState() {
// logic to verify the initial state before a transaction is started
}
@BeforeEach
fun setUpTestDataWithinTransaction() {
// set up test data within the transaction
}
@Test
// overrides the class-level @Commit setting
@Rollback
fun modifyDatabaseWithinTransaction() {
// logic which uses the test data and modifies database state
}
@AfterEach
fun tearDownWithinTransaction() {
// run "tear down" logic within the transaction
}
@AfterTransaction
fun verifyFinalDatabaseState() {
// logic to verify the final state after transaction has rolled back
}
}
在测试 ORM 代码 时避免误报当你测试处理 Hibernate 会话状态或 JPA 持久性上下文的应用程序代码时,请确保刷新运行该代码的测试方法 中的底层工作单元。未能刷新底层工作单元可能会产生错误的 正:你的测试通过了,但是相同的代码在动态的生产 环境中抛出了异常。请注意,这适用于任何维护 工作的内存单元的 ORM 框架。在以下基于 Hibernate 的示例测试用例中,一种方法演示了 假阳性,而另一种方法则正确地公开了刷新 会话的结果: 爪哇 <br/>// ...<br/><br/>@Autowired<br/>SessionFactory sessionFactory;<br/><br/>@Transactional<br/>@Test // no expected exception!<br/>public void falsePositive() {<br/> updateEntityInHibernateSession();<br/> // False positive: an exception will be thrown once the Hibernate<br/> // Session is finally flushed (i.e., in production code)<br/>}<br/><br/>@Transactional<br/>@Test(expected = ...)<br/>public void updateWithSessionFlush() {<br/> updateEntityInHibernateSession();<br/> // Manual flush is required to avoid false positive in test<br/> sessionFactory.getCurrentSession().flush();<br/>}<br/><br/>// ...<br/> <br/>// ...<br/><br/>@Autowired<br/>lateinit var sessionFactory: SessionFactory<br/><br/>@Transactional<br/>@Test // no expected exception!<br/>fun falsePositive() {<br/> updateEntityInHibernateSession()<br/> // False positive: an exception will be thrown once the Hibernate<br/> // Session is finally flushed (i.e., in production code)<br/>}<br/><br/>@Transactional<br/>@Test(expected = ...)<br/>fun updateWithSessionFlush() {<br/> updateEntityInHibernateSession()<br/> // Manual flush is required to avoid false positive in test<br/> sessionFactory.getCurrentSession().flush()<br/>}<br/><br/>// ...<br/> <gt="2085"/><><"><"><"><"gt="gt="2085">>><">>><<<"gt=2085">>>>>>>><<<<<"gt= |
---|
# 3.5.10.执行 SQL 脚本
在针对关系数据库编写集成测试时,运行 SQL 脚本来修改数据库模式或将测试数据插入到表中通常是有益的。spring-jdbc
模块通过在 Spring ApplicationContext
加载时执行 SQL 脚本,为初始化嵌入式或现有数据库提供支持。详见嵌入式数据库支持和用嵌入式数据库测试数据访问逻辑。
虽然在ApplicationContext
加载时初始化一个用于测试曾经的数据库是非常有用的,但有时能够修改数据库期间集成测试是必不可少的。下面的部分解释了如何在集成测试期间以编程方式和声明式方式运行 SQL 脚本。
# 以编程方式执行 SQL 脚本
Spring 提供了以下用于在集成测试方法中以编程方式执行 SQL 脚本的选项。
org.springframework.jdbc.datasource.init.ScriptUtils
org.springframework.jdbc.datasource.init.ResourceDatabasePopulator
org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests
org.springframework.test.context.testng.AbstractTransactionalTestNGSpringContextTests
ScriptUtils
提供了一组用于处理 SQL 脚本的静态实用程序方法,主要用于框架内的内部使用。但是,如果你需要完全控制 SQL 脚本的解析和运行方式,ScriptUtils
可能比后面介绍的其他一些替代方案更适合你的需要。有关更多详细信息,请参见javadoc (opens new window)中的单个方法。
ResourceDatabasePopulator
提供了一个基于对象的 API,用于通过使用在外部资源中定义的 SQL 脚本以编程方式填充、初始化或清理数据库。ResourceDatabasePopulator
提供了用于配置字符编码、语句分隔符、注释分隔符和解析和运行脚本时使用的错误处理标志的选项。每个配置选项都有一个合理的默认值。有关默认值的详细信息,请参见javadoc (opens new window)。要运行在ResourceDatabasePopulator
中配置的脚本,你可以调用populate(Connection)
方法来针对java.sql.Connection
运行填充器,也可以调用execute(DataSource)
方法来针对javax.sql.DataSource
运行填充器。下面的示例为测试模式和测试数据指定 SQL 脚本,将语句分隔符设置为@@
,并针对DataSource
运行脚本:
爪哇
@Test
void databaseTest() {
ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
populator.addScripts(
new ClassPathResource("test-schema.sql"),
new ClassPathResource("test-data.sql"));
populator.setSeparator("@@");
populator.execute(this.dataSource);
// run code that uses the test schema and data
}
Kotlin
@Test
fun databaseTest() {
val populator = ResourceDatabasePopulator()
populator.addScripts(
ClassPathResource("test-schema.sql"),
ClassPathResource("test-data.sql"))
populator.setSeparator("@@")
populator.execute(dataSource)
// run code that uses the test schema and data
}
请注意,ResourceDatabasePopulator
内部委托给ScriptUtils
用于解析和运行 SQL 脚本。类似地,[executeSqlScript(..)
](#testcontext-support-classes-junit4)和[AbstractTransactionalTestNGSpringContextTests
](#testcontext-support-classes-testng)中的executeSqlScript(..)
方法在内部使用ResourceDatabasePopulator
来运行 SQL 脚本。有关更多详细信息,请参见 爪哇doc 获取各种executeSqlScript(..)
方法。
# 使用 @sql 声明式执行 SQL 脚本
除了上述以编程方式运行 SQL 脚本的机制外,还可以在 Spring TestContext 框架中声明性地配置 SQL 脚本。具体地说,你可以在测试类或测试方法上声明@Sql
注释,以配置应在集成测试方法之前或之后针对给定数据库运行的 SQL 脚本的各个 SQL 语句或资源路径。对@Sql
的支持由SqlScriptsTestExecutionListener
提供,默认情况下启用。
方法级别@Sql 声明默认重写类级别声明。然而,作为 Spring Framework5.2 的,该行为可以通过 @SqlMergeMode 配置为每个测试类或每个测试方法。有关更多详细信息,请参见[使用 @SqlMergeMode 合并和覆盖配置]。 |
---|
# 路径资源语义
每个路径被解释为 Spring Resource
。普通路径(例如,"schema.sql"
)被视为与定义测试类的包相关的 Classpath 资源。以斜杠开头的路径被视为绝对 Classpath 资源(例如,"/org/example/schema.sql"
)。通过使用指定的资源协议加载引用 URL 的路径(例如,带有classpath:
、file:
、http:
前缀的路径)。
下面的示例展示了如何在类级别和基于 JUnit Jupiter 的集成测试类中的方法级别上使用@Sql
:
爪哇
@SpringJUnitConfig
@Sql("/test-schema.sql")
class DatabaseTests {
@Test
void emptySchemaTest() {
// run code that uses the test schema without any test data
}
@Test
@Sql({"/test-schema.sql", "/test-user-data.sql"})
void userTest() {
// run code that uses the test schema and test data
}
}
Kotlin
@SpringJUnitConfig
@Sql("/test-schema.sql")
class DatabaseTests {
@Test
fun emptySchemaTest() {
// run code that uses the test schema without any test data
}
@Test
@Sql("/test-schema.sql", "/test-user-data.sql")
fun userTest() {
// run code that uses the test schema and test data
}
}
# 默认脚本检测
如果没有指定 SQL 脚本或语句,则尝试检测default
脚本,这取决于声明@Sql
的位置。如果无法检测到默认值,则抛出IllegalStateException
。
类级声明:如果带注释的测试类是
com.example.MyTest
,则对应的默认脚本是classpath:com/example/MyTest.sql
。方法级别声明:如果带注释的测试方法名为
testMethod()
,并且在类com.example.MyTest
中定义,那么对应的默认脚本是classpath:com/example/MyTest.testMethod.sql
。
# 声明多个@Sql
集
如果需要为给定的测试类或测试方法配置多组 SQL 脚本,但语法配置不同,错误处理规则不同,或者每组执行阶段不同,则可以声明@Sql
的多个实例。对于 爪哇8,你可以使用@Sql
作为可重复的注释。否则,你可以使用@SqlGroup
注释作为显式容器来声明@Sql
的多个实例。
下面的示例展示了如何使用@Sql
作为 爪哇8 的可重复注释:
爪哇
@Test
@Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`"))
@Sql("/test-user-data.sql")
void userTest() {
// run code that uses the test schema and test data
}
Kotlin
// Repeatable annotations with non-SOURCE retention are not yet supported by Kotlin
在前面示例中介绍的场景中,test-schema.sql
脚本对单行注释使用了不同的语法。
下面的示例与前面的示例相同,只是@Sql
声明被组合在@SqlGroup
中。对于 爪哇8 及以上版本,@SqlGroup
的使用是可选的,但是你可能需要使用@SqlGroup
来与其他 JVM 语言(如 Kotlin)兼容。
爪哇
@Test
@SqlGroup({
@Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`")),
@Sql("/test-user-data.sql")
)}
void userTest() {
// run code that uses the test schema and test data
}
Kotlin
@Test
@SqlGroup(
Sql("/test-schema.sql", config = SqlConfig(commentPrefix = "`")),
Sql("/test-user-data.sql"))
fun userTest() {
// Run code that uses the test schema and test data
}
# 脚本执行阶段
默认情况下,SQL 脚本在相应的测试方法之前运行。但是,如果需要在测试方法之后运行一组特定的脚本(例如,清理数据库状态),则可以在executionPhase
中使用@Sql
属性,如下例所示:
爪哇
@Test
@Sql(
scripts = "create-test-data.sql",
config = @SqlConfig(transactionMode = ISOLATED)
)
@Sql(
scripts = "delete-test-data.sql",
config = @SqlConfig(transactionMode = ISOLATED),
executionPhase = AFTER_TEST_METHOD
)
void userTest() {
// run code that needs the test data to be committed
// to the database outside of the test's transaction
}
Kotlin
@Test
@SqlGroup(
Sql("create-test-data.sql",
config = SqlConfig(transactionMode = ISOLATED)),
Sql("delete-test-data.sql",
config = SqlConfig(transactionMode = ISOLATED),
executionPhase = AFTER_TEST_METHOD))
fun userTest() {
// run code that needs the test data to be committed
// to the database outside of the test's transaction
}
注意,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
支持的配置选项等价,但它们是由<jdbc:initialize-database/>
XML 名称空间元素提供的那些选项的超集。参见[@Sql
](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/test/context/jdbc/sql.html)和[@SqlConfig
](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api-api/org/context/jdbc/jdbc/sqlconframework.html)中的各个属性的 爪哇doc,以获取详细信息。
**@Sql
的事务管理 **
默认情况下,SqlScriptsTestExecutionListener
为使用@Sql
配置的脚本推断所需的事务语义。具体地说,SQL 脚本在没有事务的情况下运行,在现有 Spring 管理的事务(例如,由TransactionalTestExecutionListener
管理的事务,用于用@Transactional
注释的测试)中运行,或者在独立的事务中运行,取决于@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
自动回滚(有关详细信息,请参见事务管理)。
# 用@SqlMergeMode
合并和覆盖配置
在 Spring Framework5.2 中,可以将方法级@Sql
声明与类级声明合并。例如,这允许你为每个测试类提供一次数据库模式或一些公共测试数据的配置,然后为每个测试方法提供额外的、特定于用例的测试数据。要启用@Sql
合并,请用@SqlMergeMode(MERGE)
注释你的测试类或测试方法。要禁用特定测试方法(或特定测试子类)的合并,可以通过@SqlMergeMode(OVERRIDE)
切换回默认模式。有关示例和更多详细信息,请参阅[@SqlMergeMode
注释文档部分](# Spring-testing-annotation-sqlmergemode)。
# 3.5.11.并行测试执行
Spring Framework5.0 引入了在使用 Spring TestContext 框架时在单个 JVM 内并行执行测试的基本支持。通常,这意味着大多数测试类或测试方法可以并行运行,而不需要对测试代码或配置进行任何更改。
有关如何设置并行测试执行的详细信息,请参阅 测试框架、构建工具或 IDE 的文档。 |
---|
请记住,在测试套件中引入并发可能会导致意外的副作用、奇怪的运行时行为,以及间歇性或似乎随机地失败的测试。 Spring 因此,团队为何时不并行运行测试提供了以下一般准则。
如果测试:
使用 Spring Framework 的
@DirtiesContext
支持。使用 Spring Boot 的
@MockBean
或@SpyBean
支持。使用 JUnit4 的
@FixMethodOrder
支持或任何旨在确保测试方法以特定顺序运行的测试框架功能。但是,请注意,如果整个测试类并行运行,则不适用于此。更改共享服务或系统(如数据库、消息代理、文件系统等)的状态。这既适用于嵌入式系统,也适用于外部系统。
如果并行测试执行失败,并且异常地声明当前测试的ApplicationContext 不再活动,这通常意味着ApplicationContext 在不同的线程中从ContextCache 中被删除。这可能是由于使用 @DirtiesContext 或由于从ContextCache 中自动驱逐所致。如果@DirtiesContext 是罪魁祸首,则需要找到一种方法来避免使用 @DirtiesContext ,或者将此类测试排除在并行执行之外。如果已超过 ContextCache 的最大大小,则可以增加缓存的最大大小。详见上下文缓存讨论。 |
---|
Spring TestContext 框架中的并行测试执行只有在以下情况下才是可能的: 底层 TestContext 实现提供了一个复制构造函数,如中所解释的[ TestContext ]的 爪哇doc(https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/test/text/context.html)。 Spring 中使用的DefaultTestContext 提供了这样的构造函数。但是,如果使用提供自定义实现的 TestContext 第三方库,则需要验证它是否适合并行测试执行。 |
---|
# 3.5.12.TestContext 框架支持类
本节描述了支持 Spring TestContext 框架的各种类。
# Spring JUnit4Runner
Spring TestContext 框架通过自定义运行器(在 JUnit4.12 或更高版本上支持)提供与 JUnit4 的完全集成。通过使用@RunWith(SpringJUnit4ClassRunner.class)
或更短的@RunWith(SpringRunner.class)
变体注释测试类,开发人员可以实现基于标准 JUnit4 的单元和集成测试,并同时获得 TestContext 框架的好处,例如对加载应用程序上下文、测试实例的依赖注入、事务性测试方法执行的支持,等等。如果希望将 Spring TestContext 框架与可选的运行器(例如 JUnit4 的Runner)或第三方运行器(例如)一起使用,则可以选择使用。
下面的代码清单显示了配置使用自定义 Spring Runner
运行的测试类的最低要求:
爪哇
@RunWith(SpringRunner.class)
@TestExecutionListeners({})
public class SimpleTest {
@Test
public void testMethod() {
// test logic...
}
}
Kotlin
@RunWith(SpringRunner::class)
@TestExecutionListeners
class SimpleTest {
@Test
fun testMethod() {
// test logic...
}
}
在前面的示例中,@TestExecutionListeners
被配置为空列表,以禁用默认侦听器,否则将需要通过ApplicationContext
配置@ContextConfiguration
。
# Spring JUnit4 规则
org.springframework.test.context.junit4.rules
包提供了以下 JUnit4 规则(JUnit4.12 或更高版本支持):
SpringClassRule
SpringMethodRule
SpringClassRule
是支持 Spring TestContext 框架的类级特性的 JUnitTestRule
,而SpringMethodRule
是支持 Spring TestContext 框架的实例级和方法级特性的 JUnitMethodRule
。
与SpringRunner
相反, Spring 的基于规则的 JUnit 支持具有独立于任何org.junit.runner.Runner
实现的优点,因此可以与现有的替代运行器(例如 JUnit4 的Parameterized
)或第三方运行器(例如MockitoJUnitRunner
)组合。
要支持 TestContext 框架的全部功能,你必须将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 工具一起使用时,请务必避免误报。正如在JDBC 测试支持中提到的,AbstractTransactionalJUnit4SpringContextTests
还提供了方便的方法,通过使用前面提到的JdbcTestUtils
将方法委托给jdbcTemplate
中的方法。此外,AbstractTransactionalJUnit4SpringContextTests
提供了一个executeSqlScript(..)
方法,用于针对配置的DataSource
运行 SQL 脚本。
这些类为扩展提供了方便。如果不希望你的测试类 绑定到 Spring 特定的类层次结构,则可以使用 或Spring’s JUnit rules配置你自己的自定义测试类。 |
---|
# 朱尼特木星的 SpringExtension
Spring TestContext 框架提供了与 JUnit Jupiter 测试框架的完全集成,JUnit5 中介绍了该测试框架。通过使用@ExtendWith(SpringExtension.class)
对测试类进行注释,你可以实现标准的基于 JUnit Jupiter 的单元和集成测试,并同时获得 TestContext 框架的好处,例如对加载应用程序上下文的支持、测试实例的依赖注入、事务测试方法的执行,等等。
此外,由于 JUnit Jupiter 中丰富的扩展 API, Spring 在 Spring 支持 JUnit4 和 TestNG 的功能集之上和之上提供了以下功能:
用于测试构造函数、测试方法和测试生命周期回调方法的依赖注入。有关更多详细信息,请参见[dependency injection with
SpringExtension
]。基于 SPEL 表达式、环境变量、系统属性等对条件测试执行 (opens new window)的强大支持。有关更多详细信息和示例,请参见
@EnabledIf
和@DisabledIf
中的文档。自定义合成的注释结合了来自 Spring 和 JUnit Jupiter 的注释。有关更多详细信息,请参见
@TransactionalDevTestConfig
和@TransactionalIntegrationTest
中的示例。
下面的代码清单显示了如何配置一个测试类,以便将SpringExtension
与@ContextConfiguration
结合使用:
爪哇
// Instructs JUnit Jupiter to extend the test with Spring support.
@ExtendWith(SpringExtension.class)
// Instructs Spring to load an ApplicationContext from TestConfig.class
@ContextConfiguration(classes = TestConfig.class)
class SimpleTests {
@Test
void testMethod() {
// test logic...
}
}
Kotlin
// Instructs JUnit Jupiter to extend the test with Spring support.
@ExtendWith(SpringExtension::class)
// Instructs Spring to load an ApplicationContext from TestConfig::class
@ContextConfiguration(classes = [TestConfig::class])
class SimpleTests {
@Test
fun testMethod() {
// test logic...
}
}
由于还可以使用 JUnit5 中的注释作为元注释, Spring 提供了@SpringJUnitConfig
和@SpringJUnitWebConfig
组合注释,以简化测试ApplicationContext
和 JUnit Jupiter 的配置。
下面的示例使用@SpringJUnitConfig
来减少前面示例中使用的配置量:
爪哇
// Instructs Spring to register the SpringExtension with JUnit
// Jupiter and load an ApplicationContext from TestConfig.class
@SpringJUnitConfig(TestConfig.class)
class SimpleTests {
@Test
void testMethod() {
// test logic...
}
}
Kotlin
// Instructs Spring to register the SpringExtension with JUnit
// Jupiter and load an ApplicationContext from TestConfig.class
@SpringJUnitConfig(TestConfig::class)
class SimpleTests {
@Test
fun testMethod() {
// test logic...
}
}
类似地,下面的示例使用@SpringJUnitWebConfig
创建一个WebApplicationContext
,用于 JUnit Jupiter:
爪哇
// Instructs Spring to register the SpringExtension with JUnit
// Jupiter and load a WebApplicationContext from TestWebConfig.class
@SpringJUnitWebConfig(TestWebConfig.class)
class SimpleWebTests {
@Test
void testMethod() {
// test logic...
}
}
Kotlin
// Instructs Spring to register the SpringExtension with JUnit
// Jupiter and load a WebApplicationContext from TestWebConfig::class
@SpringJUnitWebConfig(TestWebConfig::class)
class SimpleWebTests {
@Test
fun testMethod() {
// test logic...
}
}
有关更多详细信息,请参见@SpringJUnitConfig
和@SpringJUnitWebConfig
中的文档。
# 依赖注入与SpringExtension
SpringExtension
实现了来自 JUnit Jupiter 的[ParameterResolver
](https://junit.org/junit5/DOCS/current/user-guide/#extensions-parameter-resolution)扩展 API,它允许 Spring 为测试构造函数、测试方法和测试生命周期回调方法提供依赖注入。
具体地说,SpringExtension
可以将来自测试的ApplicationContext
的依赖关系注入到使用@BeforeAll
、@AfterAll
、@BeforeEach
、@AfterEach
、@RepeatedTest
、@RepeatedTest
、@ParameterizedTest
等注释的测试构造函数和方法中。
# 构造函数注入
如果 JUnit Jupiter 测试类的构造函数中的特定参数类型为ApplicationContext
(或其子类型),或者被注释或 meta 注释为@Autowired
,@Qualifier
,或@Value
, Spring 将该特定参数的值注入相应的 Bean 或来自测试的ApplicationContext
的值。
Spring 还可以被配置为在一个测试类构造函数被认为是可自动连接的情况下自动连接用于该构造函数的所有参数。如果满足以下条件之一(按优先顺序),则构造函数被认为是可自动连接的。
构造函数用
@Autowired
注释。@TestConstructor
在测试类上存在或元存在,并且autowireMode
属性设置为ALL
。默认的测试构造函数 AutoWire 模式已更改为
ALL
。
有关@TestConstructor
的使用以及如何更改全局测试构造函数 AutoWire 模式的详细信息,请参见[@TestConstructor
](#Integration-Testing-Annotations-TestConstructor)。
如果一个测试类的构造函数被认为是可自动连接,则 Spring 承担解决构造函数中所有参数的参数的责任。 因此,在 JUnit Jupiter 注册的其他 ParameterResolver 都不能为这样的构造函数解析参数。 |
---|
测试类的构造函数注入不能与 JUnit Jupiter 的 @TestInstance(PER_CLASS) 支持一起使用如果@DirtiesContext 是用来关闭测试的 ApplicationContext 测试方法之前或之后的的,原因是 @TestInstance(PER_CLASS) 指示 JUnit Jupiter 在测试方法调用之间缓存测试实例。因此,测试实例将保留 对 bean 的引用,这些 bean 最初是从 ApplicationContext 注入的,而随后被关闭。由于在这种情况下测试类的构造函数只会被调用 一次,因此依赖项注入将不会再次发生,而随后的测试 将与来自闭合的 ApplicationContext 的 bean 进行交互,这可能会导致错误。将 @DirtiesContext 与“测试方法之前”或“测试方法之后”的模式在中与 @TestInstance(PER_CLASS) 结合,必须将 Spring 中的依赖项配置为通过字段或 setter 注入提供,以便它们可以在测试 方法调用之间重新注入。 |
---|
在下面的示例中, Spring 将从OrderService
Bean 加载的ApplicationContext
中的TestConfig.class
注入到OrderServiceIntegrationTests
构造函数中。
爪哇
@SpringJUnitConfig(TestConfig.class)
class OrderServiceIntegrationTests {
private final OrderService orderService;
@Autowired
OrderServiceIntegrationTests(OrderService orderService) {
this.orderService = orderService;
}
// tests that use the injected OrderService
}
Kotlin
@SpringJUnitConfig(TestConfig::class)
class OrderServiceIntegrationTests @Autowired constructor(private val orderService: OrderService){
// tests that use the injected OrderService
}
请注意,这个特性允许测试依赖项final
,因此是不可变的。
如果spring.test.constructor.autowire.mode
属性是all
(参见[@TestConstructor
](#Integration-Testing-Annotations-TestConstructor)),我们可以在前面的示例中省略构造函数上@Autowired
的声明,从而产生以下结果。
爪哇
@SpringJUnitConfig(TestConfig.class)
class OrderServiceIntegrationTests {
private final OrderService orderService;
OrderServiceIntegrationTests(OrderService orderService) {
this.orderService = orderService;
}
// tests that use the injected OrderService
}
Kotlin
@SpringJUnitConfig(TestConfig::class)
class OrderServiceIntegrationTests(val orderService:OrderService) {
// tests that use the injected OrderService
}
# 方法注入
如果 JUnit Jupiter 测试方法或测试生命周期回调方法中的参数类型为ApplicationContext
(或其子类型),或被注释或 meta 注释为@Autowired
,@Qualifier
,或@Value
, Spring 从测试的ApplicationContext
中将该特定参数的值注入相应的 Bean。
在下面的示例中, Spring 将从OrderService
加载的ApplicationContext
中的TestConfig.class
注入到deleteOrder()
测试方法中:
爪哇
@SpringJUnitConfig(TestConfig.class)
class OrderServiceIntegrationTests {
@Test
void deleteOrder(@Autowired OrderService orderService) {
// use orderService from the test's ApplicationContext
}
}
Kotlin
@SpringJUnitConfig(TestConfig::class)
class OrderServiceIntegrationTests {
@Test
fun deleteOrder(@Autowired orderService: OrderService) {
// use orderService from the test's ApplicationContext
}
}
由于 JUnit Jupiter 中ParameterResolver
支持的健壮性,你还可以将多个依赖注入到单个方法中,不仅来自 Spring,还来自 JUnit Jupiter 本身或其他第三方扩展。
下面的示例展示了如何让 Spring 和 JUnit Jupiter 同时向placeOrderRepeatedly()
测试方法中注入依赖项。
爪哇
@SpringJUnitConfig(TestConfig.class)
class OrderServiceIntegrationTests {
@RepeatedTest(10)
void placeOrderRepeatedly(RepetitionInfo repetitionInfo,
@Autowired OrderService orderService) {
// use orderService from the test's ApplicationContext
// and repetitionInfo from JUnit Jupiter
}
}
Kotlin
@SpringJUnitConfig(TestConfig::class)
class OrderServiceIntegrationTests {
@RepeatedTest(10)
fun placeOrderRepeatedly(repetitionInfo:RepetitionInfo, @Autowired orderService:OrderService) {
// use orderService from the test's ApplicationContext
// and repetitionInfo from JUnit Jupiter
}
}
注意,使用来自 Junit Jupiter 的@RepeatedTest
可以让测试方法访问RepetitionInfo
。
# @Nested
测试类配置
自 Spring Framework5.0 以来,Spring TestContext Framework一直支持在 JUnit Jupiter 中的@Nested
测试类上使用与测试相关的注释;然而,在 Spring Framework5.3 之前,类级测试配置注释并不像来自超类那样来自封闭类的继承。
Spring Framework5.3 引入了用于从封闭类继承测试类配置的第一类支持,并且这样的配置将在默认情况下被继承。要从默认的INHERIT
模式更改为OVERRIDE
模式,你可以用@Nested
注释单个@Nested
测试类。显式的@NestedTestConfiguration
声明将应用于带注释的测试类及其任何子类和嵌套的类。因此,你可以用@NestedTestConfiguration
注释顶级测试类,这将递归地应用于它的所有嵌套测试类。
为了允许开发团队将缺省模式更改为OVERRIDE
——例如,为了与 Spring Framework5.0 到 5.2 兼容——缺省模式可以通过 JVM 系统属性或 Classpath 根中的spring.properties
文件进行全局更改。有关详细信息,请参见“更改默认的封闭配置继承模式”注释。
尽管下面的“Hello World”示例非常简单,但它展示了如何在顶级类上声明公共配置,该类由其@Nested
测试类继承。在这个特定的示例中,只继承了TestConfig
配置类。每个嵌套测试类提供自己的一组活动配置文件,从而为每个嵌套测试类提供一个不同的ApplicationContext
(有关详细信息,请参见上下文缓存)。请参阅支持的注释列表,以查看哪些注释可以在@Nested
测试类中继承。
爪哇
@SpringJUnitConfig(TestConfig.class)
class GreetingServiceTests {
@Nested
@ActiveProfiles("lang_en")
class EnglishGreetings {
@Test
void hello(@Autowired GreetingService service) {
assertThat(service.greetWorld()).isEqualTo("Hello World");
}
}
@Nested
@ActiveProfiles("lang_de")
class GermanGreetings {
@Test
void hello(@Autowired GreetingService service) {
assertThat(service.greetWorld()).isEqualTo("Hallo Welt");
}
}
}
Kotlin
@SpringJUnitConfig(TestConfig::class)
class GreetingServiceTests {
@Nested
@ActiveProfiles("lang_en")
inner class EnglishGreetings {
@Test
fun hello(@Autowired service:GreetingService) {
assertThat(service.greetWorld()).isEqualTo("Hello World")
}
}
@Nested
@ActiveProfiles("lang_de")
inner class GermanGreetings {
@Test
fun hello(@Autowired service:GreetingService) {
assertThat(service.greetWorld()).isEqualTo("Hallo Welt")
}
}
}
# TestNG 支持类
org.springframework.test.context.testng
包为基于 TestNG 的测试用例提供了以下支持类:
AbstractTestNGSpringContextTests
AbstractTransactionalTestNGSpringContextTests
AbstractTestNGSpringContextTests
是一个抽象基测试类,它将 Spring TestContext 框架与 TestNG 环境中显式的ApplicationContext
测试支持集成在一起。扩展AbstractTestNGSpringContextTests
时,可以访问protected``applicationContext
实例变量,你可以使用该变量执行显式 Bean 查找或测试整个上下文的状态。
AbstractTransactionalTestNGSpringContextTests
是AbstractTestNGSpringContextTests
的抽象事务扩展,它为 JDBC 访问添加了一些方便的功能。该类期望在javax.sql.DataSource
Bean 和PlatformTransactionManager
Bean 中定义ApplicationContext
。扩展AbstractTransactionalTestNGSpringContextTests
时,可以访问protected``jdbcTemplate
实例变量,你可以使用该变量运行 SQL 语句来查询数据库。可以使用这样的查询来确认在运行数据库相关的应用程序代码之前和之后的数据库状态,并且 Spring 确保这样的查询在与应用程序代码相同的事务范围内运行。当与 ORM 工具一起使用时,请务必避免误报。正如在JDBC 测试支持中提到的,AbstractTransactionalTestNGSpringContextTests
还提供了方便的方法,通过使用前面提到的jdbcTemplate
将方法委托给JdbcTestUtils
中的方法。此外,AbstractTransactionalTestNGSpringContextTests
提供了一个executeSqlScript(..)
方法,用于针对配置的DataSource
运行 SQL 脚本。
这些类为扩展提供了方便。如果你不希望你的测试类 被绑定到 Spring 特定的类层次结构,那么你可以通过使用 @ContextConfiguration 、@TestExecutionListeners 以此类推,并通过手动使用 TestContextManager 来配置你自己的自定义测试类。请参阅源代码的 AbstractTestNGSpringContextTests ,以获得如何测试类的示例。 |
---|
# 3.6.WebTestClient
WebTestClient
是一个用于测试服务器应用程序的 HTTP 客户机。它封装了 Spring 的WebClient,并使用它来执行请求,但公开了一个用于验证响应的测试 facade。WebTestClient
可用于执行端到端的 HTTP 测试。 Spring MVC 和 Spring WebFlux 应用程序也可用于在没有运行服务器的情况下通过模拟服务器来测试请求和响应对象。
Kotlin 用户:参见本节相关的WebTestClient 的使用。 |
---|
# 3.6.1.设置
要设置WebTestClient
,你需要选择要绑定到的服务器设置。这可以是几个模拟服务器设置选项中的一个,也可以是与实时服务器的连接。
# 绑定到控制器
此设置允许你通过模拟请求和响应对象测试特定的控制器,而无需运行服务器。
对于 WebFlux 应用程序,使用以下方法加载与WebFlux 爪哇 配置等价的基础设施,注册给定的控制器,并创建Webhandler 链来处理请求:
爪哇
WebTestClient client =
WebTestClient.bindToController(new TestController()).build();
Kotlin
val client = WebTestClient.bindToController(TestController()).build()
对于 Spring MVC,使用以下方法委托给StandalOneMockmvcBuilder (opens new window)以加载与WebMVC 爪哇 配置等价的基础设施,注册给定的控制器,并创建MockMvc的实例来处理请求:
爪哇
WebTestClient client =
MockMvcWebTestClient.bindToController(new TestController()).build();
Kotlin
val client = MockMvcWebTestClient.bindToController(TestController()).build()
# 绑定到ApplicationContext
这种设置允许你使用 Spring MVC 或 Spring WebFlux 基础设施和控制器声明加载 Spring 配置,并使用它通过模拟请求和响应对象来处理请求,而无需运行服务器。
对于 WebFlux,使用以下方法将 Spring ApplicationContext
传递到WebHttphandlerBuilder (opens new window),以创建Webhandler 链来处理请求:
爪哇
@SpringJUnitConfig(WebConfig.class) (1)
class MyTests {
WebTestClient client;
@BeforeEach
void setUp(ApplicationContext context) { (2)
client = WebTestClient.bindToApplicationContext(context).build(); (3)
}
}
1 | 指定要加载的配置 |
---|---|
2 | 注入配置 |
3 | 创建WebTestClient |
Kotlin
@SpringJUnitConfig(WebConfig::class) (1)
class MyTests {
lateinit var client: WebTestClient
@BeforeEach
fun setUp(context: ApplicationContext) { (2)
client = WebTestClient.bindToApplicationContext(context).build() (3)
}
}
1 | 指定要加载的配置 |
---|---|
2 | 注入配置 |
3 | 创建WebTestClient |
对于 Spring MVC,使用以下方法,其中将 Spring ApplicationContext
传递到mockmvcbuilders.webappcontextsetup (opens new window),以创建MockMvc实例来处理请求:
爪哇
@ExtendWith(SpringExtension.class)
@WebAppConfiguration("classpath:META-INF/web-resources") (1)
@ContextHierarchy({
@ContextConfiguration(classes = RootConfig.class),
@ContextConfiguration(classes = WebConfig.class)
})
class MyTests {
@Autowired
WebApplicationContext wac; (2)
WebTestClient client;
@BeforeEach
void setUp() {
client = MockMvcWebTestClient.bindToApplicationContext(this.wac).build(); (3)
}
}
1 | 指定要加载的配置 |
---|---|
2 | 注入配置 |
3 | 创建WebTestClient |
Kotlin
@ExtendWith(SpringExtension.class)
@WebAppConfiguration("classpath:META-INF/web-resources") (1)
@ContextHierarchy({
@ContextConfiguration(classes = RootConfig.class),
@ContextConfiguration(classes = WebConfig.class)
})
class MyTests {
@Autowired
lateinit var wac: WebApplicationContext; (2)
lateinit var client: WebTestClient
@BeforeEach
fun setUp() { (2)
client = MockMvcWebTestClient.bindToApplicationContext(wac).build() (3)
}
}
1 | 指定要加载的配置 |
---|---|
2 | 注入配置 |
3 | 创建WebTestClient |
# 绑定到路由器功能
这种设置允许你通过模拟请求和响应对象来测试功能端点,而不需要运行服务器。
对于 WebFlux,使用以下方法委托RouterFunctions.toWebHandler
来创建服务器设置以处理请求:
爪哇
RouterFunction<?> route = ...
client = WebTestClient.bindToRouterFunction(route).build();
Kotlin
val route: RouterFunction<*> = ...
val client = WebTestClient.bindToRouterFunction(route).build()
对于 Spring MVC,目前没有测试WebMVC 功能端点的选项。
# 绑定到服务器
此设置连接到正在运行的服务器,以执行完整的端到端 HTTP 测试:
爪哇
client = WebTestClient.bindToServer().baseUrl("http://localhost:8080").build();
Kotlin
client = WebTestClient.bindToServer().baseUrl("http://localhost:8080").build()
# 客户端配置
除了前面描述的服务器设置选项外,还可以配置客户端选项,包括基本 URL、默认标头、客户端过滤器和其他选项。这些选项在bindToServer()
之后很容易获得。对于所有其他配置选项,你需要使用configureClient()
来从服务器配置转换到客户机配置,如下所示:
爪哇
client = WebTestClient.bindToController(new TestController())
.configureClient()
.baseUrl("/test")
.build();
Kotlin
client = WebTestClient.bindToController(TestController())
.configureClient()
.baseUrl("/test")
.build()
# 3.6.2.写作测试
WebTestClient
提供与WebClient相同的 API,直到使用exchange()
执行请求为止。请参阅WebClient文档中的示例,以了解如何准备包含表单数据、多部分数据等任何内容的请求。
在调用exchange()
之后,WebTestClient
偏离了WebClient
,而是继续使用工作流来验证响应。
要断言响应状态和头,请使用以下方法:
爪哇
client.get().uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON);
Kotlin
client.get().uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
如果你希望所有的期望都被断言,即使其中一个失败了,你可以使用expectAll(..)
,而不是使用多个链接的expect*(..)
调用。此功能类似于 AssertJ 中的软断言支持和 JUnit Jupiter 中的assertAll()
支持。
爪哇
client.get().uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectAll(
spec -> spec.expectStatus().isOk(),
spec -> spec.expectHeader().contentType(MediaType.APPLICATION_JSON)
);
然后,你可以选择通过以下方式之一对响应体进行解码:
expectBody(Class<T>)
:解码为单个对象。expectBodyList(Class<T>)
:将对象解码并收集到List<T>
。expectBody()
:将byte[]
或空体解码为JSON 内容。
并在生成的较高级别对象上执行断言:
爪哇
client.get().uri("/persons")
.exchange()
.expectStatus().isOk()
.expectBodyList(Person.class).hasSize(3).contains(person);
Kotlin
import org.springframework.test.web.reactive.server.expectBodyList
client.get().uri("/persons")
.exchange()
.expectStatus().isOk()
.expectBodyList<Person>().hasSize(3).contains(person)
如果内置断言不足,则可以使用该对象并执行任何其他断言:
爪哇
import org.springframework.test.web.reactive.server.expectBody
client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody(Person.class)
.consumeWith(result -> {
// custom assertions (e.g. AssertJ)...
});
Kotlin
client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody<Person>()
.consumeWith {
// custom assertions (e.g. AssertJ)...
}
或者,你可以退出工作流并获得EntityExchangeResult
:
爪哇
EntityExchangeResult<Person> result = client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody(Person.class)
.returnResult();
Kotlin
import org.springframework.test.web.reactive.server.expectBody
val result = client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk
.expectBody<Person>()
.returnResult()
当你需要用泛型解码到目标类型时,请查找接受[ ](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/core/core/parameterizedtyreference.html)而不是<gtr="2502"/>)的重载方法<gtr=“2503”。 |
---|
# 无内容
如果预期响应不包含内容,则可以断言如下:
爪哇
client.post().uri("/persons")
.body(personMono, Person.class)
.exchange()
.expectStatus().isCreated()
.expectBody().isEmpty();
Kotlin
client.post().uri("/persons")
.bodyValue(person)
.exchange()
.expectStatus().isCreated()
.expectBody().isEmpty()
如果你想忽略响应内容,那么下面的内容将在没有任何断言的情况下发布:
爪哇
client.get().uri("/persons/123")
.exchange()
.expectStatus().isNotFound()
.expectBody(Void.class);
Kotlin
client.get().uri("/persons/123")
.exchange()
.expectStatus().isNotFound
.expectBody<Unit>()
# JSON 内容
你可以在没有目标类型的情况下使用expectBody()
对原始内容执行断言,而不是通过更高级别的对象。
要用JSONAssert (opens new window)验证完整的 JSON 内容:
爪哇
client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody()
.json("{\"name\":\"Jane\"}")
Kotlin
client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody()
.json("{\"name\":\"Jane\"}")
要用JSONPath (opens new window)验证 JSON 内容:
爪哇
client.get().uri("/persons")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$[0].name").isEqualTo("Jane")
.jsonPath("$[1].name").isEqualTo("Jason");
Kotlin
client.get().uri("/persons")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$[0].name").isEqualTo("Jane")
.jsonPath("$[1].name").isEqualTo("Jason")
# 流式响应
要测试可能无限的流,例如"text/event-stream"
或"application/x-ndjson"
,首先要验证响应状态和头,然后获得FluxExchangeResult
:
爪哇
FluxExchangeResult<MyEvent> result = client.get().uri("/events")
.accept(TEXT_EVENT_STREAM)
.exchange()
.expectStatus().isOk()
.returnResult(MyEvent.class);
Kotlin
import org.springframework.test.web.reactive.server.returnResult
val result = client.get().uri("/events")
.accept(TEXT_EVENT_STREAM)
.exchange()
.expectStatus().isOk()
.returnResult<MyEvent>()
现在,你可以使用StepVerifier
中的reactor-test
来使用响应流了:
爪哇
Flux<Event> eventFlux = result.getResponseBody();
StepVerifier.create(eventFlux)
.expectNext(person)
.expectNextCount(4)
.consumeNextWith(p -> ...)
.thenCancel()
.verify();
Kotlin
val eventFlux = result.getResponseBody()
StepVerifier.create(eventFlux)
.expectNext(person)
.expectNextCount(4)
.consumeNextWith { p -> ... }
.thenCancel()
.verify()
# MockMVC 断言
WebTestClient
是一个 HTTP 客户机,因此它只能验证客户机响应中的内容,包括状态、头和主体。
在使用 mockMVC 服务器设置测试 Spring MVC 应用程序时,你有额外的选择来对服务器响应执行进一步的断言。要做到这一点,首先要在断言主体之后获得ExchangeResult
:
爪哇
// For a response with a body
EntityExchangeResult<Person> result = client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody(Person.class)
.returnResult();
// For a response without a body
EntityExchangeResult<Void> result = client.get().uri("/path")
.exchange()
.expectBody().isEmpty();
Kotlin
// For a response with a body
val result = client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody(Person.class)
.returnResult();
// For a response without a body
val result = client.get().uri("/path")
.exchange()
.expectBody().isEmpty();
然后切换到 MockMVC 服务器响应断言:
爪哇
MockMvcWebTestClient.resultActionsFor(result)
.andExpect(model().attribute("integer", 3))
.andExpect(model().attribute("string", "a string value"));
Kotlin
MockMvcWebTestClient.resultActionsFor(result)
.andExpect(model().attribute("integer", 3))
.andExpect(model().attribute("string", "a string value"));
# 3.7.MockMVC
Spring MVC 测试框架,也称为 MockMVC,为测试 Spring MVC 应用程序提供了支持。 Spring 它执行完整的 MVC 请求处理,但通过模拟请求和响应对象,而不是运行中的服务器。
MockMVC 可以单独用于执行请求和验证响应。它也可以通过WebTestClient使用,其中 MockMVC 作为服务器插入以处理请求。WebTestClient
的优点是可以选择使用更高级别的对象而不是原始数据,并且可以针对实时服务器切换到完整的端到端 HTTP 测试,并使用相同的测试 API。
# 3.7.1.概述
你可以通过实例化控制器、向其注入依赖项并调用其方法来编写 Spring MVC 的普通单元测试。然而,这样的测试不会验证请求映射、数据绑定、消息转换、类型转换、验证,也不涉及任何支持@InitBinder
、@ModelAttribute
或@ExceptionHandler
的方法。
Spring MVC 测试框架,也称为MockMvc
,旨在为 Spring MVC 控制器提供更完整的测试,而无需运行服务器。它通过调用DispacherServlet
并从spring-test
模块传递“mock” implementations of the Servlet API来实现这一点,该模块在不运行服务器的情况下复制完整的 Spring MVC 请求处理。
MockMVC 是一个服务器端测试框架,它允许你使用轻量级和有针对性的测试来验证 Spring MVC 应用程序的大部分功能。你可以单独使用它来执行请求和验证响应,也可以通过WebTestClientAPI 使用它,并将 MockMVC 插入为服务器来处理请求。
# 静态导入
当直接使用 MockMVC 来执行请求时,你将需要用于以下方面的静态导入:
MockMvcBuilders.*
MockMvcRequestBuilders.*
MockMvcResultMatchers.*
MockMvcResultHandlers.*
记住这一点的一个简单方法是搜索MockMvc*
。如果使用 Eclipse,请确保在 Eclipse 首选项中也添加上面的“最喜欢的静态成员”。
当通过WebTestClient使用 mockmvc 时,不需要静态导入。WebTestClient
提供了一个没有静态导入的 Fluent API。
# 设置选项
MockMVC 可以通过以下两种方式之一设置。一种方法是直接指向你想要测试的控制器,并以编程方式配置 Spring MVC 基础设施。第二个是指向具有 Spring MVC 和控制器基础设施的 Spring 配置。
要设置用于测试特定控制器的 mockmvc,请使用以下方法:
爪哇
class MyWebTests {
MockMvc mockMvc;
@BeforeEach
void setup() {
this.mockMvc = MockMvcBuilders.standaloneSetup(new AccountController()).build();
}
// ...
}
Kotlin
class MyWebTests {
lateinit var mockMvc : MockMvc
@BeforeEach
fun setup() {
mockMvc = MockMvcBuilders.standaloneSetup(AccountController()).build()
}
// ...
}
或者,你也可以在通过WebTestClient进行测试时使用此设置,该设置将委托给与上面所示相同的构建器。
要通过 Spring 配置设置 mockmvc,请使用以下方法:
爪哇
@SpringJUnitWebConfig(locations = "my-servlet-context.xml")
class MyWebTests {
MockMvc mockMvc;
@BeforeEach
void setup(WebApplicationContext wac) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
}
// ...
}
Kotlin
@SpringJUnitWebConfig(locations = ["my-servlet-context.xml"])
class MyWebTests {
lateinit var mockMvc: MockMvc
@BeforeEach
fun setup(wac: WebApplicationContext) {
mockMvc = MockMvcBuilders.webAppContextSetup(wac).build()
}
// ...
}
或者,你也可以在通过WebTestClient进行测试时使用此设置,该设置将委托给与上面所示相同的构建器。
你应该使用哪个设置选项?
webAppContextSetup
加载你实际的 Spring MVC 配置,从而产生一个更完整的集成测试。由于 TestContext 框架缓存了加载的 Spring 配置,因此它有助于保持测试快速运行,即使你在测试套件中引入了更多的测试。此外,还可以通过 Spring 配置将模拟服务注入控制器,以保持对 Web 层的重点测试。下面的示例使用 Mockito 声明了一个模拟服务:
<bean id="accountService" class="org.mockito.Mockito" factory-method="mock">
<constructor-arg value="org.example.AccountService"/>
</bean>
然后,你可以将模拟服务注入到测试中,以设置和验证你的期望,如下例所示:
爪哇
@SpringJUnitWebConfig(locations = "test-servlet-context.xml")
class AccountTests {
@Autowired
AccountService accountService;
MockMvc mockMvc;
@BeforeEach
void setup(WebApplicationContext wac) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}
// ...
}
Kotlin
@SpringJUnitWebConfig(locations = ["test-servlet-context.xml"])
class AccountTests {
@Autowired
lateinit var accountService: AccountService
lateinit mockMvc: MockMvc
@BeforeEach
fun setup(wac: WebApplicationContext) {
mockMvc = MockMvcBuilders.webAppContextSetup(wac).build()
}
// ...
}
另一方面,standaloneSetup
更接近于单位检验。它一次测试一个控制器。你可以手动为控制器注入模拟依赖项,并且它不涉及加载 Spring 配置。这样的测试更多地关注于样式,并使其更容易地看到正在测试的控制器,是否需要任何特定的 MVC 配置来工作,等等。standaloneSetup
也是编写临时测试以验证特定行为或调试问题的一种非常方便的方法。
与大多数“集成与单元测试”的辩论一样,没有正确或错误的答案。然而,使用standaloneSetup
确实意味着需要额外的webAppContextSetup
测试,以便验证你的 Spring MVC 配置。或者,你可以使用webAppContextSetup
编写所有测试,以便始终根据实际的 Spring MVC 配置进行测试。
# 设置功能
无论你使用哪个 MockMVC Builder,所有MockMvcBuilder
实现都提供了一些常见且非常有用的特性。例如,你可以为所有请求声明一个Accept
头,并期望在所有响应中的状态为 200,以及Content-Type
头,如下所示:
爪哇
// static import of MockMvcBuilders.standaloneSetup
MockMvc mockMvc = standaloneSetup(new MusicController())
.defaultRequest(get("/").accept(MediaType.APPLICATION_JSON))
.alwaysExpect(status().isOk())
.alwaysExpect(content().contentType("application/json;charset=UTF-8"))
.build();
Kotlin
// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed
此外,第三方框架(和应用程序)可以预先打包设置指令,例如MockMvcConfigurer
中的设置指令。 Spring 框架有一个这样的内置实现,该实现有助于跨请求保存和重用 HTTP 会话。你可以按以下方式使用它:
爪哇
// static import of SharedHttpSessionConfigurer.sharedHttpSession
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new TestController())
.apply(sharedHttpSession())
.build();
// Use mockMvc to perform requests...
Kotlin
// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed
参见 爪哇doc 的[ConfigurableMockMvcBuilder
](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/test/web/ Servlet/setup/confirablemcmvcbuilder.html),以获得所有 MockMVC Builder 特性的列表,或使用 IDE 探索可用的选项。
# 执行请求
本节展示了如何单独使用 MockMVC 来执行请求和验证响应。如果通过WebTestClient
使用 mockmvc,请查看写作测试上的相应部分。
要执行使用任何 HTTP 方法的请求,如以下示例所示:
爪哇
// static import of MockMvcRequestBuilders.*
mockMvc.perform(post("/hotels/{id}", 42).accept(MediaType.APPLICATION_JSON));
Kotlin
import org.springframework.test.web.servlet.post
mockMvc.post("/hotels/{id}", 42) {
accept = MediaType.APPLICATION_JSON
}
你还可以执行内部使用MockMultipartHttpServletRequest
的文件上传请求,这样就不会实际解析多个部分的请求。相反,你必须将其设置为类似于以下示例:
Java
mockMvc.perform(multipart("/doc").file("a1", "ABC".getBytes("UTF-8")));
Kotlin
import org.springframework.test.web.servlet.multipart
mockMvc.multipart("/doc") {
file("a1", "ABC".toByteArray(charset("UTF8")))
}
你可以用 URI 模板样式指定查询参数,如下例所示:
Java
mockMvc.perform(get("/hotels?thing={thing}", "somewhere"));
Kotlin
mockMvc.get("/hotels?thing={thing}", "somewhere")
还可以添加表示查询或表单参数的 Servlet 请求参数,如下例所示:
Java
mockMvc.perform(get("/hotels").param("thing", "somewhere"));
Kotlin
import org.springframework.test.web.servlet.get
mockMvc.get("/hotels") {
param("thing", "somewhere")
}
如果应用程序代码依赖于 Servlet 请求参数,并且没有显式地检查查询字符串(通常是这种情况),那么使用哪个选项并不重要。但是,请记住,与 URI 模板一起提供的查询参数已经被解码,而通过param(…)
方法提供的请求参数预计已经被解码。
在大多数情况下,最好是将上下文路径和 Servlet 路径留在请求 URI 之外。如果必须使用完整的请求 URI 进行测试,请确保相应地设置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 (opens new window)创建的超媒体链接时,可以使用 JSONPath 表达式来验证结果链接,如下例所示:
Java
mockMvc.perform(get("/people").accept(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.links[?(@.rel == 'self')].href").value("http://localhost:8080/people"));
Kotlin
mockMvc.get("/people") {
accept(MediaType.APPLICATION_JSON)
}.andExpect {
jsonPath("$.links[?(@.rel == 'self')].href") {
value("http://localhost:8080/people")
}
}
当 XML 响应内容包含用Spring HATEOAS (opens new window)创建的超媒体链接时,可以使用 XPath 表达式来验证生成的链接:
Java
Map<String, String> ns = Collections.singletonMap("ns", "http://www.w3.org/2005/Atom");
mockMvc.perform(get("/handle").accept(MediaType.APPLICATION_XML))
.andExpect(xpath("/person/ns:link[@rel='self']/@href", ns).string("http://localhost:8080/people"));
Kotlin
val ns = mapOf("ns" to "http://www.w3.org/2005/Atom")
mockMvc.get("/handle") {
accept(MediaType.APPLICATION_XML)
}.andExpect {
xpath("/person/ns:link[@rel='self']/@href", ns) {
string("http://localhost:8080/people")
}
}
# 异步请求
本节展示了如何单独使用 MockMVC 来测试异步请求处理。如果通过WebTestClient使用 MockMVC,则没有什么特别的事情可以使异步请求工作,因为WebTestClient
会自动执行本节中所描述的操作。
Servlet 3.0 异步请求supported in Spring MVC通过退出 Servlet 容器线程并允许应用程序异步计算响应来工作,在此之后进行异步分派以在 Servlet 容器线程上完成处理。
Spring 在 MVC 测试中,异步请求可以通过首先断言产生的异步值,然后手动执行异步调度,最后验证响应来进行测试。下面是返回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<Nothing>("body") } (3)
}.andReturn()
mockMvc.perform(asyncDispatch(mvcResult)) (4)
.andExpect {
status().isOk() (5)
content().string("body")
}
}
1 | 检查响应状态仍未更改 |
---|---|
2 | 异步处理肯定已经启动了。 |
3 | 等待并断言异步结果 |
4 | 手动执行异步分派(因为没有正在运行的容器) |
5 | 验证最终响应 |
# 流式响应
测试流响应(例如服务器发送的事件)的最佳方法是通过WebTestClient,它可以用作测试客户端,以连接到MockMvc
实例,从而在 Spring MVC 控制器上执行测试,而无需运行服务器。例如:
爪哇
WebTestClient client = MockMvcWebTestClient.bindToController(new SseController()).build();
FluxExchangeResult<Person> exchangeResult = client.get()
.uri("/persons")
.exchange()
.expectStatus().isOk()
.expectHeader().contentType("text/event-stream")
.returnResult(Person.class);
// Use StepVerifier from Project Reactor to test the streaming response
StepVerifier.create(exchangeResult.getResponseBody())
.expectNext(new Person("N0"), new Person("N1"), new Person("N2"))
.expectNextCount(4)
.consumeNextWith(person -> assertThat(person.getName()).endsWith("7"))
.thenCancel()
.verify();
WebTestClient
还可以连接到活动服务器并执行完整的端到端集成测试。这在 Spring boot 中也是支持的,在这里你可以测试正在运行的服务器 (opens new window)。
# 过滤注册
在设置MockMvc
实例时,可以注册一个或多个 Servlet Filter
实例,如下例所示:
爪哇
mockMvc = standaloneSetup(new PersonController()).addFilters(new CharacterEncodingFilter()).build();
Kotlin
// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed
通过spring-test
中的MockFilterChain
调用注册过滤器,最后一个过滤器将委托给DispatcherServlet
。
# MockMVC 与端到端测试
MockMVC 是基于来自spring-test
模块的 Servlet API 模拟实现构建的,并且不依赖于正在运行的容器。因此,与实际运行客户机和实时服务器的完整端到端集成测试相比,存在一些差异。
思考这个问题最简单的方法是从空白MockHttpServletRequest
开始。无论你向它添加什么,请求都会变成什么。可能会让你感到惊讶的是,默认情况下没有上下文路径;没有jsessionid
cookie;没有转发、错误或异步分派;因此,没有实际的 JSP 呈现。相反,“转发”和“重定向”URL 保存在MockHttpServletResponse
中,并且可以在期望的情况下断言。
这意味着,如果使用 JSP,你可以验证请求被转发到的 JSP 页面,但不呈现 HTML。换句话说,不会调用 JSP。但是,请注意,所有不依赖于转发的其他呈现技术,例如 ThymeLeaf 和 FreeMarker,都会按照预期的那样将 HTML 呈现给响应主体。对于通过@ResponseBody
方法呈现 JSON、XML 和其他格式,也是如此。
或者,你可以考虑使用@SpringBootTest
从 Spring 启动的完整的端到端集成测试支持。见Spring Boot Reference Guide (opens new window)。
每种方法都有优点和缺点。 Spring MVC 测试中提供的选项是从经典单元测试到完全集成测试的不同规模的停止。可以肯定的是, Spring MVC 测试中的所有选项都不属于经典单元测试的范畴,但它们更接近于经典单元测试。例如,你可以通过将模拟的服务注入控制器来隔离 Web 层,在这种情况下,你只能通过DispatcherServlet
来测试 Web 层,但要使用实际的 Spring 配置,因为你可能会在与上面的层隔离的情况下测试数据访问层。此外,你还可以使用独立设置,一次只关注一个控制器,并手动提供使其工作所需的配置。
使用 Spring MVC 测试时的另一个重要区别是,从概念上讲,这样的测试是服务器端的,因此你可以检查使用了什么处理程序,如果异常是用 HandleRexCeptionResolver 处理的,模型的内容是什么,有哪些绑定错误,以及其他细节。这意味着更容易编写预期值,因为服务器不是一个不透明的框,就像通过实际的 HTTP 客户机进行测试时一样。这通常是经典单元测试的一个优势:它更容易编写、推理和调试,但不会取代对完全集成测试的需求。同时,重要的是不要忽视这样一个事实,即反应是最重要的检查事项。简而言之,即使在同一个项目中,这里也有多种测试风格和策略的空间。
# 进一步的例子
该框架自己的测试包括许多样本测试 (opens new window),旨在展示如何单独或通过WebTestClient (opens new window)使用 MockMVC。浏览这些示例以获得更多的想法。
# 3.7.2.htmlunit 集成
Spring 提供MockMvc和HtmlUnit (opens new window)之间的集成。这简化了在使用基于 HTML 的视图时执行端到端测试的过程。这种集成使你能够:
通过使用HtmlUnit (opens new window)、WebDriver (opens new window)和Geb (opens new window)等工具轻松地测试 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")
}
}
如果我们想测试允许我们创建消息的窗体视图,该怎么办?例如,假设我们的表单看起来像以下片段:
<form id="messageForm" action="/messages/" method="post">
<div class="pull-right"><a href="/messages/">Messages</a></div>
<label for="summary">Summary</label>
<input type="text" class="required" id="summary" name="summary" value="" />
<label for="text">Message</label>
<textarea id="text" name="text"></textarea>
<div class="form-actions">
<input type="submit" value="Create" />
</div>
</form>
我们如何确保我们的表单产生正确的请求来创建新消息?一次幼稚的尝试可能类似于以下几点:
爪哇
mockMvc.perform(get("/messages/form"))
.andExpect(xpath("//input[@name='summary']").exists())
.andExpect(xpath("//textarea[@name='text']").exists());
Kotlin
mockMvc.get("/messages/form").andExpect {
xpath("//input[@name='summary']") { exists() }
xpath("//textarea[@name='text']") { exists() }
}
这种测试有一些明显的缺点。如果我们更新控制器以使用参数message
而不是text
,那么我们的表单测试将继续通过,即使 HTML 表单与控制器不同步。为了解决这个问题,我们可以将两个测试结合起来,如下所示:
爪哇
String summaryParamName = "summary";
String textParamName = "text";
mockMvc.perform(get("/messages/form"))
.andExpect(xpath("//input[@name='" + summaryParamName + "']").exists())
.andExpect(xpath("//textarea[@name='" + textParamName + "']").exists());
MockHttpServletRequestBuilder createMessage = post("/messages/")
.param(summaryParamName, "Spring Rocks")
.param(textParamName, "In case you didn't know, Spring Rocks!");
mockMvc.perform(createMessage)
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/messages/123"));
Kotlin
val summaryParamName = "summary";
val textParamName = "text";
mockMvc.get("/messages/form").andExpect {
xpath("//input[@name='$summaryParamName']") { exists() }
xpath("//textarea[@name='$textParamName']") { exists() }
}
mockMvc.post("/messages/") {
param(summaryParamName, "Spring Rocks")
param(textParamName, "In case you didn't know, Spring Rocks!")
}.andExpect {
status().is3xxRedirection()
redirectedUrl("/messages/123")
}
这将减少我们的测试不正确通过的风险,但仍然存在一些问题:
如果我们的页面上有多个表单怎么办?诚然,我们可以更新 XPath 表达式,但是随着我们考虑更多因素,它们变得更加复杂:字段是正确的类型吗?字段启用了吗?以此类推。
另一个问题是,我们正在做的工作是预期的两倍。我们必须首先验证视图,然后使用刚刚验证过的相同参数提交视图。理想情况下,这可以一次完成。
最后,我们仍然无法解释某些事情。例如,如果表单也有我们希望测试的 爪哇Script 验证呢?
总的问题是,测试一个 Web 页面并不涉及一个单独的交互。相反,它是用户如何与 Web 页面交互以及该 Web 页面如何与其他资源交互的组合。例如,表单视图的结果被用作用户创建消息的输入。此外,我们的表单视图可能会使用影响页面行为的额外资源,例如 爪哇Script 验证。
# 整合测试的拯救?
为了解决前面提到的问题,我们可以执行端到端集成测试,但这有一些缺点。考虑测试这个视图,它可以让我们在消息中进行页面浏览。我们可能需要进行以下测试:
我们的页面是否向用户显示通知,以表明当消息为空时没有可用的结果?
我们的页面是否正确地显示了一条消息?
我们的页面是否适当地支持分页?
要设置这些测试,我们需要确保我们的数据库包含正确的消息。这导致了一些额外的挑战:
确保数据库中有正确的消息是很乏味的。(考虑一下国外的关键限制。
测试可能会变得很慢,因为每个测试都需要确保数据库处于正确的状态。
由于我们的数据库需要处于特定的状态,因此我们不能并行运行测试。
对自动生成的 ID、时间戳等项执行断言可能很困难。
这些挑战并不意味着我们应该完全放弃端到端集成测试。相反,我们可以通过重构详细的测试,使用运行速度更快、更可靠且没有副作用的模拟服务,来减少端到端集成测试的数量。然后,我们可以实现少量真正的端到端集成测试,这些测试验证简单的工作流,以确保所有工作都正确地结合在一起。
# 进入 HTMLUnit 集成
那么,我们如何在测试页面的交互和在测试套件中保持良好性能之间实现平衡呢?答案是:“通过整合 MockMVC 和 HTMLUnit。”
# htmlunit 集成选项
当你想要将 mockmvc 与 htmlunit 集成在一起时,你有许多选项:
mockmvc 和 htmlunit:如果你想使用原始的 htmlUnit 库,请使用此选项。
MockMVC 和 WebDriver:使用此选项可以简化开发,并在集成和端到端测试之间重用代码。
MOCKMVC 和 GEB:如果你想使用 Groovy 进行测试、简化开发以及在集成和端到端测试之间重用代码,请使用此选项。
# mockmvc 和 htmlunit
本节介绍如何集成 MockMVC 和 HTMLUnit。如果你想使用原始的 HTMLUnit 库,请使用此选项。
# mockmvc 和 htmlunit 设置
首先,确保包含了对net.sourceforge.htmlunit:htmlunit
的测试依赖项。为了在 Apache HttpComponents4.5+ 中使用 HTMLUnit,你需要使用 HTMLUnit2.18 或更高版本。
通过使用MockMvcWebClientBuilder
,我们可以轻松地创建一个与 MockMVC 集成的 htmlUnitWebClient
,如下所示:
爪哇
WebClient webClient;
@BeforeEach
void setup(WebApplicationContext context) {
webClient = MockMvcWebClientBuilder
.webAppContextSetup(context)
.build();
}
Kotlin
lateinit var webClient: WebClient
@BeforeEach
fun setup(context: WebApplicationContext) {
webClient = MockMvcWebClientBuilder
.webAppContextSetup(context)
.build()
}
这是使用MockMvcWebClientBuilder 的一个简单示例。关于高级用法,参见[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 (opens new window)库:
爪哇
assertThat(newMessagePage.getUrl().toString()).endsWith("/messages/123");
String id = newMessagePage.getHtmlElementById("id").getTextContent();
assertThat(id).isEqualTo("123");
String summary = newMessagePage.getHtmlElementById("summary").getTextContent();
assertThat(summary).isEqualTo("Spring Rocks");
String text = newMessagePage.getHtmlElementById("text").getTextContent();
assertThat(text).isEqualTo("In case you didn't know, Spring Rocks!");
Kotlin
assertThat(newMessagePage.getUrl().toString()).endsWith("/messages/123")
val id = newMessagePage.getHtmlElementById("id").getTextContent()
assertThat(id).isEqualTo("123")
val summary = newMessagePage.getHtmlElementById("summary").getTextContent()
assertThat(summary).isEqualTo("Spring Rocks")
val text = newMessagePage.getHtmlElementById("text").getTextContent()
assertThat(text).isEqualTo("In case you didn't know, Spring Rocks!")
前面的代码在许多方面改进了我们的MockMVC 测试。首先,我们不再需要显式地验证表单,然后创建一个看起来像表单的请求。相反,我们请求表单,填写它,然后提交它,从而大大减少了开销。
另一个重要的因素是HTMLUnit 使用 Mozilla Rhino 引擎 (opens new window)来评估 爪哇Script。这意味着我们还可以在页面中测试 爪哇Script 的行为。
有关使用 htmlunit 的更多信息,请参见htmlunit 文档 (opens new window)。
# 高级MockMvcWebClientBuilder
在到目前为止的示例中,我们已经以尽可能简单的方式使用MockMvcWebClientBuilder
,通过基于 Spring TestContext 框架为我们加载的WebApplicationContext
构建WebClient
。下面的示例重复了这种方法:
爪哇
WebClient webClient;
@BeforeEach
void setup(WebApplicationContext context) {
webClient = MockMvcWebClientBuilder
.webAppContextSetup(context)
.build();
}
Kotlin
lateinit var webClient: WebClient
@BeforeEach
fun setup(context: WebApplicationContext) {
webClient = MockMvcWebClientBuilder
.webAppContextSetup(context)
.build()
}
我们还可以指定其他配置选项,如下例所示:
爪哇
WebClient webClient;
@BeforeEach
void setup() {
webClient = MockMvcWebClientBuilder
// demonstrates applying a MockMvcConfigurer (Spring Security)
.webAppContextSetup(context, springSecurity())
// for illustration only - defaults to ""
.contextPath("")
// By default MockMvc is used for localhost only;
// the following will use MockMvc for example.com and example.org as well
.useMockMvcForHosts("example.com","example.org")
.build();
}
Kotlin
lateinit var webClient: WebClient
@BeforeEach
fun setup() {
webClient = MockMvcWebClientBuilder
// demonstrates applying a MockMvcConfigurer (Spring Security)
.webAppContextSetup(context, springSecurity())
// for illustration only - defaults to ""
.contextPath("")
// By default MockMvc is used for localhost only;
// the following will use MockMvc for example.com and example.org as well
.useMockMvcForHosts("example.com","example.org")
.build()
}
作为一种选择,我们可以通过分别配置MockMvc
实例并将其提供给MockMvcWebClientBuilder
来执行完全相同的设置,如下所示:
爪哇
MockMvc mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
webClient = MockMvcWebClientBuilder
.mockMvcSetup(mockMvc)
// for illustration only - defaults to ""
.contextPath("")
// By default MockMvc is used for localhost only;
// the following will use MockMvc for example.com and example.org as well
.useMockMvcForHosts("example.com","example.org")
.build();
Kotlin
// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed
这更详细,但是,通过使用MockMvc
实例构建WebClient
实例,我们可以在指尖获得 MockMVC 的全部功能。
有关创建MockMvc 实例的更多信息,请参见设置选项。 |
---|
# MockMVC 和 WebDriver
在前面的部分中,我们已经了解了如何将 MockMVC 与原始的 HTMLUnitAPI 结合使用。在本节中,我们在 SeleniumWebDriver (opens new window)中使用了额外的抽象,以使事情变得更简单。
# 为什么是 Webdriver 和 MockMVC?
我们已经可以使用 HTMLUnit 和 MockMVC 了,那么为什么我们要使用 WebDriver 呢?Selenium WebDriver 提供了一个非常优雅的 API,可以让我们轻松地组织代码。为了更好地展示它是如何工作的,我们将在本节中探讨一个示例。
尽管是Selenium (opens new window)的一部分,WebDriver 并不需要 的 Selenium 服务器来运行测试。 |
---|
假设我们需要确保消息是正确创建的。测试包括查找 HTML 表单输入元素,填写它们,并做出各种断言。
这种方法会导致许多单独的测试,因为我们也希望测试错误条件。例如,如果我们只填写表单的一部分,我们希望确保得到一个错误。如果我们填写了整个表单,那么新创建的消息将在之后显示。
如果其中一个字段被命名为“Summary”,那么我们可能会在测试中的多个地方重复类似于以下内容的内容:
爪哇
HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary");
summaryInput.setValueAttribute(summary);
Kotlin
val summaryInput = currentPage.getHtmlElementById("summary")
summaryInput.setValueAttribute(summary)
那么,如果我们将id
更改为smmry
会发生什么呢?这样做将迫使我们更新所有的测试,以纳入这一变化。这违反了 dry 原则,因此我们最好将该代码提取到它自己的方法中,如下所示:
爪哇
public HtmlPage createMessage(HtmlPage currentPage, String summary, String text) {
setSummary(currentPage, summary);
// ...
}
public void setSummary(HtmlPage currentPage, String summary) {
HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary");
summaryInput.setValueAttribute(summary);
}
Kotlin
fun createMessage(currentPage: HtmlPage, summary:String, text:String) :HtmlPage{
setSummary(currentPage, summary);
// ...
}
fun setSummary(currentPage:HtmlPage , summary: String) {
val summaryInput = currentPage.getHtmlElementById("summary")
summaryInput.setValueAttribute(summary)
}
这样做可以确保在更改 UI 时不需要更新所有的测试。
我们甚至可以更进一步,将这个逻辑放在一个Object
中,该逻辑表示我们当前所在的HtmlPage
,如下例所示:
爪哇
public class CreateMessagePage {
final HtmlPage currentPage;
final HtmlTextInput summaryInput;
final HtmlSubmitInput submit;
public CreateMessagePage(HtmlPage currentPage) {
this.currentPage = currentPage;
this.summaryInput = currentPage.getHtmlElementById("summary");
this.submit = currentPage.getHtmlElementById("submit");
}
public <T> T createMessage(String summary, String text) throws Exception {
setSummary(summary);
HtmlPage result = submit.click();
boolean error = CreateMessagePage.at(result);
return (T) (error ? new CreateMessagePage(result) : new ViewMessagePage(result));
}
public void setSummary(String summary) throws Exception {
summaryInput.setValueAttribute(summary);
}
public static boolean at(HtmlPage page) {
return "Create Message".equals(page.getTitleText());
}
}
Kotlin
class CreateMessagePage(private val currentPage: HtmlPage) {
val summaryInput: HtmlTextInput = currentPage.getHtmlElementById("summary")
val submit: HtmlSubmitInput = currentPage.getHtmlElementById("submit")
fun <T> createMessage(summary: String, text: String): T {
setSummary(summary)
val result = submit.click()
val error = at(result)
return (if (error) CreateMessagePage(result) else ViewMessagePage(result)) as T
}
fun setSummary(summary: String) {
summaryInput.setValueAttribute(summary)
}
fun at(page: HtmlPage): Boolean {
return "Create Message" == page.getTitleText()
}
}
}
以前,这种模式被称为页面对象模式 (opens new window)。虽然我们可以通过 HTMLUnit 实现这一点,但 WebDriver 提供了一些工具,我们将在下面的小节中对这些工具进行探讨,以使这种模式更容易实现。
# MockMVC 和 WebDriver 设置
要在 Spring MVC 测试框架中使用 Selenium WebDriver,请确保你的项目包含对org.seleniumhq.selenium:selenium-htmlunit-driver
的测试依赖关系。
我们可以通过使用MockMvcHtmlUnitDriverBuilder
很容易地创建与 MockMVC 集成的 Selenium WebDriver,如下例所示:
爪哇
WebDriver driver;
@BeforeEach
void setup(WebApplicationContext context) {
driver = MockMvcHtmlUnitDriverBuilder
.webAppContextSetup(context)
.build();
}
Kotlin
lateinit var driver: WebDriver
@BeforeEach
fun setup(context: WebApplicationContext) {
driver = MockMvcHtmlUnitDriverBuilder
.webAppContextSetup(context)
.build()
}
这是使用MockMvcHtmlUnitDriverBuilder 的一个简单示例。有关更高级的用法,请参见[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 测试的设计。正如我们在为什么是 Webdriver 和 MockMVC?中提到的,我们可以在 HTMLUnit 中使用 Page 对象模式,但是使用 WebDriver 要容易得多。考虑以下CreateMessagePage
实现:
爪哇
public class CreateMessagePage
extends AbstractPage { (1)
(2)
private WebElement summary;
private WebElement text;
(3)
@FindBy(css = "input[type=submit]")
private WebElement submit;
public CreateMessagePage(WebDriver driver) {
super(driver);
}
public <T> T createMessage(Class<T> resultPage, String summary, String details) {
this.summary.sendKeys(summary);
this.text.sendKeys(details);
this.submit.click();
return PageFactory.initElements(driver, resultPage);
}
public static CreateMessagePage to(WebDriver driver) {
driver.get("http://localhost:9990/mail/messages/form");
return PageFactory.initElements(driver, CreateMessagePage.class);
}
}
1 | CreateMessagePage 扩展了AbstractPage 。我们没有讨论AbstractPage 的详细信息,但是,总而言之,它包含了我们所有页面的通用功能。例如,如果我们的应用程序具有导航栏、全局错误消息和其他 功能,我们可以将此逻辑放置在共享位置。 |
---|---|
2 | 对于 HTML 页面中我们感兴趣的每个部分,我们都有一个成员变量 。这些是 WebElement 型。Webdriver 的[PageFactory ](https://github.com/seleniumhq/selenium/wiki/pageFactory)让我们通过自动解析CreateMessagePage 每个WebElement ,从 htmlunit 版本的CreateMessagePage 中删除大量代码。[PageFactory#initElements(WebDriver,Class<T>) ](https://seleniumhq.github.io/selenium/DOCS/api/java/org/openqa/selenium/support/pageFactory.html#initelements-org.openqa.selenium.webdriver-java.lang.class-)方法通过使用字段名并查找由页面中元素的 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 <T> createMessage(resultPage: Class<T>, summary: String, details: String): T {
this.summary.sendKeys(summary)
text.sendKeys(details)
submit.click()
return PageFactory.initElements(driver, resultPage)
}
companion object {
fun to(driver: WebDriver): CreateMessagePage {
driver.get("http://localhost:9990/mail/messages/form")
return PageFactory.initElements(driver, CreateMessagePage::class.java)
}
}
}
1 | CreateMessagePage 扩展了AbstractPage 。我们没有讨论AbstractPage 的详细信息,但是,总而言之,它包含了我们所有页面的通用功能。例如,如果我们的应用程序具有导航栏、全局错误消息和其他 功能,我们可以将此逻辑放置在共享位置。 |
---|---|
2 | 对于 HTML 页面中我们感兴趣的每个部分,我们都有一个成员变量 。它们是 WebElement 型。Webdriver 的[PageFactory ](https://github.com/seleniumhq/selenium/wiki/pageFactory)让我们通过自动解析WebElement 每个WebElement ,从 htmlunit 版本的CreateMessagePage 中删除大量代码。[PageFactory#initElements(WebDriver,Class<T>) ](https://seleniumhq.github.io/selenium/DOCS/api/java/org/openqa/selenium/support/pageFactory.html#initelements-org.openqa.selenium.webdriver-java.lang.class-)方法通过使用字段名并查找由页面中元素的 id 或name 自动解析每个<gt r=“2696”。 |
3 | 我们可以使用[@FindBy 注释](https://github.com/seleniumhq/selenium/wiki/pageFactory#making-the-example-work-using-annotations)来覆盖默认的查找行为。我们的示例展示了如何使用@FindBy 注释来使用css 选择器(input[type=submit])查找我们的 Submit 按钮。 |
最后,我们可以验证新消息是否已成功创建。以下断言使用AssertJ (opens new window)断言程序库:
爪哇
assertThat(viewMessagePage.getMessage()).isEqualTo(expectedMessage);
assertThat(viewMessagePage.getSuccess()).isEqualTo("Successfully created a new message");
Kotlin
assertThat(viewMessagePage.message).isEqualTo(expectedMessage)
assertThat(viewMessagePage.success).isEqualTo("Successfully created a new message")
我们可以看到,我们的ViewMessagePage
允许我们与自定义域模型进行交互。例如,它公开了一个返回Message
对象的方法:
爪哇
public Message getMessage() throws ParseException {
Message message = new Message();
message.setId(getId());
message.setCreated(getCreated());
message.setSummary(getSummary());
message.setText(getText());
return message;
}
Kotlin
fun getMessage() = Message(getId(), getCreated(), getSummary(), getText())
然后,我们可以在断言中使用富域对象。
最后,当测试完成时,我们一定不要忘记关闭WebDriver
实例,如下所示:
爪哇
@AfterEach
void destroy() {
if (driver != null) {
driver.close();
}
}
Kotlin
@AfterEach
fun destroy() {
if (driver != null) {
driver.close()
}
}
有关使用 WebDriver 的更多信息,请参见 SeleniumWebDriver 文档 (opens new window)。
# 高级MockMvcHtmlUnitDriverBuilder
在到目前为止的示例中,我们已经以尽可能简单的方式使用MockMvcHtmlUnitDriverBuilder
,通过基于 Spring TestContext 框架为我们加载的WebApplicationContext
构建WebDriver
。这种方法在此重复如下:
爪哇
WebDriver driver;
@BeforeEach
void setup(WebApplicationContext context) {
driver = MockMvcHtmlUnitDriverBuilder
.webAppContextSetup(context)
.build();
}
Kotlin
lateinit var driver: WebDriver
@BeforeEach
fun setup(context: WebApplicationContext) {
driver = MockMvcHtmlUnitDriverBuilder
.webAppContextSetup(context)
.build()
}
我们还可以指定其他配置选项,如下所示:
爪哇
WebDriver driver;
@BeforeEach
void setup() {
driver = MockMvcHtmlUnitDriverBuilder
// demonstrates applying a MockMvcConfigurer (Spring Security)
.webAppContextSetup(context, springSecurity())
// for illustration only - defaults to ""
.contextPath("")
// By default MockMvc is used for localhost only;
// the following will use MockMvc for example.com and example.org as well
.useMockMvcForHosts("example.com","example.org")
.build();
}
Kotlin
lateinit var driver: WebDriver
@BeforeEach
fun setup() {
driver = MockMvcHtmlUnitDriverBuilder
// demonstrates applying a MockMvcConfigurer (Spring Security)
.webAppContextSetup(context, springSecurity())
// for illustration only - defaults to ""
.contextPath("")
// By default MockMvc is used for localhost only;
// the following will use MockMvc for example.com and example.org as well
.useMockMvcForHosts("example.com","example.org")
.build()
}
作为一种选择,我们可以通过分别配置MockMvc
实例并将其提供给MockMvcHtmlUnitDriverBuilder
来执行完全相同的设置,如下所示:
Java
MockMvc mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
driver = MockMvcHtmlUnitDriverBuilder
.mockMvcSetup(mockMvc)
// for illustration only - defaults to ""
.contextPath("")
// By default MockMvc is used for localhost only;
// the following will use MockMvc for example.com and example.org as well
.useMockMvcForHosts("example.com","example.org")
.build();
Kotlin
// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed
这更详细,但是,通过使用MockMvc
实例构建WebDriver
,我们可以在指尖获得 MockMVC 的全部功能。
有关创建MockMvc 实例的更多信息,请参见设置选项。 |
---|
# MOCKMVC 和 GEB
在上一节中,我们了解了如何在 WebDriver 中使用 MockMVC。在这一节中,我们使用Geb (opens new window)使我们的测试更加 Groovy-er。
# 为什么是 GEB 和 MOCKMVC?
GEB 由 WebDriver 支持,因此它提供了许多我们从 WebDriver 获得的相同的好处。然而,通过为我们处理一些样板代码,GEB 使事情变得更加简单。
# mockmvc 和 geb 设置
我们可以使用使用使用 MockMVC 的 Selenium WebDriver 轻松地初始化 GEBBrowser
,如下所示:
def setup() {
browser.driver = MockMvcHtmlUnitDriverBuilder
.webAppContextSetup(context)
.build()
}
这是使用MockMvcHtmlUnitDriverBuilder 的一个简单示例。有关更高级的用法,请参见[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 测试的设计。正如前面提到的,我们可以在 HTMLUnit 和 WebDriver 中使用页面对象模式,但是使用 GEB 则更容易。考虑一下我们新的基于 Groovy 的CreateMessagePage
实现:
class CreateMessagePage extends Page {
static url = 'messages/form'
static at = { assert title == 'Messages : Create'; true }
static content = {
submit { $('input[type=submit]') }
form { $('form') }
errors(required:false) { $('label.error, .alert-error')?.text() }
}
}
我们的CreateMessagePage
扩展了Page
。我们不讨论Page
的详细信息,但总而言之,它包含了我们所有页面的通用功能。我们定义了一个可以在其中找到此页面的 URL。这让我们可以导航到该页面,如下所示:
to CreateMessagePage
我们还有一个at
闭包,它确定我们是否在指定的页面上。如果我们在正确的页面上,它应该返回true
。这就是为什么我们可以断言我们在正确的页面上,如下所示:
then:
at CreateMessagePage
errors.contains('This field is required.')
我们在闭包中使用断言,这样我们就可以确定问题出在哪里 ,如果我们在错误的页面上。 |
---|
接下来,我们创建一个content
闭包,该闭包指定页面中所有感兴趣的区域。我们可以使用jQuery-ish Navigator API (opens new window)来选择我们感兴趣的内容。
最后,我们可以验证新消息是否已成功创建,如下所示:
then:
at ViewMessagePage
success == 'Successfully created a new message'
id
date
summary == expectedSummary
message == expectedMessage
有关如何最大限度地利用 GEB 的更多详细信息,请参见GEB 之书 (opens new window)用户手册。
# 3.8.测试客户端应用程序
你可以使用客户端测试来测试内部使用RestTemplate
的代码。其思想是声明预期的请求并提供“存根”响应,这样你就可以专注于孤立地测试代码(也就是说,在不运行服务器的情况下)。下面的示例展示了如何做到这一点:
Java
RestTemplate restTemplate = new RestTemplate();
MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build();
mockServer.expect(requestTo("/greeting")).andRespond(withSuccess());
// Test code that uses the above RestTemplate ...
mockServer.verify();
Kotlin
val restTemplate = RestTemplate()
val mockServer = MockRestServiceServer.bindTo(restTemplate).build()
mockServer.expect(requestTo("/greeting")).andRespond(withSuccess())
// Test code that uses the above RestTemplate ...
mockServer.verify()
在前面的示例中,MockRestServiceServer
(客户端 REST 测试的中心类)使用自定义的RestTemplate
配置ClientHttpRequestFactory
,该自定义配置根据预期断言实际请求并返回“存根”响应。在这种情况下,我们期望请求/greeting
,并希望返回带有text/plain
内容的 200 响应。我们可以根据需要定义额外的期望请求和存根响应。当我们定义期望的请求和存根响应时,RestTemplate
可以像往常一样在客户端代码中使用。在测试结束时,可以使用mockServer.verify()
来验证所有的期望都已满足。
默认情况下,请求的预期顺序是声明期望的顺序。在构建服务器时,可以设置ignoreExpectOrder
选项,在这种情况下,将检查所有期望(按顺序)以找到给定请求的匹配项。这意味着请求可以按任何顺序提出。下面的示例使用ignoreExpectOrder
:
Java
server = MockRestServiceServer.bindTo(restTemplate).ignoreExpectOrder(true).build();
Kotlin
server = MockRestServiceServer.bindTo(restTemplate).ignoreExpectOrder(true).build()
即使默认情况下是无序的请求,每个请求也只允许运行一次。expect
方法提供了一个重载变量,该变量接受一个指定计数范围的ExpectedCount
参数(例如,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 测试自己的测试包括示例测试 (opens new window)客户端 REST 测试。
# 4. 更多资源
有关测试的更多信息,请参见以下参考资料:
JUnit (opens new window):“一个程序员友好的 Java 测试框架”。 Spring 框架在其测试套件中使用,并在Spring TestContext Framework中支持。
TestNG (opens new window):受 JUnit 启发的测试框架,增加了对测试组、数据驱动测试、分布式测试和其他特性的支持。在Spring TestContext Framework中支持
AssertJ (opens new window):“Fluent Assertions for Java”,包括对 Java8Lambdas、Streams 和其他功能的支持。
模拟对象 (opens new window):维基百科条目。
MockObjects.com (opens new window):专门用于模拟对象的网站,这是一种在测试驱动开发中改进代码设计的技术。
Mockito (opens new window):基于Test Spy (opens new window)模式的 Java 模拟库。 Spring 框架在其测试套件中使用。
EasyMock (opens new window):Java 库“通过使用 Java 的代理机制动态生成接口的模拟对象(以及通过类扩展的对象),为接口提供模拟对象。”
JMock (opens new window):支持使用模拟对象对 Java 代码进行测试驱动开发的库。
DbUnit (opens new window):JUnit 扩展(也可用于 Ant 和 Maven),它的目标是数据库驱动的项目,其中包括在测试运行之间使数据库处于已知状态。
测试容器 (opens new window):支持 JUnit 测试的 Java 库,提供通用数据库、Selenium Web 浏览器或任何其他可以在 Docker 容器中运行的轻量级一次性实例。
研磨机 (opens new window):Java 负载测试框架。
斯普林莫克 (opens new window):支持使用MockK (opens new window)代替 mockito 编写的 Spring 引导集成测试。