本站(springdoc.cn)中的内容来源于 spring.io ,原始版权归属于 spring.io。由 springdoc.cn 进行翻译,整理。可供个人学习、研究,未经许可,不得进行任何转载、商用或与之相关的行为。 商标声明:Spring 是 Pivotal Software, Inc. 在美国以及其他国家的商标。 |
本章介绍了Spring对集成测试的支持和单元测试的最佳实践。Spring团队提倡测试驱动的开发(TDD)。Spring团队发现,正确使用反转控制(IoC)肯定会使单元测试和集成测试更容易(因为类上存在setter方法和适当的构造函数,使它们更容易在测试中连接在一起,而不需要设置服务定位器注册表(service locator register)和类似结构)。
2. 单元测试
与传统的J2EE/Java EE开发相比,依赖注入应该使你的代码对容器的依赖性降低。组成你的应用程序的POJOs应该可以在JUnit或TestNG测试中进行测试,通过使用 new
操作符来实例化对象,而不需要Spring或任何其他容器。你可以使用 mock 对象(结合其他有价值的测试技术)来孤立地测试你的代码。如果你遵循Spring的架构建议,由此产生的干净的分层和代码库的组件化会使单元测试更加容易。例如,你可以通过 stub 或 mock DAO 或 repositoy 接口来测试服务(service)层对象,在运行单元测试时不需要访问持久化数据。
真正的单元测试通常运行得非常快,因为没有运行时的基础设施需要设置。强调真正的单元测试是你开发方法的一部分,可以提高你的生产力。你可能不需要测试章节的这一部分来帮助你为基于IoC的应用程序编写有效的单元测试。然而,对于某些单元测试场景,Spring框架提供了模拟(mock)对象和测试支持类,本章对此进行了描述。
2.1. Mock 对象(Object)
Spring包括一些专门用于mock的包。
2.1.1. Environment
org.springframework.mock.env
包包含 Environment
和 PropertySource
抽象的mock实现(参见 Bean Definition Profile 和 PropertySource
抽象)。MockEnvironment
和 MockPropertySource
对于开发依赖环境特定属性的代码的容器外测试非常有用。
2.1.2. JNDI
org.springframework.mock.jndi
包包含JNDI SPI的部分实现,你可以用它来为测试套件或独立的应用程序建立一个简单的JNDI环境。例如,如果JDBC DataSource
实例在测试代码中与Jakarta EE容器中的JNDI名称相同,你就可以在测试场景中重复使用应用程序代码和配置,而无需修改。
org.springframework.mock.jndi 包中的 mock JNDI支持从Spring Framework 5.2开始正式废弃,转而采用第三方的完整解决方案,如 Simple-JNDI。
|
2.1.3. Servlet API
org.springframework.mock.web
包包含了一套全面的Servlet APImock对象,对于测试Web上下文、controller 和 filter 非常有用。这些mock对象是针对Spring的Web MVC框架使用的,通常比动态mock对象(如 EasyMock)或其他Servlet API mock对象(如 MockObjects)使用起来更方便。
从Spring Framework 6.0开始,org.springframework.mock.web 中的mock对象是基于Servlet 6.0的API。
|
Spring MVC测试框架建立在 mock Servlet API对象的基础上,为Spring MVC提供一个集成测试框架。参见 MockMvc。
2.1.4. Spring Web Reactive
org.springframework.mock.http.server.reactive
包包含了 ServerHttpRequest
和 ServerHttpResponse
的 mock 实现,可以在WebFlux应用程序中使用。org.springframework.mock.web.server
包包含一个模拟的 ServerWebExchange
,它依赖于这些mock的request和response对象。
MockServerHttpRequest
和 MockServerHttpResponse
都是从相同的抽象基类中延伸出来的server专用实现,并与它们共享行为。例如,一个mock request一旦被创建就是不可改变的,但是你可以使用 ServerHttpRequest
的 mutate()
方法来创建一个修改的实例。
为了让 mock response 正确地实现写契约(contract)并返回一个写完成的句柄(即 Mono<Void>
),它默认使用一个带有 cache().then()
的 Flux
,它缓冲了数据并使其可用于测试中的断言。应用程序可以设置一个自定义的写函数(例如,测试一个无限的流)。
WebTestClient 建立在 mock request 和 response 的基础上,为测试没有HTTP服务器的WebFlux应用提供支持。该客户端也可用于与运行中的服务器进行端到端测试。
2.2. 单元测试的支持类
Spring包括一些可以帮助单元测试的类。它们分为两类。
2.2.1. 一般的测试工具
org.springframework.test.util
包包含几个通用的工具,用于单元和集成测试。
AopTestUtils
是AOP相关实用方法的集合。你可以使用这些方法来获取隐藏在一个或多个Spring代理背后的底层目标对象的引用。例如,如果你通过使用 EasyMock 或Mockito等库将一个bean配置为动态mock,并且该mock被包装在Spring代理中,你可能需要直接访问底层mock,以便对其配置期望并执行验证。关于Spring的核心AOP工具,见 AopUtils
和 AopProxyUtils
。
ReflectionTestUtils
是一个基于反射的实用方法集合。你可以在测试场景中使用这些方法,在测试应用程序代码时,你需要改变一个常量的值,设置一个非 public
的字段,调用一个非 public
的setter方法,或者调用一个非 public
的配置或生命周期回调方法,其用例如下。
-
ORM框架(如JPA和Hibernate)容忍
private
或protected
的字段访问,而不是domain实体中属性的public
setter 方法。 -
Spring 支持注解(如
@Autowired
、@Inject
和@Resource
),为private
或protected
的字段、setter方法和配置方法提供依赖注入。 -
对生命周期回调方法使用注解,如
@PostConstruct
和@PreDestroy
。
TestSocketUtils
是一个简单的工具,用于查找 localhost
上可用的TCP端口,以便在集成测试场景中使用。
|
2.2.2. Spring MVC 测试工具
org.springframework.test.web
包包含 ModelAndViewAssert
,你可以将其与JUnit、TestNG或其他任何测试框架结合使用,进行处理Spring MVC ModelAndView
对象的单元测试。
Spring MVC Controller 单元测试
要将Spring MVC Controller 类作为POJO进行单元测试,可以使用 ModelAndViewAssert 与Spring的 Servlet API mock 中的 MockHttpServletRequest 、MockHttpSession 等相结合。对于Spring MVC和REST Controller 类与Spring MVC的 WebApplicationContext 配置的彻底集成测试,请使用 Spring MVC测试框架。
|
3. 集成测试
能够执行一些集成测试而不需要部署到你的应用服务器或连接到其他企业基础设施是很重要的。这样做可以让你测试一些东西,例如。
-
对Spring IoC容器上下文进行正确的装配。
-
使用JDBC或ORM工具的数据访问。这可以包括诸如SQL语句的正确性、Hibernate查询、JPA实体映射等等。
Spring框架在 spring-test
模块中为集成测试提供了一流的支持。实际JAR文件的名称可能包括发布版本,也可能是长的 org.springframework.test
形式,这取决于你从哪里得到它(见 依赖管理 一节的解释)。这个库包括 org.springframework.test
包,它包含了与Spring容器进行集成测试的有价值的类。这种测试不依赖于应用服务器或其他部署环境。这种测试的运行速度比单元测试慢,但比同等的 Selenium 测试或依赖部署到应用服务器的远程测试快得多。
单元和集成测试支持是以注解驱动的 Spring TestContext 框架 的形式提供的。TestContext 框架与实际使用的测试框架无关,它允许在各种环境下进行测试,包括JUnit、TestNG和其他。
下面一节概述了Spring集成支持的高层次目标,本章的其余部分随后将集中讨论专门的主题。
3.1. 集成测试的目标
Spring的集成测试支持有以下主要目标。
-
在测试之间管理 Spring IoC容器的缓存。
-
提供适合集成测试的 事务管理。
-
提供 Spring 特有的 base 类,协助开发者编写集成测试。
接下来的几节描述了每个目标,并提供了实施和配置细节的链接。
3.1.1. 上下文(Context)管理和缓存
Spring TestContext框架提供了Spring ApplicationContext
实例和 WebApplicationContext
实例的一致加载,以及这些上下文的缓存。对加载的上下文的缓存支持很重要,因为启动时间可能成为一个问题—不是因为Spring本身的开销,而是因为Spring容器所实例化的对象需要时间来实例化。例如,一个有50到100个Hibernate映射文件的项目可能需要10到20秒来加载映射文件,在运行每个测试 fixture 中的每个测试之前产生的成本会导致整个测试运行速度变慢,从而降低开发人员的工作效率。
测试类通常声明XML或Groovy配置元数据的资源位置数组—通常在classpath中—或用于配置应用程序的组件类数组。这些位置或类与 web.xml
或其他生产部署的配置文件中指定的位置或类相同或相似。
默认情况下,一旦加载,配置的 ApplicationContext
将在每个测试中重复使用。因此,每个测试套件只产生一次设置成本,随后的测试执行会快很多。在这里,术语 "测试套件" 是指在同一JVM中运行的所有测试—例如,从Ant、Maven或Gradle构建中为特定项目或模块运行的所有测试。在不太可能的情况下,一个测试破坏了 application context,需要重新加载(例如,通过修改Bean定义或应用程序对象的状态),TestContext框架可以被配置为在执行下一个测试之前重新加载配置并重建 application context。
见 Context 管理 和 上下文(Context)缓存 与 TestContext 框架。
3.1.2. Test Fixture 的依赖注入
当TestContext框架加载你的应用程序上下文时,它可以通过使用依赖性注入来配置你的测试类的实例。这提供了一个方便的机制,通过使用你的应用上下文中的预配置的Bean来设置测试fixture。这里有一个很大的好处是,你可以在不同的测试场景中重复使用应用上下文(例如,配置Spring管理的对象图、事务代理、DataSource
实例等),从而避免了为单个测试案例重复设置复杂的测试fixture。
举个例子,考虑一个场景,我们有一个类(HibernateTitleRepository
),实现了 Title
domain 实体的数据访问逻辑。我们想写集成测试来测试以下几个方面。
-
Spring的配置。基本上,与
HibernateTitleRepository
Bean的配置有关的一切都正确且存在吗? -
Hibernate的映射文件配置。所有的映射都是正确的,正确的懒加载设置也到位了吗?
-
HibernateTitleRepository
的逻辑。这个类的配置实例是否像预期的那样执行?
请看使用 TestContext 框架 的测试 fixture 的依赖注入。
3.1.3. 事务管理
在访问真实数据库的测试中,一个常见的问题是它们对持久性存储的状态的影响。即使你使用一个开发数据库,对状态的改变也可能影响到未来的测试。另外,许多操作—如插入或修改持久化数据—不能在事务之外执行(或验证)。
TestContext 框架解决了这个问题。默认情况下,该框架为每个测试创建和回滚一个事务。你可以编写代码,可以假设事务的存在。如果你在测试中调用事务代理的对象,它们的行为是正确的,根据它们配置的事务语义。此外,如果一个测试方法在为测试管理的事务中运行时删除了所选表的内容,那么该事务会默认回滚,数据库会返回到执行测试前的状态。事务支持是通过使用测试应用上下文中定义的 PlatformTransactionManager
Bean提供给测试的。
如果你想让一个事务提交(不常见,但当你想让一个特定的测试填充或修改数据库时,偶尔会很有用),你可以通过使用 @Commit
注解来告诉 TestContext 框架使事务提交而不是回滚。
参见 TestContext框架 的事务管理。
3.1.4. 集成测试的支持类
Spring TestContext框架提供了几个 abstract
的支持类,简化了集成测试的编写。这些基础测试类为测试框架提供了定义明确的钩子(hook),以及方便的实例变量和方法,让你访问。
-
ApplicationContext,用于执行显式Bean查找或测试整个上下文的状态。
-
JdbcTemplate
,用于执行查询数据库的SQL语句。你可以在执行数据库相关的应用程序代码之前和之后使用这种查询来确认数据库状态,Spring确保这种查询在与应用程序代码相同的事务范围内运行。当与ORM工具一起使用时,要注意避免 误报。
此外,你可能想创建你自己的、适用于整个应用程序的超类(superclass),并为你的项目指定实例变量和方法。
参见 TestContext框架 的支持类。
4. JDBC测试的支持
4.1. JdbcTestUtils
org.springframework.test.jdbc
包包含 JdbcTestUtils
,它是一个JDBC相关实用函数的集合,旨在简化标准数据库测试场景。具体来说,JdbcTestUtils
提供了以下静态实用方法。
-
countRowsInTable(..)
: 计算给定表中的行数。 -
countRowsInTableWhere(..)
: 通过使用提供的WHERE
子句,计算给定表中的行数。 -
deleteFromTables(..)
: 删除指定表中的所有行。 -
deleteFromTableWhere(..)
: 通过使用提供的WHERE
子句,从给定的表中删除记录。 -
dropTables(..)
: 删除指定的表。
|
4.2. 嵌入式数据库
spring-jdbc
模块提供对配置和启动嵌入式数据库的支持,你可以在与数据库交互的集成测试中使用它。详情请见 嵌入式数据库支持 和 使用嵌入式数据库测试数据访问逻辑。
5. Spring TestContext 框架
Spring TestContext 框架(位于 org.springframework.test.context
包中)提供通用的、注解驱动的单元和集成测试支持,与使用的测试框架无关。TestContext框架也非常重视惯例而不是配置,有合理的默认值,你可以通过基于注解的配置来覆盖。
除了通用测试基础设施,TestContext框架还为JUnit 4、JUnit Jupiter(又称JUnit 5)和TestNG提供明确的支持。对于JUnit 4和TestNG,Spring提供了 abstract
的支持类。此外,Spring为JUnit 4提供了自定义的JUnit Runner
和自定义的JUnit Rules
,为JUnit Jupiter提供了自定义的 Extension
,让你编写所谓的POJO测试类。POJO测试类不需要扩展特定的类层次结构,比如 abstract
支持类。
下面一节概述了TestContext框架的内部结构。如果你只对使用该框架感兴趣,而对用你自己的自定义监听器或自定义加载器来扩展它不感兴趣,可以直接去看配置(上下文管理、依赖注入、事务管理)、支持类和 注解支持 部分。
5.1. 关键摘要
该框架的核心由 TestContextManager
类和 TestContext
、TestExecutionListener
和 SmartContextLoader
接口组成。为每个测试类创建一个 TestContextManager
(例如,为执行JUnit Jupiter中一个测试类中的所有测试方法)。 TestContextManager
反过来管理一个 TestContext
,它持有当前测试的上下文。 TestContextManager
还随着测试的进展更新 TestContext
的状态,并委托给 TestExecutionListener
实现,该实现通过提供依赖注入、管理事务等来记录实际的测试执行。SmartContextLoader
负责为一个给定的测试类加载一个 ApplicationContext
。参见 javadoc 和Spring测试套件,以获得进一步的信息和各种实现的例子。
5.1.1. TestContext
TestContext
封装了运行测试的上下文(与实际使用的测试框架无关),并为它所负责的测试实例提供上下文管理和缓存支持。TestContext
还委托给 SmartContextLoader
,以便在请求时加载 ApplicationContext
。
5.1.2. TestContextManager
TestContextManager
是进入Spring TestContext 框架的主要入口,它负责管理单个 TestContext
,并在明确定义的测试执行点向每个注册的 TestExecutionListener
发出事件信号。
-
在某一测试框架的任何 “before class” 或 “before all” 方法之前。
-
测试实例的后处理。
-
在特定测试框架的任何 “before” 或 “before each” 方法之前。
-
在执行测试方法之前,但在测试设置之后立即进行。
-
在执行测试方法之后,但在测试拆除之前,立即进行。
-
在特定测试框架的任何 “after” 或 “after each” 方法之后。
-
在一个特定测试框架的任何 “after class” 或 “after all” 方法之后。
5.1.3. TestExecutionListener
TestExecutionListener
定义了对由监听器注册的 TestContextManager
发布的测试执行事件做出反应的API。参见 TestExecutionListener
配置。
5.1.4. Context(上下文)加载器
ContextLoader
是一个策略接口,用于为Spring TestContext 框架管理的集成测试加载一个 ApplicationContext
。你应该实现 SmartContextLoader
而不是这个接口,以提供对组件类、活动bean定义配置文件(active bean definition profile)、测试属性源、上下文层次结构和 WebApplicationContext
的支持。
SmartContextLoader
是 ContextLoader
接口的一个扩展,它取代了原来的最小 ContextLoader
SPI。具体来说,SmartContextLoader
可以选择处理资源位置、组件类或上下文初始化器。此外,SmartContextLoader
可以在它加载的上下文中设置活动的bean定义配置文件和测试属性源。
Spring提供了以下实现。
-
DelegatingSmartContextLoader
: 两个默认加载器之一,它在内部委托给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
。
5.2. 启动 TestContext 框架
Spring TestContext 框架内部的默认配置足以满足所有常见的使用情况。然而,有时开发团队或第三方框架希望改变默认的 ContextLoader
,实现自定义的 TestContext
或 ContextCache
,增强 ContextCustomizerFactory
和 TestExecutionListener
实现的默认集,等等。对于这种对 TestContext 框架运行方式的底层控制,Spring提供了一个引导策略。
TestContextBootstrapper
定义了用于引导 TestContext 框架的SPI。 TestContextBootstrapper
被 TestContextManager
用来加载当前测试的 TestExecutionListener
实现并构建它所管理的 TestContext
。你可以通过使用 @BootstrapWith
,直接或作为元注解,为一个测试类(或测试类层次结构)配置一个自定义的引导策略。如果没有通过使用 @BootstrapWith
明确地配置一个引导器(bootstrapper),那么就会使用 DefaultTestContextBootstrapper
或者 WebTestContextBootstrapper
,这取决于 @WebAppConfiguration
的存在。
由于 TestContextBootstrapper
的SPI在未来可能会发生变化(以适应新的需求),我们强烈建议实现者不要直接实现这个接口,而是扩展 AbstractTestContextBootstrapper
或其具体子类之一。
5.3. TestExecutionListener
配置
Spring提供了以下 TestExecutionListener
的实现,这些实现在默认情况下被注册,完全按照以下顺序。
-
ServletTestExecutionListener
: 为WebApplicationContext
配置Servlet API mocks。 -
DirtiesContextBeforeModesTestExecutionListener
: 处理 “before” 模式的@DirtiesContext
注解。 -
ApplicationEventsTestExecutionListener
: 提供对ApplicationEvents
的支持。 -
DependencyInjectionTestExecutionListener
: 为测试实例提供依赖注入。 -
DirtiesContextTestExecutionListener
: 处理 “after” 模式的@DirtiesContext
注解。 -
TransactionalTestExecutionListener
: 提供具有默认回滚语义的事务性测试执行。 -
SqlScriptsTestExecutionListener
: 运行通过使用@Sql
注解配置的SQL脚本。 -
EventPublishingTestExecutionListener
: 向测试的ApplicationContext
发布测试执行事件(见 测试执行事件)。
5.3.1. 注册 TestExecutionListener
实现
你可以通过使用 @TestExecutionListeners
注解为一个测试类、它的子类和它的嵌套类明确地注册 TestExecutionListener
实现。请参阅 注解支持 和 @TestExecutionListeners
的javadoc以了解细节和示例。
切换到默认的
TestExecutionListener 实现如果你扩展了一个用 Java
Kotlin
|
5.3.2. 自动发现默认的 TestExecutionListener
实现
通过使用 @TestExecutionListeners
注册 TestExecutionListener
实现,适合于在有限的测试场景中使用的自定义监听器。然而,如果一个自定义监听器需要在整个测试套件中使用,它可能变得很麻烦。这个问题通过支持通过 SpringFactoriesLoader
机制自动发现默认 TestExecutionListener
实现来解决。
具体来说,spring-test
模块在其 META-INF/spring.factories
properties文件的 org.springframework.test.context.TestExecutionListener
key下声明了所有核心默认 TestExecutionListener
实现。第三方框架和开发者可以通过他们自己的 META-INF/spring.factories
properties 文件,以同样的方式向默认监听器列表贡献他们自己的 TestExecutionListener
实现。
5.3.3. 对 TestExecutionListener
实现进行排序
当 TestContext 框架通过 上述 SpringFactoriesLoader
机制发现默认的 TestExecutionListener
实现时,通过使用Spring的 AnnotationAwareOrderComparator
对实例化的监听器进行排序,它尊重Spring的 Ordered
接口和 @Order
注解的排序。AbstractTestExecutionListener
和Spring提供的所有默认 TestExecutionListener
实现都使用适当的值实现了 Ordered
。因此,第三方框架和开发人员应该确保他们的默认 TestExecutionListener
实现通过实现 Ordered
或声明 @Order
而以适当的顺序注册。参见核心默认 TestExecutionListener
实现的 getOrder()
方法的javadoc,以了解分配给每个核心监听器的值的细节。
5.3.4. 合并 TestExecutionListener
实现
如果通过 @TestExecutionListeners
注册了一个自定义的 TestExecutionListener
,默认监听器就不会被注册。在大多数常见的测试场景中,这有效地迫使开发者在任何自定义监听器之外手动声明所有默认监听器。下面的列表演示了这种配置风格。
@ContextConfiguration
@TestExecutionListeners({
MyCustomTestExecutionListener.class,
ServletTestExecutionListener.class,
DirtiesContextBeforeModesTestExecutionListener.class,
DependencyInjectionTestExecutionListener.class,
DirtiesContextTestExecutionListener.class,
TransactionalTestExecutionListener.class,
SqlScriptsTestExecutionListener.class
})
class MyTest {
// class body...
}
@ContextConfiguration
@TestExecutionListeners(
MyCustomTestExecutionListener::class,
ServletTestExecutionListener::class,
DirtiesContextBeforeModesTestExecutionListener::class,
DependencyInjectionTestExecutionListener::class,
DirtiesContextTestExecutionListener::class,
TransactionalTestExecutionListener::class,
SqlScriptsTestExecutionListener::class
)
class MyTest {
// class body...
}
这种方法的挑战在于,它要求开发者确切地知道哪些监听器是默认注册的。此外,默认监听器的集合会随着版本的变化而变化—例如,SqlScriptsTestExecutionListener
是在Spring Framework 4.1中引入的,而 DirtiesContextBeforeModesTestExecutionListener
是在Spring Framework 4.2中引入的。此外,Spring Boot和Spring Security等第三方框架通过使用上述的 自动发现机制 注册了自己的默认 TestExecutionListener
实现。
为了避免必须意识到并重新声明所有的默认监听器,你可以将 @TestExecutionListeners
的 mergeMode
属性设置为 MergeMode.MERGE_WITH_DEFAULTS
。MERGE_WITH_DEFAULTS
表示本地声明的监听器应该与默认监听器合并。合并算法确保从列表中删除重复的内容,并确保合并后的监听器集合根据 AnnotationAwareOrderComparator
的语义进行排序,如 对 TestExecutionListener
实现进行排序 中所述。如果一个监听器实现了 Ordered
或者被 @Order
注解了,它可以影响它与默认值合并的位置。否则,本地声明的监听器在合并时将被追加到默认监听器的列表中。
例如,如果前面例子中的 MyCustomTestExecutionListener
类将其 order
值(例如 500
)配置为小于 ServletTestExecutionListener
的顺序(刚好是 1000
),那么 MyCustomTestExecutionListener
就可以自动与 ServletTestExecutionListener
前面的默认列表合并,前面的例子可以替换为以下内容。
@ContextConfiguration
@TestExecutionListeners(
listeners = MyCustomTestExecutionListener.class,
mergeMode = MERGE_WITH_DEFAULTS
)
class MyTest {
// class body...
}
@ContextConfiguration
@TestExecutionListeners(
listeners = [MyCustomTestExecutionListener::class],
mergeMode = MERGE_WITH_DEFAULTS
)
class MyTest {
// class body...
}
5.4. Application Events
自Spring Framework 5.3.3以来,TestContext 框架提供了对记录 ApplicationContext
中发布的 application event 的支持,因此可以在测试中针对这些事件执行断言。在单个测试的执行过程中发布的所有事件都可以通过 ApplicationEvents
API获得,该API允许你将事件作为 java.util.Stream
进行处理。
要在你的测试中使用 ApplicationEvents
,请做以下工作。
-
确保你的测试类有
@RecordApplicationEvents
的注解或元注解。 -
确保
ApplicationEventsTestExecutionListener
被注册。但是请注意,ApplicationEventsTestExecutionListener
是默认注册的,只有当你通过@TestExecutionListeners
的自定义配置不包括默认监听器时,才需要手动注册。 -
用
@Autowired
注解ApplicationEvents
类型的字段,并在你的测试和生命周期方法中使用ApplicationEvents
的实例(如JUnit Jupiter中的@BeforeEach
和@AfterEach
方法)。-
当使用 JUnit Jupiter 的 SpringExtension 时,你可以在测试或生命周期方法中声明一个
ApplicationEvents
类型的方法参数,以替代测试类中的@Autowired
字段。
-
下面的测试类使用JUnit Jupiter的 SpringExtension
和 AssertJ 来断言在调用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 事件被发布。 |
@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
javadoc 以了解有关 ApplicationEvents
API 的进一步细节。
5.5. 测试执行事件
Spring Framework 5.2中引入的 EventPublishingTestExecutionListener
提供了一种实现自定义 TestExecutionListener
的替代方法。测试的 ApplicationContext
中的组件可以监听由 EventPublishingTestExecutionListener
发布的以下事件,每个事件都对应于 TestExecutionListener
API中的一个方法。
-
BeforeTestClassEvent
-
PrepareTestInstanceEvent
-
BeforeTestMethodEvent
-
BeforeTestExecutionEvent
-
AfterTestExecutionEvent
-
AfterTestMethodEvent
-
AfterTestClassEvent
这些事件可以出于各种原因被消费,例如重置模拟Bean或追踪测试执行。消费测试执行事件而不是实现一个自定义的 TestExecutionListener
的一个优点是,测试执行事件可以被任何在测试 ApplicationContext
中注册的Spring Bean消费,这些Bean可以直接受益于依赖注入和 ApplicationContext
的其他功能。相反,TestExecutionListener
不是 ApplicationContext
中的一个Bean。
因此, 如果你希望确保 同样地,如果 |
为了监听测试执行事件,Spring Bean可以选择实现 org.springframework.context.ApplicationListener
接口。另外,监听器方法可以用 @EventListener
来注解,并配置为监听上面列出的特定事件类型之一(见 基于注解的事件监听器)。由于这种方法的流行,Spring提供了以下专用的 @EventListener
注解,以简化测试执行事件监听器的注册。这些注解位于 org.springframework.test.context.event.annotation
包中。
-
@BeforeTestClass
-
@PrepareTestInstance
-
@BeforeTestMethod
-
@BeforeTestExecution
-
@AfterTestExecution
-
@AfterTestMethod
-
@AfterTestClass
5.5.1. 异常(Exception)处理
默认情况下,如果一个测试执行事件监听器在消费一个事件时抛出一个异常,这个异常将传播到正在使用的底层测试框架(如JUnit或TestNG)。例如,如果 BeforeTestMethodEvent
的消耗导致了一个异常,相应的测试方法就会因为这个异常而失败。相反,如果一个异步测试执行事件监听器抛出一个异常,该异常将不会传播到底层测试框架。关于异步异常处理的进一步细节,请参考 @EventListener
的类级javadoc。
5.5.2. 异步监听器
如果你想让一个特定的测试执行事件监听器异步处理事件,你可以使用 Spring 的 常规 @Async
支持。关于进一步的细节,请参考 @EventListener
的类级 javadoc。
5.6. Context 管理
每个 TestContext
为它所负责的测试实例提供上下文管理和缓存支持。测试实例不会自动收到对配置的 ApplicationContext
的访问。然而,如果一个测试类实现了 ApplicationContextAware
接口,对 ApplicationContext
的引用将提供给测试实例。请注意,AbstractJUnit4SpringContextTests
和 AbstractTestNGSpringContextTests
实现了 ApplicationContextAware
,因此,自动提供对 ApplicationContext
的访问。
@Autowired ApplicationContext
作为实现 Java
Kotlin
同样,如果你的测试被配置为加载一个 Java
Kotlin
通过使用 |
使用TestContext框架的测试类不需要扩展任何特定的类或实现一个特定的接口来配置他们的应用上下文。相反,配置是通过在类级别声明 @ContextConfiguration
注解来实现的。如果你的测试类没有明确声明应用上下文资源位置或组件类,那么配置的 ContextLoader
会决定如何从默认位置或默认配置类中加载上下文。除了上下文资源位置和组件类之外,应用上下文(application context)还可以通过应用上下文初始化器进行配置。
下面几节解释了如何使用Spring的 @ContextConfiguration
注解,通过使用XML配置文件、Groovy脚本、组件类(通常是 @Configuration
类)或上下文初始化器来配置测试 ApplicationContext
。另外,你也可以为高级用例实现和配置自己的自定义 SmartContextLoader
。
5.6.1. 使用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 | 将 location 属性设置为一个XML文件的列表。 |
@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 | 将 location 属性设置为一个XML文件的列表。 |
@ContextConfiguration
通过标准的 Java value
属性支持 locations
属性的别名。因此,如果你不需要在 @ContextConfiguration
中声明额外的属性,你可以省略对 locations
属性名称的声明,并通过使用下面例子中演示的速记格式来声明资源位置。
@ExtendWith(SpringExtension.class)
@ContextConfiguration({"/app-config.xml", "/test-config.xml"}) (1)
class MyTest {
// class body...
}
1 | 在不使用 locations 属性的情况下指定XML文件。 |
@ExtendWith(SpringExtension::class)
@ContextConfiguration("/app-config.xml", "/test-config.xml") (1)
class MyTest {
// class body...
}
1 | 在不使用 locations 属性的情况下指定XML文件。 |
如果你从 @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 | 从默认位置加载配置。 |
@ExtendWith(SpringExtension::class)
// ApplicationContext will be loaded from
// "classpath:com/example/MyTest-context.xml"
@ContextConfiguration (1)
class MyTest {
// class body...
}
1 | 从默认位置加载配置。 |
5.6.2. 用Groovy脚本进行 Context 配置
要通过使用 Groovy Bean Definition DSL 的 Groovy 脚本为你的测试加载 ApplicationContext
,你可以用 @ContextConfiguration
注解你的测试类,并用一个包含 Groovy 脚本资源位置的数组配置 locations
或 value
属性。Groovy脚本的资源查找语义与XML配置文件的描述相同。
启用Groovy脚本支持
如果Groovy在classpath上,就会自动启用对使用Groovy脚本在Spring TestContext框架中加载 ApplicationContext 的支持。
|
下面的例子显示了如何指定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...
}
1 | 指定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...
}
1 | 指定Groovy配置文件的位置。 |
如果你省略了 @ContextConfiguration
注解中的 location
和 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 | 从默认位置加载配置。 |
@ExtendWith(SpringExtension::class)
// ApplicationContext will be loaded from
// "classpath:com/example/MyTestContext.groovy"
@ContextConfiguration (1)
class MyTest {
// class body...
}
1 | 从默认位置加载配置。 |
同时声明XML配置和Groovy脚本
你可以通过使用 下面的列表显示了如何在集成测试中结合两者。 Java
Kotlin
|
5.6.3. 使用组件类的 Context 配置
要通过使用组件类为你的测试加载 ApplicationContext
(见 基于Java的容器配置),你可以用 @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 | 指定组件类。 |
@ExtendWith(SpringExtension::class)
// ApplicationContext will be loaded from AppConfig and TestConfig
@ContextConfiguration(classes = [AppConfig::class, TestConfig::class]) (1)
class MyTest {
// class body...
}
1 | 指定组件类。 |
组件类(Component Classes)
术语 "组件类" 可以指以下任何一种。
请参阅 |
如果你省略了 @ContextConfiguration
注解中的 classes
属性,TestContext 框架会尝试检测默认配置类的存在。具体来说,AnnotationConfigContextLoader
和 AnnotationConfigWebContextLoader
会检测测试类的所有 static
嵌套类,这些类符合配置类实现的要求,如 @Configuration
javadoc 中所规定的。请注意,配置类的名称是任意的。此外,如果需要,一个测试类可以包含一个以上的 static
嵌套配置类。在下面的例子中,OrderServiceTest
类声明了一个名为 Config
的 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 类中加载配置信息。 |
@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 类中加载配置信息。 |
5.6.4. 混合使用 XML、Groovy 脚本和组件类
有时可能需要混合XML配置文件、Groovy脚本和组件类(通常是 @Configuration
类)来为你的测试配置一个 ApplicationContext
。例如,如果你在生产中使用XML配置,你可能决定要使用 @Configuration
类来为你的测试配置特定的Spring管理的组件,反之亦然。
此外,一些第三方框架(如Spring Boot)为同时从不同类型的资源(例如,XML配置文件、Groovy脚本和 @Configuration
类)加载 ApplicationContext
提供了一流的支持。从历史上看,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
就是从这里加载的,但你仍然可以自由地包含或导入其他类型的配置。
5.6.5. 带有 Context 初始化器(Initializers) 的 Context 配置
要通过使用上下文初始化器为你的测试配置 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 | 通过使用一个配置类和一个初始化器(initializer)来指定配置。 |
@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 | 通过使用一个配置类和一个初始化器(initializer)来指定配置。 |
你也可以完全省略 @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 | 只使用一个初始化器(initializer)来指定配置。 |
@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 | 只使用一个初始化器(initializer)来指定配置。 |
5.6.6. Context Configuration 的继承性
@ContextConfiguration
支持 boolean inheritLocations
和 inheritInitializers
属性,表示是否应该继承超类所声明的资源位置或组件类和上下文初始化器(initializers)。这两个标志的默认值是 true
。这意味着测试类会继承任何超类所声明的资源位置或组件类,以及上下文初始化器。具体来说,一个测试类的资源位置或组件类被附加到超类所声明的资源位置或注解类的列表中。同样地,一个给定的测试类的初始化器被添加到测试超类定义的初始化器集合中。因此,子类可以选择扩展资源位置、组件类或上下文初始化器。
如果 @ContextConfiguration
中的 inheritLocations
或 inheritInitializers
属性被设置为 false
,那么测试类的资源位置或组件类和上下文初始化器就会分别产生阴影,并有效地取代由超类定义的配置。
从Spring Framework 5.3开始,测试配置也可以从包围的类中继承下来。详见 @Nested 测试类的配置。
|
在下一个使用 XML 资源位置的例子中,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 | 在子类中定义的配置文件。 |
@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 | 在子类中定义的配置文件。 |
同样,在下一个使用组件类的例子中,ExtendedTest
的 ApplicationContext
依次从 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 | 在子类中定义的配置类。 |
// 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 | 在子类中定义的配置类。 |
在下一个使用上下文初始化器(context initializer)的例子中,ExtendedTest
的 ApplicationContext
是通过使用 BaseInitializer
和 ExtendedInitializer
来初始化的。但是请注意,初始化器被调用的顺序取决于它们是否实现了Spring 的 Ordered
接口,或被Spring的 @Order
注解或标准的 @Priority
注解所注解。下面的例子显示了一个类如何扩展另一个类并同时使用自己的初始化器和超类的初始化器(initializer)。
// ApplicationContext will be initialized by BaseInitializer
@SpringJUnitConfig(initializers = BaseInitializer.class) (1)
class BaseTest {
// class body...
}
// ApplicationContext will be initialized by BaseInitializer
// and ExtendedInitializer
@SpringJUnitConfig(initializers = ExtendedInitializer.class) (2)
class ExtendedTest extends BaseTest {
// class body...
}
1 | 在超类中定义的初始化器(Initializer )。 |
2 | 在子类中定义的初始化器(Initializer )。 |
// 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 | 在超类中定义的初始化器(Initializer )。 |
2 | 在子类中定义的初始化器(Initializer )。 |
5.6.7. 使用 Environment Profiles 的上下文(Context )配置
Spring框架对环境和配置文件(AKA "bean definition profiles")的概念有一流的支持,集成测试可以被配置为激活各种测试场景的特定bean definition profiles。这可以通过用 @ActiveProfiles
注解来实现,并提供一个在加载测试的 ApplicationContext
时应该被激活的配置文件(profiles)列表。
你可以在 SmartContextLoader SPI 的任何实现中使用 @ActiveProfiles ,但 @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
}
}
@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
配置文件中。
通过用 @ActiveProfiles("dev")
注解 TransferServiceTest
,我们指示Spring TestContext 框架加载 ApplicationContext
,并将活动配置文件设置为 {"dev"}
。结果是,一个嵌入式数据库被创建并填充了测试数据,而且 accountRepository
Bean被装配到了开发 DataSource
的引用。这可能就是我们在集成测试中想要的。
有时,将bean分配到一个 default
的配置文件(profile)是很有用的。只有在没有特别激活其他配置文件的情况下,默认配置文件中的Bean才会被包含。你可以用它来定义 "后备" 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();
}
}
@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");
}
}
@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();
}
}
@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();
}
}
@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
}
}
@SpringJUnitConfig(
TransferServiceConfig::class,
StandaloneDataConfig::class,
JndiDataConfig::class,
DefaultDataConfig::class)
@ActiveProfiles("dev")
class TransferServiceTest {
@Autowired
lateinit var transferService: TransferService
@Test
fun testTransferService() {
// test the transferService
}
}
下面的代码列表演示了如何用 @Configuration
类而不是XML实现相同的配置和集成测试。
-
TransferServiceConfig
: 通过使用@Autowired
,通过依赖注入获得一个dataSource
。 -
StandaloneDataConfig
: 定义了一个适合开发者测试的嵌入式数据库的dataSource
。 -
JndiDataConfig
: 定义了一个在生产环境中从JNDI检索的dataSource
。 -
DefaultDataConfig
: 为默认的嵌入式数据库定义一个dataSource
,以备没有配置文件(profile)时使用。
和基于XML的配置例子一样,我们仍然用 @ActiveProfiles("dev")
来注解 TransferServiceTest
,但这次我们通过使用 @ContextConfiguration
注解来指定所有四个配置类。测试类的主体本身保持完全不变。
通常的情况是,在一个给定的项目中,一组配置文件被用于多个测试类。因此,为了避免重复声明 @ActiveProfiles
注解,你可以在基类上声明一次 @ActiveProfiles
,子类自动从基类继承 @ActiveProfiles
配置。在下面的例子中,@ActiveProfiles
的声明(以及其他注解)已经被移到了 abstract 的超类 AbstractIntegrationTest
。
从 Spring Framework 5.3 开始,测试配置也可以从包围的类中继承下来。详见 @Nested 测试类的配置 。
|
@SpringJUnitConfig({
TransferServiceConfig.class,
StandaloneDataConfig.class,
JndiDataConfig.class,
DefaultDataConfig.class})
@ActiveProfiles("dev")
abstract class AbstractIntegrationTest {
}
@SpringJUnitConfig(
TransferServiceConfig::class,
StandaloneDataConfig::class,
JndiDataConfig::class,
DefaultDataConfig::class)
@ActiveProfiles("dev")
abstract class AbstractIntegrationTest {
}
// "dev" profile inherited from superclass
class TransferServiceTest extends AbstractIntegrationTest {
@Autowired
TransferService transferService;
@Test
void testTransferService() {
// test the transferService
}
}
// "dev" profile inherited from superclass
class TransferServiceTest : AbstractIntegrationTest() {
@Autowired
lateinit var transferService: TransferService
@Test
fun testTransferService() {
// test the transferService
}
}
@ActiveProfiles
还支持一个 inheritProfiles
属性,可以用来禁用活动配置文件(active profile)的继承,如下例所示。
// "dev" profile overridden with "production"
@ActiveProfiles(profiles = "production", inheritProfiles = false)
class ProductionTransferServiceTest extends AbstractIntegrationTest {
// test body
}
// "dev" profile overridden with "production"
@ActiveProfiles("production", inheritProfiles = false)
class ProductionTransferServiceTest : AbstractIntegrationTest() {
// test body
}
此外,有时需要以编程方式而不是声明方式解决测试的活动配置文件(active profile)--例如,基于:
-
当前的操作系统。
-
测试是否在持续集成构建服务器上运行。
-
某些环境变量的存在。
-
自定义类级注解的存在。
-
其他关注的问题。
为了以编程方式解析活动bean定义配置文件(profile),你可以实现一个自定义的 ActiveProfilesResolver
并通过使用 @ActiveProfiles
的 resolver
属性来注册它。有关进一步的信息,请参阅相应的 javadoc。下面的示例演示了如何实现和注册一个自定义 OperatingSystemActiveProfilesResolver
。
// "dev" profile overridden programmatically via a custom resolver
@ActiveProfiles(
resolver = OperatingSystemActiveProfilesResolver.class,
inheritProfiles = false)
class TransferServiceTest extends AbstractIntegrationTest {
// test body
}
// "dev" profile overridden programmatically via a custom resolver
@ActiveProfiles(
resolver = OperatingSystemActiveProfilesResolver::class,
inheritProfiles = false)
class TransferServiceTest : AbstractIntegrationTest() {
// test body
}
public class OperatingSystemActiveProfilesResolver implements ActiveProfilesResolver {
@Override
public String[] resolve(Class<?> testClass) {
String profile = ...;
// determine the value of profile based on the operating system
return new String[] {profile};
}
}
class OperatingSystemActiveProfilesResolver : ActiveProfilesResolver {
override fun resolve(testClass: Class<*>): Array<String> {
val profile: String = ...
// determine the value of profile based on the operating system
return arrayOf(profile)
}
}
5.6.8. 带有测试属性源(Property Sources)的 Context 配置
Spring框架对具有属性源层次结构的环境概念有一流的支持,你可以用测试特定的属性源配置集成测试。与 @Configuration
类上使用的 @PropertySource
注解不同,你可以在测试类上声明 @TestPropertySource
注解,以声明测试属性文件或内联属性的资源位置。这些测试属性源被添加到 Environment
中的 PropertySources
集合中,用于加载注解的集成测试的 ApplicationContext
。
你可以在
|
声明测试属性源(Property Sources)
你可以通过使用 @TestPropertySource
的 locations
或 value
属性来配置测试属性(properties)文件。
支持传统的和基于XML的属性文件格式—例如,"classpath:/com/example/test.properties"`或 `"file:///path/to/file.xml"
。
每个路径都被解释为一个Spring Resource
。一个普通的路径(例如,"test.properties"
)被视为一个classpath resource,它与定义测试类的包相对。以斜线开头的路径被视为绝对的classpath resource(例如:"/org/example/test.xml"
)。引用URL的路径(例如,以 classpath:
、file:
或 http:
为前缀的路径)通过使用指定的 resource 协议加载。 resource位置通配符(如 */.properties
)是不允许的:每个位置必须精确地评估为一个 .properties
或 .xml
resource。
下面的例子使用了一个测试属性文件(test properties file):
@ContextConfiguration
@TestPropertySource("/test.properties") (1)
class MyIntegrationTests {
// class body...
}
1 | 指定一个具有绝对路径的属性文件。 |
@ContextConfiguration
@TestPropertySource("/test.properties") (1)
class MyIntegrationTests {
// class body...
}
1 | 指定一个具有绝对路径的属性文件。 |
你可以通过使用 @TestPropertySource
的 properties
属性,以键值对的形式配置内联属性,如下例所示。所有的键值对被添加到包围的 Environment
中,作为一个具有最高优先级的单一测试 PropertySource
。
支持的键值对的语法与为Java属性(properties)文件中的条目定义的语法相同:
-
key=value
-
key:value
-
key value
下面的例子设置了两个内联的属性:
@ContextConfiguration
@TestPropertySource(properties = {"timezone = GMT", "port: 4242"}) (1)
class MyIntegrationTests {
// class body...
}
1 | 通过使用键值语法的两种变化来设置两个属性。 |
@ContextConfiguration
@TestPropertySource(properties = ["timezone = GMT", "port: 4242"]) (1)
class MyIntegrationTests {
// class body...
}
1 | 通过使用键值语法的两种变化来设置两个属性。 |
从Spring Framework 5.2开始, 此外,你可以在一个测试类上声明多个组成注解,每个注解都用 直接存在的 |
默认属性(Properties)文件检测
如果 @TestPropertySource
被声明为一个空注解(也就是说,没有明确的 locations
或 properties
值),就会尝试检测一个相对于声明该注解的类的默认属性文件。例如,如果被注解的测试类是 com.example.MyTest
,相应的默认属性文件是 classpath:com/example/MyTest.properties
。如果不能检测到默认,就会抛出一个 IllegalStateException
。
优先级
测试属性(properties)比在操作系统环境中定义的属性、Java系统属性或应用程序通过使用 @PropertySource
声明性地或以编程方式添加的属性源有更高的优先权。因此,测试属性可以被用来选择性地覆盖从系统和应用程序属性源加载的属性。此外,内联的属性比从资源位置加载的属性有更高的优先权。然而,请注意,通过 @DynamicPropertySource
注册的属性比通过 @TestPropertySource
加载的属性有更高的优先权。
在下一个例子中,timezone
和 port
属性以及 "/test.properties"
中定义的任何属性都会覆盖系统和应用程序属性源中定义的任何同名属性。此外,如果 "/test.properties"
文件为 timezone
和 port
属性定义了条目,这些条目会被通过使用 properties
声明的内联属性覆盖。下面的例子显示了如何在文件和内联中指定属性:
@ContextConfiguration
@TestPropertySource(
locations = "/test.properties",
properties = {"timezone = GMT", "port: 4242"}
)
class MyIntegrationTests {
// class body...
}
@ContextConfiguration
@TestPropertySource("/test.properties",
properties = ["timezone = GMT", "port: 4242"]
)
class MyIntegrationTests {
// class body...
}
继承和重写测试属性源
@TestPropertySource
支持布尔值 inheritLocations
和 inheritProperties
属性,表示是否应该继承由超类声明的属性文件和内联属性的资源 locations。这两个标志的默认值是 true
。这意味着测试类会继承任何超类所声明的 locations 和内联属性。具体来说,测试类的 locations 和内联属性被附加到超类所声明的 locations 和内联属性上。因此,子类可以选择扩展 locations 和内联属性。注意,后面出现的属性会影射(也就是覆盖)前面出现的同名属性。此外,前面提到的优先规则也适用于继承的测试属性源。
如果 @TestPropertySource
中的 inheritLocations
或 inheritProperties
属性被设置为 false
,那么测试类的位置或内联属性就会分别产生阴影(shadow),并有效地取代由超类定义的配置。
从Spring Framework 5.3开始,测试配置也可以从包围的类中继承下来。详见 @Nested 测试类的配置。
|
在下一个例子中,BaseTest
的 ApplicationContext
是通过仅使用 base.properties
文件作为测试属性源加载的。与此相反,ExtendedTest
的 ApplicationContext
是通过使用 base.properties
和 extended.properties
文件作为测试属性源 locations 来加载的。下面的例子显示了如何通过使用 properties
文件在子类和其超类中定义属性:
@TestPropertySource("base.properties")
@ContextConfiguration
class BaseTest {
// ...
}
@TestPropertySource("extended.properties")
@ContextConfiguration
class ExtendedTest extends BaseTest {
// ...
}
@TestPropertySource("base.properties")
@ContextConfiguration
open class BaseTest {
// ...
}
@TestPropertySource("extended.properties")
@ContextConfiguration
class ExtendedTest : BaseTest() {
// ...
}
在下一个例子中,BaseTest
的 ApplicationContext
只通过使用内联的 key1
属性被加载。与此相反,ExtendedTest
的 ApplicationContext
是通过使用内联的 key1
和 key2
属性加载的。下面的例子显示了如何通过使用内联属性在子类和它的超类中定义属性:
@TestPropertySource(properties = "key1 = value1")
@ContextConfiguration
class BaseTest {
// ...
}
@TestPropertySource(properties = "key2 = value2")
@ContextConfiguration
class ExtendedTest extends BaseTest {
// ...
}
@TestPropertySource(properties = ["key1 = value1"])
@ContextConfiguration
open class BaseTest {
// ...
}
@TestPropertySource(properties = ["key2 = value2"])
@ContextConfiguration
class ExtendedTest : BaseTest() {
// ...
}
5.6.9. 使用动态属性源的 Context 配置
从Spring Framework 5.2.5开始,TestContext框架通过 @DynamicPropertySource
注解提供了对动态属性的支持。这个注解可用于集成测试,它需要将具有动态值的属性添加到集成测试加载的 ApplicationContext
的 Environment
中的 PropertySources
集合。
|
与应用于类级别的 @TestPropertySource
注解不同,@DynamicPropertySource
必须应用于一个 static
方法,该方法接受一个 DynamicPropertyRegistry
参数,用于向 Environment
添加name-value对。值是动态的,并通过一个 Supplier
提供,该 Supplier
只有在属性被解析时才会被调用。通常情况下,方法引用被用来提供值,正如在下面的例子中所看到的,它使用 Testcontainers 项目来管理Spring ApplicationContext
之外的Redis容器。管理的Redis容器的IP地址和端口通过 redis.host
和 redis.port
属性提供给测试的 ApplicationContext
中的组件。这些属性可以通过Spring的 Environment
抽象访问,也可以直接注入Spring管理的组件中—例如,分别通过 @Value("${redis.host}")
和 @Value("${redis.port}")
。
如果你在基类中使用了 |
@SpringJUnitConfig(/* ... */)
@Testcontainers
class ExampleIntegrationTests {
@Container
static GenericContainer redis =
new GenericContainer("redis:5.0.3-alpine").withExposedPorts(6379);
@DynamicPropertySource
static void redisProperties(DynamicPropertyRegistry registry) {
registry.add("redis.host", redis::getHost);
registry.add("redis.port", redis::getFirstMappedPort);
}
// tests ...
}
@SpringJUnitConfig(/* ... */)
@Testcontainers
class ExampleIntegrationTests {
companion object {
@Container
@JvmStatic
val redis: GenericContainer =
GenericContainer("redis:5.0.3-alpine").withExposedPorts(6379)
@DynamicPropertySource
@JvmStatic
fun redisProperties(registry: DynamicPropertyRegistry) {
registry.add("redis.host", redis::getHost)
registry.add("redis.port", redis::getFirstMappedPort)
}
}
// tests ...
}
5.6.10. 加载 WebApplicationContext
为了指示 TestContext 框架加载一个 WebApplicationContext
而不是标准的 ApplicationContext
,你可以用 @WebAppConfiguration
来注解各自的测试类。
@WebAppConfiguration
在你的测试类上的存在指示了TestContext框架(TCF)应该为你的集成测试加载一个 WebApplicationContext
(WAC)。在后台,TCF确保一个 MockServletContext
被创建并提供给你的测试的WAC。默认情况下,MockServletContext
的基本资源路径被设置为 src/main/webapp
。这被解释为相对于你的JVM根的路径(通常是你的项目的路径)。如果你熟悉Maven项目中Web应用的目录结构,就会知道 src/main/webapp
是WAR根目录的默认位置。如果你需要覆盖这个默认位置,你可以在 @WebAppConfiguration
注解中提供一个替代路径(例如,@WebAppConfiguration("src/test/webapp")
)。如果你希望从classpath而不是文件系统中引用基础资源路径,你可以使用Spring的 classpath:
前缀。
请注意,Spring对 WebApplicationContext
实现的测试支持与它对标准 ApplicationContext
实现的支持是一样的。当使用 WebApplicationContext
进行测试时,你可以通过使用 @ContextConfiguration
来自由声明XML配置文件、Groovy脚本或 @Configuration
类。你也可以自由地使用任何其他测试注解,如 @ActiveProfiles
、 @TestExecutionListeners
、@Sql
、@Rollback
等。
本节剩下的例子展示了一些加载 WebApplicationContext
的各种配置选项。下面的例子展示了TestContext框架对惯例的支持,而不是配置:
@ExtendWith(SpringExtension.class)
// defaults to "file:src/main/webapp"
@WebAppConfiguration
// detects "WacTests-context.xml" in the same package
// or static nested @Configuration classes
@ContextConfiguration
class WacTests {
//...
}
@ExtendWith(SpringExtension::class)
// defaults to "file:src/main/webapp"
@WebAppConfiguration
// detects "WacTests-context.xml" in the same package
// or static nested @Configuration classes
@ContextConfiguration
class WacTests {
//...
}
如果你用 @WebAppConfiguration
来注解一个测试类,而没有指定资源基础路径,那么资源路径实际上默认为 file:src/main/webapp
。同样地,如果你声明了 @ContextConfiguration
而没有指定资源 locations
、组件 classes
或 context initializers
,Spring会尝试通过惯例(即 WacTests-context.xml
与 WacTests
类或静态嵌套的 @Configuration
类在同一个包里)来检测你的配置的存在。
下面的例子显示了如何用 @WebAppConfiguration
显式地声明资源基本路径和用 @ContextConfiguration
声明XML资源 location:
@ExtendWith(SpringExtension.class)
// file system resource
@WebAppConfiguration("webapp")
// classpath resource
@ContextConfiguration("/spring/test-servlet-config.xml")
class WacTests {
//...
}
@ExtendWith(SpringExtension::class)
// file system resource
@WebAppConfiguration("webapp")
// classpath resource
@ContextConfiguration("/spring/test-servlet-config.xml")
class WacTests {
//...
}
这里需要注意的是这两个注解的路径的不同语义。默认情况下,@WebAppConfiguration
资源路径是基于文件系统的,而 @ContextConfiguration
资源位置是基于classpath的。
下面的例子表明,我们可以通过指定 Spring resource prefix 来覆盖这两个注解的默认资源语义:
@ExtendWith(SpringExtension.class)
// classpath resource
@WebAppConfiguration("classpath:test-web-resources")
// file system resource
@ContextConfiguration("file:src/main/webapp/WEB-INF/servlet-config.xml")
class WacTests {
//...
}
@ExtendWith(SpringExtension::class)
// classpath resource
@WebAppConfiguration("classpath:test-web-resources")
// file system resource
@ContextConfiguration("file:src/main/webapp/WEB-INF/servlet-config.xml")
class WacTests {
//...
}
将这个例子中的注释与上一个例子进行对比。
5.6.11. 使用Web Mock
为了提供全面的Web测试支持,TestContext框架有一个 ServletTestExecutionListener
,默认是启用的。当针对 WebApplicationContext
进行测试时,这个 TestExecutionListener
通过在每个测试方法之前使用Spring Web的RequestContextHolder来设置默认的线程本地状态,并根据用 @WebAppConfiguration
配置的基本资源路径创建一个 MockHttpServletRequest
、一个 MockHttpServletResponse
和一个 ServletWebRequest
。 ServletTestExecutionListener
还确保 MockHttpServletResponse
和 ServletWebRequest
可以被注入到测试实例中,并且,一旦测试完成,它将清理线程本地状态。
一旦你为你的测试加载了 WebApplicationContext
,你可能会发现你需要与Web mocks交互—例如,设置你的测试夹具或在调用你的Web组件后执行断言。下面的例子显示了哪些模拟可以被自动装配到你的测试实例。请注意,WebApplicationContext
和 MockServletContext
都在整个测试套件中被缓存,而其他 mock 是由 ServletTestExecutionListener
管理每个测试方法。
@SpringJUnitWebConfig
class WacTests {
@Autowired
WebApplicationContext wac; // cached
@Autowired
MockServletContext servletContext; // cached
@Autowired
MockHttpSession session;
@Autowired
MockHttpServletRequest request;
@Autowired
MockHttpServletResponse response;
@Autowired
ServletWebRequest webRequest;
//...
}
@SpringJUnitWebConfig
class WacTests {
@Autowired
lateinit var wac: WebApplicationContext // cached
@Autowired
lateinit var servletContext: MockServletContext // cached
@Autowired
lateinit var session: MockHttpSession
@Autowired
lateinit var request: MockHttpServletRequest
@Autowired
lateinit var response: MockHttpServletResponse
@Autowired
lateinit var webRequest: ServletWebRequest
//...
}
5.6.12. 上下文(Context)缓存
一旦TestContext框架为一个测试加载了 ApplicationContext
(或 WebApplicationContext
),该上下文就会被缓存,并在同一测试套件中声明相同的唯一上下文配置的所有后续测试中重复使用。要理解缓存是如何工作的,重要的是要理解 "唯一" 和 "测试套件 "的含义。
ApplicationContext
可以由用于加载它的配置参数的组合来唯一地识别。因此,配置参数的唯一组合被用来生成一个key,在这个key下,上下文被缓存。TestContext框架使用以下配置参数来建立上下文缓存key:
-
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
为 @ContextConfiguration
的 location
(或 value
)属性指定了 {"app-config.xml", "test-config.xml"}
,TestContext框架就会加载相应的 ApplicationContext
并将其存储在 static
context缓存中,该缓存的key只基于这些 locations。因此,如果 TestClassB
也定义了 {"app-config.xml", "test-config.xml"}
作为它的locations(无论是显式还是隐式通过继承),但没有定义 @WebAppConfiguration
、不同的 ContextLoader
、不同的活动配置文件(active profile)、不同的context初始化器(initializers)、不同的测试属性源或不同的父context,那么两个测试类就共享同一个 ApplicationContext
。这意味着加载 application context 的设置成本只发生一次(每个测试套件),随后的测试执行会快很多。
测试套件和fork进程
Spring TestContext框架在静态缓存中存储应用上下文。这意味着上下文实际上是存储在一个 为了从缓存机制中获益,所有的测试必须在同一进程或测试套件中运行。这可以通过在IDE中作为一个组执行所有测试来实现。同样,当使用 Ant、Maven 或 Gradle 等构建框架执行测试时,必须确保构建框架在测试之间不fork。例如,如果 Maven Surefire 插件的 |
上下文缓存的大小是有限制的,默认最大大小为32。每当达到最大容量时,就会使用最近使用最少的驱逐策略(LRU)来驱逐和关闭陈旧的上下文。你可以通过设置名为 spring.test.context.cache.maxSize
的JVM系统属性,从命令行或构建脚本中配置最大尺寸。作为替代方法,你可以通过 SpringProperties
机制设置相同的属性。
由于在一个给定的测试套件中加载大量的应用程序上下文会导致套件运行时间过长,因此准确了解有多少上下文被加载和缓存往往是有益的。要查看底层上下文缓存的统计数据,你可以将 org.springframework.test.context.cache
日志类别的日志级别设为 DEBUG
。
在不太可能发生的情况下,测试破坏了应用程序上下文并需要重新加载(例如,通过修改Bean定义或应用程序对象的状态),你可以用 @DirtiesContext
来注解你的测试类或测试方法(见 Spring测试注解 中关于 @DirtiesContext
的讨论)。这将指示Spring在运行需要相同应用上下文的下一个测试之前,从缓存中删除上下文并重建应用上下文。请注意,对 @DirtiesContext
注解的支持是由 DirtiesContextBeforeModesTestExecutionListener
和 DirtiesContextTestExecutionListener
提供的,它们默认是启用的。
ApplicationContext生命周期和控制台日志
当你需要调试一个用Spring TestContext框架执行的测试时,分析控制台输出(即输出到 关于由Spring框架本身或由在 测试的 一个测试的
如果上下文在某个特定的测试方法之后根据 当Spring |
5.6.13. 上下文(Context)层次结构
当编写依赖于加载的Spring ApplicationContext
的集成测试时,针对单个上下文进行测试通常是足够的。然而,有些时候,针对 ApplicationContext
实例的层次结构进行测试是有益的,甚至是必要的。例如,如果你正在开发一个Spring MVC Web应用程序,你通常有一个由Spring的 ContextLoaderListener
加载的root WebApplicationContext
和一个由Spring的 DispatcherServlet
加载的子 WebApplicationContext
。这导致了一个父子上下文层次结构,其中共享组件和基础设施配置在根上下文中被声明,并在子上下文中被特定的Web组件消费。另一个用例可以在Spring批处理(Batch)应用程序中找到,在那里你通常有一个为共享批处理基础设施提供配置的父上下文和一个为特定批处理作业配置的子上下文。
你可以通过用 @ContextHierarchy
注解声明上下文配置来编写使用上下文分层的集成测试,可以在单个测试类或测试类分层中声明。如果在一个测试类层次结构中的多个类上声明了上下文层次结构,你也可以合并或覆盖上下文层次结构中特定的、命名的级别的上下文配置。当合并层次结构中特定级别的配置时,配置资源类型(即 XML 配置文件或组件类)必须一致。否则,完全可以接受在一个上下文层次结构中的不同级别使用不同的资源类型进行配置。
本节中剩余的基于JUnit Jupiter的例子展示了集成测试的常见配置场景,需要使用上下文层次。
具有上下文层次结构的单一测试类
ControllerIntegrationTests
代表了一个典型的Spring MVC Web应用程序的集成测试场景,它声明了一个由两层组成的上下文层次,一个是root WebApplicationContext
(通过使用 TestAppConfig
@Configuration
类加载),一个是调度器Servlet WebApplicationContext
(通过使用 WebConfig
@Configuration
类加载)。被自动装配到测试实例的 WebApplicationContext
是子上下文(即层次结构中最低的上下文)。下面的列表显示了这种配置情况:
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextHierarchy({
@ContextConfiguration(classes = TestAppConfig.class),
@ContextConfiguration(classes = WebConfig.class)
})
class ControllerIntegrationTests {
@Autowired
WebApplicationContext wac;
// ...
}
@ExtendWith(SpringExtension::class)
@WebAppConfiguration
@ContextHierarchy(
ContextConfiguration(classes = [TestAppConfig::class]),
ContextConfiguration(classes = [WebConfig::class]))
class ControllerIntegrationTests {
@Autowired
lateinit var wac: WebApplicationContext
// ...
}
具有隐性父级上下文的类层次结构
本例中的测试类在测试类层次结构中定义了一个上下文层次结构。AbstractWebTests
声明了Spring驱动的Web应用中root 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 {}
@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 Framework 合并 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 {}
@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 {}
@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() {}
Dirtying a context within a context hierarchy
如果你在一个测试中使用 @DirtiesContext ,其上下文被配置为上下文层次结构的一部分,你可以使用 hierarchyMode 标志来控制如何清除上下文缓存。关于进一步的细节,请参见 Spring Testing Annotations 中关于 @DirtiesContext 的讨论以及 @DirtiesContext javadoc。
|
5.7. Test Fixture 的依赖注入
当你使用 DependencyInjectionTestExecutionListener
(默认配置)时,你的测试实例的依赖关系从你用 @ContextConfiguration
或相关注解配置的应用上下文中的Bean注入。你可以使用setter注入、字段注入或两者兼而有之,这取决于你选择哪种注解,以及你是把它们放在setter方法还是字段上。如果你使用JUnit Jupiter,你也可以选择使用构造器注入(见 SpringExtension
依赖注入)。为了与Spring基于注解的注入支持保持一致,你也可以使用Spring的 @Autowired
注解或来自JSR-330的 @Inject
注解进行字段和setter注入。
对于JUnit Jupiter以外的测试框架,TestContext框架不参与测试类的实例化。因此,对构造函数使用 @Autowired 或 @Inject 对测试类没有影响。
|
尽管在生产代码中不鼓励字段注入,但字段注入在测试代码中其实是很自然的。这种区别的理由是你永远不会直接实例化你的测试类。因此,没有必要在你的测试类上调用 public 构造函数或 setter 方法。
|
因为 @Autowired
是用来 按类型执行自动装配 的,如果你有多个相同类型的Bean定义,你就不能对这些特定的Bean依赖这种方法。在这种情况下,你可以将 @Autowired
与 @Qualifier
结合使用。你也可以选择将 @Inject
与 @Named
结合使用。另外,如果你的测试类可以访问它的 ApplicationContext
,你可以通过使用(例如)调用 applicationContext.getBean("titleRepository", TitleRepository.class)
来进行显式查找。
如果你不希望依赖注入应用于你的测试实例,就不要用 @Autowired
或 @Inject
来注解字段或 setter 方法。另外,你可以通过明确地用 @TestExecutionListeners
来配置你的类,并从监听器列表中省略 DependencyInjectionTestExecutionListener.class
来完全禁用依赖注入。
考虑测试 HibernateTitleRepository
类的情景,如 目标 部分所述。接下来的两个代码列表演示了 @Autowired
在字段和 setter 方法上的使用。application context 配置在所有示例代码列表之后呈现。
以下代码列表中的依赖注入行为并不是针对JUnit Jupiter的。同样的DI技术可以与任何支持的测试框架一起使用。 下面的例子对静态断言方法进行了调用,例如 |
第一个代码清单显示了一个基于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);
}
}
@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);
}
}
@ExtendWith(SpringExtension::class)
// specifies the Spring configuration to load for this test fixture
@ContextConfiguration("repository-config.xml")
class HibernateTitleRepositoryTests {
// this instance will be dependency injected by type
lateinit var titleRepository: HibernateTitleRepository
@Autowired
fun setTitleRepository(titleRepository: HibernateTitleRepository) {
this.titleRepository = titleRepository
}
@Test
fun findById() {
val title = titleRepository.findById(10)
assertNotNull(title)
}
}
前面的代码列表使用了由 @ContextConfiguration
注解引用的同一个 XML 上下文文件(也就是 repository-config.xml
)。下面显示了这个配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- this bean will be injected into the HibernateTitleRepositoryTests class -->
<bean id="titleRepository" class="com.foo.repository.hibernate.HibernateTitleRepository">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>
<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
<!-- configuration elided for brevity -->
</bean>
</beans>
如果你是从一个Spring提供的测试基类中扩展出来的,而这个基类恰好在它的一个setter方法上使用了 Java
Kotlin
指定的qualifier值表示要注入的特定 |
5.8. 测试 Request 和 Session scope 的 Bean
Spring早年就支持 Request 和 Session scope 的 Bean,你可以通过以下步骤测试你的 Request 和 Session scope 的 Bean:
-
通过用
@WebAppConfiguration
注解你的测试类,确保为你的测试加载一个WebApplicationContext
。 -
将 mock request 或 session 注入到你的测试实例中,并适当地准备你的测试 fixture。
-
调用你从配置的
WebApplicationContext
中获取的Web组件(使用依赖注入)。 -
对mock进行断言。
下一个代码片断显示了登录用例的XML配置。请注意,userService
Bean对 request scope 的 loginAction
Bean有依赖性。另外,LoginAction
是通过使用 SpEL表达式 来实例化的,它从当前的HTTP请求中获取用户名和密码。在我们的测试中,我们想通过TestContext框架管理的mock来配置这些请求参数。下面的列表显示了这个用例的配置:
<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
中设置请求参数来设置我们的测试fixture。当 loginUser()
方法在我们的 userService
上被调用时,我们确信用户服务可以访问当前 MockHttpServletRequest
的 request scope 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
}
}
@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
}
}
下面的代码片段与我们之前看到的request scope Bean的代码片段相似。然而,这一次, userService
Bean对 session scope 的 userPreferences
Bean有依赖性。请注意, UserPreferences
Bean是通过使用一个SpEL表达式来实例化的,该表达式从当前的HTTP会话中检索出theme。在我们的测试中,我们需要在 TestContext 框架所管理的 mock session 中配置一个theme。下面的例子展示了如何做到这一点:
<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
属性来设置我们的测试fixture。当 processUserPreferences()
方法在我们的 userService
上被调用时,我们确信 user service 可以访问当前 MockHttpSession
的session scope userPreferences
,并且我们可以根据配置的 theme 对结果进行断言。下面的例子展示了如何做到这一点:
@SpringJUnitWebConfig
class SessionScopedBeanTests {
@Autowired UserService userService;
@Autowired MockHttpSession session;
@Test
void sessionScope() throws Exception {
session.setAttribute("theme", "blue");
Results results = userService.processUserPreferences();
// assert results
}
}
@SpringJUnitWebConfig
class SessionScopedBeanTests {
@Autowired lateinit var userService: UserService
@Autowired lateinit var session: MockHttpSession
@Test
fun sessionScope() {
session.setAttribute("theme", "blue")
val results = userService.processUserPreferences()
// assert results
}
}
5.9. 事务管理
在TestContext框架中,事务是由 TransactionalTestExecutionListener
管理的,即使你没有在你的测试类上明确声明 @TestExecutionListeners
,它也是默认配置的。然而,要启用对事务的支持,你必须在 ApplicationContext
中配置一个 PlatformTransactionManager
Bean,该bean以 @ContextConfiguration
语义加载(后面会提供进一步细节)。此外,你必须在测试的类或方法级别声明Spring的 @Transactional
注解。
5.9.1. 测试管理的事务
测试管理的事务是通过使用 TransactionalTestExecutionListener
声明性地管理的事务,或通过使用 TestTransaction
(后面描述)程序性地管理的事务。你不应该把这种事务与Spring管理的事务(那些直接由Spring在测试加载的 ApplicationContext
中管理的事务)或应用程序管理的事务(那些在被测试调用的应用程序代码中以编程方式管理的事务)混淆。Spring管理的和应用管理的事务通常参与测试管理的事务。然而,如果Spring管理的或应用管理的事务被配置为除 REQUIRED
或 SUPPORTS
之外的任何传播类型,你应该谨慎行事(详见关于 事务传播 的讨论)。
抢占式超时和测试管理的事务
当从测试框架中使用任何形式的抢占式超时与Spring的测试管理事务相结合时,必须谨慎行事。 具体来说,Spring的测试支持在当前测试方法被调用之前将事务状态绑定到当前线程(通过 可能发生这种情况的情况包括但不限于以下情况。
|
5.9.2. 启用和停用事务
用 @Transactional
来注解一个测试方法会使测试在一个事务中运行,默认情况下,在测试完成后会自动回滚。如果一个测试类被 @Transactional
注解,该类层次结构中的每个测试方法都会在事务中运行。没有用 @Transactional
注解的测试方法(在类或方法级别)不会在事务中运行。注意,@Transactional
不支持测试生命周期方法—例如,用 JUnit Jupiter 的 @BeforeAll
、@BeforeEach
等注解的方法。此外,被 @Transactional
注解但 propagation
属性被设置为 NOT_SUPPORTED
或 NEVER
的测试也不会在事务中运行。
属性 | 为测试管理的事务提供支持 |
---|---|
|
yes |
|
只支持 |
|
no |
|
no |
|
no |
|
no: 使用 |
|
no: 使用 |
方法级生命周期方法—例如,用 JUnit Jupiter 的 如果你需要在事务中运行套装级(suite-level)或类级(class-level)生命周期方法中的代码,你可能希望将相应的 |
请注意,AbstractTransactionalJUnit4SpringContextTests
和 AbstractTransactionalTestNGSpringContextTests
是在类的层面上预设了事务支持。
下面的例子演示了为基于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"));
}
}
@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
自动回滚。
5.9.3. 事务的回滚和提交行为
默认情况下,测试事务将在测试完成后自动回滚;然而,事务提交和回滚行为可以通过 @Commit
和 @Rollback
注解来声明性地配置。更多细节见 注解支持 部分的相应条目。
5.9.4. 编程式事务管理
你可以通过使用 TestTransaction
的静态方法与测试管理的事务进行编程式的交互。例如,你可以在测试方法、before方法和after方法中使用 TestTransaction
来开始或结束当前测试管理的事务,或者配置当前测试管理的事务进行回滚或提交。只要启用 TransactionalTestExecutionListener
,对 TestTransaction
的支持就自动可用。
下面的例子演示了 TestTransaction
的一些功能。更多细节请参见 TestTransaction
的javadoc。
@ContextConfiguration(classes = TestConfig.class)
public class ProgrammaticTransactionManagementTests extends
AbstractTransactionalJUnit4SpringContextTests {
@Test
public void transactionalTest() {
// assert initial state in test database:
assertNumUsers(2);
deleteFromTables("user");
// changes to the database will be committed!
TestTransaction.flagForCommit();
TestTransaction.end();
assertFalse(TestTransaction.isActive());
assertNumUsers(0);
TestTransaction.start();
// perform other actions against the database that will
// be automatically rolled back after the test completes...
}
protected void assertNumUsers(int expected) {
assertEquals("Number of rows in the [user] table.", expected, countRowsInTable("user"));
}
}
@ContextConfiguration(classes = [TestConfig::class])
class ProgrammaticTransactionManagementTests : AbstractTransactionalJUnit4SpringContextTests() {
@Test
fun transactionalTest() {
// assert initial state in test database:
assertNumUsers(2)
deleteFromTables("user")
// changes to the database will be committed!
TestTransaction.flagForCommit()
TestTransaction.end()
assertFalse(TestTransaction.isActive())
assertNumUsers(0)
TestTransaction.start()
// perform other actions against the database that will
// be automatically rolled back after the test completes...
}
protected fun assertNumUsers(expected: Int) {
assertEquals("Number of rows in the [user] table.", expected, countRowsInTable("user"))
}
}
5.9.5. 在事务之外运行代码
偶尔,你可能需要在事务性测试方法之前或之后运行某些代码,但在事务性上下文之外—例如,在运行测试之前验证初始数据库状态,或在测试运行后验证预期的事务性提交行为(如果测试被配置为提交事务)。TransactionalTestExecutionListener
支持 @BeforeTransaction
和 @AfterTransaction
注解,正是为了这种情况。你可以用这些注解之一来注解测试类中的任何 void
方法或测试接口中的任何 void
默认方法,TransactionalTestExecutionListener
确保你的事务前方法或事务后方法在适当的时间运行。
任何之前的方法(比如用JUnit Jupiter的 @BeforeEach 注解的方法)和任何之后的方法(比如用JUnit Jupiter的 @AfterEach 注解的方法)都在事务中运行。此外,对于没有被配置为在事务中运行的测试方法,用 @BeforeTransaction 或 @AfterTransaction 注解的方法不会被运行。
|
5.9.6. 配置 Transaction Manager
TransactionalTestExecutionListener
期望在测试的Spring ApplicationContext
中定义一个 PlatformTransactionManager
Bean。如果在测试的 ApplicationContext
中存在多个 PlatformTransactionManager
的实例,你可以通过使用 @Transactional("myTxMgr")
或 @Transactional(transactionManager = "myTxMgr")
声明一个qualifier,或者 TransactionManagementConfigurer
可以通过 @Configuration
类来实现。咨询 TestContextTransactionUtils.retrieveTransactionManager()
的 javadoc 以了解用于在测试的 ApplicationContext
中查找事务管理器的算法细节。
5.9.7. 所有与事务相关的注解的演示
下面这个基于JUnit Jupiter的例子显示了一个虚构的集成测试场景,突出了所有与事务相关的注解。这个例子不是为了展示最佳实践,而是为了展示如何使用这些注解。更多信息和配置实例请参见 注解支持 部分。用于 @Sql
的事务管理 包含一个额外的例子,该例子将 @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
}
}
@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持久化上下文状态的应用程序代码时,确保在运行该代码的测试方法中flush底层工作单元。不flush底层工作单元会产生误报结果: 你的测试通过了,但同样的代码在实际的生产环境中却抛出了异常。注意,这适用于任何维护内存工作单元的ORM框架。在下面这个基于Hibernate的测试案例中,一个方法显示了一个误报,而另一个方法正确地暴露了 flush session 的结果: Java
Kotlin
下面的例子显示了JPA的匹配方法: Java
Kotlin
|
测试ORM实体生命周期的回调
与关于在测试ORM代码时避免 误报 的说明类似,如果你的应用程序使用了实体生命周期回调(也称为实体监听器),请确保在运行该代码的测试方法中flush底层工作单元。如果没有flush或clear底层工作单元,会导致某些生命周期回调不被调用。 例如,当使用JPA时, 下面的例子展示了如何刷新 Java
Kotlin
参见Spring框架测试套件中的 JpaEntityListenerTests,了解使用所有JPA生命周期回调的工作实例。 |
5.10. 执行SQL脚本
当针对关系型数据库编写集成测试时,运行SQL脚本来修改数据库模式或向表中插入测试数据往往是有益的。spring-jdbc
模块通过在加载 Spring ApplicationContext
时执行SQL脚本,为初始化嵌入式或现有数据库提供了支持。详情参见 嵌入式数据库支持 和 使用嵌入式数据库测试数据访问逻辑。
尽管在加载 ApplicationContext
时初始化一次用于测试的数据库是非常有用的,但有时在集成测试期间能够修改数据库是非常必要的。下面的章节解释了如何在集成测试期间以编程方式和声明方式运行SQL脚本。
5.10.1. 以编程方式执行SQL脚本
Spring为在集成测试方法中以编程方式执行SQL脚本提供了以下选项。
-
org.springframework.jdbc.datasource.init.ScriptUtils
-
org.springframework.jdbc.datasource.init.ResourceDatabasePopulator
-
org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests
-
org.springframework.test.context.testng.AbstractTransactionalTestNGSpringContextTests
ScriptUtils
为处理SQL脚本提供了一系列静态的实用方法,主要用于框架的内部使用。然而,如果你需要完全控制SQL脚本的解析和运行,ScriptUtils
可能比后面描述的其他一些替代方法更适合你的需要。更多细节请参见 javadoc 中关于 ScriptUtils
的各个方法。
ResourceDatabasePopulator
提供了一个基于对象的API,用于通过使用外部资源中定义的SQL脚本,以编程方式填充、初始化或清理数据库。ResourceDatabasePopulator
提供了配置字符编码、语句分隔符、注释定界符以及解析和运行脚本时使用的错误处理标志的选项。每个配置选项都有一个合理的默认值。关于默认值的细节,请参见 javadoc。为了运行配置在 ResourceDatabasePopulator
中的脚本,你可以调用 populate(Connection)
方法来针对 java.sql.Connection
运行 populator,或者调用 execute(DataSource)
方法来针对 javax.sql.DataSource
运行 populator。下面的例子为测试schema和测试数据指定了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
}
@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 脚本。同样,AbstractTransactionalJUnit4SpringContextTests
和 AbstractTransactionalTestNGSpringContextTests
中的 executeSqlScript(..)
方法在内部使用 ResourceDatabasePopulator
来运行SQL脚本。更多细节请参见各种 executeSqlScript(..)
方法的Javadoc。
5.10.2. 用 @Sql 声明式地执行SQL脚本
除了上述以编程方式运行SQL脚本的机制外,你还可以在Spring TestContext框架中声明性地配置SQL脚本。具体来说,你可以在测试类或测试方法上声明 @Sql
注解,以配置单个SQL语句或SQL脚本的资源路径,这些脚本应在集成测试方法之前或之后针对给定的数据库运行。对 @Sql
的支持是由 SqlScriptsTestExecutionListener
提供的,它在默认情况下是启用的。
方法级的 @Sql 声明默认会覆盖类级的声明。然而,从Spring Framework 5.2开始,这种行为可以通过 @SqlMergeMode 对每个测试类或每个测试方法进行配置。更多细节请参见 用 @SqlMergeMode 合并和覆盖配置。
|
路径资源(Path Resource)语义
每个路径都被解释为一个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
}
}
@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
的多个实例。在Java 8中,你可以将 @Sql
作为一个可重复的注释。否则,你可以使用 @SqlGroup
注解作为声明多个 @Sql
实例的明确容器。
下面的例子显示了如何使用 @Sql
作为Java 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
}
// Repeatable annotations with non-SOURCE retention are not yet supported by Kotlin
在前面的例子中介绍的情况下,test-schema.sql
脚本对单行注释使用了不同的语法。
下面的例子与前面的例子相同,只是 @Sql
的声明被分组在 @SqlGroup
中。在Java 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
}
@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脚本会在相应的测试方法之前运行。然而,如果你需要在测试方法之后运行一组特定的脚本(例如,清理数据库状态),你可以使用 @Sql
中的 executionPhase
属性,如下例所示:
@Test
@Sql(
scripts = "create-test-data.sql",
config = @SqlConfig(transactionMode = ISOLATED)
)
@Sql(
scripts = "delete-test-data.sql",
config = @SqlConfig(transactionMode = ISOLATED),
executionPhase = AFTER_TEST_METHOD
)
void userTest() {
// run code that needs the test data to be committed
// to the database outside of the test's transaction
}
@Test
@SqlGroup(
Sql("create-test-data.sql",
config = SqlConfig(transactionMode = ISOLATED)),
Sql("delete-test-data.sql",
config = SqlConfig(transactionMode = ISOLATED),
executionPhase = AFTER_TEST_METHOD))
fun userTest() {
// run code that needs the test data to be committed
// to the database outside of the test's transaction
}
注意,ISOLATED
和 AFTER_TEST_METHOD
分别从 Sql.TransactionMode
和 Sql.ExecutionPhase
静态导入。
用 @SqlConfig
进行脚本配置
你可以通过使用 @SqlConfig
注解来配置脚本解析和错误处理。当作为一个集成测试类的类级注解声明时,@SqlConfig
作为测试类层次结构中所有SQL脚本的全局配置。当通过使用 @Sql
注解的 config
属性直接声明时,@SqlConfig
作为本地配置,用于在包围的 @Sql
注解中声明的SQL脚本。@SqlConfig
中的每个属性都有一个隐含的默认值,它被记录在相应属性的javadoc中。由于《Java语言规范》中为注解属性定义的规则,不幸的是,不可能为注解属性分配一个 null
值。因此,为了支持对继承的全局配置的覆盖,@SqlConfig
属性有一个明确的默认值,即 ""
(对于字符串)、{}
(对于数组)或 DEFAULT
(对于枚举)。这种方法让 @SqlConfig
的本地声明通过提供 ""
、{}
或 DEFAULT
以外的值,有选择地覆盖 @SqlConfig
的全局声明中的个别属性。只要本地的 @SqlConfig
属性没有提供 ""
、{}
或 DEFAULT
以外的明确值,全局的 @SqlConfig
属性就会被继承。因此,明确的本地配置会覆盖全局配置。
@Sql
和 @SqlConfig
提供的配置选项与 ScriptUtils
和 ResourceDatabasePopulator
支持的配置选项相当,但它们是 <jdbc:initialize-database/>
XML命名空间元素提供的配置选项的超集。详情请参见 @Sql
和 @SqlConfig
中单个属性的javadoc。
用于 @Sql
的事务管理
默认情况下,SqlScriptsTestExecutionListener
为使用 @Sql
配置的脚本推断所需的事务语义。具体来说,SQL脚本会在没有事务的情况下运行,在现有的Spring管理的事务中运行(例如,由 TransactionalTestExecutionListener
管理的、用 @Transactional
注解的测试的事务),或者在一个孤立的事务中运行,这取决于 @SqlConfig
中 transactionMode
属性的配置值以及测试的 ApplicationContext
中是否有 PlatformTransactionManager
。然而,作为一个最低限度,javax.sql.DataSource
必须存在于测试的 ApplicationContext
中。
如果 SqlScriptsTestExecutionListener
用来检测 DataSource
和 PlatformTransactionManager
并推断事务语义的算法不适合你的需要,你可以通过设置 @SqlConfig
的 dataSource
和 transactionManager
属性来指定明确的名称。此外,你可以通过设置 @SqlConfig
的 transactionMode
属性来控制事务传播行为(例如,脚本是否应该在一个孤立的事务中运行)。尽管彻底讨论所有支持 @Sql
的事务管理选项超出了本参考手册的范围,但 @SqlConfig
和 SqlScriptsTestExecutionListener
的 javadoc 提供了详细的信息,下面的例子展示了一个典型的测试场景,它使用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.");
}
}
@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
自动回滚(详见 transaction management)。
用 @SqlMergeMode
合并和覆盖配置
从Spring Framework 5.2 开始,可以将方法级 @Sql
声明与类级声明合并。例如,这允许你在每个测试类中提供数据库schema的配置或一些常见的测试数据,然后在每个测试方法中提供额外的、特定于用例的测试数据。要启用 @Sql
合并,用 @SqlMergeMode(MERGE)
注解你的测试类或测试方法。要禁用特定测试方法(或特定测试子类)的合并,你可以通过 @SqlMergeMode(OVERRIDE)
切换回默认模式。请查阅 @SqlMergeMode
注解文档部分,以了解示例和更多细节。
5.11. 并行测试执行
Spring Framework 5.0 引入了对使用Spring TestContext框架时在单个JVM内并行执行测试的基本支持。一般来说,这意味着大多数测试类或测试方法可以在不改变测试代码或配置的情况下并行运行。
关于如何设置并行测试执行的细节,请参见你的测试框架、构建工具或IDE的文档。 |
请记住,在你的测试套件中引入并发可能会导致意外的副作用,奇怪的运行时行为,以及间歇性或看似随机的测试失败。因此,Spring团队提供了以下关于何时不要并行运行测试的一般准则。
如果是测试,请不要并行运行测试:
-
使用Spring框架的
@DirtiesContext
支持。 -
使用Spring Boot的
@MockBean
或@SpyBean
支持。 -
使用JUnit 4的
@FixMethodOrder
支持或任何旨在确保测试方法以特定顺序运行的测试框架功能。然而,请注意,如果整个测试类是并行运行的,这并不适用。 -
改变共享服务或系统的状态,如数据库、消息代理、文件系统和其他。这同时适用于嵌入式和外部系统。
如果并行测试执行失败,出现异常,说明当前测试的 这可能是由于使用了 |
在Spring TestContext框架中,只有当底层的 TestContext 实现提供一个拷贝构造函数时,平行测试执行才有可能,正如 TestContext 的javadoc所解释的那样。Spring中使用的 DefaultTestContext 提供了这样一个构造函数。然而,如果你使用一个提供自定义 TestContext 实现的第三方库,你需要验证它是否适合于并行测试执行。
|
5.12. TestContext 框架支持类
本节描述了支持 Spring TestContext 框架的各种类。
5.12.1. Spring JUnit 4 Runner
Spring TestContext框架通过本节描述了支持Spring TestContext框架的各种类.自定义runner(在JUnit 4.12 或更高版本上支持),提供了与JUnit 4的完全集成。通过用 @RunWith(SpringJUnit4ClassRunner.class)
或更短的 @RunWith(SpringRunner.class)
变体来注解测试类,开发者可以实现基于JUnit 4的标准单元和集成测试,同时获得TestContext框架的好处,如支持加载应用上下文、测试实例的依赖注入、事务性测试方法执行等。如果你想将Spring TestContext框架与其他runner(如JUnit 4的 Parameterized
runner)或第三方runner(如 MockitoJUnitRunner
)一起使用,你可以选择性地使用 Spring 对 JUnit rule 的支持 。
下面的代码列表显示了配置一个测试类与自定义Spring Runner
一起运行的最低要求:
@RunWith(SpringRunner.class)
@TestExecutionListeners({})
public class SimpleTest {
@Test
public void testMethod() {
// test logic...
}
}
@RunWith(SpringRunner::class)
@TestExecutionListeners
class SimpleTest {
@Test
fun testMethod() {
// test logic...
}
}
在前面的例子中,@TestExecutionListeners
被配置为一个空列表,以禁用默认监听器,否则需要通过 @ContextConfiguration
配置 ApplicationContext
。
5.12.2. Spring JUnit 4 Rule
org.springframework.test.context.junit4.rules
包提供了以下JUnit 4 rule(在JUnit 4.12 或更高版本上支持):
-
SpringClassRule
-
SpringMethodRule
SpringClassRule
是一个JUnit TestRule
,支持Spring TestContext 框架的类级功能,而 SpringMethodRule
是一个 JUnit MethodRule
,支持Spring TestContext框架的实例级和方法级功能。
与 SpringRunner
相比,Spring基于rule的JUnit支持具有独立于任何 org.junit.runner.Runner
实现的优势,因此可以与现有的 alternative runner(如JUnit 4的 Parameterized
)或第三方 runner(如 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...
}
}
// 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()
}
}
5.12.3. JUnit 4 支持类
org.springframework.test.context.junit4
包为基于JUnit 4的测试用例提供以下支持类(在JUnit 4.12 或更高版本上支持):
-
AbstractJUnit4SpringContextTests
-
AbstractTransactionalJUnit4SpringContextTests
AbstractJUnit4SpringContextTests
是一个抽象的基础测试类,它将Spring TestContext框架与JUnit 4环境下的显式 ApplicationContext
测试支持整合在一起。当你扩展 AbstractJUnit4SpringContextTests
时,你可以访问一个 protected
applicationContext
实例变量,你可以用它来执行显式bean查找或测试整个上下文的状态。
AbstractTransactionalJUnit4SpringContextTests
是 AbstractJUnit4SpringContextTests
的抽象事务性扩展,它为JDBC访问添加了一些便利功能。这个类希望在 ApplicationContext
中定义一个 javax.sql.DataSource
Bean和一个 PlatformTransactionManager
Bean。当你扩展 AbstractTransactionalJUnit4SpringContextTests
时,你可以访问一个 protected
jdbcTemplate
实例变量,你可以用它来运行SQL语句来查询数据库。你可以在运行数据库相关的应用程序代码之前和之后使用这种查询来确认数据库状态,Spring确保这种查询在与应用程序代码相同的事务范围内运行。当与ORM工具结合使用时,一定要避免 误报。正如在 JDBC测试的支持 中提到的, AbstractTransactionalJUnit4SpringContextTests
也提供了便利的方法,通过使用上述的 jdbcTemplate
委托给 JdbcTestUtils
中的方法。此外, AbstractTransactionalJUnit4SpringContextTests
提供了一个 executeSqlScript(..)
方法,用于针对配置的 DataSource
运行SQL脚本。
这些类是扩展的一种便利。如果你不希望你的测试类被束缚在Spring特定的类层次中,你可以通过使用 @RunWith(SpringRunner.class) 或 Spring 的 JUnit rule 配置你自己的自定义测试类。
|
5.12.4. JUnit Jupiter 的 SpringExtension
Spring TestContext 框架提供了与JUnit 5中引入的 JUnit Jupiter测试框架的完全集成。通过用 @ExtendWith(SpringExtension.class)
注解测试类,你可以实现基于JUnit Jupiter的标准单元和集成测试,并同时获得TestContext框架的好处,如支持加载应用上下文(application context)、测试实例的依赖注入、事务性测试方法执行等。
此外,由于JUnit Jupiter中丰富的扩展API,Spring在JUnit 4和TestNG支持的功能集之外,还提供了以下功能:
-
测试构造函数、测试方法和测试生命周期回调方法的依赖注入。更多细节请参见
SpringExtension
依赖注入。 -
强大的支持基于SpEL表达式、环境变量、系统属性等的 条件测试执行。更多的细节和例子,请参见 Spring JUnit Jupiter 测试注解 中的
@EnabledIf
和@DisabledIf
的文档。 -
自定义组成的注解,结合Spring和JUnit Jupiter的注解。更多细节请参见 对测试的元注解支持 中的
@TransactionalDevTestConfig
和@TransactionalIntegrationTest
示例。
下面的代码列表显示了如何配置一个测试类,使其与 @ContextConfiguration
一起使用 SpringExtension
:
// Instructs JUnit Jupiter to extend the test with Spring support.
@ExtendWith(SpringExtension.class)
// Instructs Spring to load an ApplicationContext from TestConfig.class
@ContextConfiguration(classes = TestConfig.class)
class SimpleTests {
@Test
void testMethod() {
// test logic...
}
}
// Instructs JUnit Jupiter to extend the test with Spring support.
@ExtendWith(SpringExtension::class)
// Instructs Spring to load an ApplicationContext from TestConfig::class
@ContextConfiguration(classes = [TestConfig::class])
class SimpleTests {
@Test
fun testMethod() {
// test logic...
}
}
由于你也可以在 JUnit 5 中使用注解作为元注解,Spring提供了 @SpringJUnitConfig
和 @SpringJUnitWebConfig
组成的注解以简化测试 ApplicationContext
和JUnit Jupiter的配置。
下面的例子使用 @SpringJUnitConfig
来减少前面例子中的配置量:
// Instructs Spring to register the SpringExtension with JUnit
// Jupiter and load an ApplicationContext from TestConfig.class
@SpringJUnitConfig(TestConfig.class)
class SimpleTests {
@Test
void testMethod() {
// test logic...
}
}
// Instructs Spring to register the SpringExtension with JUnit
// Jupiter and load an ApplicationContext from TestConfig.class
@SpringJUnitConfig(TestConfig::class)
class SimpleTests {
@Test
fun testMethod() {
// test logic...
}
}
类似地,下面的例子使用 @SpringJUnitWebConfig
来创建一个 WebApplicationContext
以用于 JUnit Jupiter:
// Instructs Spring to register the SpringExtension with JUnit
// Jupiter and load a WebApplicationContext from TestWebConfig.class
@SpringJUnitWebConfig(TestWebConfig.class)
class SimpleWebTests {
@Test
void testMethod() {
// test logic...
}
}
// Instructs Spring to register the SpringExtension with JUnit
// Jupiter and load a WebApplicationContext from TestWebConfig::class
@SpringJUnitWebConfig(TestWebConfig::class)
class SimpleWebTests {
@Test
fun testMethod() {
// test logic...
}
}
更多细节请参见 Spring JUnit Jupiter 测试注解 中 @SpringJUnitConfig
和 @SpringJUnitWebConfig
的文档。
SpringExtension
依赖注入
SpringExtension
实现了JUnit Jupiter的 ParameterResolver
扩展API,它让Spring为测试构造函数、测试方法和测试生命周期回调方法提供依赖注入。
具体来说,SpringExtension
可以将测试的 ApplicationContext
中的依赖注入到用 @BeforeAll
、@AfterAll
、@BeforeEach
、@AfterEach
、@Test
、@RepeatedTest
、@ParameterizedTest
等注解的测试构造器和方法中。
构造函数注入
如果JUnit Jupiter测试类的构造函数中的特定参数是 ApplicationContext
类型(或其子类型),或被 @Autowired
、@Qualifier
或 @Value
注解或元注解,Spring会用测试的 ApplicationContext
中的相应Bean或值来注入该特定参数的值。
如果一个测试类的构造函数被认为是自动的,Spring也可以被配置为自动装配该构造函数的所有参数。如果满足以下条件之一(按优先级排序),构造函数就被认为是自动的。
-
该构造函数用
@Autowired
注解。 -
@TestConstructor
在测试类上存在或元存在,其autowireMode
属性设置为ALL
。 -
默认的测试构造器自动装配模式已改为
ALL
。
关于 @TestConstructor
的使用以及如何改变全局测试构造器的自动装配模式,请参见 @TestConstructor
的细节。
如果一个测试类的构造函数被认为是自动的,Spring就会承担起解析构造函数中所有参数的参数的责任。因此,在JUnit Jupiter注册的其他 ParameterResolver 都不能为这样的构造器解析参数。
|
如果 原因是 要在 "测试方法前 "或 "测试方法后" 模式下与 |
在下面的例子中,Spring 将 TestConfig.class
加载的 ApplicationContext
中的 OrderService
Bean 注入到 OrderServiceIntegrationTests
构造函数中。
@SpringJUnitConfig(TestConfig.class)
class OrderServiceIntegrationTests {
private final OrderService orderService;
@Autowired
OrderServiceIntegrationTests(OrderService orderService) {
this.orderService = orderService;
}
// tests that use the injected OrderService
}
@SpringJUnitConfig(TestConfig::class)
class OrderServiceIntegrationTests @Autowired constructor(private val orderService: OrderService){
// tests that use the injected OrderService
}
请注意,这个功能让测试的依赖关系是 final
的,因此是不可改变的。
如果 spring.test.constructor.autowire.mode
属性为 all
(见 @TestConstructor
),我们可以省略前面例子中对构造函数的 @Autowired
声明,结果如下。
@SpringJUnitConfig(TestConfig.class)
class OrderServiceIntegrationTests {
private final OrderService orderService;
OrderServiceIntegrationTests(OrderService orderService) {
this.orderService = orderService;
}
// tests that use the injected OrderService
}
@SpringJUnitConfig(TestConfig::class)
class OrderServiceIntegrationTests(val orderService:OrderService) {
// tests that use the injected OrderService
}
方法注入
如果JUnit Jupiter 测试方法或测试生命周期回调方法中的参数是 ApplicationContext
类型(或其子类型),或被 @Autowired
、@Qualifier
或 @Value
注解或元注解,Spring 会从测试的 ApplicationContext
中为该特定参数注入相应的bean值。
在下面的例子中,Spring 将 TestConfig.class
中加载的 ApplicationContext
中的 OrderService
注入到 deleteOrder()
测试方法中:
@SpringJUnitConfig(TestConfig.class)
class OrderServiceIntegrationTests {
@Test
void deleteOrder(@Autowired OrderService orderService) {
// use orderService from the test's ApplicationContext
}
}
@SpringJUnitConfig(TestConfig::class)
class OrderServiceIntegrationTests {
@Test
fun deleteOrder(@Autowired orderService: OrderService) {
// use orderService from the test's ApplicationContext
}
}
由于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
}
}
@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 Framework 5.0 开始,Spring TestContext 框架就支持在JUnit Jupiter的 @Nested
测试类上使用测试相关的注解;然而,直到Spring Framework 5.3,类级的测试配置注解并没有像从超类中继承那样从包围类中继承。
Spring Framework 5.3 引入了对从包围类中继承测试类配置的一流支持,并且这种配置将被默认继承。要从默认的 INHERIT
模式变为 OVERRIDE
模式,你可以用 @NestedTestConfiguration(EnclosingConfiguration.OVERRIDE)
来注解一个单独的 @Nested
测试类。一个明确的 @NestedTestConfiguration
声明将适用于被注解的测试类以及它的任何子类和嵌套类。因此,你可以用 @NestedTestConfiguration
注解一个顶层测试类,它将递归地适用于所有的嵌套测试类。
为了让开发团队将默认值改为 OVERRIDE
--例如,为了与Spring Framework 5.0 到5.2 兼容—可以通过JVM系统属性或 classpath root 的 spring.properties
文件全局地改变默认模式。详情见 "改变默认的包围配置继承模式" 说明。
尽管下面的 "Hello World" 例子非常简单,但它显示了如何在一个顶层类上声明普通配置,并由其 @Nested
测试类继承。在这个特殊的例子中,只有 TestConfig
配置类被继承了。每个嵌套的测试类提供它自己的活动配置文件集,导致每个嵌套的测试类有一个不同的 ApplicationContext
(详见 上下文(Context)缓存)。查阅 支持的注解列表,看看哪些注释可以在 @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");
}
}
}
@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")
}
}
}
5.12.5. TestNG 支持类
org.springframework.test.context.testng
包为基于 TestNG 的测试用例提供以下支持类:
-
AbstractTestNGSpringContextTests
-
AbstractTransactionalTestNGSpringContextTests
AbstractTestNGSpringContextTests
是一个抽象的基础测试类,它将Spring TestContext框架与TestNG环境下的显式 ApplicationContext
测试支持整合在一起。当你扩展 AbstractTestNGSpringContextTests
时,你可以访问一个 protected
applicationContext
实例变量,你可以用它来执行显式bean查找或测试整个上下文的状态。
AbstractTransactionalTestNGSpringContextTests
是 AbstractTestNGSpringContextTests
的一个抽象的事务性扩展,为JDBC访问增加了一些便利的功能。这个类希望在 ApplicationContext
中定义一个 javax.sql.DataSource
Bean和一个 PlatformTransactionManager
Bean。当你扩展 AbstractTransactionalTestNGSpringContextTests
时,你可以访问一个 protected
jdbcTemplate
实例变量,你可以用它来运行SQL语句来查询数据库。你可以在运行数据库相关的应用程序代码之前和之后使用这种查询来确认数据库状态,Spring确保这种查询在与应用程序代码相同的事务范围内运行。当与ORM工具结合使用时,一定要避免 误报。正如在 JDBC测试的支持 中提到的, AbstractTransactionalTestNGSpringContextTests
也提供了便利的方法,通过使用上述的 jdbcTemplate
来委托给 JdbcTestUtils
的方法。此外, AbstractTransactionalTestNGSpringContextTests
提供了一个 executeSqlScript(..)
方法,用于针对配置的 DataSource
运行SQL脚本。
这些类是扩展的一种便利。如果你不希望你的测试类被束缚在Spring特定的类层次中,你可以通过使用 @ContextConfiguration 、@TestExecutionListeners 等配置你自己的自定义测试类,并通过 TestContextManager 手动对你的测试类进行工具化。请参阅 AbstractTestNGSpringContextTests 的源代码,了解如何为你的测试类提供工具的例子。
|
5.13. AOT 对测试的支持
本章介绍了Spring的AOT(Ahead of Time)对使用Spring TestContext框架的集成测试的支持。
测试支持通过以下功能扩展了 Spring的核心AOT支持。
-
对当前项目中使用 TestContext 框架加载
ApplicationContext
的所有集成测试进行构建时检测。-
为基于JUnit Jupiter和JUnit 4的测试类提供显式支持,也为TestNG和其他使用Spring核心测试注解的测试框架提供隐式支持—只要测试是使用为当前项目注册的JUnit Platform
TestEngine
运行。
-
-
构建时的AOT处理:当前项目中每个独特的测试
ApplicationContext
将被 刷新以进行AOT处理。 -
运行时AOT支持:当在AOT运行时模式下执行时,Spring集成测试将使用AOT优化的
ApplicationContext
,它可以透明地参与 上下文(context)缓存。
目前在AOT模式下不支持 |
为了提供特定于测试的运行时提示,以便在 GraalVM 原生镜像中使用,你有以下选择。
-
实现一个自定义的
TestRuntimeHintsRegistrar
,并通过META-INF/spring/aot.factories
全局注册。 -
实现一个自定义的
RuntimeHintsRegistrar
,并通过META-INF/spring/aot.factories
全局注册,或通过@ImportRuntimeHints
在本地的测试类上注册它。 -
用
@Reflective
或@RegisterReflectionForBinding
来注解一个测试类。 -
关于Spring 的核心 runtime hint 和注解支持的详细信息,请参见 Runtime Hint。
|
如果你实现了一个自定义的 ContextLoader
,它必须实现 AotContextLoader
,以便提供AOT构建时处理和AOT运行时执行支持。然而,请注意,Spring 框架和 Spring Boot 提供的所有 context loader 实现都已经实现了 AotContextLoader
。
如果你实现了一个自定义的 TestExecutionListener
,它必须实现 AotTestExecutionListener
才能参与AOT处理。请看 spring-test
模块中的 SqlScriptsTestExecutionListener
的例子。
6. WebTestClient
WebTestClient
是一个为测试服务器应用程序而设计的HTTP客户端。它包装了Spring的WebClient ,并使用它来执行请求,但暴露了一个测试界面(facade)来验证响应。WebTestClient
可以用来执行端到端的HTTP测试。它还可以用来测试Spring MVC和Spring WebFlux应用程序,而不需要通过mock服务器请求和响应对象来运行服务器。
Kotlin用户:请参阅与使用 WebTestClient 有关的 这一节。
|
6.1. 设置
要设置一个 WebTestClient
,你需要选择一个服务器设置来与之绑定。这可以是几个mock服务器设置中的一个,也可以是与一个实时服务器的连接。
6.1.1. 绑定到 Controller
这种设置允许你通过 mock 请求和响应对象来测试特定的 controller,而无需运行服务器。
对于WebFlux应用程序,使用下面的方法,加载相当于 WebFlux Java 配置 的基础设施,注册给定的控制器,并创建一个 WebHandler chain 来处理请求:
WebTestClient client =
WebTestClient.bindToController(new TestController()).build();
val client = WebTestClient.bindToController(TestController()).build()
对于Spring MVC,使用下面的方法,它委托给 StandaloneMockMvcBuilder
来加载相当于 WebMvc Java 配置 的基础设施,注册指定的controller,并创建一个 MockMvc 实例来处理请求:
WebTestClient client =
MockMvcWebTestClient.bindToController(new TestController()).build();
val client = MockMvcWebTestClient.bindToController(TestController()).build()
6.1.2. 绑定到 ApplicationContext
这种设置允许你用Spring MVC或Spring WebFlux基础设施和 controller 声明加载Spring配置,并通过 mock request 和 response 对象使用它来处理请求,而无需运行服务器。
对于WebFlux,使用以下内容,其中 Spring ApplicationContext
被传递给 WebHttpHandlerBuilder
以创建 WebHandler chain 来处理请求:
@SpringJUnitConfig(WebConfig.class) (1)
class MyTests {
WebTestClient client;
@BeforeEach
void setUp(ApplicationContext context) { (2)
client = WebTestClient.bindToApplicationContext(context).build(); (3)
}
}
1 | 指定要加载的配置 |
2 | 注入配置 |
3 | 创建 WebTestClient |
@SpringJUnitConfig(WebConfig::class) (1)
class MyTests {
lateinit var client: WebTestClient
@BeforeEach
fun setUp(context: ApplicationContext) { (2)
client = WebTestClient.bindToApplicationContext(context).build() (3)
}
}
1 | 指定要加载的配置 |
2 | 注入配置 |
3 | 创建 WebTestClient |
对于Spring MVC,使用下面的方法,其中Spring ApplicationContext
被传递给 MockMvcBuilders.webAppContextSetup 以创建一个 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 |
@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 |
6.1.3. 绑定到路由(Router) Function
这种设置允许你通过 mock request 和 response 对象来测试 功能端点,而不需要运行服务器。
对于 WebFlux,使用以下内容,它委托给 RouterFunctions.toWebHandler
来创建一个服务器设置(server setup)来处理请求:
RouterFunction<?> route = ...
client = WebTestClient.bindToRouterFunction(route).build();
val route: RouterFunction<*> = ...
val client = WebTestClient.bindToRouterFunction(route).build()
对于Spring MVC,目前还没有测试 WebMvc 功能端点 的选项。
6.1.4. 绑定到服务器(Server)
这个设置连接到一个正在运行的服务器,进行完整的、端到端的HTTP测试:
client = WebTestClient.bindToServer().baseUrl("http://localhost:8080").build();
client = WebTestClient.bindToServer().baseUrl("http://localhost:8080").build()
6.1.5. 客户端(Client)配置
除了前面描述的服务器设置选项外,你还可以配置客户端选项,包括基本URL、默认header、客户端过滤器等。这些选项在 bindToServer()
之后可以随时使用。对于所有其他配置选项,你需要使用 configureClient()
来从服务器配置过渡到客户端配置,如下所示:
client = WebTestClient.bindToController(new TestController())
.configureClient()
.baseUrl("/test")
.build();
client = WebTestClient.bindToController(TestController())
.configureClient()
.baseUrl("/test")
.build()
6.2. 编写测试
WebTestClient
提供了一个与 WebClient 相同的API,直至使用 exchange()
来执行请求。关于如何准备一个带有任何内容(包括form data、multipart data等)的请求的例子,请参阅 WebClient 文档。
在调用 exchange()
之后,WebTestClient
与 WebClient
相背离,而是继续用工作流程来验证响应。
要断定响应状态和header,请使用以下方法:
client.get().uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON);
client.get().uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
如果你想让所有的期望(expectation)被断言,即使其中一个失败了,你可以使用 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()
: 对于 JSON 内容 或空 body,解码为byte[]
。
并对产生的更高层次的对象执行断言:
client.get().uri("/persons")
.exchange()
.expectStatus().isOk()
.expectBodyList(Person.class).hasSize(3).contains(person);
client.get().uri("/persons")
.exchange()
.expectStatus().isOk()
.expectBodyList<Person>().hasSize(3).contains(person)
如果内置的断言是不够的,你可以消消费对象,并执行任何其他断言:
client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody(Person.class)
.consumeWith(result -> {
// custom assertions (e.g. AssertJ)...
});
client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody<Person>()
.consumeWith {
// custom assertions (e.g. AssertJ)...
}
或者你可以退出工作流并获得一个 EntityExchangeResult
:
EntityExchangeResult<Person> result = client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody(Person.class)
.returnResult();
val result = client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk
.expectBody<Person>()
.returnResult()
当你需要用泛型解码到目标类型时,寻找接受 ParameterizedTypeReference 而不是 Class<T> 的重载方法。
|
6.2.1. 无内容
如果不期望响应有内容,你可以按以下方式断言:
client.post().uri("/persons")
.body(personMono, Person.class)
.exchange()
.expectStatus().isCreated()
.expectBody().isEmpty();
client.post().uri("/persons")
.bodyValue(person)
.exchange()
.expectStatus().isCreated()
.expectBody().isEmpty()
如果你想忽略响应内容,下面的内容是在没有任何断言的情况下释放的:
client.get().uri("/persons/123")
.exchange()
.expectStatus().isNotFound()
.expectBody(Void.class);
client.get().uri("/persons/123")
.exchange()
.expectStatus().isNotFound
.expectBody<Unit>()
6.2.2. JSON 内容
你可以在没有目标类型的情况下使用 expectBody()
来对原始内容进行断言,而不是通过更高级别的 Object。
要用 JSONAssert 验证完整的JSON内容:
client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody()
.json("{\"name\":\"Jane\"}")
client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody()
.json("{\"name\":\"Jane\"}")
要用 JSONPath 验证JSON内容:
client.get().uri("/persons")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$[0].name").isEqualTo("Jane")
.jsonPath("$[1].name").isEqualTo("Jason");
client.get().uri("/persons")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$[0].name").isEqualTo("Jane")
.jsonPath("$[1].name").isEqualTo("Jason")
6.2.3. 流式(Stream)响应
要测试潜在的无限流,如 "text/event-stream"
或 "application/x-ndjson"
,首先要验证响应状态和header,然后获得 FluxExchangeResult
:
FluxExchangeResult<MyEvent> result = client.get().uri("/events")
.accept(TEXT_EVENT_STREAM)
.exchange()
.expectStatus().isOk()
.returnResult(MyEvent.class);
val result = client.get().uri("/events")
.accept(TEXT_EVENT_STREAM)
.exchange()
.expectStatus().isOk()
.returnResult<MyEvent>()
现在你已经准备好用 reactor-test
的 StepVerifier
来消费响应流了:
Flux<Event> eventFlux = result.getResponseBody();
StepVerifier.create(eventFlux)
.expectNext(person)
.expectNextCount(4)
.consumeNextWith(p -> ...)
.thenCancel()
.verify();
val eventFlux = result.getResponseBody()
StepVerifier.create(eventFlux)
.expectNext(person)
.expectNextCount(4)
.consumeNextWith { p -> ... }
.thenCancel()
.verify()
6.2.4. MockMvc 断言
WebTestClient
是一个HTTP客户端,因此它只能验证客户端响应中的内容,包括状态、header信息和body。
当用MockMvc服务器设置来测试Spring MVC应用程序时,你有额外的选择来对服务器响应进行进一步断言。要做到这一点,首先要在断言 body 后获得一个 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();
// 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 server 响应(response)断言:
MockMvcWebTestClient.resultActionsFor(result)
.andExpect(model().attribute("integer", 3))
.andExpect(model().attribute("string", "a string value"));
MockMvcWebTestClient.resultActionsFor(result)
.andExpect(model().attribute("integer", 3))
.andExpect(model().attribute("string", "a string value"));
7. MockMvc
Spring MVC测试框架,也被称为MockMvc,为测试Spring MVC应用程序提供支持。它执行完整的Spring MVC请求处理,但通过 mock request 和 response 对象而不是运行中的服务器。
MockMvc 可以单独使用来执行请求(request)和验证响应(response)。它也可以通过 WebTestClient 来使用,其中MockMvc被插入作为服务器来处理请求。 WebTestClient
的优点是可以选择使用更高级别的对象而不是原始数据,以及能够切换到针对实时服务器的完整的、端到端的HTTP测试并使用相同的测试API。
7.1. 概览
你可以通过实例化一个controller,为其注入依赖,并调用其方法,为Spring MVC编写普通的单元测试。然而,这样的测试并不能验证请求映射、数据绑定、消息转换、类型转换、验证,也不涉及任何支持 @InitBinder
、@ModelAttribute
或 @ExceptionHandler
方法。
Spring MVC测试框架,也被称为 MockMvc
,旨在为Spring MVC controller 提供更完整的测试,而无需运行服务器。它通过调用 DispatcherServlet
和传递来自 spring-test
模块的 Servlet API的 “mock” 实现 来实现这一目标,该模块在没有运行服务器的情况下复制了完整的Spring MVC请求处理。
MockMvc是一个服务器端的测试框架,它可以让你使用轻量级和有针对性的测试来验证Spring MVC应用程序的大部分功能。你可以单独使用它来执行请求和验证响应,或者你也可以通过 WebTestClient API使用它,并将MockMvc插入作为服务器来处理请求。
7.2. 静态导入
当直接使用MockMvc来执行请求时,你将需要静态导入:
-
MockMvcBuilders.*
-
MockMvcRequestBuilders.*
-
MockMvcResultMatchers.*
-
MockMvcResultHandlers.*
记住的一个简单方法是搜索 MockMvc*
。如果使用Eclipse,请确保在 Eclipse preferences 中将上述内容添加为 “favorite static members”。
当通过 WebTestClient 使用MockMvc时,你不需要静态导入。WebTestClient
提供了一个 fluent API,无需静态导入。
7.3. 设置的选择
MockMvc可以通过两种方式之一进行设置。一种是直接指向你想测试的 controller,并以编程方式配置Spring MVC基础设施。第二种是指向带有Spring MVC和 controller基础设施的Spring配置。
要设置MockMvc来测试一个特定的 controller,请使用以下方法:
class MyWebTests {
MockMvc mockMvc;
@BeforeEach
void setup() {
this.mockMvc = MockMvcBuilders.standaloneSetup(new AccountController()).build();
}
// ...
}
class MyWebTests {
lateinit var mockMvc : MockMvc
@BeforeEach
fun setup() {
mockMvc = MockMvcBuilders.standaloneSetup(AccountController()).build()
}
// ...
}
或者你也可以在通过 WebTestClient 进行测试时使用这个设置,它委托给上面所示的同一个 builder。
要通过Spring配置来设置 MockMvc,请使用以下内容:
@SpringJUnitWebConfig(locations = "my-servlet-context.xml")
class MyWebTests {
MockMvc mockMvc;
@BeforeEach
void setup(WebApplicationContext wac) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}
// ...
}
@SpringJUnitWebConfig(locations = ["my-servlet-context.xml"])
class MyWebTests {
lateinit var mockMvc: MockMvc
@BeforeEach
fun setup(wac: WebApplicationContext) {
mockMvc = MockMvcBuilders.webAppContextSetup(wac).build()
}
// ...
}
或者你也可以在通过 WebTestClient 进行测试时使用这个设置,它委托给上面所示的同一个 builder。
你应该使用哪个设置选项?
webAppContextSetup
加载你的实际Spring MVC配置,导致一个更完整的集成测试。由于TestContext框架缓存了加载的Spring配置,它有助于保持测试快速运行,即使你在测试套件中引入更多的测试。此外,你可以通过Spring配置将mock service注入到 controller中,以保持对Web层的测试专注。下面的例子用Mockito声明了一个mock service:
<bean id="accountService" class="org.mockito.Mockito" factory-method="mock">
<constructor-arg value="org.example.AccountService"/>
</bean>
然后,你可以将 mock service 注入测试中,以设置和验证你的期望,如下例所示:
@SpringJUnitWebConfig(locations = "test-servlet-context.xml")
class AccountTests {
@Autowired
AccountService accountService;
MockMvc mockMvc;
@BeforeEach
void setup(WebApplicationContext wac) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}
// ...
}
@SpringJUnitWebConfig(locations = ["test-servlet-context.xml"])
class AccountTests {
@Autowired
lateinit var accountService: AccountService
lateinit mockMvc: MockMvc
@BeforeEach
fun setup(wac: WebApplicationContext) {
mockMvc = MockMvcBuilders.webAppContextSetup(wac).build()
}
// ...
}
另一方面,StandaloneSetup
更接近于一个单元测试。它一次测试一个 controller。你可以手动注入 controller的mock依赖,它不涉及加载Spring配置。这样的测试更注重风格,更容易看到哪个 controller正在被测试,是否需要任何特定的Spring MVC配置来工作,等等。StandaloneSetup
也是一种非常方便的方式,可以编写临时的测试来验证特定的行为或调试一个问题。
与大多数 "集成测试与单元测试" 的辩论一样,没有正确或错误的答案。然而,使用 standaloneSetup
确实意味着需要额外的 webAppContextSetup
测试,以验证你的Spring MVC配置。另外,你可以用 webAppContextSetup
来编写所有的测试,以便始终针对你的实际Spring MVC配置进行测试。
7.4. 设置功能
无论你使用哪种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();
// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed
此外,第三方框架(和应用程序)可以预先打包设置指令,例如 MockMvcConfigurer
中的指令。Spring框架有一个这样的内置实现,它可以帮助保存和重用跨请求的HTTP Session。你可以按以下方式使用它:
// static import of SharedHttpSessionConfigurer.sharedHttpSession
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new TestController())
.apply(sharedHttpSession())
.build();
// Use mockMvc to perform requests...
// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed
参见 ConfigurableMockMvcBuilder
的javadoc,以获得所有MockMvc builder 功能的列表,或者使用IDE来探索可用选项。
7.5. 执行请求
本节展示了如何单独使用MockMvc来执行请求和验证响应。如果通过 WebTestClient
使用MockMvc,请参见 编写测试 上的相应章节。
要执行使用任何HTTP方法的请求,如以下例子所示:
// static import of MockMvcRequestBuilders.*
mockMvc.perform(post("/hotels/{id}", 42).accept(MediaType.APPLICATION_JSON));
mockMvc.post("/hotels/{id}", 42) {
accept = MediaType.APPLICATION_JSON
}
你也可以执行文件上传请求,在内部使用 MockMultipartHttpServletRequest
,这样就没有实际解析 multipart request。相反,你必须把它设置成类似于下面的例子:
mockMvc.perform(multipart("/doc").file("a1", "ABC".getBytes("UTF-8")));
mockMvc.multipart("/doc") {
file("a1", "ABC".toByteArray(charset("UTF8")))
}
你可以在URI模板风格中指定查询参数,如下例所示:
mockMvc.perform(get("/hotels?thing={thing}", "somewhere"));
mockMvc.get("/hotels?thing={thing}", "somewhere")
你也可以添加代表查询或表单参数的Servlet请求参数,如下例所示:
mockMvc.perform(get("/hotels").param("thing", "somewhere"));
mockMvc.get("/hotels") {
param("thing", "somewhere")
}
如果应用程序代码依赖于Servlet请求参数,并且不明确检查查询字符串(这是最常见的情况),那么你使用哪个选项并不重要。然而,请记住,与URI模板一起提供的查询参数是被解码的,而通过 param(…)
方法提供的请求参数预计已经被解码了。
在大多数情况下,最好不要把上下文路径(context path)和Servlet路径放在请求URI中。如果你必须用完整的请求URI进行测试,请确保相应地设置 contextPath
和 servletPath
,以便请求映射(request mapping)工作,如下面的例子所示:
mockMvc.perform(get("/app/main/hotels/{id}").contextPath("/app").servletPath("/main"))
mockMvc.get("/app/main/hotels/{id}") {
contextPath = "/app"
servletPath = "/main"
}
在前面的例子中,每次执行请求都要设置 contextPath
和 servletPath
是很麻烦的。相反,你可以设置默认的请求属性,如下面的例子所示:
class MyWebTests {
MockMvc mockMvc;
@BeforeEach
void setup() {
mockMvc = standaloneSetup(new AccountController())
.defaultRequest(get("/")
.contextPath("/app").servletPath("/main")
.accept(MediaType.APPLICATION_JSON)).build();
}
}
// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed
前面的属性会影响通过 MockMvc
实例执行的每个请求。如果同样的属性也在一个给定的请求中被指定,它将覆盖默认值。这就是为什么默认请求中的HTTP方法和URI并不重要,因为它们必须在每个请求中被指定。
7.6. 定义期望值
你可以通过在执行请求后附加一个或多个 andExpect(..)
调用来定义期望,如下例所示。一旦一个期望失败,其他的期望将不会被断言。
// static import of MockMvcRequestBuilders.* and MockMvcResultMatchers.*
mockMvc.perform(get("/accounts/1")).andExpect(status().isOk());
mockMvc.get("/accounts/1").andExpect {
status { isOk() }
}
你可以通过在执行一个请求后附加 andExpectAll(..)
来定义多个期望,正如下面的例子所示。与 andExpect(..)
相反,andExpectAll(..)
保证所有提供的期望将被断言,所有失败将被跟踪和报告。
// static import of MockMvcRequestBuilders.* and MockMvcResultMatchers.*
mockMvc.perform(get("/accounts/1")).andExpectAll(
status().isOk(),
content().contentType("application/json;charset=UTF-8"));
mockMvc.get("/accounts/1").andExpectAll {
status { isOk() }
content { contentType(APPLICATION_JSON) }
}
MockMvcResultMatchers.*
提供了一些预期(expectation),其中一些预期又进一步嵌套了更详细的预期。
断言一般分为两类。第一类断言验证了响应的属性(例如,响应状态、header和body)。这些是要断言的最重要的结果。
第二类断言超越了响应。这些断言让你检查Spring MVC的具体方面,例如哪个 controller方法处理了请求,是否引发和处理了异常,Model 的内容是什么,选择了什么视图(View),添加了什么 Flash 属性,等等。它们还可以让你检查 Servlet 的具体方面,如 request 和 Session attributes。
下面的测试断言,绑定或验证失败:
mockMvc.perform(post("/persons"))
.andExpect(status().isOk())
.andExpect(model().attributeHasErrors("person"));
mockMvc.post("/persons").andExpect {
status { isOk() }
model {
attributeHasErrors("person")
}
}
很多时候,在编写测试时,转储(dump)执行请求的结果是很有用的。你可以这样做,print()
是 MockMvcResultHandlers
的一个静态导入:
mockMvc.perform(post("/persons"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(model().attributeHasErrors("person"));
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)
将结果数据打印到一个自定义的Writer。如果你想把结果数据记录下来,而不是打印出来,你可以调用 log()
方法,它把结果数据记录为 org.springframework.test.web.servlet.result
logging类别下的一条 DEBUG
消息。
在某些情况下,你可能想直接访问结果,并验证一些以其他方式无法验证的东西。这可以通过在所有其他期望之后附加 .andReturn()
来实现,如下例所示:
MvcResult mvcResult = mockMvc.perform(post("/persons")).andExpect(status().isOk()).andReturn();
// ...
var mvcResult = mockMvc.post("/persons").andExpect { status { isOk() } }.andReturn()
// ...
如果所有的测试都重复相同的期望,你可以在构建 MockMvc
实例时设置一次共同的期望,正如下面的例子所示:
standaloneSetup(new SimpleController())
.alwaysExpect(status().isOk())
.alwaysExpect(content().contentType("application/json;charset=UTF-8"))
.build()
// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed
请注意,共同的期望总是被应用,如果不创建一个单独的 MockMvc
实例,就不能被重写。
当JSON响应内容包含用 Spring HATEOAS, 创建的超媒体链接时,你可以通过使用JsonPath表达式来验证产生的链接,如下例所示:
mockMvc.perform(get("/people").accept(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.links[?(@.rel == 'self')].href").value("http://localhost:8080/people"));
mockMvc.get("/people") {
accept(MediaType.APPLICATION_JSON)
}.andExpect {
jsonPath("$.links[?(@.rel == 'self')].href") {
value("http://localhost:8080/people")
}
}
当XML响应内容包含用 Spring HATEOAS 创建的超媒体链接时,你可以通过使用XPath表达式来验证产生的链接:
Map<String, String> ns = Collections.singletonMap("ns", "http://www.w3.org/2005/Atom");
mockMvc.perform(get("/handle").accept(MediaType.APPLICATION_XML))
.andExpect(xpath("/person/ns:link[@rel='self']/@href", ns).string("http://localhost:8080/people"));
val ns = mapOf("ns" to "http://www.w3.org/2005/Atom")
mockMvc.get("/handle") {
accept(MediaType.APPLICATION_XML)
}.andExpect {
xpath("/person/ns:link[@rel='self']/@href", ns) {
string("http://localhost:8080/people")
}
}
7.7. 异步请求
本节展示了如何单独使用MockMvc来测试异步请求处理。如果通过 WebTestClient 使用MockMvc,没有什么特别的事情要做,以使异步请求工作,因为 WebTestClient
会自动做本节所描述的事情。
在 Spring MVC中支持的 Servlet异步请求,其工作方式是退出Servlet容器线程,允许应用程序异步计算响应,之后进行异步调度,在Servlet容器线程上完成处理。
在 Spring MVC 测试中,可以通过首先断言产生的异步值来测试异步请求,然后手动执行异步调度,最后验证响应。下面是一个测试 controller方法的例子,这些方法返回 DeferredResult
、Callable
或响应式类型,如 Reactor Mono
:
// 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 | 手动执行一个ASYNC调度(因为没有运行中的容器)。 |
5 | 验证最终的响应。 |
@Test
fun test() {
var mvcResult = mockMvc.get("/path").andExpect {
status { isOk() } (1)
request { asyncStarted() } (2)
// TODO Remove unused generic parameter
request { asyncResult<Nothing>("body") } (3)
}.andReturn()
mockMvc.perform(asyncDispatch(mvcResult)) (4)
.andExpect {
status { isOk() } (5)
content().string("body")
}
}
1 | 检查响应状态仍未改变。 |
2 | 异步处理必须已经开始。 |
3 | 等待并断言异步的结果。 |
4 | 手动执行一个ASYNC调度(因为没有运行中的容器)。 |
5 | 验证最终的响应。 |
7.8. 流式(Stream)响应
测试流式响应(如 Server-Sent)的最好方法是通过 WebTestClient,它可以作为测试客户端连接到 MockMvc
实例,在没有运行服务器的情况下对Spring MVC controller进行测试。比如说:
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中也得到了支持,你可以 测试一个正在运行的服务器。
7.9. Filter 注册
在设置 MockMvc
实例时,你可以注册一个或多个Servlet Filter
实例,如下例所示:
mockMvc = standaloneSetup(new PersonController()).addFilters(new CharacterEncodingFilter()).build();
// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed
注册的过滤器通过 spring-test
的 MockFilterChain
被调用,最后一个 filter 委托给 DispatcherServlet
。
7.10. MockMvc与端到端的测试
MockMVc
是建立在 spring-test
模块的Servlet API mock实现上的,不依赖于运行中的容器。因此,与有实际客户端和运行中的服务器的完全端到端集成测试相比,有一些区别。
思考这个问题的最简单的方法是,从一个空白的 MockHttpServletRequest
开始。无论你向它添加什么,都会成为这个请求。可能会让你吃惊的是,默认情况下没有上下文路径;没有 jsessionid
cookie;没有转发(forwarding)、错误或异步调度;因此,没有实际的JSP渲染。相反,“forwarded” 和 “redirected” 的URL被保存在 MockHttpServletResponse
中,并且可以用期望来断言。
这意味着,如果你使用JSP,你可以验证请求被转发到的JSP页面,但没有HTML被渲染。换句话说,JSP并没有被调用。然而,请注意,所有其他不依赖转发的渲染技术,如Thymeleaf和Freemarker,都会按照预期将HTML渲染到响应体。通过 @ResponseBody
方法渲染JSON、XML和其他格式也是如此。
另外,你也可以考虑用 @SpringBootTest
从Spring Boot获得完整的端到端集成测试支持。见 Spring Boot 参考指南。
每种方法都有优点和缺点。在Spring MVC测试中提供的选项在从经典单元测试到完全集成测试的范围内是不同的。可以肯定的是,Spring MVC测试中的所有选项都不属于经典单元测试的范畴,但它们离经典单元测试更近一些。例如,你可以通过将 mock service 注入 controller来隔离Web层,在这种情况下,你只通过 DispatcherServlet
来测试Web层,但要使用实际的Spring配置,就像你可能在隔离上面的层来测试数据访问层一样。另外,你可以使用独立的设置,一次只关注一个 controller,并手动提供使其工作所需的配置。
在使用Spring MVC Test时,另一个重要的区别是,从概念上讲,这种测试是服务器端的,所以你可以检查使用了什么处理程序,是否用 HandlerExceptionResolver 处理了一个异常,Model 的内容是什么,有什么binding error,以及其他细节。这意味着写预期比较容易,因为服务器不是一个不透明的盒子,就像通过实际的HTTP客户端测试时那样。这通常是经典单元测试的一个优势: 它更容易编写、推理和调试,但不能取代对完整集成测试的需要。同时,重要的是不要忽略了响应是最重要的检查内容这一事实。简而言之,即使在同一个项目中,这里也有多种风格和策略的测试空间。
7.11. 更多实例
该框架自己的测试包括 许多样本测试,旨在展示如何单独使用 MockMvc 或通过 WebTestClient。浏览这些例子以获得进一步的想法。
7.12. HtmlUnit 整合
MockMvc可以与不依赖Servlet容器的模板技术一起使用(例如,Thymeleaf、FreeMarker和其他),但它不能与JSP一起使用,因为它们依赖Servlet容器。 |
7.12.1. 为什么要整合 HtmlUnit?
人们想到的最明显的问题是 "为什么我需要这个?" 答案最好是通过探索一个非常基本的示例应用程序来找到。假设你有一个Spring MVC Web 应用,支持对 Message
对象的CRUD操作。该应用还支持对所有 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"));
@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")
}
}
如果我们想测试让我们创建 message 的表单视图呢?例如,假设我们的表单看起来像下面的片段:
<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>
我们如何确保我们的表单产生正确的请求来创建一个新的 message?一个天真的尝试可能类似于以下:
mockMvc.perform(get("/messages/form"))
.andExpect(xpath("//input[@name='summary']").exists())
.andExpect(xpath("//textarea[@name='text']").exists());
mockMvc.get("/messages/form").andExpect {
xpath("//input[@name='summary']") { exists() }
xpath("//textarea[@name='text']") { exists() }
}
这个测试有一些明显的缺点。如果我们更新 controller,使用参数 message
而不是 text
,我们的表单测试就会继续通过,尽管HTML表单与 controller不同步。为了解决这个问题,我们可以将我们的两个测试结合起来,如下所示:
String summaryParamName = "summary";
String textParamName = "text";
mockMvc.perform(get("/messages/form"))
.andExpect(xpath("//input[@name='" + summaryParamName + "']").exists())
.andExpect(xpath("//textarea[@name='" + textParamName + "']").exists());
MockHttpServletRequestBuilder createMessage = post("/messages/")
.param(summaryParamName, "Spring Rocks")
.param(textParamName, "In case you didn't know, Spring Rocks!");
mockMvc.perform(createMessage)
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/messages/123"));
val summaryParamName = "summary";
val textParamName = "text";
mockMvc.get("/messages/form").andExpect {
xpath("//input[@name='$summaryParamName']") { exists() }
xpath("//textarea[@name='$textParamName']") { exists() }
}
mockMvc.post("/messages/") {
param(summaryParamName, "Spring Rocks")
param(textParamName, "In case you didn't know, Spring Rocks!")
}.andExpect {
status().is3xxRedirection()
redirectedUrl("/messages/123")
}
这将减少我们的测试不正确通过的风险,但仍有一些问题:
-
如果我们的页面上有多个表单怎么办?诚然,我们可以更新我们的XPath表达式,但随着我们考虑到更多的因素,它们会变得更加复杂: 这些字段的类型是否正确?这些字段是否启用?等等。
-
另一个问题是,我们正在做双倍的工作,这是我们所期望的。我们必须首先验证视图,然后用我们刚刚验证的参数提交视图。理想情况下,这可以一次完成。
-
最后,我们仍然无法解释一些事情。例如,如果表单中有我们希望测试的JavaScript验证,怎么办?
总的问题是,测试一个网页并不涉及单一的互动。相反,它是用户如何与网页互动以及该网页如何与其他资源互动的组合。例如,一个表单视图的结果被用作用户创建信息的输入。此外,我们的表单视图有可能使用影响页面行为的额外资源,如JavaScript验证。
集成测试来拯救?
为了解决前面提到的问题,我们可以进行端到端的集成测试,但这有一些缺点。考虑测试让我们翻阅 message 的视图。我们可能需要以下测试:
-
当 message 为空时,我们的页面是否会向用户显示一个通知,表明没有结果?
-
我们的页面是否正确地显示了一条 message?
-
我们的页面是否正确支持分页?
为了设置这些测试,我们需要确保我们的数据库包含适当的 message。这就导致了一些额外的挑战:
-
确保适当的 message 在数据库中是很繁琐的。(考虑到外键约束)。
-
测试会变得缓慢,因为每个测试都需要确保数据库处于正确的状态。
-
由于我们的数据库需要处于一个特定的状态,我们不能并行地运行测试。
-
对自动生成的ID、时间戳和其他项目进行断言可能很困难。
这些挑战并不意味着我们应该完全放弃端到端的集成测试。相反,我们可以通过重构我们的详细测试来减少端到端集成测试的数量,使用运行速度更快、更可靠、没有副作用的 mock service。然后,我们可以实现少量的真正的端到端集成测试,验证简单的工作流程,以确保所有东西都能正常工作。
HtmlUnit 整合选项
当你想把MockMvc和HtmlUnit整合时,你有很多选择:
-
MockMvc 和 HtmlUnit: 如果你想使用原始的HtmlUnit库,请使用这个选项。
-
MockMvc 和 WebDriver: 使用这个选项可以在集成和端到端测试之间缓解开发和重用代码。
-
MockMvc 和 Geb: 如果你想使用Groovy进行测试,简化开发,并在集成和端到端测试之间重复使用代码,请使用该选项。
7.12.2. MockMvc 和 HtmlUnit
本节描述了如何整合MockMvc和HtmlUnit。如果你想使用原始的HtmlUnit库,请使用这个选项。
MockMvc 和 HtmlUnit 的设置
首先,确保你已经包含了 net.sourceforge.htmlunit:htmlunit
的测试依赖。为了在Apache HttpComponents 4.5以上版本中使用 HtmlUnit,你需要使用HtmlUnit 2.18或者更高。
我们可以通过使用 MockMvcWebClientBuilder
轻松创建一个与 MockMvc 集成的HtmlUnit WebClient
,如下所示:
WebClient webClient;
@BeforeEach
void setup(WebApplicationContext context) {
webClient = MockMvcWebClientBuilder
.webAppContextSetup(context)
.build();
}
lateinit var webClient: WebClient
@BeforeEach
fun setup(context: WebApplicationContext) {
webClient = MockMvcWebClientBuilder
.webAppContextSetup(context)
.build()
}
这是一个使用 MockMvcWebClientBuilder 的简单例子。关于高级用法,请看 高级的 MockMvcWebClientBuilder .。
|
这确保了任何引用 localhost
作为服务器的URL都被引导到我们 的MockMvc
实例,而不需要真正的HTTP连接。任何其他的URL都是通过使用网络连接来请求的,和平常一样。这让我们可以轻松地测试CDN的使用。
MockMvc 和 HtmlUnit 的用法
现在我们可以像平时一样使用HtmlUnit,但不需要将我们的应用程序部署到Servlet容器中。例如,我们可以通过以下方式请求视图创建一个 message:
HtmlPage createMsgFormPage = webClient.getPage("http://localhost/messages/form");
val createMsgFormPage = webClient.getPage("http://localhost/messages/form")
默认的上下文路径(context path)是 "" 。另外,我们也可以指定上下文路径,如 高级的 MockMvcWebClientBuilder 中所述。
|
一旦我们有了对 HtmlPage
的引用,我们就可以填写表单并提交,以创建一个 message,如下例所示:
HtmlForm form = createMsgFormPage.getHtmlElementById("messageForm");
HtmlTextInput summaryInput = createMsgFormPage.getHtmlElementById("summary");
summaryInput.setValueAttribute("Spring Rocks");
HtmlTextArea textInput = createMsgFormPage.getHtmlElementById("text");
textInput.setText("In case you didn't know, Spring Rocks!");
HtmlSubmitInput submit = form.getOneHtmlElementByAttribute("input", "type", "submit");
HtmlPage newMessagePage = submit.click();
val form = createMsgFormPage.getHtmlElementById("messageForm")
val summaryInput = createMsgFormPage.getHtmlElementById("summary")
summaryInput.setValueAttribute("Spring Rocks")
val textInput = createMsgFormPage.getHtmlElementById("text")
textInput.setText("In case you didn't know, Spring Rocks!")
val submit = form.getOneHtmlElementByAttribute("input", "type", "submit")
val newMessagePage = submit.click()
最后,我们可以验证一个新的消息是否被成功创建。下面的断言使用 AssertJ 库:
assertThat(newMessagePage.getUrl().toString()).endsWith("/messages/123");
String id = newMessagePage.getHtmlElementById("id").getTextContent();
assertThat(id).isEqualTo("123");
String summary = newMessagePage.getHtmlElementById("summary").getTextContent();
assertThat(summary).isEqualTo("Spring Rocks");
String text = newMessagePage.getHtmlElementById("text").getTextContent();
assertThat(text).isEqualTo("In case you didn't know, Spring Rocks!");
assertThat(newMessagePage.getUrl().toString()).endsWith("/messages/123")
val id = newMessagePage.getHtmlElementById("id").getTextContent()
assertThat(id).isEqualTo("123")
val summary = newMessagePage.getHtmlElementById("summary").getTextContent()
assertThat(summary).isEqualTo("Spring Rocks")
val text = newMessagePage.getHtmlElementById("text").getTextContent()
assertThat(text).isEqualTo("In case you didn't know, Spring Rocks!")
前面的代码在很多方面改进了我们的 MockMvc测试 。首先,我们不再需要明确地验证我们的表单,然后创建一个看起来像表单的请求。相反,我们请求表单,填写并提交它,从而大大减少了开销。
另一个重要因素是, HtmlUnit 使用 Mozilla Rhino 引擎 来评估JavaScript。这意味着我们也可以测试页面中JavaScript的行为。
关于使用 HtmlUnit 的更多信息,请参见 HtmlUnit 文档。
高级的 MockMvcWebClientBuilder
在迄今为止的例子中,我们以最简单的方式使用了 MockMvcWebClientBuilder
,即基于Spring TestContext 框架为我们加载的 WebApplicationContext
构建一个 WebClient
。这种方法在下面的例子中得到了重复:
WebClient webClient;
@BeforeEach
void setup(WebApplicationContext context) {
webClient = MockMvcWebClientBuilder
.webAppContextSetup(context)
.build();
}
lateinit var webClient: WebClient
@BeforeEach
fun setup(context: WebApplicationContext) {
webClient = MockMvcWebClientBuilder
.webAppContextSetup(context)
.build()
}
我们还可以指定额外的配置选项,如下例所示:
WebClient webClient;
@BeforeEach
void setup() {
webClient = MockMvcWebClientBuilder
// demonstrates applying a MockMvcConfigurer (Spring Security)
.webAppContextSetup(context, springSecurity())
// for illustration only - defaults to ""
.contextPath("")
// By default MockMvc is used for localhost only;
// the following will use MockMvc for example.com and example.org as well
.useMockMvcForHosts("example.com","example.org")
.build();
}
lateinit var webClient: WebClient
@BeforeEach
fun setup() {
webClient = MockMvcWebClientBuilder
// demonstrates applying a MockMvcConfigurer (Spring Security)
.webAppContextSetup(context, springSecurity())
// for illustration only - defaults to ""
.contextPath("")
// By default MockMvc is used for localhost only;
// the following will use MockMvc for example.com and example.org as well
.useMockMvcForHosts("example.com","example.org")
.build()
}
作为替代方案,我们可以通过单独配置 MockMvc
实例并将其提供给 MockMvcWebClientBuilder
来进行完全相同的设置,如下所示:
MockMvc mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
webClient = MockMvcWebClientBuilder
.mockMvcSetup(mockMvc)
// for illustration only - defaults to ""
.contextPath("")
// By default MockMvc is used for localhost only;
// the following will use MockMvc for example.com and example.org as well
.useMockMvcForHosts("example.com","example.org")
.build();
// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed
这就比较啰嗦了,但是,通过用 MockMvc
实例构建 WebClient
,我们就有了 MockMvc
的全部功能,触手可及。
关于创建 MockMvc 实例的其他信息,请看 设置的选择。
|
7.12.3. MockMvc 和 WebDriver
在前面的章节中,我们已经看到了如何将MockMvc与原始的HtmlUnit APIs结合使用。在这一节中,我们在Selenium WebDriver 中使用额外的抽象来使事情变得更加简单。
为什么选择 WebDriver 和 MockMvc?
我们已经可以使用HtmlUnit和MockMvc,那么为什么还要使用WebDriver呢?Selenium WebDriver提供了一个非常优雅的API,让我们轻松地组织我们的代码。为了更好地展示它是如何工作的,我们在本节中探讨一个例子。
尽管是 Selenium 的一部分,WebDriver并不要求Selenium服务器来运行你的测试。 |
假设我们需要确保一条 message 被正确创建。测试包括寻找HTML表单的输入元素,填写它们,并进行各种断言。
这种方法导致了许多单独的测试,因为我们也想测试错误条件。例如,我们要确保如果我们只填写了表单的一部分,就会出现错误。如果我们填写了整个表单,之后应该显示新创建的message。
如果其中一个字段被命名为 “summary”,我们可能会有类似以下的东西在我们的测试中的多个地方重复:
HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary");
summaryInput.setValueAttribute(summary);
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);
}
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)
}
这样做可以确保我们在改变用户界面时不必更新所有的测试。
我们甚至可以更进一步,把这个逻辑放在一个代表我们当前所在的 HtmlPage
的 Object
中,就像下面的例子所示:
public class CreateMessagePage {
final HtmlPage currentPage;
final HtmlTextInput summaryInput;
final HtmlSubmitInput submit;
public CreateMessagePage(HtmlPage currentPage) {
this.currentPage = currentPage;
this.summaryInput = currentPage.getHtmlElementById("summary");
this.submit = currentPage.getHtmlElementById("submit");
}
public <T> T createMessage(String summary, String text) throws Exception {
setSummary(summary);
HtmlPage result = submit.click();
boolean error = CreateMessagePage.at(result);
return (T) (error ? new CreateMessagePage(result) : new ViewMessagePage(result));
}
public void setSummary(String summary) throws Exception {
summaryInput.setValueAttribute(summary);
}
public static boolean at(HtmlPage page) {
return "Create Message".equals(page.getTitleText());
}
}
class CreateMessagePage(private val currentPage: HtmlPage) {
val summaryInput: HtmlTextInput = currentPage.getHtmlElementById("summary")
val submit: HtmlSubmitInput = currentPage.getHtmlElementById("submit")
fun <T> createMessage(summary: String, text: String): T {
setSummary(summary)
val result = submit.click()
val error = at(result)
return (if (error) CreateMessagePage(result) else ViewMessagePage(result)) as T
}
fun setSummary(summary: String) {
summaryInput.setValueAttribute(summary)
}
fun at(page: HtmlPage): Boolean {
return "Create Message" == page.getTitleText()
}
}
}
以前,这种模式被称为 Page Object Pattern。虽然我们当然可以用 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();
}
lateinit var driver: WebDriver
@BeforeEach
fun setup(context: WebApplicationContext) {
driver = MockMvcHtmlUnitDriverBuilder
.webAppContextSetup(context)
.build()
}
这是一个使用 MockMvcHtmlUnitDriverBuilder 的简单例子。对于更高级的用法,请看 高级的 MockMvcHtmlUnitDriverBuilder 。
|
前面的例子确保任何引用 localhost
作为服务器的URL都会被引导到我们的 MockMvc
实例,而不需要真正的HTTP连接。任何其他的URL都是通过使用网络连接来请求的,和平常一样。这让我们可以轻松地测试CDN的使用情况。
MockMvc 和 WebDriver 的用法
现在我们可以像平时一样使用WebDriver,但不需要将我们的应用程序部署到Servlet容器中。例如,我们可以通过以下方式请求视图创建一个 message:
CreateMessagePage page = CreateMessagePage.to(driver);
val page = CreateMessagePage.to(driver)
然后我们可以填写表单并提交,创建一个message,如下所示
ViewMessagePage viewMessagePage =
page.createMessage(ViewMessagePage.class, expectedSummary, expectedText);
val viewMessagePage =
page.createMessage(ViewMessagePage::class, expectedSummary, expectedText)
这通过利用 Page Object Pattern 改进了我们的 HtmlUnit测试 的设计。正如我们在《为什么选择 WebDriver 和 MockMvc?》中提到的,我们可以在HtmlUnit中使用 Page Object Pattern,但在WebDriver中要容易得多。请看下面的 CreateMessagePage
实现:
public class CreateMessagePage extends AbstractPage { (1)
(2)
private WebElement summary;
private WebElement text;
@FindBy(css = "input[type=submit]") (3)
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 通过自动解析每个 WebElement ,让我们从HtmlUnit版本的 CreateMessagePage 中移除大量的代码。 PageFactory#initElements(WebDriver,Class<T>) 方法通过使用字段名并通过HTML页面中元素的 id 或 name 来自动解析每个 WebElement 。 |
3 | 我们可以使用 @FindBy 注解 来覆盖默认的查找行为。我们的例子显示了如何使用 @FindBy 注解,用 css 选择器(input[type=submit] )来查找我们的提交按钮。 |
class CreateMessagePage(private val driver: WebDriver) : AbstractPage(driver) { (1)
(2)
private lateinit var summary: WebElement
private lateinit var text: WebElement
@FindBy(css = "input[type=submit]") (3)
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 通过自动解析每个 WebElement ,让我们从HtmlUnit版本的 CreateMessagePage 中移除大量的代码。 PageFactory#initElements(WebDriver,Class<T>) 方法通过使用字段名并通过HTML页面中元素的 id 或 name 来自动解析每个 WebElement 。 |
3 | 我们可以使用 @FindBy 注解 来覆盖默认的查找行为。我们的例子显示了如何使用 @FindBy 注解,用 css 选择器(input[type=submit] )来查找我们的提交按钮。 |
最后,我们可以验证一个新的 message 是否被成功创建。下面的断言使用 AssertJ 断言库:
assertThat(viewMessagePage.getMessage()).isEqualTo(expectedMessage);
assertThat(viewMessagePage.getSuccess()).isEqualTo("Successfully created a new message");
assertThat(viewMessagePage.message).isEqualTo(expectedMessage)
assertThat(viewMessagePage.success).isEqualTo("Successfully created a new message")
我们可以看到,我们的 ViewMessagePage
让我们与我们的自定义 domain 模型互动。例如,它暴露了一个方法,返回一个 Message
对象:
public Message getMessage() throws ParseException {
Message message = new Message();
message.setId(getId());
message.setCreated(getCreated());
message.setSummary(getSummary());
message.setText(getText());
return message;
}
fun getMessage() = Message(getId(), getCreated(), getSummary(), getText())
然后我们可以在我们的断言中使用丰富的 domain 对象。
最后,我们一定不要忘记在测试完成后关闭 WebDriver
实例,如下所示:
@AfterEach
void destroy() {
if (driver != null) {
driver.close();
}
}
@AfterEach
fun destroy() {
if (driver != null) {
driver.close()
}
}
关于使用 WebDriver 的其他信息,请参见 Selenium WebDriver 文档。
高级的 MockMvcHtmlUnitDriverBuilder
在迄今为止的例子中,我们以最简单的方式使用了 MockMvcHtmlUnitDriverBuilder
,即基于Spring TestContext 框架为我们加载的 WebApplicationContext
构建一个 WebDriver
。在此重复这一方法,具体如下:
WebDriver driver;
@BeforeEach
void setup(WebApplicationContext context) {
driver = MockMvcHtmlUnitDriverBuilder
.webAppContextSetup(context)
.build();
}
lateinit var driver: WebDriver
@BeforeEach
fun setup(context: WebApplicationContext) {
driver = MockMvcHtmlUnitDriverBuilder
.webAppContextSetup(context)
.build()
}
我们还可以指定额外的配置选项,如下所示:
WebDriver driver;
@BeforeEach
void setup() {
driver = MockMvcHtmlUnitDriverBuilder
// demonstrates applying a MockMvcConfigurer (Spring Security)
.webAppContextSetup(context, springSecurity())
// for illustration only - defaults to ""
.contextPath("")
// By default MockMvc is used for localhost only;
// the following will use MockMvc for example.com and example.org as well
.useMockMvcForHosts("example.com","example.org")
.build();
}
lateinit var driver: WebDriver
@BeforeEach
fun setup() {
driver = MockMvcHtmlUnitDriverBuilder
// demonstrates applying a MockMvcConfigurer (Spring Security)
.webAppContextSetup(context, springSecurity())
// for illustration only - defaults to ""
.contextPath("")
// By default MockMvc is used for localhost only;
// the following will use MockMvc for example.com and example.org as well
.useMockMvcForHosts("example.com","example.org")
.build()
}
作为一个替代方案,我们可以通过单独配置MockMvc实例并将其提供给 MockMvcHtmlUnitDriverBuilder
来进行完全相同的设置,如下所示:
MockMvc mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
driver = MockMvcHtmlUnitDriverBuilder
.mockMvcSetup(mockMvc)
// for illustration only - defaults to ""
.contextPath("")
// By default MockMvc is used for localhost only;
// the following will use MockMvc for example.com and example.org as well
.useMockMvcForHosts("example.com","example.org")
.build();
// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed
这就比较啰嗦了,但是,通过用 MockMvc
实例构建 WebDriver
,我们可以随心所欲地使用MockMvc的全部功能。
关于创建 MockMvc 实例的其他信息,请看 设置的选择。
|
7.12.4. MockMvc 和 Geb
在上一节中,我们看到了如何使用MockMvc和WebDriver。在这一节中,我们使用 Geb 来使我们的测试更加Groovy-er。
为什么选择 Geb 和 MockMvc?
Geb是由WebDriver支持的,所以它提供了许多与WebDriver 相同的好处。然而,Geb通过为我们处理一些模板代码,使事情变得更加简单。
MockMvc 和 Geb 的设置
我们可以通过使用 MockMvc 的 Selenium WebDriver 轻松地初始化一个Geb Browser
,如下所示:
def setup() {
browser.driver = MockMvcHtmlUnitDriverBuilder
.webAppContextSetup(context)
.build()
}
这是一个使用 MockMvcHtmlUnitDriverBuilder 的简单例子。对于更高级的用法,请看 高级的 MockMvcHtmlUnitDriverBuilder 。
|
This ensures that any URL referencing localhost
as the server is directed to our
MockMvc
instance without the need for a real HTTP connection. Any other URL is
requested by using a network connection as normal. This lets us easily test the use of
CDNs.
这确保了任何引用 localhost
作为服务器的URL都会被引导到我们的 MockMvc
实例,而不需要真正的HTTP连接。任何其他的URL都是通过使用正常的网络连接来请求的。这让我们可以轻松地测试CDN的使用。
MockMvc 和 Geb 的用法
现在我们可以像平时一样使用Geb,但不需要将我们的应用程序部署到Servlet容器中。例如,我们可以通过以下方式请求视图创建一条 message:
to CreateMessagePage
然后我们可以填写表单并提交,创建一个message,如下所示:
when:
form.summary = expectedSummary
form.text = expectedMessage
submit.click(ViewMessagePage)
任何未被识别的方法调用或属性访问或未被发现的引用都被转发到当前的 page 对象。这消除了很多我们在直接使用WebDriver时需要的模板代码。
与直接使用WebDriver一样,这通过使用 Page Object Pattern 改进了我们 HtmlUnit测试 的设计。如前所述,我们可以在HtmlUnit和WebDriver中使用 Page Object Pattern ,但在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 的 Navigator API 来选择我们感兴趣的内容。
最后,我们可以验证是否成功创建了一条新的 message,如下所示:
then:
at ViewMessagePage
success == 'Successfully created a new message'
id
date
summary == expectedSummary
message == expectedMessage
关于如何充分利用Geb的更多细节,请参阅《 Geb之书》用户手册。
8. 测试客户应用
你可以使用客户端测试来测试内部使用 RestTemplate
的代码。这个想法是声明预期的请求并提供 “stub” 响应,这样你就可以专注于孤立地测试代码(也就是说,不运行服务器)。下面的例子展示了如何做到这一点:
RestTemplate restTemplate = new RestTemplate();
MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build();
mockServer.expect(requestTo("/greeting")).andRespond(withSuccess());
// Test code that uses the above RestTemplate ...
mockServer.verify();
val restTemplate = RestTemplate()
val mockServer = MockRestServiceServer.bindTo(restTemplate).build()
mockServer.expect(requestTo("/greeting")).andRespond(withSuccess())
// Test code that uses the above RestTemplate ...
mockServer.verify()
在前面的例子中,MockRestServiceServer
(客户端REST测试的中心类)用一个自定义的 ClientHttpRequestFactory
来配置 RestTemplate
,该工厂根据预期来断定实际请求,并返回 “stub” 响应。在这种情况下,我们期待一个对 /greeting
的请求,并希望返回一个带有 text/plain
内容的200响应。我们可以根据需要定义额外的预期请求和stub响应。当我们定义了预期请求和stub响应后,RestTemplate
可以像往常一样在客户端代码中使用。在测试结束时,可以使用 mockServer.verify()
来验证所有的预期都得到了满足。
默认情况下,请求是按照期望值声明的顺序被期望的。你可以在构建服务器时设置 ignoreExpectOrder
选项,在这种情况下,所有的期望值都会被检查(按顺序)以找到与给定请求的匹配。这意味着允许请求以任何顺序出现。下面的例子使用了 ignoreExpectOrder
:
server = MockRestServiceServer.bindTo(restTemplate).ignoreExpectOrder(true).build();
server = MockRestServiceServer.bindTo(restTemplate).ignoreExpectOrder(true).build()
即使默认情况下是无序的请求,每个请求也只允许运行一次。expect
方法提供了一个重载变体,它接受一个指定计数范围的 ExpectedCount
参数(例如,once
, manyTimes
, max
, min
, between
,等等)。下面的例子使用了 times
:
RestTemplate restTemplate = new RestTemplate();
MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build();
mockServer.expect(times(2), requestTo("/something")).andRespond(withSuccess());
mockServer.expect(times(3), requestTo("/somewhere")).andRespond(withSuccess());
// ...
mockServer.verify();
val restTemplate = RestTemplate()
val mockServer = MockRestServiceServer.bindTo(restTemplate).build()
mockServer.expect(times(2), requestTo("/something")).andRespond(withSuccess())
mockServer.expect(times(3), requestTo("/somewhere")).andRespond(withSuccess())
// ...
mockServer.verify()
注意,当 ignoreExpectOrder
没有被设置时(默认情况),因此,请求是按照声明的顺序被预期的,那么这个顺序只适用于任何预期请求中的第一个。例如,如果 "/something" 被期望两次,然后是 "/something" 三次,那么在有 "/something" 的请求之前,应该有一个 "/something" 的请求,但是,除了随后的 "/something" 和 "/something" ,请求可以在任何时间出现。
作为上述所有的替代方案,客户端测试支持也提供了一个 ClientHttpRequestFactory
实现,你可以将其配置到 RestTemplate
中,将其绑定到 MockMvc
实例上。这允许使用实际的服务器端逻辑来处理请求,但不需要运行一个服务器。下面的例子展示了如何做到这一点:
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
this.restTemplate = new RestTemplate(new MockMvcClientHttpRequestFactory(mockMvc));
// Test code that uses the above RestTemplate ...
val mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build()
restTemplate = RestTemplate(MockMvcClientHttpRequestFactory(mockMvc))
// Test code that uses the above RestTemplate ...
在某些情况下,可能需要执行对远程服务的实际调用,而不是 mock 响应。下面的例子展示了如何通过 ExecutingResponseCreator
来做到这一点:
RestTemplate restTemplate = new RestTemplate();
// Create ExecutingResponseCreator with the original request factory
ExecutingResponseCreator withActualResponse = new ExecutingResponseCreator(restTemplate.getRequestFactory());
MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build();
mockServer.expect(requestTo("/profile")).andRespond(withSuccess());
mockServer.expect(requestTo("/quoteOfTheDay")).andRespond(withActualResponse);
// Test code that uses the above RestTemplate ...
mockServer.verify();
val restTemplate = RestTemplate()
// Create ExecutingResponseCreator with the original request factory
val withActualResponse = new ExecutingResponseCreator(restTemplate.getRequestFactory())
val mockServer = MockRestServiceServer.bindTo(restTemplate).build()
mockServer.expect(requestTo("/profile")).andRespond(withSuccess())
mockServer.expect(requestTo("/quoteOfTheDay")).andRespond(withActualResponse)
// Test code that uses the above RestTemplate ...
mockServer.verify()
在前面的例子中,我们使用 RestTemplate
中的 ClientHttpRequestFactory
创建了 ExecutingResponseCreator
,然后 MockRestServiceServer
用另一个 mock 响应的工厂替换了它。然后我们用两种响应来定义期望:
-
一个针对
/profile
端点的 stub200
响应(不会执行实际请求)。 -
通过调用
/quoteOfTheDay
端点获得的响应。
在第二种情况下,请求是通过先前捕获的 ClientHttpRequestFactory
执行的。这就产生了一个响应,这个响应可能来自一个实际的远程服务器,这取决于 RestTemplate
最初是如何配置的。
8.1. 静态导入
与服务器端测试一样,客户端测试的 fluent API需要一些静态导入。通过搜索 MockRest*
,可以很容易地找到这些。Eclipse用户应该将 MockRestRequestMatchers.*
和 MockRestResponseCreators.*
作为 “favorite static members” 添加到 Eclipse preferences 中的 Java → Editor → Content Assist → Favorites。这允许在输入静态方法名称的第一个字符后使用内容辅助。其他IDE(如IntelliJ)可能不需要任何额外的配置。检查对静态成员的代码完成的支持情况。
8.2. 客户端REST测试的更多例子
Spring MVC Test自己的测试包括 示例测试 的客户端REST测试。
9. 附录
9.1. 注解
本节涵盖了你在测试Spring应用程序时可以使用的注解。它包括以下主题:
9.1.1. 标准注解的支持
以下注解支持Spring TestContext框架的所有配置的标准语义。请注意,这些注解不是专门针对测试的,可以在Spring框架的任何地方使用。
-
@Autowired
-
@Qualifier
-
@Value
-
@Resource
(jakarta.annotation) 如果JSR-250存在的话 -
@ManagedBean
(jakarta.annotation) 如果JSR-250存在的话 -
@Inject
(jakarta.inject) 如果JSR-330存在的话 -
@Named
(jakarta.inject) 如果JSR-330存在的话 -
@PersistenceContext
(jakarta.persistence) 如果JPA存在的话 -
@PersistenceUnit
(jakarta.persistence) 如果JPA存在的话 -
@Transactional
(org.springframework.transaction.annotation) 具有 有限的属性支持
JSR-250 生命周期注解
在Spring TestContext框架中,你可以在 如果一个测试类中的方法被 |
9.1.2. Spring 测试注解
Spring框架提供了以下一组Spring特定的注解,你可以在单元和集成测试中与TestContext框架一起使用。更多信息,包括默认属性值、属性别名和其他细节,请参见相应的javadoc。
Spring的测试注解包括以下内容:
@BootstrapWith
@BootstrapWith
是一个类级注解,你可以用它来配置Spring TestContext框架的引导方式。具体来说,你可以使用 @BootstrapWith
来指定一个自定义的 TestContextBootstrapper
。更多细节请参见 引导TestContext框架 的部分。
@ContextConfiguration
@ContextConfiguration
定义了类级元数据,用于确定如何为集成测试加载和配置 ApplicationContext
。具体来说,@ContextConfiguration
声明了应用上下文资源 locations
或用于加载上下文的组件 classes
。
资源 location 通常是位于classpath中的XML配置文件或Groovy脚本,而组件类通常是 @Configuration
类。然而,资源 location 也可以指文件系统中的文件和脚本,而组件类可以是 @Component
类、@Service
类等等。详情请见 组件类(Component Classes)。
下面的例子显示了一个指向XML文件的 @ContextConfiguration
注解:
@ContextConfiguration("/test-config.xml") (1)
class XmlApplicationContextTests {
// class body...
}
1 | 指向一个XML文件。 |
@ContextConfiguration("/test-config.xml") (1)
class XmlApplicationContextTests {
// class body...
}
1 | 指向一个XML文件。 |
下面的例子显示了一个指向一个类的 @ContextConfiguration
注解:
@ContextConfiguration(classes = TestConfig.class) (1)
class ConfigClassApplicationContextTests {
// class body...
}
1 | 指向一个类。 |
@ContextConfiguration(classes = [TestConfig::class]) (1)
class ConfigClassApplicationContextTests {
// class body...
}
1 | 指向一个类。 |
作为声明资源 location 或组件类的替代或补充,你可以使用 @ContextConfiguration
来声明 ApplicationContextInitializer
类。下面的例子展示了这样一个情况:
@ContextConfiguration(initializers = CustomContextInitializer.class) (1)
class ContextInitializerTests {
// class body...
}
1 | 声明一个 initializer 类。 |
@ContextConfiguration(initializers = [CustomContextInitializer::class]) (1)
class ContextInitializerTests {
// class body...
}
1 | 声明一个 initializer 类。 |
你也可以选择使用 @ContextConfiguration
来声明 ContextLoader
策略。然而,请注意,你通常不需要明确地配置加载器,因为默认的加载器支持 initializers
和资源 locations
或组件 classes
。
下面的例子同时使用了一个 location 和一个 loader:
@ContextConfiguration(locations = "/test-context.xml", loader = CustomContextLoader.class) (1)
class CustomLoaderXmlApplicationContextTests {
// class body...
}
1 | 同时配置一个 location 和一个自定义loader。 |
@ContextConfiguration("/test-context.xml", loader = CustomContextLoader::class) (1)
class CustomLoaderXmlApplicationContextTests {
// class body...
}
1 | 同时配置一个location 和一个自定义loader。 |
@ContextConfiguration 提供了对继承资源 location 或配置类以及由超类或包围类声明的 context initializer 的支持。
|
更多细节请参见 Context 管理, @Nested
测试类的配置, 以及 @Contextconfiguration
javadocs.
@WebAppConfiguration
@WebAppConfiguration
是一个类级注解,你可以用它来声明为集成测试加载的 ApplicationContext
应该是一个 WebApplicationContext
。仅仅在测试类上存在 @WebAppConfiguration
就可以确保为测试加载 WebApplicationContext
,使用默认值 "file:src/main/webapp"
作为Web应用程序 root 的路径(也就是资源基础路径)。资源基础路径在幕后被用来创建一个 MockServletContext
,作为测试的 WebApplicationContext
的 ServletContext
。
下面的例子展示了如何使用 @WebAppConfiguration
注解:
@ContextConfiguration
@WebAppConfiguration (1)
class WebAppTests {
// class body...
}
1 | @WebAppConfiguration 注解。 |
@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 resource。 |
@ContextConfiguration
@WebAppConfiguration("classpath:test-web-resources") (1)
class WebAppTests {
// class body...
}
1 | 指定一个 classpath resource。 |
请注意,@WebAppConfiguration
必须与 @ContextConfiguration
一起使用,无论是在一个测试类中还是在一个测试类的层次结构中。请参阅 @WebAppConfiguration
javadoc以了解更多细节。
@ContextHierarchy
@ContextHierarchy
是一个类级注解,用于为集成测试定义 ApplicationContext
实例的层次结构。@ContextHierarchy
应该用一个或多个 @ContextConfiguration
实例的列表来声明,每个实例都定义了上下文层次结构中的一个层次。下面的例子演示了 @ContextHierarchy
在单个测试类中的使用(@ContextHierarchy
也可以在测试类的层次结构中使用):
@ContextHierarchy({
@ContextConfiguration("/parent-config.xml"),
@ContextConfiguration("/child-config.xml")
})
class ContextHierarchyTests {
// class body...
}
@ContextHierarchy(
ContextConfiguration("/parent-config.xml"),
ContextConfiguration("/child-config.xml"))
class ContextHierarchyTests {
// class body...
}
@WebAppConfiguration
@ContextHierarchy({
@ContextConfiguration(classes = AppConfig.class),
@ContextConfiguration(classes = WebConfig.class)
})
class WebIntegrationTests {
// class body...
}
@WebAppConfiguration
@ContextHierarchy(
ContextConfiguration(classes = [AppConfig::class]),
ContextConfiguration(classes = [WebConfig::class]))
class WebIntegrationTests {
// class body...
}
如果你需要在测试类层次结构中合并或覆盖上下文层次结构的某个特定级别的配置,你必须通过在类层次结构中的每个相应级别上为 @ContextConfiguration
中的 name
属性提供相同的值来明确地命名该级别。参见 上下文(Context)层次结构 和 @ContextHierarchy
javadoc 获取更多的例子。
@ActiveProfiles
@ActiveProfiles
是一个类级注解,用于声明在加载集成测试的 ApplicationContext
时,哪些Bean定义配置文件(profile)应该是活动的。
下面的例子表明 dev
profile应该是活动的:
s
@ContextConfiguration
@ActiveProfiles("dev") (1)
class DeveloperTests {
// class body...
}
1 | 表示 dev profile 应该是活动的。 |
@ContextConfiguration
@ActiveProfiles("dev") (1)
class DeveloperTests {
// class body...
}
1 | 表示 dev profile 应该是活动的。 |
下面的例子表明,dev
和 integration
profile 都应该是活动的:
@ContextConfiguration
@ActiveProfiles({"dev", "integration"}) (1)
class DeveloperIntegrationTests {
// class body...
}
1 | 表示 dev 和 integration profile 应该是活动的。 |
@ContextConfiguration
@ActiveProfiles(["dev", "integration"]) (1)
class DeveloperIntegrationTests {
// class body...
}
1 | 表示 dev 和 integration profile 应该是活动的。 |
@ActiveProfiles 默认提供了对继承由超类和包围类声明的活动豆定义配置文件的支持。你也可以通过实现一个自定义的 ActiveProfilesResolver 并通过使用 @ActiveProfiles 的 resolver 属性来注册它,从而以编程方式解决 active bean definition profile。
|
参见 使用 Environment Profiles 的上下文(Context )配置, @Nested
测试类的配置, 以及 @ActiveProfiles
javadoc 获取示例和更多细节。
@TestPropertySource
@TestPropertySource
是一个类级别的注解,你可以用它来配置properties文件和内联properties的位置(location),以添加到为集成测试加载的 ApplicationContext
的 Environment
中的 PropertySources
集合。
下面的例子演示了如何从classpath声明一个properties文件:
@ContextConfiguration
@TestPropertySource("/test.properties") (1)
class MyIntegrationTests {
// class body...
}
1 | 从classpath根部的 test.properties 获取属性。 |
@ContextConfiguration
@TestPropertySource("/test.properties") (1)
class MyIntegrationTests {
// class body...
}
1 | 从classpath根部的 test.properties 获取属性。 |
下面的例子演示了如何声明内联 properties:
@ContextConfiguration
@TestPropertySource(properties = { "timezone = GMT", "port: 4242" }) (1)
class MyIntegrationTests {
// class body...
}
1 | 声明 timezone 和 port 属性。 |
@ContextConfiguration
@TestPropertySource(properties = ["timezone = GMT", "port: 4242"]) (1)
class MyIntegrationTests {
// class body...
}
1 | 声明 timezone 和 port 属性。 |
参见 带有测试属性源(Property Sources)的 Context 配置 中的例子和进一步细节。
@DynamicPropertySource
@DynamicPropertySource
是一个方法级注解,你可以用它来注册动态属性,将其添加到集成测试加载的 ApplicationContext
的 Environment
中的 PropertySources
集中。当你不知道属性的值时,动态属性很有用—例如,如果属性由外部资源管理,如由 Testcontainers 项目管理的容器。
下面的例子演示了如何注册一个动态属性:
@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 属性,以便从服务器中延迟地检索。 |
@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 属性,以便从服务器中延迟地检索。 |
更多细节请参见 使用动态属性源的 Context 配置。
@DirtiesContext
@DirtiesContext
表示底层的Spring ApplicationContext
在测试的执行过程中被破坏了(也就是说,测试以某种方式修改或破坏了它—例如,通过改变单体Bean的状态),应该被关闭。当一个应用程序上下文(application context)被标记为脏时,它就会从测试框架的缓存中删除并关闭。因此,对于任何需要具有相同配置元数据的上下文的后续测试,底层的Spring容器将被重建。
你可以在同一个类或类的层次结构中使用 @DirtiesContext
作为类级和方法级注解。在这种情况下,根据配置的 methodMode
和 classMode
,ApplicationContext
在任何这种注解的方法之前或之后,以及在当前测试类之前或之后都被标记为脏。
下面的例子解释了在各种配置情况下,什么时候上下文(context)会被搅乱:
-
在当前测试类之前,当在 class mode 设置为
BEFORE_CLASS
的类上声明时。Java@DirtiesContext(classMode = BEFORE_CLASS) (1) class FreshContextTests { // some tests that require a new Spring container }
1 将当前测试类之前的上下文(context)破坏。 Kotlin@DirtiesContext(classMode = BEFORE_CLASS) (1) class FreshContextTests { // some tests that require a new Spring container }
1 将当前测试类之前的上下文(context)破坏。 -
在当前测试类之后,当在 class mode 设置为
AFTER_CLASS
(即默认的 class mode)的类上声明。Java@DirtiesContext (1) class ContextDirtyingTests { // some tests that result in the Spring container being dirtied }
1 在当前测试类之后破坏上下文(Context) Kotlin@DirtiesContext (1) class ContextDirtyingTests { // some tests that result in the Spring container being dirtied }
1 在当前测试类之后破坏上下文(Context). -
在当前测试类中的每个测试方法之前,当在 class mode 设置为
BEFORE_EACH_TEST_METHOD
的类上声明时。Java@DirtiesContext(classMode = BEFORE_EACH_TEST_METHOD) (1) class FreshContextTests { // some tests that require a new Spring container }
1 在每个测试方法之前破坏上下文(Context). Kotlin@DirtiesContext(classMode = BEFORE_EACH_TEST_METHOD) (1) class FreshContextTests { // some tests that require a new Spring container }
1 在每个测试方法之前破坏上下文(Context) -
在当前测试类的每个测试方法之后,当在 class mode 设置为
AFTER_EACH_TEST_METHOD
的类上声明时。Java@DirtiesContext(classMode = AFTER_EACH_TEST_METHOD) (1) class ContextDirtyingTests { // some tests that result in the Spring container being dirtied }
1 在每个测试方法之后,破坏上下文(Context)。 Kotlin@DirtiesContext(classMode = AFTER_EACH_TEST_METHOD) (1) class ContextDirtyingTests { // some tests that result in the Spring container being dirtied }
1 在每个测试方法之后,破坏上下文(Context)。 -
在当前测试之前,当在一个方法上声明时,method mode 设置为
BEFORE_METHOD
。Java@DirtiesContext(methodMode = BEFORE_METHOD) (1) @Test void testProcessWhichRequiresFreshAppCtx() { // some logic that requires a new Spring container }
1 将当前测试方法之前的上下文(Context)破坏。 Kotlin@DirtiesContext(methodMode = BEFORE_METHOD) (1) @Test fun testProcessWhichRequiresFreshAppCtx() { // some logic that requires a new Spring container }
1 将当前测试方法之前的上下文(Context)破坏。 -
在当前测试之后,当在一个方法上声明时,method mode 设置为
AFTER_METHOD
(即,默认的 method mode)。Java@DirtiesContext (1) @Test void testProcessWhichDirtiesAppCtx() { // some logic that results in the Spring container being dirtied }
1 在当前的测试方法之后破坏上下文(Context)。 Kotlin@DirtiesContext (1) @Test fun testProcessWhichDirtiesAppCtx() { // some logic that results in the Spring container being dirtied }
1 在当前的测试方法之后破坏上下文(Context)。
如果你在一个测试中使用 @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 | 使用当前级别的算法。 |
@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
javadoc。
@TestExecutionListeners
@TestExecutionListeners
用于为一个特定的测试类、其子类和其嵌套类注册监听器。如果你想全局注册一个监听器,你应该通过 TestExecutionListener
配置 中描述的自动发现机制来注册它。
下面的例子显示了如何注册两个 TestExecutionListener
的实现:
@ContextConfiguration
@TestExecutionListeners({CustomTestExecutionListener.class, AnotherTestExecutionListener.class}) (1)
class CustomTestExecutionListenerTests {
// class body...
}
1 | 注册两个 TestExecutionListener 实现。 |
@ContextConfiguration
@TestExecutionListeners(CustomTestExecutionListener::class, AnotherTestExecutionListener::class) (1)
class CustomTestExecutionListenerTests {
// class body...
}
1 | 注册两个 TestExecutionListener 实现。 |
默认情况下,@TestExecutionListeners
提供了对从超类或包围类中继承监听器的支持。参见 @Nested
测试类的配置 和 @TestExecutionListeners
javadoc 以了解一个例子和进一步的细节。如果你发现你需要切换回使用默认的 TestExecutionListener
实现,请参阅 注册 TestExecutionListener
实现 中的说明。
@RecordApplicationEvents
@RecordApplicationEvents
是一个类级注解,用于指示Spring TestContext框架记录在执行单个测试期间在 ApplicationContext
中发布的所有应用程序事件。
记录的事件可以通过测试中的 ApplicationEvents
API访问。
参见 Application Events 和 @RecordApplicationEvents
javadoc 以了解一个例子和更多细节。
@Commit
@Commit
表示一个事务性测试方法的事务应该在测试方法完成后被提交。你可以用 @Commit
直接替代 @Rollback(false)
来更明确地传达代码的意图。与 @Rollback
类似,@Commit
也可以被声明为类级或方法级注解。
下面的例子展示了如何使用 @Commit
注解:
@Commit (1)
@Test
void testProcessWithoutRollback() {
// ...
}
1 | 将测试的结果提交到数据库。 |
@Commit (1)
@Test
fun testProcessWithoutRollback() {
// ...
}
1 | 将测试的结果提交到数据库。 |
@Rollback
@Rollback
表示在测试方法完成后,事务性测试方法的事务是否应该被回滚。如果为 true
,事务将被回滚。否则,事务被提交(参见 @Commit
)。即使 @Rollback
没有明确声明,Spring TestContext 框架中集成测试的回滚也默认为 true
。
当声明为类级注解时,@Rollback
定义了测试类层次结构中所有测试方法的默认回滚语义。当声明为方法级注解时,@Rollback
为特定的测试方法定义了回滚语义,可能会覆盖类级的 @Rollback
或 @Commit
语义。
下面的例子导致一个测试方法的结果不被回滚(也就是说,结果被提交到数据库):
@Rollback(false) (1)
@Test
void testProcessWithoutRollback() {
// ...
}
1 | 不要回滚结果。 |
@Rollback(false) (1)
@Test
fun testProcessWithoutRollback() {
// ...
}
1 | 不要回滚结果。 |
@BeforeTransaction
methods.
@BeforeTransaction
表明,对于那些通过使用Spring的 @Transactional
注解而被配置为在事务中运行的测试方法,注解的 void
方法应该在事务开始前运行。 @BeforeTransaction
方法不需要是 public
的,可以在基于Java 8的接口默认方法上声明。
下面的例子展示了如何使用 @BeforeTransaction
注解:
@BeforeTransaction (1)
void beforeTransaction() {
// logic to be run before a transaction is started
}
1 | 在事务前运行这个方法。 |
@BeforeTransaction (1)
fun beforeTransaction() {
// logic to be run before a transaction is started
}
1 | 在事务前运行这个方法。 |
@AfterTransaction
@AfterTransaction
表明,对于那些通过使用Spring的 @Transactional
注解而被配置为在事务中运行的测试方法,注解的 void
方法应在事务结束后运行。@AfterTransaction
方法不需要是 public
的,可以在基于Java 8的接口默认方法上声明。
@AfterTransaction (1)
void afterTransaction() {
// logic to be run after a transaction has ended
}
1 | 在一个事务之后运行这个方法。 |
@AfterTransaction (1)
fun afterTransaction() {
// logic to be run after a transaction has ended
}
1 | 在一个事务之后运行这个方法。 |
@Sql
@Sql
用于注解测试类或测试方法,以配置在集成测试期间针对给定数据库运行的SQL脚本。下面的例子显示了如何使用它:
@Test
@Sql({"/test-schema.sql", "/test-user-data.sql"}) (1)
void userTest() {
// run code that relies on the test schema and test data
}
1 | 为这个测试运行两个脚本。 |
@Test
@Sql("/test-schema.sql", "/test-user-data.sql") (1)
fun userTest() {
// run code that relies on the test schema and test data
}
1 | 为这个测试运行两个脚本。 |
更多细节请参见 用 @Sql 声明式地执行SQL脚本。
@SqlConfig
@SqlConfig
定义了元数据,用于确定如何解析和运行用 @Sql
注解配置的SQL脚本。下面的例子展示了如何使用它:
@Test
@Sql(
scripts = "/test-user-data.sql",
config = @SqlConfig(commentPrefix = "`", separator = "@@") (1)
)
void userTest() {
// run code that relies on the test data
}
1 | 在SQL脚本中设置注释前缀和分隔符。 |
@Test
@Sql("/test-user-data.sql", config = SqlConfig(commentPrefix = "`", separator = "@@")) (1)
fun userTest() {
// run code that relies on the test data
}
1 | 在SQL脚本中设置注释前缀和分隔符。 |
@SqlMergeMode
@SqlMergeMode
用于注解测试类或测试方法,以配置方法级 @Sql
声明是否与类级 @Sql
声明合并。如果 @SqlMergeMode
没有在测试类或测试方法上声明,默认情况下将使用 OVERRIDE
合并模式。在 OVERRIDE
模式下,方法级的 @Sql
声明将有效覆盖类级的 @Sql
声明。
注意,方法级的 @SqlMergeMode
声明会覆盖类级的声明。
下面的例子展示了如何在类的层面上使用 @SqlMergeMode
。
@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 mode)为 MERGE 。 |
@SpringJUnitConfig(TestConfig::class)
@Sql("/test-schema.sql")
@SqlMergeMode(MERGE) (1)
class UserTests {
@Test
@Sql("/user-test-data-001.sql")
fun standardUserProfile() {
// run code that relies on test data set 001
}
}
1 | 为类中的所有测试方法设置 @Sql 合并模式(merge mode)为 MERGE 。 |
下面的例子展示了如何在方法层面使用 @SqlMergeMode
。
@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 mode 式为 MERGE 。 |
@SpringJUnitConfig(TestConfig::class)
@Sql("/test-schema.sql")
class UserTests {
@Test
@Sql("/user-test-data-001.sql")
@SqlMergeMode(MERGE) (1)
fun standardUserProfile() {
// run code that relies on test data set 001
}
}
1 | 为一个特定的测试方法设置 @Sql merge mode 式为 MERGE 。 |
@SqlGroup
@SqlGroup
是一个容器注解,它聚合了几个 @Sql
注解。你可以原生地使用 @SqlGroup
来声明几个嵌套的 @Sql
注解,或者你可以结合Java 8对可重复注解的支持来使用它,在同一个类或方法上可以多次声明 @Sql
,隐含地生成这个容器注解。下面的例子展示了如何声明一个SQL group:
@Test
@SqlGroup({ (1)
@Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`")),
@Sql("/test-user-data.sql")
})
void userTest() {
// run code that uses the test schema and test data
}
1 | 声明一组SQL脚本。 |
@Test
@SqlGroup( (1)
Sql("/test-schema.sql", config = SqlConfig(commentPrefix = "`")),
Sql("/test-user-data.sql"))
fun userTest() {
// run code that uses the test schema and test data
}
1 | 声明一组SQL脚本。 |
9.1.3. Spring JUnit 4测试注解
以下注解只有在与 SpringRunner、Spring 的 JUnit4 rule 或 Spring的JUnit 4支持类 一起使用时才被支持:
@IfProfileValue
@IfProfileValue
表示注解的测试在特定的测试环境中被启用。如果配置的 ProfileValueSource
为所提供的 name
返回一个匹配的 value
,则测试被启用。否则,测试将被禁用,并且实际上被忽略。
你可以在类级、方法级或两者中应用 @IfProfileValue
。对于该类或其子类中的任何方法, @IfProfileValue
的类级使用优先于方法级使用。具体来说,如果一个测试在类级和方法级都被启用,那么它就是被启用的。没有 @IfProfileValue
意味着测试是隐式启用的。这类似于JUnit 4的 @Ignore
注解的语义,除了 @Ignore
的存在总是禁用一个测试。
下面的例子显示了一个有 @IfProfileValue
注解的测试:
@IfProfileValue(name="java.vendor", value="Oracle Corporation") (1)
@Test
public void testProcessWhichRunsOnlyOnOracleJvm() {
// some logic that should run only on Java VMs from Oracle Corporation
}
1 | 只有当Java供应商是 "Oracle Corporation" 时,才能运行这个测试。 |
@IfProfileValue(name="java.vendor", value="Oracle Corporation") (1)
@Test
fun testProcessWhichRunsOnlyOnOracleJvm() {
// some logic that should run only on Java VMs from Oracle Corporation
}
1 | 只有当Java供应商是 "Oracle Corporation" 时,才能运行这个测试。 |
另外,你可以用一个 values
列表来配置 @IfProfileValue
(具有 OR
语义),以便在JUnit 4环境中实现类似TestNG的测试组支持。考虑一下下面的例子:
@IfProfileValue(name="test-groups", values={"unit-tests", "integration-tests"}) (1)
@Test
public void testProcessWhichRunsForUnitOrIntegrationTestGroups() {
// some logic that should run only for unit and integration test groups
}
1 | 为单元测试和集成测试运行这个测试。 |
@IfProfileValue(name="test-groups", values=["unit-tests", "integration-tests"]) (1)
@Test
fun testProcessWhichRunsForUnitOrIntegrationTestGroups() {
// some logic that should run only for unit and integration test groups
}
1 | 为单元测试和集成测试运行这个测试。 |
@ProfileValueSourceConfiguration
@ProfileValueSourceConfiguration
是一个类级别的注解,它指定了在检索通过 @IfProfileValue
注解配置的配置文件值时要使用什么类型的 ProfileValueSource
。如果没有为测试声明 @ProfileValueSourceConfiguration
,则默认使用 SystemProfileValueSource
。下面的例子显示了如何使用 @ProfileValueSourceConfiguration
:
@ProfileValueSourceConfiguration(CustomProfileValueSource.class) (1)
public class CustomProfileValueSourceTests {
// class body...
}
1 | 使用一个自定义的 profile 值源。 |
@ProfileValueSourceConfiguration(CustomProfileValueSource::class) (1)
class CustomProfileValueSourceTests {
// class body...
}
1 | 使用一个自定义的 profile 值源。 |
@Timed
@Timed
表示注解的测试方法必须在指定的时间段(以毫秒为单位)内完成执行。如果文本的执行时间超过了指定的时间段,则测试失败。
这个时间段包括运行测试方法本身,任何重复的测试(见 @Repeat
),以及任何测试fixture的设置或拆除。下面的例子显示了如何使用它:
@Timed(millis = 1000) (1)
public void testProcessWithOneSecondTimeout() {
// some logic that should not take longer than 1 second to run
}
1 | 将测试的时间段设置为一秒。 |
@Timed(millis = 1000) (1)
fun testProcessWithOneSecondTimeout() {
// some logic that should not take longer than 1 second to run
}
1 | 将测试的时间段设置为一秒。 |
Spring的 @Timed
注解与JUnit 4的 @Test(timeout=…)
支持有着不同的语义。具体来说,由于JUnit 4处理测试执行超时的方式(即在一个单独的 Thread
中执行测试方法),@Test(timeout=…)
在测试时间过长的情况下会预先失败。另一方面,Spring的 @Timed
不会抢先让测试失败,而是等待测试完成后再失败。
@Repeat
@Repeat
表示注解的测试方法必须重复运行。测试方法的运行次数在注释中被指定。
重复执行的范围包括测试方法本身的执行,以及任何测试 fixture 的设置或拆除。当与 SpringMethodRule
一起使用时,该范围还包括 SpringMethodRule
实现对测试实例的准备。下面的例子展示了如何使用 @Repeat
注解:
@Repeat(10) (1)
@Test
public void testProcessRepeatedly() {
// ...
}
1 | 重复这个测试十次。 |
@Repeat(10) (1)
@Test
fun testProcessRepeatedly() {
// ...
}
1 | 重复这个测试十次。 |
9.1.4. Spring JUnit Jupiter 测试注解
当与 SpringExtension
和JUnit Jupiter(即JUnit 5中的编程模型)结合使用时,支持以下注解:
@SpringJUnitConfig
@SpringJUnitConfig
是一个组成注解,它结合了JUnit Jupiter的 @ExtendWith(SpringExtension.class)
和 Spring TestContext 框架的 @ContextConfiguration
。它可以作为 @ContextConfiguration
的替代品在类的层面上使用。关于配置选项,@ContextConfiguration
和 @SpringJUnitConfig
之间的唯一区别是,组件类可以用 @SpringJUnitConfig
中的 value
属性来声明。
下面的例子展示了如何使用 @SpringJUnitConfig
注解来指定一个配置类:
@SpringJUnitConfig(TestConfig.class) (1)
class ConfigurationClassJUnitJupiterSpringTests {
// class body...
}
1 | 指定配置类。 |
@SpringJUnitConfig(TestConfig::class) (1)
class ConfigurationClassJUnitJupiterSpringTests {
// class body...
}
1 | 指定配置类。 |
下面的例子显示了如何使用 @SpringJUnitConfig
注解来指定一个配置文件的位置:
@SpringJUnitConfig(locations = "/test-config.xml") (1)
class XmlJUnitJupiterSpringTests {
// class body...
}
1 | 指定一个配置文件的位置。 |
@SpringJUnitConfig(locations = ["/test-config.xml"]) (1)
class XmlJUnitJupiterSpringTests {
// class body...
}
1 | 指定一个配置文件的位置。 |
参见 Context 管理 以及 @SpringJUnitConfig
和 @ContextConfiguration
的 javadoc 以了解更多细节。
@SpringJUnitWebConfig
@SpringJUnitWebConfig
是一个组成注解,它将JUnit Jupiter中的 @ExtendWith(SpringExtension.class)
与Spring TestContext框架中的 @ContextConfiguration
和 @WebAppConfiguration
相结合。你可以在类的层面上使用它,作为 @ContextConfiguration
和 @WebAppConfiguration
的直接替代。关于配置选项,@ContextConfiguration
和 @SpringJUnitWebConfig
之间的唯一区别是,你可以通过使用 @SpringJUnitWebConfig
的 value
属性来声明组件类。此外,你只能通过使用 @SpringJUnitWebConfig
中的 resourcePath
属性来覆盖 @WebAppConfiguration
的 value
属性。
下面的例子展示了如何使用 @SpringJUnitWebConfig
注解来指定一个配置类:
@SpringJUnitWebConfig(TestConfig.class) (1)
class ConfigurationClassJUnitJupiterSpringWebTests {
// class body...
}
1 | 指定配置类。 |
@SpringJUnitWebConfig(TestConfig::class) (1)
class ConfigurationClassJUnitJupiterSpringWebTests {
// class body...
}
1 | 指定配置类。 |
下面的例子展示了如何使用 @SpringJUnitWebConfig
注解来指定一个配置文件的位置:
@SpringJUnitWebConfig(locations = "/test-config.xml") (1)
class XmlJUnitJupiterSpringWebTests {
// class body...
}
1 | 指定一个配置文件的位置。 |
@SpringJUnitWebConfig(locations = ["/test-config.xml"]) (1)
class XmlJUnitJupiterSpringWebTests {
// class body...
}
1 | 指定一个配置文件的位置。 |
参见 Context 管理 以及 @SpringJUnitWebConfig
, @ContextConfiguration
,以及 @WebAppConfiguration
了解更多细节。
@TestConstructor
@TestConstructor
是一个类型级别的注解,用于配置测试类构造器的参数如何从测试的 ApplicationContext
中的组件自动装配。
如果 @TestConstructor
在一个测试类上不存在或元存在,将使用默认的测试构造器自动装配模式。关于如何改变默认模式的细节,见下面的提示。然而,请注意,构造函数上的 @Autowired
的局部声明优先于 @TestConstructor
和默认模式。
改变默认的测试构造器自动装配模式
默认的测试构造器自动装配模式可以通过将 从Spring Framework 5.3 开始,默认模式也可以被配置为 JUnit平台配置参数。 如果 |
从Spring框架 5.2 开始,@TestConstructor 只支持与 SpringExtension 一起使用JUnit Jupiter。请注意,SpringExtension 通常会自动为你注册—例如,在使用 @SpringJUnitConfig 和 @SpringJUnitWebConfig 等注解或Spring Boot Test的各种测试相关注解时。
|
@NestedTestConfiguration
@NestedTestConfiguration
是一个类型级别的注解,用于配置Spring测试配置注解在内部测试类的包围类层次结构中的处理方式。
如果 @NestedTestConfiguration
不存在或元存在于测试类、其超类型层次结构或其包围类层次结构中,将使用默认的包围配置继承模式。关于如何改变默认模式的细节,见下面的提示。
改变默认的包围式配置继承模式
默认的包围配置继承模式是 |
Spring TestContext 框架 对以下注解尊重 @NestedTestConfiguration
语义。
@NestedTestConfiguration 的使用通常只对JUnit Jupiter中的 @Nested 测试类有意义;但是,可能有其他支持Spring和嵌套测试类的测试框架会使用这个注解。
|
见 @Nested
测试类的配置 中的例子和进一步的细节。
@EnabledIf
@EnabledIf
用于发出信号,表明注解的JUnit Jupiter测试类或测试方法被启用,并且如果提供的表达式评估为 true
,就应该运行。具体来说,如果 expression
评估为 Boolean.TRUE
或一个等于 true
的 String
串(忽略大小写),则测试被启用。当应用在类的层面上时,该类中的所有测试方法默认也会自动启用。
表达式可以是以下任何一种:
-
Spring 表达式语言(SpEL)表达式。比如说:
@EnabledIf("#{systemProperties['os.name'].toLowerCase().contains('mac')
}") -
Spring
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 {}
@Target(AnnotationTarget.TYPE, AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@EnabledIf(
expression = "#{systemProperties['os.name'].toLowerCase().contains('mac')}",
reason = "Enabled on Mac OS"
)
annotation class EnabledOnMac {}
|
从JUnit 5.7 开始,JUnit Jupiter也有一个名为 |
@DisabledIf
@DisabledIf
用于发出信号,如果提供的 expression
评估为 true
,则注解的JUnit Jupiter测试类或测试方法被禁用,不应运行。具体来说,如果 expression
评估为 Boolean.TRUE
或一个等于 true
的 String
(忽略大小写),则测试被禁用。当在类的层面上应用时,该类中的所有测试方法也会自动禁用。
表达式可以是以下任何一种:
-
Spring 表达式语言(SpEL)表达式。比如说:
@DisabledIf("#{systemProperties['os.name'].toLowerCase().contains('mac')}")
-
Spring
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 {}
@Target(AnnotationTarget.TYPE, AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@DisabledIf(
expression = "#{systemProperties['os.name'].toLowerCase().contains('mac')}",
reason = "Disabled on Mac OS"
)
annotation class DisabledOnMac {}
|
从JUnit 5.7 开始,JUnit Jupiter也有一个名为 |
9.1.5. 对测试的元注解支持
你可以使用大多数与测试相关的注解作为 元注解 来创建自定义的组成注解,并减少整个测试套件的配置重复。
你可以结合 TestContext框架 使用以下每个元注解。
-
@BootstrapWith
-
@ContextConfiguration
-
@ContextHierarchy
-
@ActiveProfiles
-
@TestPropertySource
-
@DirtiesContext
-
@WebAppConfiguration
-
@TestExecutionListeners
-
@Transactional
-
@BeforeTransaction
-
@AfterTransaction
-
@Commit
-
@Rollback
-
@Sql
-
@SqlConfig
-
@SqlMergeMode
-
@SqlGroup
-
@Repeat
(只在JUnit 4上支持) -
@Timed
(只在JUnit 4上支持) -
@IfProfileValue
(只在JUnit 4上支持) -
@ProfileValueSourceConfiguration
(只在JUnit 4上支持) -
@SpringJUnitConfig
(只在JUnit Jupiter上支持) -
@SpringJUnitWebConfig
(只在JUnit Jupiter上支持) -
@TestConstructor
(只在JUnit Jupiter上支持) -
@NestedTestConfiguration
(只在JUnit Jupiter上支持) -
@EnabledIf
(只在JUnit Jupiter上支持) -
@DisabledIf
(只在JUnit Jupiter上支持)
请考虑以下例子:
@RunWith(SpringRunner.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
public class OrderRepositoryTests { }
@RunWith(SpringRunner.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
public class UserRepositoryTests { }
@RunWith(SpringRunner::class)
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
@ActiveProfiles("dev")
@Transactional
class OrderRepositoryTests { }
@RunWith(SpringRunner::class)
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
@ActiveProfiles("dev")
@Transactional
class UserRepositoryTests { }
如果我们发现我们在整个基于JUnit 4的测试套件中重复前面的配置,我们可以通过引入一个自定义的组成注解来减少重复,该注解集中了Spring的通用测试配置,如下所示:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
public @interface TransactionalDevTestConfig { }
@Target(AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
@ActiveProfiles("dev")
@Transactional
annotation class TransactionalDevTestConfig { }
然后我们可以使用我们自定义的 @TransactionalDevTestConfig
注解来简化基于JUnit 4的各个测试类的配置,如下所示:
@RunWith(SpringRunner.class)
@TransactionalDevTestConfig
public class OrderRepositoryTests { }
@RunWith(SpringRunner.class)
@TransactionalDevTestConfig
public class UserRepositoryTests { }
@RunWith(SpringRunner::class)
@TransactionalDevTestConfig
class OrderRepositoryTests
@RunWith(SpringRunner::class)
@TransactionalDevTestConfig
class UserRepositoryTests
如果我们编写使用JUnit Jupiter的测试,我们可以进一步减少代码重复,因为JUnit 5中的注解也可以作为元注解使用。考虑一下下面的例子:
@ExtendWith(SpringExtension.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
class OrderRepositoryTests { }
@ExtendWith(SpringExtension.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
class UserRepositoryTests { }
@ExtendWith(SpringExtension::class)
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
@ActiveProfiles("dev")
@Transactional
class OrderRepositoryTests { }
@ExtendWith(SpringExtension::class)
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
@ActiveProfiles("dev")
@Transactional
class UserRepositoryTests { }
如果我们发现我们在基于JUnit Jupiter的测试套件中重复前面的配置,我们可以通过引入一个自定义的组成注解来减少重复,该注解集中了Spring和JUnit Jupiter的通用测试配置,如下所示:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(SpringExtension.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
public @interface TransactionalDevTestConfig { }
@Target(AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@ExtendWith(SpringExtension::class)
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
@ActiveProfiles("dev")
@Transactional
annotation class TransactionalDevTestConfig { }
然后我们可以使用我们自定义的 @TransactionalDevTestConfig
注解来简化基于JUnit Jupiter的各个测试类的配置,如下所示:
@TransactionalDevTestConfig
class OrderRepositoryTests { }
@TransactionalDevTestConfig
class UserRepositoryTests { }
@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 { }
@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() { }
@TransactionalIntegrationTest
fun saveOrder() { }
@TransactionalIntegrationTest
fun deleteOrder() { }
更多细节,请参见 Spring注解编程模型 wiki页面。
9.2. 更多资源
有关测试的更多信息,请参见以下资源:
-
JUnit: "一个对程序员友好的Java和JVM的测试框架"。由Spring框架在其测试套件中使用,并在 Spring TestContext Framework 中得到支持。
-
TestNG: 一个受JUnit启发的测试框架,增加了对测试组、数据驱动测试、分布式测试和其他功能的支持。在 Spring TestContext Framework 中支持
-
AssertJ: "Java的 fluent 断言",包括对Java 8 lambdas、流(streams)和许多其他功能的支持。
-
Mock Objects: 维基百科中的文章。
-
MockObjects.com: 专门介绍mock对象的网站,这是一种在测试驱动开发中改进代码设计的技术。
-
Mockito: 基于 Test Spypattern 的Java mock 库。由Spring框架在其测试套件中使用。
-
EasyMock: Java库 "通过使用Java的代理机制即时生成接口(和通过类扩展的对象),为其提供Mock对象"。
-
JMock: 支持用mock对象对Java代码进行测试驱动开发的库。
-
DbUnit: JUnit扩展(也可用于Ant和Maven),针对数据库驱动的项目,除其他外,在测试运行之间将你的数据库置于已知状态。
-
Testcontainers: 支持JUnit测试的Java库,为普通数据库、Selenium web browser或其他可以在Docker容器中运行的东西提供轻量级的、可抛弃的实例。
-
The Grinder: Java负载测试框架。
-
SpringMockK: 支持用Kotlin编写的Spring Boot集成测试,使用 MockK 而不是Mockito。