本章介绍了Spring对集成测试的支持以及单元测试的最佳实践。Spring团队倡导测试驱动开发(TDD)。Spring团队发现,控制反转(IoC)的正确使用确实会使单元测试和集成测试变得更容易(因为类上存在setter方法和适当的构造函数,使得它们更容易在测试中连接在一起,而不必设置服务定位器注册表和类似的结构)。
1. Introduction to Spring Testing
测试是企业软件开发不可或缺的一部分。本章重点讨论IoC原则对单元测试的增值作用,以及Spring框架支持集成测试的好处。(彻底处理企业中的测试超出了本参考手册的范围。)
2. Unit Testing
与传统的J2EE/Java EE开发相比,依赖项注入应该会减少代码对容器的依赖。组成您的应用程序的POJO应该在JUnit或TestNG测试中是可测试的,使用new
操作符实例化对象,而不使用Spring或任何其他容器。您可以使用模拟对象(与其他有价值的测试技术结合使用)来隔离测试代码。如果您遵循了Spring的体系结构建议,那么代码库的干净分层和组件化将有助于更轻松地进行单元测试。例如,您可以通过清除或模拟DAO或存储库接口来测试服务层对象,而不需要在运行单元测试时访问持久数据。
真正的单元测试通常运行得非常快,因为不需要设置运行时基础设施。强调将真正的单元测试作为开发方法的一部分可以提高您的工作效率。您可能不需要测试章节的这一节来帮助您为基于IOC的应用程序编写有效的单元测试。然而,对于某些单元测试场景,Spring框架提供了模拟对象和测试支持类,本章将对此进行描述。
2.1. Mock Objects
Spring包含许多专门用于模拟的包:
2.1.1. Environment
org.springmework.mock.env
包包含Environment
和PropertySource
抽象的模拟实现(请参阅Bean定义概要和PropertySource
抽象)。MockEnvironment
和MockPropertySource
对于开发依赖于环境特定属性的代码的容器外测试非常有用。
2.1.2. JNDI
org.springfrawork.mock.jndi
包包含JNDI SPI的部分实现,您可以使用该实现为测试套件或独立应用程序设置简单的JNDI环境。例如,如果JDBCDataSource
实例在测试代码中绑定到与在Jakarta EE容器中相同的JNDI名称,则无需修改即可在测试场景中重用应用程序代码和配置。
The mock JNDI support in the org.springframework.mock.jndi package is officially deprecated as of Spring Framework 5.2 in favor of complete solutions from third parties such as Simple-JNDI. |
2.1.3. Servlet API
org.springfrawork.mock.web
包包含一组全面的Servlet API模拟对象,这些对象对测试Web上下文、控制器和过滤器很有用。这些模拟对象是针对Spring的Web MVC框架使用的,通常比动态模拟对象(如EasyMock)或替代的Servlet API模拟对象(如模拟对象)更易于使用。
Since Spring Framework 6.0, the mock objects in org.springframework.mock.web are based on the Servlet 6.0 API. |
Spring MVC测试框架构建在模拟Servlet API对象之上,为Spring MVC提供集成测试框架。请参阅MockMvc。
2.1.4. Spring Web Reactive
org.springframework.mock.http.server.reactive
包包含在WebFlux应用程序中使用的ServerHttpRequest
和ServerHttpResponse
的模拟实现。org.springframework.mock.web.server
包包含一个依赖于这些模拟请求和响应对象的模拟<代码>ServerWebExchange
。
MockServerHttpRequest
和MockServerHttpResponse
都是从与特定于服务器的实现相同的抽象基类扩展而来,并与它们共享行为。例如,模拟请求一旦创建就是不可变的,但是您可以使用ServerHttpRequest
中的amplate()
方法来创建修改后的实例。
为了让模拟响应正确地实现写约定并返回写完成句柄(即mono<;void>;
),它默认使用Flux
和cache().Then()
,这将缓冲数据并使其可用于测试中的断言。应用程序可以设置自定义写入函数(例如,测试无限流)。
WebTestClient构建在模拟请求和响应的基础上,为在没有HTTP服务器的情况下测试WebFlux应用程序提供支持。该客户端还可以用于与正在运行的服务器进行端到端测试。
2.2. Unit Testing Support Classes
Spring包含许多可以帮助进行单元测试的类。它们分为两类:
2.2.1. General Testing Utilities
org.springfrawork.test.util
包包含几个用于单元测试和集成测试的通用实用程序。
AopTestUtils
是与AOP相关的实用工具方法的集合。您可以使用这些方法来获取对隐藏在一个或多个Spring代理后面的底层目标对象的引用。例如,如果您使用EasyMock或Mockito等库将Bean配置为动态模拟,并且该模拟包装在Spring代理中,则可能需要直接访问底层模拟以配置对其的预期并执行验证。有关Spring的核心AOP实用程序,请参阅AopUtils
和AopProxyUtils
。
ReflectionTestUtils
是基于反射的实用工具方法的集合。在测试以下用例的应用程序代码时,您可以在需要更改常量的值、设置非公共
字段、调用非公共
setter方法、或调用非公共
配置或生命周期回调方法的测试场景中使用这些方法:
-
允许
私有
或受保护的
字段访问的ORM框架(如JPA和Hibernate),而不是域实体中属性的公共
setter方法。 -
Spring对批注(如
@AuTower
、@Inject
和@Resource
)支持,为私有
或受保护的
字段、setter方法和配置方法提供依赖项注入。 -
生命周期回调方法使用
@PostConstruct
和@PreDestroy
等注释。
TestSocketUtils
是一个简单的实用程序,用于在本地主机
上查找可用于集成测试场景的可用TCP端口。
|
2.2.2. Spring MVC Testing Utilities
org.springmework.test.web
包包含ModelAndViewAssert
,您可以将其与JUnit、TestNG或处理Spring MVCModelAndView
对象的单元测试的任何其他测试框架结合使用。
Unit testing Spring MVC Controllers
To unit test your Spring MVC Controller classes as POJOs, use ModelAndViewAssert combined with MockHttpServletRequest , MockHttpSession , and so on from Spring’s Servlet API mocks. For thorough integration testing of your Spring MVC and REST Controller classes in conjunction with your WebApplicationContext configuration for Spring MVC, use the Spring MVC Test Framework instead. |
3. Integration Testing
这一节(本章的大部分剩余部分)介绍了Spring应用程序的集成测试。它包括以下主题:
3.1. Overview
能够执行一些集成测试而不需要部署到应用程序服务器或连接到其他企业基础设施,这一点很重要。这样做可以让您测试以下内容:
-
正确连接您的Spring IOC容器上下文。
-
使用JDBC或ORM工具进行数据访问。这可能包括SQL语句、Hibernate查询、JPA实体映射等的正确性。
Spring框架在Spring-test
模块中为集成测试提供了一流的支持。实际JAR文件的名称可能包括发布版本,也可能是长的org.springFrawork.test
形式,这取决于您从哪里获得它(有关说明,请参阅依赖项管理的部分)。该库包括org.springFrawork.test
包,其中包含用于与Spring容器进行集成测试的有价值的类。此测试不依赖于应用程序服务器或其他部署环境。这类测试的运行速度比单元测试慢,但比等价的Selify测试或依赖于部署到应用程序服务器的远程测试快得多。
单元和集成测试支持以注释驱动的Spring TestContext框架的形式提供。TestContext框架与实际使用的测试框架无关,它允许在各种环境中插入测试工具,包括JUnit、TestNG和其他环境。
3.2. Goals of Integration Testing
Spring的集成测试支持有以下主要目标:
-
在测试之间管理Spring IOC容器缓存。
-
提供测试装置实例的依赖项注入。
-
提供适用于集成测试的事务管理。
-
提供特定于Spring的基类,帮助开发人员编写集成测试。
接下来的几个部分描述了每个目标,并提供了指向实施和配置详细信息的链接。
3.2.1. Context Management and Caching
Spring TestContext框架提供了对SpringApplicationContext
实例和WebApplicationContext
实例的一致加载以及对这些上下文的缓存。支持缓存加载的上下文很重要,因为启动时可能会成为 - 的问题,这不是因为Spring本身的开销,而是因为由Spring容器实例化的对象需要时间来实例化。例如,包含50到100个Hibernate映射文件的项目可能需要10到20秒来加载映射文件,而在每个测试夹具中运行每个测试之前产生的成本会导致整体测试运行速度变慢,从而降低开发人员的工作效率。
测试类通常声明用于xml或Groovy配置元数据 - 的资源位置数组,通常在类路径 - 中,或者声明用于配置应用程序的组件类数组。这些位置或类与生产部署的web.xml
或其他配置文件中指定的位置或类相同或相似。
默认情况下,加载后,配置的ApplicationContext
将在每次测试中重复使用。因此,每个测试套件只产生一次设置成本,并且后续测试执行速度要快得多。在这种情况下,术语“测试套件”指的是运行在同一JVMGradle- - 中的所有测试。例如,对于给定的项目或模块,所有测试都从Ant、Maven或Gradle版本运行。在测试损坏应用程序上下文并需要重新加载(例如,通过修改应用程序对象的Bean定义或状态)的不太可能的情况下,可以将TestContext框架配置为在执行下一个测试之前重新加载配置并重建应用程序上下文。
3.2.2. Dependency Injection of Test Fixtures
当TestContext框架加载您的应用程序上下文时,它可以选择使用依赖项注入来配置测试类的实例。这为通过使用应用程序上下文中的预配置Bean来设置测试夹具提供了一种方便的机制。这里的一个强大好处是,您可以跨各种测试场景重用应用程序上下文(例如,用于配置Spring管理的对象图、事务代理、DataSource
实例等),从而避免为单个测试用例重复复杂的测试装置设置。
例如,考虑一个场景,其中我们有一个实现标题
域实体的数据访问逻辑的类(HibernateTitleRepository
)。我们想要编写测试以下方面的集成测试:
-
Spring配置:基本上,与
HibernateTitleRepository
Bean的配置相关的一切都正确且存在吗? -
Hibernate映射文件配置:映射是否正确,延迟加载设置是否正确?
-
HibernateTitleRepository
的逻辑:此类的已配置实例是否按预期执行?
请参阅使用TestContext框架注入测试装置的依赖项。
3.2.3. Transaction Management
访问实际数据库的测试中的一个常见问题是它们对持久性存储的状态的影响。即使您使用开发数据库,对状态的更改也可能会影响未来的测试。此外,许多 - 操作,如插入或修改持久性数据 - ,不能在事务之外执行(或验证)。
TestContext框架解决了这个问题。默认情况下,框架为每个测试创建并回滚一个事务。您可以编写可以假定存在事务的代码。如果在测试中调用事务性代理对象,则根据其配置的事务性语义,它们的行为是正确的。此外,如果测试方法在为测试管理的事务内运行时删除所选表的内容,则默认情况下,事务将回滚,并且数据库将返回到执行测试之前的状态。通过使用测试的应用程序上下文中定义的PlatformTransactionManager
Bean为测试提供事务支持。
如果您希望提交事务(不常见,但有时在您希望特定测试填充或修改数据库时很有用),您可以通过使用@Commit
注释告诉TestContext框架使事务提交而不是回滚。
请参阅使用TestContext框架的事务管理。
3.2.4. Support Classes for Integration Testing
Spring TestContext框架提供了几个抽象
支持类,它们简化了集成测试的编写。这些基本测试类提供了到测试框架的定义良好的挂钩,以及方便的实例变量和方法,使您可以访问:
-
ApplicationContext
,用于执行显式的Bean查找或测试整个上下文的状态。 -
JdbcTemplate
,用于执行查询数据库的SQL语句。在执行与数据库相关的应用程序代码之前和之后,您都可以使用这样的查询来确认数据库状态,而Spring确保这样的查询在与应用程序代码相同的事务范围内运行。当与ORM工具结合使用时,务必避免误报。
此外,您可能希望使用特定于您的项目的实例变量和方法创建您自己的自定义、应用程序范围的超类。
请参阅TestContext框架的支持类。
3.3. JDBC Testing Support
org.springfrawork.test.jdbc
包包含JdbcTestUtils
,它是一组与JDBC相关的实用函数,旨在简化标准数据库测试场景。具体地说,JdbcTestUtils
提供了以下静态实用工具方法。
-
CountRowsInTable(..)
:统计给定表中的行数。 -
CountRowsInTableWhere(..)
:使用提供的WHERE
子句统计给定表中的行数。 -
deleteFromTables(..)
:从指定表中删除所有行。 -
deleteFromTableWhere(..)
:使用所提供的where
子句从给定表中删除行。 -
dropTables(..)
:删除指定表。
|
3.4. Annotations
本节介绍了在测试Spring应用程序时可以使用的注释。它包括以下主题:
3.4.1. Spring Testing Annotations
Spring框架提供了以下一组特定于Spring的注释,您可以在单元测试和集成测试中与TestContext框架结合使用。有关更多信息,包括默认属性值、属性别名和其他详细信息,请参阅相应的javadoc。
Spring的测试注释包括以下内容:
@BootstrapWith
@BootstrapWith
是一个类级批注,您可以使用它来配置如何引导Spring TestContext框架。具体地说,您可以使用@BootstRapWith
指定一个定制的TestContextBootstRapper
。有关更多详细信息,请参阅引导TestContext框架一节。
@ContextConfiguration
@ConextConfiguration
定义类级别的元数据,用于确定如何为集成测试加载和配置ApplicationContext
。具体地说,@ConextConfiguration
声明了用于加载上下文的应用程序上下文资源位置
或组件类
。
资源位置通常是位于类路径中的XML配置文件或Groovy脚本,而组件类通常是@configuration
类。但是,资源位置也可以引用文件系统中的文件和脚本,组件类可以是@Component
类、@Service
类等。有关详细信息,请参阅组件类。
下面的示例显示了引用XML文件的@ConextConfiguration
批注:
@ContextConfiguration("/test-config.xml") (1)
class XmlApplicationContextTests {
// class body...
}
1 | Referring to an XML file. |
下面的示例显示了引用类的@ConextConfiguration
批注:
@ContextConfiguration(classes = TestConfig.class) (1)
class ConfigClassApplicationContextTests {
// class body...
}
1 | Referring to a class. |
作为替代方法或除了声明资源位置或组件类之外,您还可以使用@ContextConfiguration
声明ApplicationContextInitializer
类。以下示例显示了这样的情况:
@ContextConfiguration(initializers = CustomContextInitializer.class) (1)
class ContextInitializerTests {
// class body...
}
1 | Declaring an initializer class. |
您也可以选择使用@ConextConfiguration
来声明ConextLoader
策略。但是请注意,您通常不需要显式配置加载器,因为默认加载器支持初始值设定项
和资源位置
或组件类
。
以下示例同时使用位置和加载器:
@ContextConfiguration(locations = "/test-context.xml", loader = CustomContextLoader.class) (1)
class CustomLoaderXmlApplicationContextTests {
// class body...
}
1 | Configuring both a location and a custom loader. |
@ContextConfiguration provides support for inheriting resource locations or configuration classes as well as context initializers that are declared by superclasses or enclosing classes. |
有关更多详细信息,请参阅上下文管理、@nesteed
测试类配置和@ConextConfiguration
javados。
@WebAppConfiguration
@WebAppConfiguration
是一个类级批注,可用于声明为集成测试加载的ApplicationContext
应为WebApplicationContext
。仅在测试类上存在@WebAppConfiguration
就可以确保为测试加载WebApplicationContext
,并使用指向Web应用程序根的路径(即资源库路径)的默认值“file:src/main/webapp”
。资源基本路径在后台用于创建MockServletContext
,它用作测试的WebApplicationContext
的ServletContext
。
以下示例显示如何使用@WebAppConfiguration
批注:
@ContextConfiguration
@WebAppConfiguration (1)
class WebAppTests {
// class body...
}
1 | The @WebAppConfiguration annotation. |
要覆盖缺省值,可以使用隐式值
属性指定不同的基本资源路径。同时支持classpath:
和file:
资源前缀。如果未提供资源前缀,则假定该路径为文件系统资源。以下示例显示如何指定类路径资源:
@ContextConfiguration
@WebAppConfiguration("classpath:test-web-resources") (1)
class WebAppTests {
// class body...
}
1 | Specifying a classpath resource. |
请注意,@WebAppConfiguration
必须与@ConextConfiguration
一起使用,无论是在单个测试类中还是在测试类层次结构中。有关更多详细信息,请参阅@WebAppConfiguration
javadoc。
@ContextHierarchy
@ContextHierarchy
是一个类级批注,用于定义集成测试的ApplicationContext
实例的层次结构。@ConextHierarchy
应该用一个或多个@ConextConfiguration
实例的列表声明,每个实例都定义了上下文层次结构中的一个级别。以下示例演示了@ConextHierarchy
在单个测试类中的使用(@ConextHierarchy
也可以在测试类层次结构中使用):
@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...
}
如果您需要合并或覆盖测试类层次结构中给定的上下文层次结构级别的配置,则必须通过向类层次结构中每个相应级别的@ConextConfiguration
中的name
属性提供相同的值来显式命名该级别。有关更多示例,请参阅上下文层次结构和@ConextHierarchy
javadoc。
@ActiveProfiles
@ActiveProfiles
是一个类级批注,用于声明在为集成测试加载ApplicationContext
时,哪些Bean定义概要文件应该是活动的。
以下示例指示dev
配置文件应处于活动状态:
@ContextConfiguration
@ActiveProfiles("dev") (1)
class DeveloperTests {
// class body...
}
1 | Indicate that the dev profile should be active. |
以下示例指示dev
和集成
配置文件都应处于活动状态:
@ContextConfiguration
@ActiveProfiles({"dev", "integration"}) (1)
class DeveloperIntegrationTests {
// class body...
}
1 | Indicate that the dev and integration profiles should be active. |
@ActiveProfiles provides support for inheriting active bean definition profiles declared by superclasses and enclosing classes by default. You can also resolve active bean definition profiles programmatically by implementing a custom ActiveProfilesResolver and registering it by using the resolver attribute of @ActiveProfiles . |
有关示例和更多详细信息,请参阅环境概要文件的上下文配置、@nesteed
测试类配置和@ActiveProfilesjavadoc。
@TestPropertySource
@TestPropertySource
是一个类级批注,您可以使用它来配置属性文件和内联属性的位置,这些属性文件和内联属性将添加到Environment
中为集成测试加载的ApplicationContext
的PropertySources
集中。
下面的示例演示如何从类路径声明属性文件:
@ContextConfiguration
@TestPropertySource("/test.properties") (1)
class MyIntegrationTests {
// class body...
}
1 | Get properties from test.properties in the root of the classpath. |
下面的示例演示如何声明内联属性:
@ContextConfiguration
@TestPropertySource(properties = { "timezone = GMT", "port: 4242" }) (1)
class MyIntegrationTests {
// class body...
}
1 | Declare timezone and port properties. |
有关示例和更多详细信息,请参阅使用测试属性源的上下文配置。
@DynamicPropertySource
@DynamicPropertySource
是方法级批注,可用于注册动态属性以添加到Environment
中为集成测试加载的ApplicationContext
的PropertySources
集中。当您事先不知道属性的值时,动态属性非常有用-例如,如果属性由外部资源管理,例如由Testtainers项目管理的容器。
下面的示例演示如何注册动态属性:
@ContextConfiguration
class MyIntegrationTests {
static MyExternalServer server = // ...
@DynamicPropertySource (1)
static void dynamicProperties(DynamicPropertyRegistry registry) { (2)
registry.add("server.port", server::getPort); (3)
}
// tests ...
}
1 | Annotate a static method with @DynamicPropertySource . |
2 | Accept a DynamicPropertyRegistry as an argument. |
3 | Register a dynamic server.port property to be retrieved lazily from the server. |
有关详细信息,请参阅动态属性源上下文配置。
@DirtiesContext
@DirtiesContext
表示底层的SpringApplicationContext
在测试执行期间已被破坏(即,测试以某种方式 - 修改或损坏了它,例如,通过更改Singleton Bean的状态),应该关闭。当应用程序上下文被标记为脏时,它将从测试框架的缓存中移除并关闭。因此,对于需要具有相同配置元数据的上下文的任何后续测试,都会重新构建底层的Spring容器。
您可以将@DirtiesContext
用作同一个类或类层次结构中的类级和方法级批注。在这种情况下,ApplicationContext
在任何此类带注释的方法之前或之后以及当前测试类之前或之后被标记为脏,这取决于配置的方法模式
和类模式
。
以下示例说明了在各种配置场景中,环境何时会被破坏:
-
在当前测试类之前,当在类模式设置为
BEFORE_CLASS
的类上声明时。JavaKotlin@DirtiesContext(classMode = BEFORE_CLASS) (1) class FreshContextTests { // some tests that require a new Spring container }
1 Dirty the context before the current test class. -
在当前测试类之后,当在类模式设置为
After_CLASS
(即,默认类模式)的类上声明时。JavaKotlin@DirtiesContext (1) class ContextDirtyingTests { // some tests that result in the Spring container being dirtied }
1 Dirty the context after the current test class. -
在当前测试类中的每个测试方法之前,当在类模式设置为
BEVER_EACH_TEST_METHOD的类上声明时。
JavaKotlin@DirtiesContext(classMode = BEFORE_EACH_TEST_METHOD) (1) class FreshContextTests { // some tests that require a new Spring container }
1 Dirty the context before each test method. -
在当前测试类中的每个测试方法之后,当在类模式设置为
After_each_test_method的类上声明时。
JavaKotlin@DirtiesContext(classMode = AFTER_EACH_TEST_METHOD) (1) class ContextDirtyingTests { // some tests that result in the Spring container being dirtied }
1 Dirty the context after each test method. -
在当前测试之前,当在方法模式设置为
BEFORE_METHOD
的方法上声明时。JavaKotlin@DirtiesContext(methodMode = BEFORE_METHOD) (1) @Test void testProcessWhichRequiresFreshAppCtx() { // some logic that requires a new Spring container }
1 Dirty the context before the current test method. -
在当前测试之后,在方法模式设置为
After_METHOD
(即,默认方法模式)的方法上声明时。JavaKotlin@DirtiesContext (1) @Test void testProcessWhichDirtiesAppCtx() { // some logic that results in the Spring container being dirtied }
1 Dirty the context after the current test method.
如果您在测试中使用@DirtiesContext
,而该测试的上下文被配置为使用@ConextHierarchy
的上下文层次结构的一部分,则可以使用HierarchyMode
标志来控制如何清除上下文缓存。默认情况下,使用穷举算法来清除上下文缓存,不仅包括当前级别,还包括共享当前测试共有的祖先上下文的所有其他上下文层次结构。驻留在公共祖先上下文子层次结构中的所有ApplicationContext
实例都从上下文缓存中移除并关闭。如果穷举算法对于特定用例来说过于苛刻,您可以指定更简单的Current Level算法,如下例所示。
@ContextHierarchy({ @ContextConfiguration("/parent-config.xml"), @ContextConfiguration("/child-config.xml") })
class BaseTests {
// class body...
}
class ExtendedTests extends BaseTests {
@Test
@DirtiesContext(hierarchyMode = CURRENT_LEVEL) (1)
void test() {
// some logic that results in the child context being dirtied
}
}
1 | Use the current-level algorithm. |
有关<代码>穷举
和<代码>CURRENT_LEVEL
算法的更多详细信息,请参阅DirtiesContext.HierarchyMode
javadoc。
@TestExecutionListeners
@TestExecutionListeners
用于注册特定测试类、其子类及其嵌套类的侦听器。如果您希望全局注册侦听器,则应该通过TestExecutionListener
配置中描述的自动发现机制进行注册。
以下示例显示如何注册两个TestExecutionListener
实现:
@ContextConfiguration
@TestExecutionListeners({CustomTestExecutionListener.class, AnotherTestExecutionListener.class}) (1)
class CustomTestExecutionListenerTests {
// class body...
}
1 | Register two TestExecutionListener implementations. |
默认情况下,@TestExecutionListeners
支持从超类或封闭类继承侦听器。有关示例和更多详细信息,请参阅@nested
测试类配置和@TestExecutionListenersjavadoc。如果您发现需要切换回使用默认的TestExecutionListener
实现,请参阅注册TestExecutionListener
实现中的说明。
@RecordApplicationEvents
@RecordApplicationEvents
是一个类级别的批注,用于指示Spring TestContext框架记录单个测试执行期间在ApplicationContext
中发布的所有应用程序事件。
可以通过测试内部的ApplicationEvents
接口访问录制的事件。
有关示例和更多详细信息,请参阅应用程序事件和@RecordApplicationEvents
javadoc。
@Commit
@Commit
表示事务性测试方法的事务应该在测试方法完成后提交。您可以使用@Commit
作为@Rollback(FALSE)
的直接替代,以更明确地传达代码的意图。与@Rollback
类似,@Commit
也可以声明为类级别或方法级别的批注。
下面的示例显示如何使用@Commit
批注:
@Commit (1)
@Test
void testProcessWithoutRollback() {
// ...
}
1 | Commit the result of the test to the database. |
@Rollback
@Rollback
事务性测试方法的事务是否应该在测试方法完成后回滚。如果为True
,则回滚事务。否则,事务被提交(另请参见@Commit
)。在Spring TestContext框架中,集成测试的回滚默认为true
,即使没有显式声明@Rollback
。
当声明为类级注释时,@Rollback
为测试类层次结构中的所有测试方法定义了默认的回滚语义。当声明为方法级批注时,@Rollback
定义特定测试方法的回滚语义,可能会覆盖类级@Rollback
或@Commit
语义。
下面的示例导致测试方法的结果不会回滚(即,结果提交到数据库):
@Rollback(false) (1)
@Test
void testProcessWithoutRollback() {
// ...
}
1 | Do not roll back the result. |
@BeforeTransaction
@BeForeTransaction
表示,对于已使用Spring的@Transaction
注释配置为在事务内运行的测试方法,应在启动事务之前运行带注释的void
方法。@BeForeTransaction
方法不要求为公共
,可以在基于Java 8的接口默认方法上声明。
下面的示例显示如何使用@BeForeTransaction
批注:
@BeforeTransaction (1)
void beforeTransaction() {
// logic to be run before a transaction is started
}
1 | Run this method before a transaction. |
@AfterTransaction
@AfterTransaction
表示,对于已使用Spring的@Transaction
注释配置为在事务内运行的测试方法,应在事务结束后运行带注释的void
方法。@AfterTransaction
方法不要求为公共
,可以在基于Java 8的接口默认方法上声明。
@AfterTransaction (1)
void afterTransaction() {
// logic to be run after a transaction has ended
}
1 | Run this method after a transaction. |
@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 | Run two scripts for this test. |
有关详细信息,请参阅使用@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 | Set the comment prefix and the separator in SQL scripts. |
@SqlMergeMode
@SqlMergeMode
用于标注测试类或测试方法,配置方法级@SQL
声明是否与类级@SQL
声明合并。如果测试类或测试方法上未声明@SqlMergeMode
,则默认情况下将使用覆盖
合并模式。使用覆盖
模式,方法级@SQL
声明将有效地覆盖类级@SQL
声明。
请注意,方法级@SqlMergeModel
声明覆盖类级声明。
下面的示例展示了如何在类级别使用@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 | Set the @Sql merge mode to MERGE for all test methods in the class. |
以下示例说明如何在方法级别使用@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 | Set the @Sql merge mode to MERGE for a specific test method. |
@SqlGroup
@SqlGroup
是一个容器批注,聚合了几个@SQL
批注。您可以在本地使用@SqlGroup
声明几个嵌套的@SQL
批注,也可以将其与Java 8对可重复批注的支持结合使用,其中@SQL
可以在同一个类或方法上声明多次,从而隐式生成该容器批注。以下示例显示如何声明一个SQL组:
@Test
@SqlGroup({ (1) @Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`")), @Sql("/test-user-data.sql") )}
void userTest() {
// run code that uses the test schema and test data
}
1 | Declare a group of SQL scripts. |
3.4.2. Standard Annotation Support
以下注释受Spring TestContext框架的所有配置的标准语义支持。请注意,这些注释不是特定于测试的,可以在Spring框架中的任何地方使用。
-
@自动连接
-
@限定符
-
@Value
-
@Resource
(jakarta.注解)(如果存在JSR-250 -
@ManagedBean
(jakarta.注解)(如果存在JSR-250 -
如果存在JSR-330,则
@Inject
(jakarta.inject) -
如果存在JSR-330,则
@已命名
(jakarta.inject) -
@PersistenceContext
(jakarta.Persistence)(如果存在JPA -
@PersistenceUnit
(jakarta.Persistence)(如果存在JPA -
@Transaction
(org.springFramework.Transaction.Annotation),有限属性支持
JSR-250 Lifecycle Annotations
在Spring TestContext框架中,您可以在 如果测试类中的方法使用 |
3.4.3. Spring JUnit 4 Testing Annotations
@IfProfileValue
@IfProfileValue
表示为特定测试环境启用了带注释的测试。如果配置的ProfileValueSource
为提供的名称
返回匹配的值
,则启用测试。否则,该测试将被禁用并实际上被忽略。
您可以在类级别、方法级别或两者都应用@IfProfileValue
。对于该类或其子类中的任何方法,类级别的使用@IfProfileValue
优先于方法级别的使用。具体地说,如果在类级别和方法级别都启用了测试,则启用该测试。@IfProfileValue
的缺失意味着测试是隐式启用的。这类似于JUnit4的@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 | Run this test only when the Java vendor is "Oracle Corporation". |
或者,您可以使用值
列表(使用OR
语义)配置@IfProfileValue
,以在JUnit4环境中实现对测试组的类似于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 | Run this test for unit tests and integration tests. |
@ProfileValueSourceConfiguration
@ProfileValueSourceConfiguration
是一个类级批注,它指定在检索通过@IfProfileValue
批注配置的配置文件值时使用哪种类型的ProfileValueSource
。如果没有为测试声明@ProfileValueSourceConfiguration
,则默认使用SystemProfileValueSource
。以下示例说明如何使用@ProfileValueSourceConfiguration
:
@ProfileValueSourceConfiguration(CustomProfileValueSource.class) (1)
public class CustomProfileValueSourceTests {
// class body...
}
1 | Use a custom profile value source. |
@Timed
@Timed
表示带注释的测试方法必须在指定的时间段(以毫秒为单位)内完成执行。如果文本执行时间超过指定的时间段,则测试失败。
时间段包括运行测试方法本身、测试的任何重复(参见@Repeat
)以及测试夹具的任何设置或拆除。以下示例显示如何使用它:
@Timed(millis = 1000) (1)
public void testProcessWithOneSecondTimeout() {
// some logic that should not take longer than 1 second to run
}
1 | Set the time period for the test to one second. |
Spring的@Timed
注释的语义与JUnit4的@Test(TimeOut=…)不同)支持。具体地说,由于JUnit4处理测试执行超时的方式(即通过在单独的<代码>线程
中执行测试方法),<代码>@测试(TimeOut=…)如果测试花费的时间太长,则会抢先使测试失败。另一方面,Spring的@timed
不会抢先通过测试,而是等待测试完成后才会失败。
@Repeat
@Repeat
表示带注释的测试方法必须重复运行。测试方法要运行的次数在注释中指定。
重复执行的范围包括测试方法本身的执行以及测试夹具的任何设置或拆除。当与SpringMethodRule
一起使用时,作用域还包括通过TestExecutionListener
实现准备测试实例。下面的示例显示如何使用@Repeat
批注:
@Repeat(10) (1)
@Test
public void testProcessRepeatedly() {
// ...
}
1 | Repeat this test ten times. |
3.4.4. Spring JUnit Jupiter Testing Annotations
当与SpringExtension
和JUnitJupiter(即JUnit5中的编程模型)结合使用时,支持以下批注:
@SpringJUnitConfig
@SpringJUnitConfig
是一个组合批注,它将来自JUnitJupiter的@ExtendWith(SpringExtension.class)
与来自Spring TestContext框架的@ConextConfiguration
组合在一起。它可以在类级别用作@ConextConfiguration
的临时替代。关于配置选项,@ConextConfiguration
和@SpringJUnitConfig
的唯一区别是组件类可以在@SpringJUnitConfig
中用值
属性声明。
以下示例显示如何使用@SpringJUnitConfig
批注指定配置类:
@SpringJUnitConfig(TestConfig.class) (1)
class ConfigurationClassJUnitJupiterSpringTests {
// class body...
}
1 | Specify the configuration class. |
下面的示例显示如何使用@SpringJUnitConfig
批注指定配置文件的位置:
@SpringJUnitConfig(locations = "/test-config.xml") (1)
class XmlJUnitJupiterSpringTests {
// class body...
}
1 | Specify the location of a configuration file. |
有关详细信息,请参阅上下文管理以及@SpringJUnitConfig
和
@ConextConfiguration
的javadoc。
@SpringJUnitWebConfig
@SpringJUnitWebConfig
是一个组合批注,它将来自JUnitJupiter的@ExtendWith(SpringExtension.class)
与来自Spring TestContext框架的@ConextConfiguration
和@WebAppConfiguration
组合在一起。您可以在类级别使用它作为@ConextConfiguration
和@WebAppConfiguration
的临时替代。关于配置选项,@ConextConfiguration
和@SpringJUnitWebConfig
的唯一区别是可以通过@SpringJUnitWebConfig
中的值
属性声明组件类。此外,您只能通过使用@SpringJUnitWebConfig
中的resource cePath
属性来覆盖@WebAppConfiguration
中的值
属性。
以下示例显示如何使用@SpringJUnitWebConfig
批注指定配置类:
@SpringJUnitWebConfig(TestConfig.class) (1)
class ConfigurationClassJUnitJupiterSpringWebTests {
// class body...
}
1 | Specify the configuration class. |
下面的示例显示如何使用@SpringJUnitWebConfig
批注指定配置文件的位置:
@SpringJUnitWebConfig(locations = "/test-config.xml") (1)
class XmlJUnitJupiterSpringWebTests {
// class body...
}
1 | Specify the location of a configuration file. |
有关更多详细信息,请参阅上下文管理以及@SpringJUnitWebConfig
,@ContextConfiguration
,和@WebAppConfiguration
的javadoc。
@TestConstructor
@TestConstructor
是一个类型级注释,用于配置如何从测试的ApplicationContext
中的组件自动连接测试类构造函数的参数。
如果测试类上不存在@TestConstructor
或元存在,则将使用默认的测试构造函数自动布线模式。有关如何更改默认模式的详细信息,请参阅下面的提示。然而,请注意,构造函数上的@AuTower
的本地声明优先于@TestConstructor
和默认模式。
Changing the default test constructor autowire mode
通过将代码JVM系统属性设置为< 从Spring Framework5.3开始,默认模式也可以配置为JUnit平台配置参数。 如果未设置 |
As of Spring Framework 5.2, @TestConstructor is only supported in conjunction with the SpringExtension for use with JUnit Jupiter. Note that the SpringExtension is often automatically registered for you – for example, when using annotations such as @SpringJUnitConfig and @SpringJUnitWebConfig or various test-related annotations from Spring Boot Test. |
@NestedTestConfiguration
@NestedTestConfiguration
是一个类型级批注,用于配置如何在内部测试类的封闭类层次结构中处理Spring测试配置批注。
如果测试类、其超类型层次结构或其封闭类层次结构中不存在@NestedTestConfiguration
或元存在,则将使用默认的封闭配置继承模式。有关如何更改默认模式的详细信息,请参阅下面的提示。
Changing the default enclosing configuration inheritance mode
默认的包含配置继承模式是< |
Spring TestContext框架遵循以下批注的@NestedTestConfiguration
语义。
The use of @NestedTestConfiguration typically only makes sense in conjunction with @Nested test classes in JUnit Jupiter; however, there may be other testing frameworks with support for Spring and nested test classes that make use of this annotation. |
有关示例和更多详细信息,请参阅@nesteed
测试类配置。
@EnabledIf
@EnabledIf
用于表示带注释的JUnit Jupiter测试类或测试方法已启用,如果提供的表达式
的计算结果为true
,则应运行该测试类或测试方法。具体地说,如果表达式的计算结果为Boolan.TRUE
或字符串
等于true
(忽略大小写),则启用测试。当应用于类级别时,该类中的所有测试方法在默认情况下也会自动启用。
表达式可以是以下任意一种:
-
Spring表达式语言(Spel)表达式。例如:
@EnabledIf(“#{systemProperties[‘os.name’].toLowerCase().contains(‘mac’)}”)
-
在Spring
环境
中可用的属性的占位符。例如:@EnabledIf(“${smoke.tests.enabled}”)
-
文本文字。例如:
@EnabledIf(“true”)
然而,请注意,不是属性占位符动态解析结果的文本文本没有实际价值,因为@EnabledIf(“False”)
等同于@Disable
和@EnabledIf(“true”)
在逻辑上没有意义。
您可以使用@EnabledIf
作为元批注来创建自定义合成批注。例如,您可以创建一个自定义@EnabledOnMac
批注,如下所示:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@EnabledIf( expression = "#{systemProperties['os.name'].toLowerCase().contains('mac')}", reason = "Enabled on Mac OS" )
public @interface EnabledOnMac {}
|
从JUnit5.7开始,JUnitJupiter也有一个名为 |
@DisabledIf
@DisabledIf
用于通知带注释的JUnit Jupiter测试类或测试方法被禁用,如果提供的表达式
的计算结果为true
,则不应运行该测试类或测试方法。具体地说,如果表达式的计算结果为Boolan.TRUE
或字符串
等于true
(忽略大小写),则测试将被禁用。在类级别应用时,该类中的所有测试方法也会自动禁用。
表达式可以是以下任意一种:
-
Spring表达式语言(Spel)表达式。例如:
@DisabledIf(“#{systemProperties[‘os.name’].toLowerCase().contains(‘mac’)}”)
-
在Spring
环境
中可用的属性的占位符。例如:@DisabledIf(“${smoke.tests.disabled}”)
-
文本文字。例如:
@DisabledIf(“true”)
然而,请注意,不是属性占位符动态解析结果的文本文字没有实际价值,因为@DisabledIf(“true”)
等同于@DisabledIf(“False”)
和@DisabledIf(“False”)
在逻辑上没有意义。
您可以使用@DisabledIf
作为元批注来创建自定义合成批注。例如,您可以创建一个自定义@DisabledOnMac
批注,如下所示:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@DisabledIf( expression = "#{systemProperties['os.name'].toLowerCase().contains('mac')}", reason = "Disabled on Mac OS" )
public @interface DisabledOnMac {}
|
从JUnit5.7开始,JUnitJupiter也有一个名为 |
3.4.5. Meta-Annotation Support for Testing
您可以使用大多数与测试相关的批注作为元批注来创建自定义组合批注,并减少测试套件中的配置重复。
您可以将以下各项作为元注释与TestContext框架结合使用。
-
@BootstrapWith
-
@ConextConfiguration
-
@ConextHierarchy
-
@ActiveProfiles
-
@TestPropertySource
-
@DirtiesContext
-
@WebAppConfiguration
-
@TestExecutionListeners
-
@事务性
-
@BeForeTransaction
-
@AfterTransaction
-
@Commit
-
@回滚
-
@SQL
-
@SqlConfig
-
@SqlMergeMode
-
@SqlGroup
-
@Repeat
(仅在JUnit4上受支持) -
@Timed
(仅在JUnit4上受支持) -
@IfProfileValue
(仅在JUnit4上受支持) -
@ProfileValueSourceConfiguration
(仅JUnit4支持) -
@SpringJUnitConfig
(仅在JUnit Jupiter上受支持) -
@SpringJUnitWebConfig
(仅在JUnit Jupiter上受支持) -
@TestConstructor
(仅在JUnit Jupiter上受支持) -
@NestedTestConfiguration
(仅在JUnit Jupiter上受支持) -
@EnabledIf
(仅在JUnit Jupiter上受支持) -
@DisabledIf
(仅在JUnit Jupiter上受支持)
请考虑以下示例:
@RunWith(SpringRunner.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
public class OrderRepositoryTests { }
@RunWith(SpringRunner.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
public class UserRepositoryTests { }
如果我们发现我们在基于JUnit4的测试套件中重复前面的配置,我们可以通过引入一个定制的组合注释来减少重复,该注释集中了Spring的通用测试配置,如下所示:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
public @interface TransactionalDevTestConfig { }
然后,我们可以使用定制的@TransactionalDevTestConfig
注释来简化各个基于JUnit4的测试类的配置,如下所示:
@RunWith(SpringRunner.class)
@TransactionalDevTestConfig
public class OrderRepositoryTests { }
@RunWith(SpringRunner.class)
@TransactionalDevTestConfig
public class UserRepositoryTests { }
如果我们编写使用JUnitJupiter的测试,我们可以进一步减少代码重复,因为JUnit5中的批注也可以用作元批注。请考虑以下示例:
@ExtendWith(SpringExtension.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
class OrderRepositoryTests { }
@ExtendWith(SpringExtension.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
class UserRepositoryTests { }
如果我们发现我们在基于JUnitJupiter的测试套件中重复前面的配置,我们可以通过引入一个定制的组合注释来减少重复,该注释集中了Spring和JUnitJupiter的公共测试配置,如下所示:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(SpringExtension.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
public @interface TransactionalDevTestConfig { }
然后,我们可以使用定制的@TransactionalDevTestConfig
注释来简化各个基于JUnitJupiter的测试类的配置,如下所示:
@TransactionalDevTestConfig
class OrderRepositoryTests { }
@TransactionalDevTestConfig
class UserRepositoryTests { }
由于JUnit Jupiter支持使用@Test
、@RepeatedTest
、参数化测试
等作为元批注,因此您还可以在测试方法级别创建自定义组合批注。例如,如果我们希望创建一个组合批注,该批注将来自JUnit Jupiter的@Test
和@tag
批注与来自Spring的@Transaction
批注结合在一起,我们可以创建一个@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 { }
然后,我们可以使用定制的@TransactionalIntegrationTest
注释来简化各个基于JUnitJupiter的测试方法的配置,如下所示:
@TransactionalIntegrationTest
void saveOrder() { }
@TransactionalIntegrationTest
void deleteOrder() { }
有关更多详细信息,请参阅Spring Annotation Programming Model维基页面。
3.5. Spring TestContext Framework
Spring TestContext框架(位于org.springFramework.test.Context
包中)提供了通用的、注释驱动的单元和集成测试支持,这与正在使用的测试框架无关。TestContext框架还非常重视约定优先于配置,具有合理的缺省值,您可以通过基于注释的配置来覆盖这些缺省值。
除了通用的测试基础设施,TestContext框架还提供了对JUnit4、JUnitJupiter(又名JUnit5)和TestNG的显式支持。对于JUnit4和TestNG,Spring提供了抽象
支持类。此外,Spring为JUnit4提供了一个定制的JUnitRunner
和定制的JUnit规则
,并为JUnit Jupiter提供了一个定制的扩展
,让您可以编写所谓的POJO测试类。POJO测试类不需要扩展特定的类层次结构,例如抽象
支持类。
以下部分概述了TestContext框架的内部结构。如果您只对使用框架感兴趣,而对使用您自己的定制侦听器或定制加载器扩展它不感兴趣,那么可以直接转到配置(上下文管理、依赖注入、事务管理)、支持类和注释支持部分。
3.5.1. Key Abstractions
该框架的核心由TestConextManager
类和TestContext
、TestExecutionListener
和SmartConextLoader
接口组成。为每个测试类创建TestConextManager
(例如,用于执行JUnit Jupiter中单个测试类中的所有测试方法)。TestConextManager
又管理保存当前测试上下文的TestContext
。TestConextManager
还随着测试的进行更新TestContext
的状态,并委托给TestExecutionListener
实现,这些实现通过提供依赖项注入、管理事务等来检测实际的测试执行。SmartConextLoader
负责加载给定测试类的ApplicationContext
。有关各种实现的进一步信息和示例,请参阅javadoc和Spring测试套件。
TestContext
TestContext
封装运行测试的上下文(与实际使用的测试框架无关),并为它负责的测试实例提供上下文管理和缓存支持。TestContext
还委托SmartConextLoader
在请求时加载ApplicationContext
。
TestContextManager
TestConextManager
是进入Spring TestContext框架的主要入口点,负责管理单个TestContext
,并在定义良好的测试执行点向每个注册的TestExecutionListener
发送事件:
-
在特定测试框架的任何“在类之前”或“在所有方法之前”之前。
-
测试实例后处理。
-
在特定测试框架的任何“之前”或“每个之前”方法之前。
-
紧接在测试方法执行之前但在测试设置之后。
-
紧接在测试方法执行之后但在测试拆卸之前。
-
在特定测试框架的任何“After”或“After Each”方法之后。
-
在特定测试框架的任何“类后”或“毕竟”方法之后。
TestExecutionListener
TestExecutionListener
定义用于响应监听器注册到的TestConextManager
发布的测试执行事件的API。参见TestExecutionListener
配置。
Context Loaders
ConextLoader
是一个策略接口,用于为Spring TestContext框架管理的集成测试加载ApplicationContext
。您应该实现SmartConextLoader
而不是此接口,以提供对组件类、活动Bean定义配置文件、测试属性源、上下文层次结构和WebApplicationContext
支持的支持。
SmartConextLoader
是ConextLoader
接口的扩展,它取代了原始的最小ConextLoader
SPI。具体地说,SmartConextLoader
可以选择处理资源位置、组件类或上下文初始值设定项。此外,SmartConextLoader
可以设置活动的Bean定义配置文件,并在它加载的上下文中测试属性源。
Spring提供了以下实现:
-
DelegatingSmartConextLoader
:两个默认加载器之一,它在内部委托给AnnotationConfigConextLoader
、GenericXmlConextLoader
或GenericGroovyXmlConextLoader
,具体取决于为测试类声明的配置,或者取决于是否存在默认位置或默认配置类。只有当Groovy位于类路径上时,才会启用Groovy支持。 -
WebDelegatingSmartConextLoader AnnotationConfigWebContextLoader
,>:它是两个默认加载器之一,它根据为测试类声明的配置或是否存在默认位置或默认配置类,在内部将一个<GenericGroovyXmlWebContextLoader
,>GenericXmlWebConextLoader委托给它。仅当测试类上存在@WebAppConfiguration
时,才使用WebConextLoader
。只有当Groovy位于类路径上时,才会启用Groovy支持。 -
AnnotationConfigConextLoader
:从组件类加载标准ApplicationContext
。 -
AnnotationConfigWebContextLoader
:从组件类加载WebApplicationContext
。 -
GenericGroovyXmlConextLoader
:从Groovy脚本或XML配置文件的资源位置加载标准的ApplicationContext
。 -
GenericGroovyXmlWebContextLoader
:从资源位置加载WebApplicationContext
,这些资源位置要么是Groovy脚本,要么是XML配置文件。 -
GenericXmlConextLoader
:从XML资源位置加载标准的ApplicationContext
。 -
GenericXmlWebConextLoader
:从XML资源位置加载WebApplicationContext
。
3.5.2. Bootstrapping the TestContext Framework
Spring TestContext框架内部的默认配置足以满足所有常见用例。然而,有时开发团队或第三方框架想要更改默认的ConextLoader
,实现自定义的TestContext
或ConextCache
,扩充ConextCustomizerFactory
和TestExecutionListener
的默认实现集,等等。对于这种对TestContext框架如何操作的低级控制,Spring提供了一种自举策略。
TestContextBootstrapper
定义用于引导TestContext框架的SPI。TestConextManager
使用TestContextBootstrapper
加载当前测试的TestExecutionListener
实现,并构建它管理的TestContext
。您可以使用@BootstRapWith
为测试类(或测试类层次结构)配置定制的引导策略,可以直接使用,也可以作为元注释使用。如果没有使用@BootstrapWith
显式配置引导程序,则根据是否存在@WebAppConfiguration
,使用DefaultTestContextBootstrapper
或WebTestContextBootstrapper
。
由于TestContextBootstrapper
SPI可能会在未来进行更改(以适应新的需求),我们强烈建议实现者不要直接实现此接口,而是扩展AbstractTestContextBootstrapper
或其具体的子类之一。
3.5.3. TestExecutionListener
Configuration
Spring提供了以下默认注册的TestExecutionListener
实现,完全按照以下顺序进行注册:
-
ServletTestExecutionListener
:为WebApplicationContext
配置Servlet API模拟。 -
DirtiesContextBeforeModesTestExecutionListener
:处理“BEFORE”模式的@DirtiesContext
注释。 -
ApplicationEventsTestExecutionListener
:支持ApplicationEvents
。 -
DependencyInjectionTestExecutionListener
:为测试实例提供依赖项注入。 -
DirtiesContextTestExecutionListener
:处理“After”模式的@DirtiesContext
注释。 -
TransactionalTestExecutionListener
:为事务性测试执行提供了默认的回滚语义。 -
SqlScriptsTestExecutionListener
:运行使用@SQL
注释配置的SQL脚本。 -
EventPublishingTestExecutionListener
:将测试执行事件发布到测试的<代码>应用程序上下文 (请参阅测试执行事件)。
Registering TestExecutionListener
Implementations
您可以使用@TestExecutionListeners
注释为测试类、其子类及其嵌套类显式注册TestExecutionListener
实现。有关详细信息和示例,请参阅批注支持和@TestExecutionListeners
的javadoc。
Switching to default
TestExecutionListener implementations
如果您扩展了一个用
Java
Kotlin
|
Automatic Discovery of Default TestExecutionListener
Implementations
使用@TestExecutionListeners
注册TestExecutionListener
实现适用于有限测试场景中使用的自定义监听器。但是,如果需要在整个测试套件中使用自定义侦听器,则可能会变得很麻烦。这个问题通过支持通过SpringFactoriesLoader
机制自动发现默认的TestExecutionListener
实现来解决。
具体地说,Spring-test
模块在其META-INF/spring.Factory
属性文件中的org.springframework.test.context.TestExecutionListener
注册表项下声明了所有核心默认TestExecutionListener
实现。第三方框架和开发人员可以通过自己的META-INF/spring.Factory
属性文件,以相同的方式将自己的TestExecutionListener
实现贡献给默认侦听器列表。
Merging TestExecutionListener
Implementations
如果自定义TestExecutionListener
是通过@TestExecutionListeners
注册的,则不会注册默认监听器。在大多数常见的测试场景中,这实际上会迫使开发人员手动声明除任何自定义侦听器之外的所有默认侦听器。下面的清单演示了这种配置风格:
@ContextConfiguration
@TestExecutionListeners({ MyCustomTestExecutionListener.class, ServletTestExecutionListener.class, DirtiesContextBeforeModesTestExecutionListener.class, DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class, TransactionalTestExecutionListener.class, SqlScriptsTestExecutionListener.class })
class MyTest {
// class body...
}
这种方法的挑战在于,它要求开发人员准确地知道默认情况下注册了哪些监听器。此外,默认侦听器的集合可以在不同的版本中更改。例如,<DirtiesContextBeforeModesTestExecutionListener
> >SqlScriptsTestExecutionListener
是在Spring Framework4.1中引入的,而SqlScriptsTestExecutionListener是在Spring Framework4.2中引入的。此外,像Spring Boot和Spring Security这样的第三方框架通过使用前面提到的自动发现机制来注册它们自己的默认TestExecutionListener
实现。
为了避免知道并重新声明所有默认侦听器,您可以将@TestExecutionListeners
的mergeMode
属性设置为MergeMode.MERGE_WITH_DEFAULTS
。MERGE_WITH_DEFAULTS
表示本地声明的监听程序应该与默认监听程序合并。合并算法确保从列表中删除重复项,并根据AnnotationAwareOrderCompator
的语义对合并后的侦听器的结果集进行排序,如排序TestExecutionListener
实现中所述。如果侦听器实现Order
或使用@Order
注释,它可能会影响它与默认值合并的位置。否则,本地声明的侦听器将在合并时附加到默认侦听器列表中。
例如,如果上例中的MyCustomTestExecutionListener
类将其order
值(例如,500
)配置为小于ServletTestExecutionListener
(恰好是1000
)的顺序,则MyCustomTestExecutionListener
可以自动与ServletTestExecutionListener
前面的默认列表合并,并且前面的示例可以替换为以下内容:
@ContextConfiguration
@TestExecutionListeners( listeners = MyCustomTestExecutionListener.class, mergeMode = MERGE_WITH_DEFAULTS )
class MyTest {
// class body...
}
3.5.4. Application Events
从Spring Framework5.3.3开始,TestContext框架支持记录ApplicationContext
中发布的应用程序事件,这样就可以在测试中针对这些事件执行断言。在单个测试执行期间发布的所有事件都可以通过ApplicationEvents
API获得,该API允许您将事件作为java.util.Stream
进行处理。
若要在测试中使用ApplicationEvents
,请执行以下操作。
-
确保您的测试类使用
@RecordApplicationEvents
.进行了注释或元注释 -
确保
ApplicationEventsTestExecutionListener
已注册。但是,请注意,ApplicationEventsTestExecutionListener
在默认情况下是注册的,只有当您通过<代码>@TestExecutionListeners进行了不包括默认侦听器的自定义配置时,才需要手动注册。 -
用@AuTower<>注释ApplicationEvents类型字段,并在测试和生命周期方法中使用ApplicationEvents的实例(如JUnit Jupiter中的@BeForeEach和@AfterEach方法)。
-
当使用SpringExtension for JUnit Jupiter时,您可以在测试或生命周期方法中声明一个
ApplicationEvents
类型的方法参数,作为测试类中@AuTower
字段的替代。
-
下面的测试类使用SpringExtension
for JUnit Jupiter和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 | Annotate the test class with @RecordApplicationEvents . |
2 | Inject the ApplicationEvents instance for the current test. |
3 | Use the ApplicationEvents API to count how many OrderSubmitted events were published. |
有关ApplicationEvents
接口的详细信息,请参阅ApplicationEvents
javadoc。
3.5.5. Test Execution Events
Spring Framework5.2中引入的EventPublishingTestExecutionListener
提供了另一种实现自定义TestExecutionListener
的方法。测试的ApplicationContext
中的组件可以侦听EventPublishingTestExecutionListener
,发布的以下事件,每个事件都对应于TestExecutionListener
API中的一个方法。
-
BeForeTestClassEvent
-
准备测试实例事件
-
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
3.5.6. Context Management
每个TestContext
为其负责的测试实例提供上下文管理和缓存支持。测试实例不会自动收到对配置的ApplicationContext
的访问权限。但是,如果测试类实现了ApplicationConextAware
接口,则会向测试实例提供对ApplicationContext
的引用。请注意,AbstractJUnit4SpringConextTests
和AbstractTestNGSpringConextTests
实现了ApplicationConextAware
,因此提供了对ApplicationContext
的访问。
@Autowired ApplicationContext
作为实现
Java
Kotlin
同样,如果您的测试配置为加载
Java
Kotlin
通过使用< |
使用TestContext框架的测试类不需要扩展任何特定的类或实现特定的接口来配置其应用程序上下文。相反,配置是通过在类级别声明@ConextConfiguration
注释来实现的。如果您的测试类没有显式声明应用程序上下文资源位置或组件类,则配置的ConextLoader
将确定如何从默认位置或默认配置类加载上下文。除了上下文资源位置和组件类之外,还可以通过应用程序上下文初始化器配置应用程序上下文。
以下各节解释如何使用Spring的@ConextConfiguration
注释,通过使用XML配置文件、Groovy脚本、组件类(通常是@Configuration
类)或上下文初始化器来配置测试ApplicationContext
。或者,您也可以针对高级用例实现和配置您自己的自定义SmartConextLoader
。
Context Configuration with XML resources
要使用XML配置文件为您的测试加载ApplicationContext
,请使用@ConextConfiguration
注释您的测试类,并使用包含XML配置元数据的资源位置的数组配置Locations
属性。普通路径或相对路径(例如,context.xml
)被视为相对于定义测试类的包的类路径资源。以斜杠开头的路径被视为绝对类路径位置(例如,/org/Example/config.xml
)。表示资源URL的路径(即以classpath:
、file:
、http:
等为前缀的路径)按原样使用。
@ExtendWith(SpringExtension.class)
// ApplicationContext will be loaded from "/app-config.xml" and
// "/test-config.xml" in the root of the classpath
@ContextConfiguration(locations={"/app-config.xml", "/test-config.xml"}) (1)
class MyTest {
// class body...
}
1 | Setting the locations attribute to a list of XML files. |
@ConextConfiguration
通过标准的Java值
属性支持Locations
属性的别名。因此,如果您不需要在@ConextConfiguration
中声明其他属性,则可以省略Locations
属性名称的声明,并使用以下示例中演示的速记格式声明资源位置:
@ExtendWith(SpringExtension.class)
@ContextConfiguration({"/app-config.xml", "/test-config.xml"}) (1)
class MyTest {
// class body...
}
1 | Specifying XML files without using the location attribute. |
如果在@ConextConfiguration
注释中同时省略了位置
和值
属性,则TestContext框架会尝试检测默认的XML资源位置。具体地说,GenericXmlContextLoader
和GenericXmlWebConextLoader
根据测试类的名称检测默认位置。如果您的类名为com.example.MyTest
,GenericXmlConextLoader
从“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 | Loading configuration from the default location. |
Context Configuration with Groovy Scripts
要通过使用使用Groovy Bean定义DSL的Groovy脚本为您的测试加载ApplicationContext
,您可以使用@ConextConfiguration
注释您的测试类,并使用包含Groovy脚本资源位置的数组配置位置
或值
属性。Groovy脚本的资源查找语义与XML配置文件中描述的相同。
Enabling Groovy script support
Support for using Groovy scripts to load an ApplicationContext in the Spring TestContext Framework is enabled automatically if Groovy is on the classpath. |
下面的示例显示如何指定Groovy配置文件:
@ExtendWith(SpringExtension.class)
// ApplicationContext will be loaded from "/AppConfig.groovy" and
// "/TestConfig.groovy" in the root of the classpath
@ContextConfiguration({"/AppConfig.groovy", "/TestConfig.Groovy"}) (1)
class MyTest {
// class body...
}
1 | Specifying the location of Groovy configuration files. |
如果您在@ConextConfiguration
注释中同时省略了位置
和值
属性,那么TestContext框架将尝试检测默认的Groovy脚本。具体地说,GenericGroovyXmlConextLoader
和GenericGroovyXmlWebConextLoader
根据测试类的名称检测默认位置。如果您的类被命名为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 | Loading configuration from the default location. |
Declaring XML configuration and Groovy scripts simultaneously
您可以使用 下面的清单显示了如何在集成测试中将两者结合起来:
Java
Kotlin
|
Context Configuration with Component Classes
要使用组件类为您的测试加载ApplicationContext
(请参阅基于Java的容器配置),您可以使用@ConextConfiguration
注释测试类,并使用包含对组件类的引用的数组来配置Class
属性。以下示例显示了如何执行此操作:
@ExtendWith(SpringExtension.class)
// ApplicationContext will be loaded from AppConfig and TestConfig
@ContextConfiguration(classes = {AppConfig.class, TestConfig.class}) (1)
class MyTest {
// class body...
}
1 | Specifying component classes. |
Component Classes
术语“组件类”可以指以下任一项:
有关组件类的配置和语义的更多信息,请参见 |
如果您在@ConextConfiguration
注释中省略了CLASS
属性,那么TestContext框架会尝试检测默认配置类的存在。具体地说,AnnotationConfigConextLoader
和AnnotationConfigWebConextLoader
检测满足配置类实现要求的测试类的所有静态
类,如@configuration
javadoc中指定的。请注意,配置类的名称是任意的。此外,如果需要,一个测试类可以包含多个静态
嵌套配置类。在下面的示例中,OrderServiceTest
类声明了一个名为Config
的静态
嵌套配置类,该类自动用于加载测试类的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 | Loading configuration information from the nested Config class. |
Mixing XML, Groovy Scripts, and Component Classes
有时可能需要混合使用XML配置文件、Groovy脚本和组件类(通常是@Configuration
类)来为您的测试配置ApplicationContext
。例如,如果您在生产中使用XML配置,您可能决定使用@Configuration
类为您的测试配置特定的Spring管理的组件,反之亦然。
此外,一些第三方框架(如Spring Boot)为同时从不同类型的资源(例如,XML配置文件、Groovy脚本和@Configuration
类)加载ApplicationContext
提供了一流的支持。从历史上看,Spring框架不支持标准部署的这一点。因此,Spring框架在Spring-test
模块中交付的大多数SmartConextLoader
实现对于每个测试上下文只支持一种资源类型。然而,这并不意味着您不能同时使用两者。一般规则的一个例外是GenericGroovyXmlConextLoader
和GenericGroovyXmlWebConextLoader
同时支持XML配置文件和Groovy脚本。此外,第三方框架可以选择通过@ConextConfiguration
同时支持位置
和类
的声明,并且,借助TestContext框架中的标准测试支持,您可以选择以下选项。
如果您希望使用资源位置(例如,XML或Groovy)和@Configuration
类来配置测试,则必须选择其中一个作为入口点,并且必须包含或导入另一个。例如,在XML或Groovy脚本中,您可以通过使用组件扫描或将它们定义为普通的Spring Bean来包括@Configuration
类,而在@Configuration
类中,您可以使用@ImportResource
来导入XML配置文件或Groovy脚本。请注意,此行为在语义上等同于您在生产中配置应用程序的方式:在生产配置中,您定义了一组XML或Groovy资源位置或一组@Configuration
类,从中加载您的生产ApplicationContext
,但您仍然可以自由地包括或导入其他类型的配置。
Context Configuration with Context Initializers
若要使用上下文初始值设定项为您的测试配置ApplicationContext
,请使用@ConextConfiguration
注释您的测试类,并使用包含对实现ApplicationContextInitializer
的类的引用的数组来配置Initializers
属性。然后使用声明的上下文初始值设定项来初始化为测试加载的ConfigurableApplicationContext
。请注意,每个声明的初始值设定项支持的具体ConfigurableApplicationContext
类型必须与正在使用的SmartConextLoader
创建的ApplicationContext
类型兼容(通常是GenericApplicationContext
)。此外,调用初始化器的顺序取决于它们是实现了Spring的Order
接口,还是使用了Spring的@Order
批注或标准的@优先级
批注。以下示例显示如何使用初始值设定项:
@ExtendWith(SpringExtension.class)
// ApplicationContext will be loaded from TestConfig
// and initialized by TestAppCtxInitializer
@ContextConfiguration( classes = TestConfig.class, initializers = TestAppCtxInitializer.class) (1)
class MyTest {
// class body...
}
1 | Specifying configuration by using a configuration class and an initializer. |
您还可以完全省略@ConextConfiguration
中的XML配置文件、Groovy脚本或组件类的声明,而只声明ApplicationContextInitializer
类,这些类然后负责在上下文 - 中注册Bean,例如,通过编程方式从XML文件或配置类加载Bean定义。以下示例显示了如何执行此操作:
@ExtendWith(SpringExtension.class)
// ApplicationContext will be initialized by EntireAppInitializer
// which presumably registers beans in the context
@ContextConfiguration(initializers = EntireAppInitializer.class) (1)
class MyTest {
// class body...
}
1 | Specifying configuration by using only an initializer. |
Context Configuration Inheritance
@ConextConfiguration
支持布尔型inheritLocations
和inheritInitializers
属性,这些属性指示是否应该继承超类声明的资源位置或组件类和上下文初始值设定项。这两个标志的默认值都是true
。这意味着测试类继承任何超类声明的资源位置或组件类以及上下文初始值设定项。具体地说,测试类的资源位置或组件类被附加到超类声明的资源位置或注释类的列表中。类似地,给定测试类的初始化器被添加到由测试超类定义的初始化器集合中。因此,子类可以选择扩展资源位置、组件类或上下文初始值设定项。
如果@ConextConfiguration
中的inheritLocations
或inheritInitializers
属性设置为FALSE
,则测试类的资源位置或组件类以及上下文初始值设定项将分别隐藏并有效替换超类定义的配置。
As of Spring Framework 5.3, test configuration may also be inherited from enclosing classes. See @Nested test class configuration for details. |
在下一个使用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 | Configuration file defined in the superclass. |
2 | Configuration file defined in the subclass. |
类似地,在下一个使用组件类的示例中,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 | Configuration class defined in the superclass. |
2 | Configuration class defined in the subclass. |
在下一个使用上下文初始化器的示例中,使用BaseInitializer
和ExtendedInitializer
初始化ExtendedTest
的ApplicationContext
。但是请注意,调用初始化器的顺序取决于它们是实现了Spring的Order
接口,还是使用了Spring的@Order
批注或标准的@优先级
批注。下面的示例说明一个类如何扩展另一个类并同时使用其自己的初始值设定项和超类的初始值设定项:
// ApplicationContext will be initialized by BaseInitializer
@SpringJUnitConfig(initializers = BaseInitializer.class) (1)
class BaseTest {
// class body...
}
// ApplicationContext will be initialized by BaseInitializer
// and ExtendedInitializer
@SpringJUnitConfig(initializers = ExtendedInitializer.class) (2)
class ExtendedTest extends BaseTest {
// class body...
}
1 | Initializer defined in the superclass. |
2 | Initializer defined in the subclass. |
Context Configuration with Environment Profiles
Spring框架对环境和概要文件的概念提供了一流的支持(也称为“Bean定义概要文件”),并且可以配置集成测试来为各种测试场景激活特定的Bean定义概要文件。这是通过使用@ActiveProfiles
注释来注释测试类,并提供在为测试加载ApplicationContext
时应该激活的配置文件列表来实现的。
You can use @ActiveProfiles with any implementation of the SmartContextLoader SPI, but @ActiveProfiles is not supported with implementations of the older ContextLoader SPI. |
考虑两个具有XML配置和@Configuration
类的示例:
<!-- app-config.xml -->
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:jee="http://www.springframework.org/schema/jee" xsi:schemaLocation="...">
<bean id="transferService" class="com.bank.service.internal.DefaultTransferService">
<constructor-arg ref="accountRepository"/>
<constructor-arg ref="feePolicy"/>
</bean>
<bean id="accountRepository" class="com.bank.repository.internal.JdbcAccountRepository">
<constructor-arg ref="dataSource"/>
</bean>
<bean id="feePolicy" class="com.bank.service.internal.ZeroFeePolicy"/>
<beans profile="dev">
<jdbc:embedded-database id="dataSource">
<jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
<jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
</jdbc:embedded-database>
</beans>
<beans profile="production">
<jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
</beans>
<beans profile="default">
<jdbc:embedded-database id="dataSource">
<jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
</jdbc:embedded-database>
</beans>
</beans>
@ExtendWith(SpringExtension.class)
// ApplicationContext will be loaded from "classpath:/app-config.xml"
@ContextConfiguration("/app-config.xml")
@ActiveProfiles("dev")
class TransferServiceTest {
@Autowired
TransferService transferService;
@Test
void testTransferService() {
// test the transferService
}
}
当TransferServiceTest
运行时,其ApplicationContext
将从类路径根目录下的app-config.xml
配置文件中加载。如果您检查app-config.xml
,您可以看到bactRepository
Bean依赖于DataSource
Bean。但是,DataSource
未定义为顶级Bean。相反,DataSource
定义了三次:生产
配置文件中、开发
配置文件中和默认
配置文件中。
通过使用@ActiveProfiles(“dev”)
注释TransferServiceTest
,我们指示Spring TestContext框架加载ApplicationContext
,其中活动配置文件设置为{“dev”}
。结果,创建了一个嵌入式数据库并填充了测试数据,并且count Repository
Bean连接了对开发DataSource
的引用。这很可能是我们在集成测试中想要的。
有时将Bean分配给默认
配置文件很有用。只有当没有特别激活其他配置文件时,才会包含默认配置文件中的Bean。您可以使用它来定义要在应用程序的默认状态中使用的“回退”Bean。例如,您可以显式地为开发
和生产
配置文件提供数据源,但在这两个配置文件都不活动时将内存中的数据源定义为默认数据源。
下面的代码清单演示了如何使用@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("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("default")
public class DefaultDataConfig {
@Bean
public DataSource dataSource() {
return new 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();
}
}
@SpringJUnitConfig({ TransferServiceConfig.class, StandaloneDataConfig.class, JndiDataConfig.class, DefaultDataConfig.class})
@ActiveProfiles("dev")
class TransferServiceTest {
@Autowired
TransferService transferService;
@Test
void testTransferService() {
// test the transferService
}
}
在这个变体中,我们将XML配置拆分成四个独立的@Configuration
类:
-
TransferServiceConfig
:通过依赖注入获取一个DataSource
,使用@AuTower
。 -
StandaloneDataConfig
:为适合开发人员测试的嵌入式数据库定义一个DataSource
。 -
JndiDataConfig
:定义在生产环境中从JNDI检索的DataSource
。 -
DefaultDataConfig
:在没有配置文件处于活动状态的情况下,为默认嵌入式数据库定义一个DataSource
。
与基于XML的配置示例一样,我们仍然使用@ActiveProfiles(“dev”)
注释TransferServiceTest
,但这次我们使用@ConextConfiguration
注释指定了所有四个配置类。测试类本身的主体完全保持不变。
通常的情况是,在给定项目中的多个测试类中使用一组配置文件。因此,为了避免重复声明@ActiveProfiles
注释,您可以在基类上声明一次@ActiveProfiles
,子类会自动从基类继承@ActiveProfiles
配置。在下面的示例中,@ActiveProfiles
(以及其他批注)的声明已移动到抽象超类AbstractIntegrationTest
:
As of Spring Framework 5.3, test configuration may also be inherited from enclosing classes. See @Nested test class configuration for details. |
@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
}
}
@ActiveProfiles
还支持可用于禁用活动配置文件继承的inheritProfiles
属性,如下例所示:
// "dev" profile overridden with "production"
@ActiveProfiles(profiles = "production", inheritProfiles = false)
class ProductionTransferServiceTest extends AbstractIntegrationTest {
// test body
}
此外,有时有必要以编程方式来解析测试的活动配置文件,而不是声明式的 - ,例如,基于:
-
当前操作系统。
-
测试是否在持续集成生成服务器上运行。
-
某些环境变量的存在。
-
自定义类级批注的存在。
-
其他方面的担忧。
要以编程方式解析活动的Bean定义配置文件,您可以实现一个自定义的ActiveProfilesResolver
,并使用@ActiveProfiles
的解析器
属性注册它。有关更多信息,请参阅相应的javadoc。以下示例演示如何实现和注册自定义OperatingSystemActiveProfilesResolver
:
// "dev" profile overridden programmatically via a custom resolver
@ActiveProfiles( resolver = OperatingSystemActiveProfilesResolver.class, inheritProfiles = false)
class TransferServiceTest extends 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};
}
}
Context Configuration with Test Property Sources
Spring框架对具有属性源层次结构的环境的概念提供了一流的支持,并且您可以使用特定于测试的属性源来配置集成测试。与@Configuration
类上使用的@PropertySource
注释不同,您可以在测试类上声明@TestPropertySource
注释,以声明测试属性文件或内联属性的资源位置。这些测试属性源被添加到为带注释的集成测试加载的ApplicationContext
的环境
中的PropertySources
集合中。
您可以将
|
Declaring Test Property Sources
您可以使用@TestPropertySource
的位置
或值
属性来配置测试属性文件。
传统的和基于XML语言的属性文件格式都支持代码转换,例如“classpath:/com/example/test.properties”
或“file:///path/to/file.xml”.“ -
每条路径都被解释为一个Spring资源
。普通路径(例如,“test.properties”
)被视为相对于定义测试类的包的类路径资源。以斜杠开头的路径被视为绝对类路径资源(例如:“/org/Example/test.xml”
)。使用指定的资源协议加载引用URL的路径(例如,以classpath:
、file:
或http:
为前缀的路径)。不允许使用资源位置通配符(如*/.properties
):每个位置的计算结果必须恰好为一个.properties
或.xml
资源。
下面的示例使用测试属性文件:
@ContextConfiguration
@TestPropertySource("/test.properties") (1)
class MyIntegrationTests {
// class body...
}
1 | Specifying a properties file with an absolute path. |
您可以使用@TestPropertySource
的属性
以键-值对的形式配置内联属性,如下例所示。所有键-值对都作为具有最高优先级的单个测试
PropertySource
添加到封闭的Environment
中。
支持的键-值对语法与为Java属性文件中的条目定义的语法相同:
-
key=值
-
键:值
-
密钥值
下面的示例设置两个内联属性:
@ContextConfiguration
@TestPropertySource(properties = {"timezone = GMT", "port: 4242"}) (1)
class MyIntegrationTests {
// class body...
}
1 | Setting two properties by using two variations of the key-value syntax. |
从Spring Framework5.2开始, 此外,您可以在一个测试类上声明多个合成批注,每个批注都用 直接呈现 |
Default Properties File Detection
如果@TestPropertySource
被声明为空批注(即,位置
或属性
属性没有显式值),则会尝试检测相对于声明该批注的类的默认属性文件。例如,如果带注释的测试类是com.example.MyTest
,则对应的默认属性文件是classpath:com/example/MyTest.properties
.如果无法检测到默认值,则抛出IllegalStateException
。
Precedence
测试属性的优先级高于在操作系统环境、Java系统属性或应用程序通过@PropertySource
或以编程方式声明添加的属性源中定义的属性。因此,测试属性可用于选择性地重写从系统和应用程序属性源加载的属性。此外,内联属性比从资源位置加载的属性具有更高的优先级。然而,请注意,通过@DynamicPropertySource
注册的属性比通过@TestPropertySource
加载的属性具有更高的优先级。
在下一个示例中,时区
和端口
属性以及在“/test.properties”
中定义的任何属性将覆盖系统和应用程序属性源中定义的任何同名属性。此外,如果“/test.properties”
文件定义了时区
和端口
属性的条目,则这些属性将被使用属性
声明的内联属性覆盖。下面的示例说明如何在文件和内联中指定属性:
@ContextConfiguration
@TestPropertySource( locations = "/test.properties", properties = {"timezone = GMT", "port: 4242"} )
class MyIntegrationTests {
// class body...
}
Inheriting and Overriding Test Property Sources
@TestPropertySource
支持用于指示是否应继承超类声明的属性文件的资源位置和内联属性的布尔继承位置
和继承属性
属性。这两个标志的默认值都是true
。这意味着测试类继承任何超类声明的位置和内联属性。具体地说,测试类的位置和内联属性被附加到超类声明的位置和内联属性。因此,子类可以选择扩展位置和内联属性。请注意,稍后出现的属性会隐藏(即覆盖)先前出现的同名属性。此外,上述优先规则也适用于继承的测试属性源。
如果@TestPropertySource
中的inheritLocations
或inheritProperties
属性设置为FALSE
,则测试类的位置或内联属性将分别隐藏并有效替换超类定义的配置。
As of Spring Framework 5.3, test configuration may also be inherited from enclosing classes. See @Nested test class configuration for details. |
在下一个示例中,仅使用base.Properties
文件作为测试属性源来加载BaseTest
的ApplicationContext
。相比之下,ExtendedTest
的ApplicationContext
是通过使用base.Properties
和扩展的.Properties
文件作为测试属性源位置来加载的。下面的示例说明如何使用属性
文件在子类及其超类中定义属性:
@TestPropertySource("base.properties")
@ContextConfiguration
class BaseTest {
// ...
}
@TestPropertySource("extended.properties")
@ContextConfiguration
class ExtendedTest extends BaseTest {
// ...
}
在下一个示例中,仅使用内联的key1
属性加载BaseTest
的ApplicationContext
。相比之下,ExtendedTest
的ApplicationContext
是使用内联的key1
和key2
属性加载的。下面的示例说明如何使用内联属性在子类及其超类中定义属性:
@TestPropertySource(properties = "key1 = value1")
@ContextConfiguration
class BaseTest {
// ...
}
@TestPropertySource(properties = "key2 = value2")
@ContextConfiguration
class ExtendedTest extends BaseTest {
// ...
}
Context Configuration with Dynamic Property Sources
从Spring Framework5.2.5开始,TestContext框架通过@DynamicPropertySource
注释提供对动态属性的支持。此批注可用于需要将具有动态值的属性添加到为集成测试加载的ApplicationContext
的Environment
中的PropertySources
集合的集成测试中。
|
与在类级别应用的@TestPropertySource批注不同,@DynamicPropertySource
必须应用于接受单个DynamicPropertyRegistry
参数的静态
方法,该参数用于将名称-值对添加到Environment
。值是动态的,并通过供应商
提供,该供应商仅在解析属性时调用。通常,方法引用用于提供值,如下面的示例所示,该示例使用TestContainers项目在SpringApplicationContext
外部管理Redis容器。托管Redis容器的IP地址和端口通过redis.host
和redis.port
属性提供给测试的ApplicationContext
中的组件。这些属性可以通过Spring的Environment
抽象访问,也可以直接注入到Spring管理的组件中--例如,分别通过@Value(“${redis.host}”)
和@Value(“${redis.port}”)
。
如果您在基类中使用 |
@SpringJUnitConfig(/* ... */)
@Testcontainers
class ExampleIntegrationTests {
@Container
static RedisContainer redis = new RedisContainer();
@DynamicPropertySource
static void redisProperties(DynamicPropertyRegistry registry) {
registry.add("redis.host", redis::getHost);
registry.add("redis.port", redis::getMappedPort);
}
// tests ...
}
Loading a WebApplicationContext
要指示TestContext框架加载WebApplicationContext
而不是标准的ApplicationContext
,您可以使用@WebAppConfiguration
注释相应的测试类。
测试类上的@WebAppConfiguration
指示TestContext框架(TCF)应该为您的集成测试加载WebApplicationContext
(WAC)。在后台,TCF确保创建了MockServletContext
并将其提供给测试的WAC。默认情况下,MockServletContext
的基本资源路径设置为src/main/webapp
。这被解释为相对于JVM根目录的路径(通常是项目的路径)。如果您熟悉Maven项目中Web应用程序的目录结构,您就知道src/main/webapp
是WAR根目录的默认位置。如果需要覆盖此默认设置,您可以提供指向<代码>@WebAppConfiguration
批注的替代路径(例如,@WebAppConfiguration(“src/test/webapp”)
).如果希望从类路径而不是文件系统引用基本资源路径,可以使用Spring的classpath:
前缀。
注意,Spring对WebApplicationContext
实现的测试支持与对标准ApplicationContext
实现的支持不相上下。当使用WebApplicationContext
进行测试时,您可以使用@ConextConfiguration
声明XML配置文件、Groovy脚本或@Configuration
类。您还可以自由使用任何其他测试注释,例如@ActiveProfiles
、@TestExecutionListeners
、@SQL
、@Rollback
等。
本节中的其余示例显示了用于加载WebApplicationContext
的一些不同配置选项。下面的示例显示了TestContext框架对约定优先于配置的支持:
@ExtendWith(SpringExtension.class)
// defaults to "file:src/main/webapp"
@WebAppConfiguration
// detects "WacTests-context.xml" in the same package
// or static nested @Configuration classes
@ContextConfiguration
class WacTests {
//...
}
如果您使用@WebAppConfiguration
注释测试类,而没有指定资源基础路径,则资源路径实际上默认为file:src/main/webapp
。类似地,如果您声明@ConextConfiguration
而没有指定资源位置
、组件类
或上下文初始值设定项
,则Spring会尝试使用约定(即WacTest-context.xml
与WacTest
类或静态嵌套的@Configuration
类位于同一个包中)来检测配置的存在。
以下示例显示如何使用@WebAppConfiguration
显式声明资源基本路径,并使用@ConextConfiguration
显式声明XML资源位置:
@ExtendWith(SpringExtension.class)
// file system resource
@WebAppConfiguration("webapp")
// classpath resource
@ContextConfiguration("/spring/test-servlet-config.xml")
class WacTests {
//...
}
这里需要注意的重要一点是,具有这两个注释的路径的语义不同。默认情况下,@WebAppConfiguration
资源路径是基于文件系统的,而@ConextConfiguration
资源位置是基于类路径的。
下面的示例显示,我们可以通过指定一个Spring资源前缀来覆盖这两个批注的默认资源语义:
@ExtendWith(SpringExtension.class)
// classpath resource
@WebAppConfiguration("classpath:test-web-resources")
// file system resource
@ContextConfiguration("file:src/main/webapp/WEB-INF/servlet-config.xml")
class WacTests {
//...
}
将本例中的注释与上一个示例进行对比。
Working with Web Mocks
为了提供全面的Web测试支持,TestContext框架有一个默认启用的ServletTestExecutionListener
。当针对
TestExecutionListener
>WebApplicationContext进行测试时,这个
会在每个测试方法之前使用Spring Web的
RequestContextHolder
来设置默认的线程本地状态,并基于
@WebAppConfiguration
配置的基本资源路径创建一个
MockHttpServletRequest
、一个
MockHttpServletResponse
和一个
ServletWebRequest
。
ServletTestExecutionListener
还确保
MockHttpServletResponse
和
ServletWebRequest
可以注入到测试实例中,并在测试完成后清除线程本地状态。
一旦您为您的测试加载了一个WebApplicationContext
,您可能会发现您需要与Web模拟 - 交互,以设置您的测试夹具或在调用您的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;
//...
}
Context Caching
一旦TestContext框架加载了一个测试的ApplicationContext
(或WebApplicationContext
),该上下文就会被缓存,并在同一测试套件中声明相同的唯一上下文配置的所有后续测试中重复使用。要了解缓存的工作原理,重要的是要了解什么是“唯一的”和“测试套件”。
ApplicationContext
可以由用于加载它的配置参数的组合唯一标识。因此,配置参数的唯一组合被用来生成用于缓存上下文的键。TestContext框架使用以下配置参数来构建上下文缓存键:
-
位置
(来自@ConextConfiguration
) -
类
(来自@ConextConfiguration
) -
contextInitializerClasss
(来自@ConextConfiguration
) -
ConextCustomizers
(来自ConextCustomizerFactory
)--这包括@DynamicPropertySource
方法以及来自Spring Boot测试支持的各种功能,如@MockBean
和@SpyBean
。 -
ConextLoader
(来自@ConextConfiguration
) -
父级
(来自@ConextHierarchy
) -
活动配置文件
(来自@ActiveProfiles
) -
PropertySourceLocations
(来自@TestPropertySource
) -
PropertySourceProperties
(来自@TestPropertySource
) -
resource BasePath
(来自@WebAppConfiguration
)
例如,如果TestClassA
为@ConextConfiguration
的Locations
(或Value
)属性指定了{“app-config.xml”,“test-config.xml”}
,则TestContext框架加载相应的ApplicationContext
,并将其存储在静态
上下文缓存中的一个仅基于这些位置的键下。因此,如果TestClassB
还通过继承显式或隐式地为其位置定义了{“app-config.xml”,“test-config.xml”}
,但没有定义@WebAppConfiguration
、不同的ConextLoader
、不同的活动配置文件、不同的上下文初始值设定项、不同的测试属性源或不同的父上下文,则两个测试类共享相同的ApplicationContext
。这意味着加载应用程序上下文的设置成本只发生一次(每个测试套件),并且后续测试执行速度要快得多。
Test suites and forked processes
Spring TestContext框架将应用程序上下文存储在静态缓存中。这意味着上下文实际上存储在 为了从缓存机制中获益,所有测试必须在相同的进程或测试套件中运行。这可以通过在IDE中作为一个组执行所有测试来实现。同样,在使用构建框架(如Ant、Maven或Gradle)执行测试时,确保构建框架不会在测试之间分叉非常重要。例如,如果将Maven surefire插件的 |
上下文高速缓存的大小以默认的最大大小32为界。只要达到最大大小,就会使用最近最少使用(LRU)逐出策略来逐出和关闭陈旧的上下文。通过设置名为spring.test.context.cache.maxSize
.的jvm系统属性,可以从命令行或构建脚本配置最大大小或者,您可以通过SpringProperties
机制设置相同的属性。
由于在给定测试套件中加载大量应用程序上下文可能会导致套件花费不必要的长时间来运行,因此准确地知道加载和缓存了多少上下文通常是有益的。要查看底层上下文缓存的统计信息,您可以将调试日志类别的日志级别设置为<org.springframework.test.context.cache
>调试
。
在测试损坏应用程序上下文并需要重新加载的情况下(例如,通过修改应用程序对象的Bean定义或状态),您可以使用@DirtiesContext
注释您的测试类或测试方法(请参阅Spring测试注释中@DirtiesContext
的讨论)。这将指示Spring从缓存中删除上下文并重新构建应用程序上下文,然后再运行需要相同应用程序上下文的下一个测试。请注意,对@DirtiesContext
注释的支持是由DirtiesContextBeforeModesTestExecutionListener
和DirtiesContextTestExecutionListener
,提供的,它们在默认情况下处于启用状态。
ApplicationContext lifecycle and console logging
当您需要调试使用Spring TestContext框架执行的测试时,分析控制台输出(即输出到 关于由Spring框架本身或由 测试的 可以通过以下场景之一关闭测试的
如果在特定测试方法之后根据 当通过JVM关闭挂钩关闭Spring |
Context Hierarchies
在编写依赖于加载的SpringApplicationContext
的集成测试时,针对单个上下文进行测试通常就足够了。但是,有时针对ApplicationContext
实例的层次结构进行测试是有益的,甚至是必要的。例如,如果您正在开发一个Spring MVC Web应用程序,通常会有一个根WebApplicationContext
由Spring的ContextLoaderListener
加载,以及一个子WebApplicationContext
由Spring的DispatcherServlet
加载。这导致了父-子上下文层次结构,其中共享组件和基础设施配置在根上下文中声明,并在子上下文中由特定于Web的组件使用。另一个用例可以在Spring批处理应用程序中找到,在那里您通常有一个父上下文,它为共享批处理基础设施提供配置,而子上下文为特定批处理作业的配置提供配置。
您可以编写使用上下文层次结构的集成测试,方法是在单个测试类上或测试类层次结构内使用@ConextHierarchy
注释声明上下文配置。如果在测试类层次结构中的多个类上声明了上下文层次结构,则还可以合并或覆盖上下文层次结构中特定命名级别的上下文配置。合并层次结构中给定级别的配置时,配置资源类型(即,XML配置文件或组件类)必须一致。否则,在上下文层次结构中使用不同的资源类型配置不同的级别是完全可以接受的。
本节中剩余的基于JUnitJupiter的示例展示了需要使用上下文层次结构的集成测试的常见配置场景。
具有上下文层次结构的单个测试类
ControllerIntegrationTests
通过声明一个包含两个级别的上下文层次结构来表示Spring MVC Web应用程序的典型集成测试场景,一个级别用于根WebApplicationContext
(通过使用TestAppConfig
@Configuration
类加载),另一个用于Dispatcher ServletWebApplicationContext
(通过使用WebConfig
@Configuration
类加载)。自动连接到测试实例中的WebApplicationContext
是子上下文(即层次结构中最低的上下文)的上下文。以下清单显示了此配置方案:
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextHierarchy({ @ContextConfiguration(classes = TestAppConfig.class), @ContextConfiguration(classes = WebConfig.class) })
class ControllerIntegrationTests {
@Autowired
WebApplicationContext wac;
// ...
}
具有隐式父上下文的类层次结构
本例中的测试类定义了测试类层次结构中的上下文层次结构。AbstractWebTests
声明Spring Web应用程序中根WebApplicationContext
的配置。但是请注意,AbstractWebTest
没有声明@ContextHierarchy
。因此,AbstractWebTest
的子类可以选择性地参与上下文层次结构,或者遵循@ConextConfiguration
的标准语义。SoapWebServiceTests
和RestWebServiceTests
都扩展了AbstractWebTests
,并使用@ConextHierarchy
定义了上下文层次结构。结果是加载了三个应用程序上下文(@ConextConfiguration
的每个声明一个),并且根据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 {}
合并上下文层次结构配置的类层次结构
本例中的类显示了命名层次结构级别的使用,以便合并上下文层次结构中特定级别的配置。BaseTests
在层次结构中定义两个级别:父
和子
。ExtendedTests
扩展BaseTest
,并指示Spring TestContext框架合并子
层次结构的上下文配置,方法是确保@ConextConfiguration
中的name
属性中声明的名称都是子
。结果是加载了三个应用程序上下文:一个用于/app-config.xml
,一个用于/user-config.xml
,一个用于{“/user-config.xml”,“/order-config.xml”}
。与前面的示例一样,从/app-config.xml
加载的应用程序上下文被设置为从/user-config.xml
和{“/user-config.xml”,“/order-config.xml”}
加载的上下文的父上下文。以下清单显示了此配置方案:
@ExtendWith(SpringExtension.class)
@ContextHierarchy({ @ContextConfiguration(name = "parent", locations = "/app-config.xml"), @ContextConfiguration(name = "child", locations = "/user-config.xml") })
class BaseTests {}
@ContextHierarchy( @ContextConfiguration(name = "child", locations = "/order-config.xml") )
class ExtendedTests extends BaseTests {}
具有覆盖上下文层次结构配置的类层次结构
与前面的示例不同,此示例演示了如何通过将@ConextConfiguration
中的heritLocations
标志设置为FALSE
来覆盖上下文层次结构中给定命名级别的配置。因此,ExtendedTests
的应用程序上下文仅从/test-user-config.xml
加载,并将其父级设置为从/app-config.xml
加载的上下文。以下清单显示了此配置方案:
@ExtendWith(SpringExtension.class)
@ContextHierarchy({ @ContextConfiguration(name = "parent", locations = "/app-config.xml"), @ContextConfiguration(name = "child", locations = "/user-config.xml") })
class BaseTests {}
@ContextHierarchy( @ContextConfiguration( name = "child", locations = "/test-user-config.xml", inheritLocations = false ))
class ExtendedTests extends BaseTests {}
Dirtying a context within a context hierarchy
If you use @DirtiesContext in a test whose context is configured as part of a context hierarchy, you can use the hierarchyMode flag to control how the context cache is cleared. For further details, see the discussion of @DirtiesContext in Spring Testing Annotations and the @DirtiesContext javadoc. |
3.5.7. Dependency Injection of Test Fixtures
当您使用DependencyInjectionTestExecutionListener
(它是默认配置的)时,测试实例的依赖项是从您使用@ConextConfiguration
或相关注释配置的应用程序上下文中的Bean注入的。您可以使用setter注入和/或字段注入,这取决于您选择了哪些批注,以及您是将它们放在setter方法还是字段上。如果您使用的是JUnitJupiter,您还可以选择使用构造函数注入(参见依赖项注入和SpringExtension
)。为了与Spring的基于注释的注入支持保持一致,您还可以使用Spring的@AuTower
注释或来自JSR-330的@Inject
注释来进行字段和setter注入。
For testing frameworks other than JUnit Jupiter, the TestContext framework does not participate in instantiation of the test class. Thus, the use of @Autowired or @Inject for constructors has no effect for test classes. |
Although field injection is discouraged in production code, field injection is actually quite natural in test code. The rationale for the difference is that you will never instantiate your test class directly. Consequently, there is no need to be able to invoke a public constructor or setter method on your test class. |
因为@Autwire
用于按类型执行自动装配,所以如果您有多个相同类型的Bean定义,则不能为那些特定的Bean依赖此方法。在这种情况下,您可以将@AuTower
与@限定符
结合使用。您也可以选择将@Inject
与@Named
结合使用。或者,如果您的测试类可以访问其<代码>应用程序上下文
,则可以通过(例如)调用applicationContext.getBean(“titleRepository”,标题存储库
来执行显式查找。
如果您不希望将依赖项注入应用于测试实例,请不要使用@AuTower
或@Inject
注释字段或setter方法。或者,您可以通过使用@TestExecutionListeners
显式配置您的类并从侦听器列表中省略DependencyInjectionTestExecutionListener.class
来完全禁用依赖项注入。
考虑测试HibernateTitleRepository
类的场景,如目标部分所述。接下来的两个代码清单演示了如何在字段和setter方法上使用@AuTower
。在所有示例代码清单之后显示应用程序上下文配置。
以下代码清单中的依赖项注入行为并不特定于JUnitJupiter。相同的依赖注入技术可以与任何支持的测试框架结合使用。 下面的示例调用静态断言方法,如 |
第一个代码清单显示了一个测试类的基于JUnitJupiter的实现,它使用@AuTower
进行字段注入:
@ExtendWith(SpringExtension.class)
// specifies the Spring configuration to load for this test fixture
@ContextConfiguration("repository-config.xml")
class HibernateTitleRepositoryTests {
// this instance will be dependency injected by type
@Autowired
HibernateTitleRepository titleRepository;
@Test
void findById() {
Title title = titleRepository.findById(new Long(10));
assertNotNull(title);
}
}
或者,您也可以将类配置为使用@AuTower
进行setter注入,如下所示:
@ExtendWith(SpringExtension.class)
// specifies the Spring configuration to load for this test fixture
@ContextConfiguration("repository-config.xml")
class HibernateTitleRepositoryTests {
// this instance will be dependency injected by type
HibernateTitleRepository titleRepository;
@Autowired
void setTitleRepository(HibernateTitleRepository titleRepository) {
this.titleRepository = titleRepository;
}
@Test
void findById() {
Title title = titleRepository.findById(new Long(10));
assertNotNull(title);
}
}
前面的代码清单使用的是@ConextConfiguration
注释引用的同一个XML上下文文件(即store-config.xml
)。下面显示了此配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- this bean will be injected into the HibernateTitleRepositoryTests class -->
<bean id="titleRepository" class="com.foo.repository.hibernate.HibernateTitleRepository">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>
<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
<!-- configuration elided for brevity -->
</bean>
</beans>
如果您从Spring提供的测试基类进行扩展,而该基类恰好在其某个setter方法上使用了
Java
Kotlin
指定的限定符值指示要注入的特定 |
3.5.8. Testing Request- and Session-scoped Beans
从早年起,Spring就支持请求和会话范围的Bean,您可以通过以下步骤测试请求范围和会话范围的Bean:
-
通过使用
@WebAppConfiguration
注释您的测试类,确保为您的测试加载了WebApplicationContext
。 -
将模拟请求或会话注入到您的测试实例中,并根据需要准备测试夹具。
-
调用您从配置的
WebApplicationContext
中检索到的Web组件(使用依赖项注入)。 -
针对模拟执行断言。
下一个代码片段显示了登录用例的XML配置。请注意,userService
Bean依赖于请求范围的loginAction
Bean。此外,LoginAction
通过使用Spel表达式实例化,这些表达式从当前的HTTP请求中检索用户名和密码。在我们的测试中,我们希望通过TestContext框架管理的模拟来配置这些请求参数。下面的清单显示了此用例的配置:
<beans>
<bean id="userService" class="com.example.SimpleUserService" c:loginAction-ref="loginAction"/>
<bean id="loginAction" class="com.example.LoginAction" c:username="#{request.getParameter('user')}" c:password="#{request.getParameter('pswd')}" scope="request">
<aop:scoped-proxy/>
</bean>
</beans>
在RequestScopedBeanTest
中,我们将UserService
(即被测试的主题)和MockHttpServletRequest
注入到我们的测试实例中。在questScope()
测试方法中,我们通过在提供的MockHttpServletRequest
中设置请求参数来设置测试夹具。当在userService
上调用loginUser()
方法时,我们确信用户服务可以访问当前MockHttpServletRequest
的请求范围loginAction
(即,我们刚刚在其中设置了参数)。然后,我们可以根据用户名和密码的已知输入对结果执行断言。以下清单显示了如何执行此操作:
@SpringJUnitWebConfig
class RequestScopedBeanTests {
@Autowired UserService userService;
@Autowired MockHttpServletRequest request;
@Test
void requestScope() {
request.setParameter("user", "enigma");
request.setParameter("pswd", "$pr!ng");
LoginResults results = userService.loginUser();
// assert results
}
}
下面的代码片段类似于我们之前看到的请求作用域Bean的代码片段。但是,这一次,userService
Bean依赖于会话范围的UserPreferences
Bean。注意,UserPreferences
Bean是通过使用Spel表达式实例化的,该表达式从当前的HTTP会话中检索主题。在我们的测试中,我们需要在由TestContext框架管理的模拟会话中配置一个主题。以下示例显示了如何执行此操作:
<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
中设置预期的主题
属性来设置测试夹具。当在userService
上调用cessUserPreferences()
方法时,我们可以确保用户服务可以访问当前MockHttpSession
的会话范围的userPreferences
,并且我们可以根据配置的主题对结果执行断言。以下示例显示了如何执行此操作:
@SpringJUnitWebConfig
class SessionScopedBeanTests {
@Autowired UserService userService;
@Autowired MockHttpSession session;
@Test
void sessionScope() throws Exception {
session.setAttribute("theme", "blue");
Results results = userService.processUserPreferences();
// assert results
}
}
3.5.9. Transaction Management
在TestContext框架中,事务由默认配置的代码管理,即使您没有在测试类上显式声明<TransactionalTestExecutionListener
,>@TestExecutionListeners。然而,要启用对事务的支持,您必须在ApplicationContext
中配置一个PlatformTransactionManager
Bean,该Bean加载了@ConextConfiguration
语义(稍后将提供更多详细信息)。此外,您必须在测试的类或方法级别声明Spring的@Transaction
注释。
Test-managed Transactions
测试管理事务是通过使用TransactionalTestExecutionListener
以声明方式或使用TestTransaction
(稍后介绍)以编程方式管理的事务。不应将此类事务与Spring管理的事务(由Spring在为测试加载的ApplicationContext
中直接管理的事务)或应用程序管理的事务(在测试调用的应用程序代码中以编程方式管理的事务)混淆。Spring管理的事务和应用程序管理的事务通常参与测试管理的事务。但是,如果使用Required
或Support
以外的任何传播类型配置Spring管理的或应用程序管理的事务(有关详细信息,请参阅事务传播的讨论),请务必小心。
Preemptive timeouts and test-managed transactions
在将测试框架中的任何形式的抢占式超时与Spring的测试管理事务结合使用时,必须谨慎。 具体地说,在调用当前测试方法之前,Spring的测试支持将事务状态绑定到当前线程(通过 可能发生这种情况的情况包括但不限于以下情况。
|
Enabling and Disabling Transactions
使用@Transaction
注释测试方法会导致测试在一个事务中运行,默认情况下,该事务在测试完成后自动回滚。如果一个测试类用@Transaction
注释,则该类层次结构中的每个测试方法都在一个事务中运行。未使用@Transaction
注释的测试方法(在类或方法级别)不在事务内运行。注意,测试生命周期方法不支持@Transaction
--例如,使用JUnit Jupiter的@BeForeAll
、@BeForeEach
等注释的方法。此外,使用@Transaction
注释但将传播
属性设置为NOT_SUPPORTED
或Never
的测试不会在事务中运行。
Attribute | Supported for test-managed transactions |
---|---|
|
是 |
|
仅支持 |
|
不是 |
|
不是 |
|
不是 |
|
否:改用 |
|
否:改用 |
方法级生命周期方法--例如,用JUnitJupiter的 如果您需要在事务内的套件级别或类级别生命周期方法中运行代码,您可能希望将相应的 |
请注意,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"));
}
}
Transaction Rollback and Commit Behavior
默认情况下,测试事务将在测试完成后自动回滚;但是,可以通过@Commit
和@Rollback
注释声明性地配置事务提交和回滚行为。有关详细信息,请参阅批注支持部分中的相应条目。
Programmatic Transaction Management
您可以通过使用TestTransaction
中的静态方法以编程方式与测试管理的事务交互。例如,您可以在测试方法中、方法之前和方法之后使用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"));
}
}
Running Code Outside of a Transaction
有时,您可能需要在事务性测试方法之前或之后但在事务上下文 - 之外运行某些代码,以便在运行测试之前验证初始数据库状态,或者在测试运行后验证预期的事务性提交行为(如果测试配置为提交事务)。对于这样的场景,TransactionalTestExecutionListener
支持@BeForeTransaction
和@AfterTransaction
注释。您可以使用这些批注之一来批注测试类中的任何void
方法或测试接口中的任何void
默认方法,并且TransactionalTestExecutionListener
可以确保您的Bre Transaction方法或After Transaction方法在适当的时间运行。
Any before methods (such as methods annotated with JUnit Jupiter’s @BeforeEach ) and any after methods (such as methods annotated with JUnit Jupiter’s @AfterEach ) are run within a transaction. In addition, methods annotated with @BeforeTransaction or @AfterTransaction are not run for test methods that are not configured to run within a transaction. |
Configuring a Transaction Manager
TransactionalTestExecutionListener
期望在SpringApplicationContext
中为测试定义一个PlatformTransactionManager
Bean。如果测试的ApplicationContext
中有多个PlatformTransactionManager
实例,您可以使用@Transaction(“myTxMgr”)
或@Transaction(TransactionManager=“myTxMgr”)
声明一个限定符,或者TransactionManagementConfigurer
可以由@Configuration
类实现。有关用于在测试的<TestContextTransactionUtils.retrieveTransactionManager()
>ApplicationContext中查找事务管理器的算法的详细信息,请参考javadoc。
Demonstration of All Transaction-related Annotations
下面的基于JUnitJupiter的示例显示了一个虚构的集成测试场景,其中突出显示了所有与事务相关的注释。该示例的目的不是演示最佳实践,而是演示如何使用这些注释。有关详细信息和配置示例,请参阅注释支持一节。@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
}
}
Avoid false positives when testing ORM code
当您测试操纵Hibernate会话状态或JPA持久化上下文的应用程序代码时,请确保刷新运行该代码的测试方法中的底层工作单元。未能刷新底层工作单元可能会产生误报:您的测试通过了,但相同的代码在活动的生产环境中抛出异常。请注意,这适用于任何维护内存中工作单元的ORM框架。在下面的基于Hibernate的示例测试用例中,一个方法显示假阳性,另一个方法正确地显示刷新会话的结果:
Java
Kotlin
以下示例显示了JPA的匹配方法:
Java
Kotlin
|
Testing ORM entity lifecycle callbacks
与在测试ORM代码时避免误报的说明类似,如果您的应用程序使用实体生命周期回调(也称为实体侦听器),请确保刷新运行该代码的测试方法中的底层工作单元。未能刷新或清除底层工作单元可能会导致无法调用某些生命周期回调。 例如,使用JPA时,除非在实体保存或更新后调用 下面的示例展示了如何刷新
Java
Kotlin
有关使用所有JPA生命周期回调的工作示例,请参阅Spring框架测试套件中的JpaEntityListenerTests。 |
3.5.10. Executing SQL Scripts
在编写针对关系数据库的集成测试时,运行SQL脚本来修改数据库架构或将测试数据插入到表中通常是有益的。Spring-jdbc
模块通过在加载SpringApplicationContext
时执行SQL脚本,为初始化嵌入式或现有数据库提供支持。有关详细信息,请参阅嵌入式数据库支持和使用嵌入式数据库测试数据访问逻辑。
虽然在加载ApplicationContext
时初始化一次用于测试的数据库非常有用,但有时必须能够在集成测试期间修改数据库。以下各节说明如何在集成测试期间以编程方式和声明方式运行SQL脚本。
Executing SQL scripts programmatically
Spring提供了以下选项,用于在集成测试方法中以编程方式执行SQL脚本。
-
org.springframework.jdbc.datasource.init.ScriptUtils
-
org.springframework.jdbc.datasource.init.ResourceDatabasePopulator
-
org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests
-
org.springframework.test.context.testng.AbstractTransactionalTestNGSpringContextTests
ScriptUtils
提供用于使用SQL脚本的静态实用程序方法集合,主要用于框架内部使用。但是,如果您需要完全控制如何解析和运行SQL脚本,ScriptUtils
可能比后面介绍的其他一些替代方案更适合您的需要。有关详细信息,请参阅ScriptUtils
中的各个方法的javadoc。
ResourceDatabasePopator
提供基于对象的API,用于通过使用外部资源中定义的SQL脚本以编程方式填充、初始化或清理数据库。ResourceDatabasePopator
提供用于配置分析和运行脚本时使用的字符编码、语句分隔符、注释分隔符和错误处理标志的选项。每个配置选项都有一个合理的缺省值。有关默认值的详细信息,请参阅javadoc。若要运行在ResourceDatabasePopator
中配置的脚本,可以调用Popate(Connection)
方法对java.sql.Connection
运行填充器,或调用Execute(DataSource)
方法对javax.sql.DataSource
运行填充器。下面的示例为测试架构和测试数据指定SQL脚本,将语句分隔符设置为@@
,然后针对数据源
运行脚本:
@Test
void databaseTest() {
ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
populator.addScripts(
new ClassPathResource("test-schema.sql"),
new ClassPathResource("test-data.sql"));
populator.setSeparator("@@");
populator.execute(this.dataSource);
// run code that uses the test schema and data
}
请注意,ResourceDatabasePopator
在内部委托ScriptUtils
解析和运行SQL脚本。类似地,AbstractTransactionalJUnit4SpringContextTests
和AbstractTransactionalTestNGSpringContextTests
>中的ecuteSqlScrip(..)
方法在内部使用ResourceDatabasePopator
来运行SQL脚本。有关详细信息,请参阅各种ecuteSqlScript(..)
方法的Javadoc。
Executing SQL scripts declaratively with @Sql
除了前面提到的以编程方式运行SQL脚本的机制之外,您还可以在Spring TestContext框架中以声明方式配置SQL脚本。具体地说,您可以在测试类或测试方法上声明@SQL
注释,以配置单个SQL语句或指向应该在集成测试方法之前或之后针对给定数据库运行的SQL脚本的资源路径。对@SQL
的支持由SqlScriptsTestExecutionListener
提供,默认情况下处于启用状态。
Method-level @Sql declarations override class-level declarations by default. As of Spring Framework 5.2, however, this behavior may be configured per test class or per test method via @SqlMergeMode . See Merging and Overriding Configuration with @SqlMergeMode for further details. |
Path Resource Semantics
每条路径都被解释为一个Spring资源
。普通路径(例如,“schema.sql”
)被视为相对于定义测试类的包的类路径资源。以斜杠开头的路径被视为绝对类路径资源(例如,“/org/Example/schema.sql”
)。使用指定的资源协议加载引用URL的路径(例如,以classpath:
、file:
、http:
为前缀的路径)。
以下示例显示如何在基于JUnit Jupiter的集成测试类中的类级别和方法级别使用@SQL
:
@SpringJUnitConfig
@Sql("/test-schema.sql")
class DatabaseTests {
@Test
void emptySchemaTest() {
// run code that uses the test schema without any test data
}
@Test
@Sql({"/test-schema.sql", "/test-user-data.sql"})
void userTest() {
// run code that uses the test schema and test data
}
}
Default Script Detection
如果未指定SQL脚本或语句,则会尝试检测默认
脚本,具体取决于声明@SQL
的位置。如果无法检测到默认值,则抛出IllegalStateException
。
-
类级别声明:如果带注释的测试类为
com.example.MyTest
,则对应的默认脚本为classpath:com/example/MyTest.sql
. -
方法级声明:如果带注释的测试方法被命名为
testMethod()
,并且在com.example.MyTest
类中定义,则对应的默认脚本为classpath:com/example/MyTest.testMethod.sql
.
Declaring Multiple @Sql
Sets
如果您需要为给定的测试类或测试方法配置多个SQL脚本集,但每个集具有不同的语法配置、不同的错误处理规则或不同的执行阶段,则可以声明@SQL
的多个实例。在Java 8中,您可以使用@SQL
作为可重复的批注。否则,您可以使用@SqlGroup
注释作为声明@SQL
的多个实例的显式容器。
以下示例显示如何将@SQL
用作Java 8的可重复批注:
@Test
@Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`"))
@Sql("/test-user-data.sql")
void userTest() {
// run code that uses the test schema and test data
}
在上一个示例中呈现的场景中,test-schema.sql
脚本对单行注释使用了不同的语法。
下面的示例与前面的示例相同,只是@SQL
声明组合在@SqlGroup
中。在Java 8和更高版本中,@SqlGroup
的使用是可选的,但您可能需要使用@SqlGroup
以与其他JVM语言(如Kotlin)兼容。
@Test
@SqlGroup({ @Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`")), @Sql("/test-user-data.sql") )}
void userTest() {
// run code that uses the test schema and test data
}
Script Execution Phases
默认情况下,SQL脚本在相应的测试方法之前运行。但是,如果您需要在测试方法之后运行一组特定的脚本(例如,清理数据库状态),则可以在@SQL
中使用ecutionPhase
属性,如下面的示例所示:
@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
}
请注意,Isolated
和After_TEST_METHOD
分别是从Sql.TransactionModel
和Sql.ExecutionState
静态导入的。
Script Configuration with @SqlConfig
您可以使用@SqlConfig
注释来配置脚本解析和错误处理。当被声明为集成测试类上的类级注释时,@SqlConfig
充当测试类层次结构中所有SQL脚本的全局配置。当使用@SQL
注释的CONFIG
属性直接声明时,@SqlConfig
用作封闭的@SQL
注释中声明的SQL脚本的本地配置。@SqlConfig
中的每个属性都有一个隐式缺省值,该值记录在相应属性的javadoc中。由于Java语言规范中为批注属性定义的规则,不幸的是,不可能为批注属性赋值NULL
。因此,为了支持重写继承的全局配置,@SqlConfig
属性具有显式缺省值“”
(对于字符串)、{}
(对于数组)或默认
(对于枚举)。此方法通过提供“”
、{}
或Default
以外的值,允许@SqlConfig
的局部声明有选择地覆盖@SqlConfig
的全局声明中的单个属性。只要本地@SqlConfig
属性不提供“”
、{}
或Default
以外的显式值,就会继承全局@SqlConfig
属性。因此,显式本地配置优先于全局配置。
@SQL
和@SqlConfig
提供的配置选项等同于ScriptUtils
和ResourceDatabasePopator
支持的配置选项,但它们是<;jdbc:initialize-database/>;
XML命名空间元素提供的配置选项的超集。详见@SQL
和@SqlConfig中各个属性的javadoc。
@SQL
的事务管理
默认情况下,SqlScriptsTestExecutionListener
为使用@SQL
配置的脚本推断所需的事务语义。具体地说,根据<TransactionalTestExecutionListener
>@SqlConfig中的Transaction
属性的配置值以及测试的ApplicationContext
中是否存在PlatformTransactionManager
,在现有的Spring管理的事务中(例如,由测试的代码>@Transaction由代码管理的测试的事务)或在独立的事务中运行SQL脚本。但是,作为最低要求,测试的ApplicationContext
中必须存在javax.sql.DataSource
。
如果SqlScriptsTestExecutionListener
用来检测DataSource
和PlatformTransactionManager
并推断事务语义的算法不符合您的需要,您可以通过设置@SqlConfig
的DataSource
和TransactionManager
属性来指定显式名称。此外,您可以通过设置@SqlConfig
的TransactionMode
属性来控制事务传播行为(例如,脚本是否应该在独立的事务中运行)。尽管对@SQL事务管理的所有受支持选项的全面讨论超出了本参考手册的范围,但@SqlConfig和SqlScriptsTestExecutionListener
的javadoc提供了详细信息,并且下面的示例显示了使用JUnitJupiter和使用@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.");
}
}
注意,在运行usersTest()
方法之后,不需要清理数据库,因为TransactionalTestExecutionListener
会自动回滚对数据库所做的任何更改(无论是在测试方法中还是在/test-data.sql
脚本中)(有关详细信息,请参阅事务管理)。
Merging and Overriding Configuration with @SqlMergeMode
从Spring Framework5.2开始,可以将方法级@SQL
声明与类级声明合并。例如,这允许您为每个测试类提供一次数据库模式或一些公共测试数据的配置,然后为每个测试方法提供额外的、特定于用例的测试数据。要启用@SQL
合并,请使用@SqlMergeMode(Merge)
注释您的测试类或测试方法。要禁用特定测试方法(或特定测试子类)的合并,您可以通过@SqlMergeMode(Override)
切换回默认模式。有关示例和更多详细信息,请参阅@SqlMergeMode
批注文档部分。
3.5.11. Parallel Test Execution
当使用Spring TestContext框架时,Spring Framework5.0引入了对在单个JVM中并行执行测试的基本支持。通常,这意味着大多数测试类或测试方法可以并行运行,而不需要对测试代码或配置进行任何更改。
For details on how to set up parallel test execution, see the documentation for your testing framework, build tool, or IDE. |
请记住,将并发性引入测试套件可能会导致意外的副作用、奇怪的运行时行为,以及间歇性或随机失败的测试。因此,Spring团队为何时不并行运行测试提供了以下一般指导原则。
如果测试符合以下条件,请不要并行运行测试:
-
使用Spring框架的
@DirtiesContext
支持。 -
使用Spring Boot的
@MockBean
或@SpyBean
支持。 -
使用JUnit4的
@FixMethodOrder
支持或任何旨在确保测试方法以特定顺序运行的测试框架功能。但是,请注意,如果整个测试类是并行运行的,则这不适用。 -
更改共享服务或系统(如数据库、Message Broker、文件系统等)的状态。这既适用于嵌入式系统,也适用于外部系统。
如果并行测试执行失败,并出现异常,声明当前测试的 这可能是由于使用了 |
Parallel test execution in the Spring TestContext Framework is only possible if the underlying TestContext implementation provides a copy constructor, as explained in the javadoc for TestContext . The DefaultTestContext used in Spring provides such a constructor. However, if you use a third-party library that provides a custom TestContext implementation, you need to verify that it is suitable for parallel test execution. |
3.5.12. TestContext Framework Support Classes
本节描述支持Spring TestContext框架的各种类。
Spring JUnit 4 Runner
Spring TestContext框架通过一个定制运行器(在JUnit4.12或更高版本上受支持)提供与JUnit4的完全集成。通过使用@RunWith(SpringJUnit4ClassRunner.class)
或更短的@runwith(SpringRunner.class)
变体注释测试类,开发人员可以实现标准的基于JUnit4的单元和集成测试,同时获得TestContext框架的好处,例如支持加载应用程序上下文、测试实例的依赖注入、事务性测试方法执行等。如果希望将Spring TestContext框架与其他运行器(如JUnit4的参数化
运行器)或第三方运行器(如MockitoJUnitRunner
)一起使用,您可以选择使用Spring对JUnit规则的支持。
以下代码清单显示了配置测试类以使用自定义SpringRunner
运行的最低要求:
@RunWith(SpringRunner.class)
@TestExecutionListeners({})
public class SimpleTest {
@Test
public void testMethod() {
// test logic...
}
}
在上面的示例中,@TestExecutionListeners
配置了一个空列表,以禁用默认监听器,否则需要通过@ConextConfiguration
配置ApplicationContext
。
Spring JUnit 4 Rules
org.springframework.test.context.junit4.rules
包提供以下JUnit4规则(在JUnit4.12或更高版本上受支持):
-
SpringClassRule
-
SpringMethodRule
SpringClassRule
是支持Spring TestContext框架类级功能的JUnitTestRule
,而SpringMethodRule
是支持Spring TestContext框架实例级和方法级功能的JUnit方法规则
。
与SpringRunner
相比,Spring基于规则的JUnit支持具有独立于任何org.juit.runner.Runner
实现的优势,因此可以与现有的替代运行器(如JUnit4的参数化
)或第三方运行器(如MockitoJUnitRunner
)结合使用。
若要支持TestContext框架的全部功能,您必须将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...
}
}
JUnit 4 Support Classes
org.springframework.test.context.junit4
包为基于JUnit4的测试用例提供了以下支持类(在JUnit4.12或更高版本上受支持):
-
AbstractJUnit4SpringContext测试
-
AbstractTransactionalJUnit4SpringContextTests
AbstractJUnit4SpringConextTests
是一个抽象的基本测试类,它在JUnit4环境中集成了Spring TestContext框架和显式ApplicationContext
测试支持。当您扩展AbstractJUnit4SpringContextTests
,时,您可以访问<代码>受保护的
<代码>应用上下文
实例变量,您可以使用该变量来执行显式的Bean查找或测试整个上下文的状态。
AbstractTransactionalJUnit4SpringContextTests
是<代码>AbstractJUnit4SpringConextTests的抽象事务扩展,它为JDBCAccess添加了一些方便的功能。此类需要在ApplicationContext
中定义一个javax.sql.DataSource
Bean和一个PlatformTransactionManager
Bean。当您扩展AbstractTransactionalJUnit4SpringContextTests
,时,您可以访问受保护的
JDBCDEMPLATE
实例变量,您可以使用该变量来运行SQL语句来查询数据库。在运行与数据库相关的应用程序代码之前和之后,您都可以使用这样的查询来确认数据库状态,而Spring确保这样的查询在与应用程序代码相同的事务范围内运行。当与ORM工具结合使用时,务必避免误报。正如JDBC测试支持中所提到的,AbstractTransactionalJUnit4SpringContextTests
还通过使用前面提到的jdbcTemplate
提供了委托给JdbcTestUtils
中的方法的方便方法。此外,AbstractTransactionalJUnit4SpringContextTests
提供了一个ecuteSqlScrip(..)
方法,用于针对配置的数据源
运行SQL脚本。
These classes are a convenience for extension. If you do not want your test classes to be tied to a Spring-specific class hierarchy, you can configure your own custom test classes by using @RunWith(SpringRunner.class) or Spring’s JUnit rules. |
SpringExtension for JUnit Jupiter
Spring TestContext框架提供了与JUnit5中引入的JUnitJupiter测试框架的完全集成。通过使用@ExtendWith(SpringExtension.class)
,注释测试类,您可以实现标准的基于JUnitJupiter的单元和集成测试,同时获得TestContext框架的好处,例如支持加载应用程序上下文、测试实例的依赖注入、事务性测试方法执行等。
此外,由于JUnitJupiter中丰富的扩展API,Spring在JUnit4和TestNG支持的特性集之外提供了以下特性:
-
测试构造函数、测试方法和测试生命周期回调方法的依赖项注入。有关更多详细信息,请参阅使用
SpringExtension
进行依赖注入。 -
基于Spel表达式、环境变量、系统属性等对条件测试执行的强大支持。有关详细信息和示例,请参阅Spring JUnit Jupiter测试注释中的
@EnabledIf
和@DisabledIf
文档。 -
定制合成的批注结合了来自Spring和JUnitJupiter的批注。有关详细信息,请参阅Meta-Annotation Support for Testing中的
@TransactionalDevTestConfig
和@TransactionalIntegrationTest
示例。
以下代码清单显示如何将测试类配置为将SpringExtension
与@ConextConfiguration
结合使用:
// Instructs JUnit Jupiter to extend the test with Spring support.
@ExtendWith(SpringExtension.class)
// Instructs Spring to load an ApplicationContext from TestConfig.class
@ContextConfiguration(classes = TestConfig.class)
class SimpleTests {
@Test
void testMethod() {
// test logic...
}
}
由于您还可以在JUnit5中将批注用作元批注,因此Spring提供了@SpringJUnitConfig
和@SpringJUnitWebConfig
组合批注,以简化测试ApplicationContext
和JUnitJupiter的配置。
下面的示例使用@SpringJUnitConfig
减少上一个示例中使用的配置量:
// Instructs Spring to register the SpringExtension with JUnit
// Jupiter and load an ApplicationContext from TestConfig.class
@SpringJUnitConfig(TestConfig.class)
class SimpleTests {
@Test
void testMethod() {
// test logic...
}
}
同样,下面的示例使用@SpringJUnitWebConfig
创建一个WebApplicationContext
以用于JUnit Jupiter:
// Instructs Spring to register the SpringExtension with JUnit
// Jupiter and load a WebApplicationContext from TestWebConfig.class
@SpringJUnitWebConfig(TestWebConfig.class)
class SimpleWebTests {
@Test
void testMethod() {
// test logic...
}
}
有关更多详细信息,请参阅Spring JUnitJupiter测试注释中的@SpringJUnitConfig
和@SpringJUnitWebConfig
的文档。
Dependency Injection with SpringExtension
SpringExtension
实现了JUnit Jupiter的参数解析
扩展API,它允许Spring为测试构造函数、测试方法和测试生命周期回调方法提供依赖项注入。
具体地说,SpringExtension
可以将测试的ApplicationContext
中的依赖项注入到测试构造函数和方法中,这些构造函数和方法用@BeForeAll
、@AfForeAll
、@BeForeEach
、@AfterEach
、@RepeatedTest
、@参数测试
等标注。
Constructor Injection
如果JUnitJupiter测试类的构造函数中的特定参数是ApplicationContext
类型(或其子类型),或者使用@Autwire
、@限定符
或@Value
进行注释或元注释,则Spring会使用测试的ApplicationContext
中的相应Bean或值来注入该特定参数的值。
如果构造函数被认为是可自动生成的,那么也可以将Spring配置为自动生成测试类构造函数的所有参数。如果满足下列条件之一(按优先顺序),则认为构造函数是可自动生成的。
-
该构造函数使用
@Autwire
进行注释。 -
@TestConstructor
在测试类上存在或元存在,并且auTower ireMode
属性设置为All
。 -
默认的测试构造函数自动布线模式已更改为
全部
。
有关@TestConstructor
的用法以及如何更改全局测试构造函数自动布线模式的详细信息,请参阅@TestConstructor
。
If the constructor for a test class is considered to be autowirable, Spring assumes the responsibility for resolving arguments for all parameters in the constructor. Consequently, no other ParameterResolver registered with JUnit Jupiter can resolve parameters for such a constructor. |
如果在测试方法之前或之后使用 原因是 要将 |
在下面的示例中,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
}
请注意,此功能允许测试依赖项是最终
,因此是不可变的。
如果spring.test.constructor.autowire.mode
属性设置为all
(参见@TestConstructor
),我们可以省略上一个示例中构造函数上的@Autwire
声明,结果如下所示。
@SpringJUnitConfig(TestConfig.class)
class OrderServiceIntegrationTests {
private final OrderService orderService;
OrderServiceIntegrationTests(OrderService orderService) {
this.orderService = orderService;
}
// tests that use the injected OrderService
}
Method Injection
如果JUnitJupiter测试方法或测试生命周期回调方法中的参数是ApplicationContext
类型(或其子类型),或者使用@AuTower
、@限定符
或@Value
进行注释或元注释,则Spring将使用测试的ApplicationContext
中的相应Bean为该特定参数注入值。
在下面的示例中,Spring将TestConfig.class
加载的ApplicationContext
中的OrderService
注入到deleteOrder()
测试方法中:
@SpringJUnitConfig(TestConfig.class)
class OrderServiceIntegrationTests {
@Test
void deleteOrder(@Autowired OrderService orderService) {
// use orderService from the test's ApplicationContext
}
}
由于JUnit Jupiter中参数解析程序
支持的健壮性,您还可以将多个依赖项注入到单个方法中,不仅来自Spring,还来自JUnit Jupiter本身或其他第三方扩展。
下面的例子展示了如何让Spring和JUnitJupiter同时将依赖项注入placeOrderRepeatly()
测试方法。
@SpringJUnitConfig(TestConfig.class)
class OrderServiceIntegrationTests {
@RepeatedTest(10)
void placeOrderRepeatedly(RepetitionInfo repetitionInfo, @Autowired OrderService orderService) {
// use orderService from the test's ApplicationContext
// and repetitionInfo from JUnit Jupiter
}
}
注意,使用来自JUnit Jupiter的@RepeatedTest
使测试方法能够访问RepeatedInfo
。
@Nested
test class configuration
从Spring Framework5.0开始,Spring TestContext框架就支持在JUnit Jupiter中的@嵌套
测试类上使用与测试相关的批注;然而,直到Spring Framework5.3类级测试配置批注才像从超类继承一样从封闭类继承。
Spring Framework5.3引入了对从封装类继承测试类配置的一流支持,默认情况下将继承此类配置。若要从默认继承
模式更改为重写
模式,可以使用@NestedTestConfiguration(EnclosingConfiguration.OVERRIDE)
.注释单个@嵌套
测试类显式的@NestedTestConfiguration
声明将应用于带注释的测试类及其任何子类和嵌套类。因此,您可以用@NestedTestConfiguration
来注释顶级测试类,并且这将递归地应用于它的所有嵌套测试类。
为了允许开发团队将缺省值更改为覆盖
--例如,为了与Spring Framework5.0到5.2兼容--可以通过JVM系统属性或类路径根目录中的spring.properties
文件全局更改缺省模式。有关详细信息,请参阅“更改默认封闭配置继承模式”说明。
尽管下面的“Hello World”示例非常简单,但它展示了如何在由@nesteed
测试类继承的顶级类上声明公共配置。在这个特定的例子中,只继承了TestConfig
配置类。每个嵌套的测试类都提供了自己的一组活动配置文件,从而为每个嵌套的测试类生成了不同的ApplicationContext
(有关详细信息,请参阅上下文缓存)。请参考支持的批注列表,了解哪些批注可以在@nested
测试类中继承。
@SpringJUnitConfig(TestConfig.class)
class GreetingServiceTests {
@Nested
@ActiveProfiles("lang_en")
class EnglishGreetings {
@Test
void hello(@Autowired GreetingService service) {
assertThat(service.greetWorld()).isEqualTo("Hello World");
}
}
@Nested
@ActiveProfiles("lang_de")
class GermanGreetings {
@Test
void hello(@Autowired GreetingService service) {
assertThat(service.greetWorld()).isEqualTo("Hallo Welt");
}
}
}
TestNG Support Classes
org.springframework.test.context.testng
包为基于TestNG的测试用例提供了以下支持类:
-
AbstractTestNGSpringConextTests
-
AbstractTransactionalTestNGSpringContextTests
AbstractTestNGSpringConextTests
是一个抽象的基本测试类,它集成了Spring TestContext框架与TestNG环境中显式的ApplicationContext
测试支持。当您扩展AbstractTestNGSpringContextTests
,时,您可以访问<代码>受保护的
<代码>应用上下文
实例变量,您可以使用该变量来执行显式的Bean查找或测试整个上下文的状态。
AbstractTransactionalTestNGSpringContextTests
是<代码>AbstractTestNGSpringConextTest的抽象事务扩展,它为JDBCAccess添加了一些方便的功能。此类需要在ApplicationContext
中定义一个javax.sql.DataSource
Bean和一个PlatformTransactionManager
Bean。当您扩展AbstractTransactionalTestNGSpringContextTests
,时,您可以访问受保护的
JDBCDEMPLATE
实例变量,您可以使用该变量来运行SQL语句来查询数据库。在运行与数据库相关的应用程序代码之前和之后,您都可以使用这样的查询来确认数据库状态,而Spring确保这样的查询在与应用程序代码相同的事务范围内运行。当与ORM工具结合使用时,务必避免误报。正如JDBC测试支持中所提到的,AbstractTransactionalTestNGSpringContextTests
还通过使用前面提到的jdbcTemplate
提供了委托给JdbcTestUtils
中的方法的方便方法。此外,AbstractTransactionalTestNGSpringContextTests
提供了一个ecuteSqlScrip(..)
方法,用于针对配置的数据源
运行SQL脚本。
These classes are a convenience for extension. If you do not want your test classes to be tied to a Spring-specific class hierarchy, you can configure your own custom test classes by using @ContextConfiguration , @TestExecutionListeners , and so on and by manually instrumenting your test class with a TestContextManager . See the source code of AbstractTestNGSpringContextTests for an example of how to instrument your test class. |
3.5.13. Ahead of Time Support for Tests
本章介绍了Spring对使用Spring TestContext框架的集成测试的提前(AOT)支持。
该测试支持使用以下特性扩展了Spring的核心AOT支持。
AOT模式当前不支持 |
要提供特定于测试的运行时提示以在GraalVM本机映像中使用,您可以选择以下选项。
-
实现一个自定义
TestRuntimeHintsRegistrar
并通过META-INF/Spring/aot.Factory
全局注册它。 -
实现自定义
RuntimeHintsRegistrar
并通过<代码>META-INF/Spring/aot.Factory@ImportRuntimeHints
.在测试类上本地注册它 -
有关Spring的核心运行时提示和批注支持的详细信息,请参阅运行时提示。
|
如果实现自定义ConextLoader
,则它必须实现AotConextLoader
,以便提供AOT构建时处理和AOT运行时执行支持。但是请注意,由Spring框架和Spring Boot提供的所有上下文加载器实现都已经实现了AotConextLoader
。
如果您实现自定义
AotTestExecutionListener
>TestExecutionListener,则它必须实现
Spring-test模块中的
SqlScriptsTestExecutionListener
。
3.6. WebTestClient
WebTestClient
是为测试服务器应用程序而设计的HTTP客户端。它包装了Spring的WebClient,并使用它来执行请求,但公开了一个用于验证响应的测试外观。WebTestClient
可以进行端到端的HTTP测试。它还可以用于通过模拟服务器请求和响应对象在没有运行服务器的情况下测试Spring MVC和Spring WebFlux应用程序。
Kotlin users: See this section related to use of the WebTestClient . |
3.6.1. Setup
若要设置WebTestClient
,您需要选择要绑定到的服务器设置。这可以是几个模拟服务器设置选项之一,也可以是到活动服务器的连接。
Bind to Controller
此设置允许您通过模拟请求和响应对象测试特定的控制器,而无需运行服务器。
对于WebFlux应用程序,使用以下工具加载相当于WebFlux Java配置的基础设施,注册给定的控制器,并创建WebHandler链来处理请求:
WebTestClient client =
WebTestClient.bindToController(new TestController()).build();
对于Spring MVC,使用以下委托StandaloneMockMvcBuilder加载相当于WebMvc Java配置的基础设施,注册给定的控制器,并创建MockMvc的实例来处理请求:
WebTestClient client =
MockMvcWebTestClient.bindToController(new TestController()).build();
Bind to ApplicationContext
此设置允许您使用Spring MVC或Spring WebFlux基础设施和控制器声明加载Spring配置,并使用它通过模拟请求和响应对象处理请求,而无需运行服务器。
对于WebFlux,使用以下代码将SpringApplicationContext
传递给WebHttpHandlerBuilder以创建处理请求的WebHandler链:
@SpringJUnitConfig(WebConfig.class) (1)
class MyTests {
WebTestClient client;
@BeforeEach
void setUp(ApplicationContext context) { (2)
client = WebTestClient.bindToApplicationContext(context).build(); (3)
}
}
1 | Specify the configuration to load |
2 | Inject the configuration |
3 | Create the WebTestClient |
对于Spring MVC,在将Spring
@ExtendWith(SpringExtension.class)
@WebAppConfiguration("classpath:META-INF/web-resources") (1)
@ContextHierarchy({ @ContextConfiguration(classes = RootConfig.class), @ContextConfiguration(classes = WebConfig.class) })
class MyTests {
@Autowired
WebApplicationContext wac; (2)
WebTestClient client;
@BeforeEach
void setUp() {
client = MockMvcWebTestClient.bindToApplicationContext(this.wac).build(); (3)
}
}
1 | Specify the configuration to load |
2 | Inject the configuration |
3 | Create the WebTestClient |
Bind to Router Function
此设置允许您通过模拟请求和响应对象测试功能端点,而无需运行服务器。
对于WebFlux,使用以下委托给RouterFunctions.toWebHandler
的服务器设置来处理请求:
RouterFunction<?> route = ... client = WebTestClient.bindToRouterFunction(route).build();
对于Spring MVC,目前没有测试WebMvc功能端点的选项。
3.6.2. Writing Tests
WebTestClient
提供与WebClient相同的API,直到使用Exchange()
执行请求。有关如何准备包含任何内容(包括表单数据、多部分数据等)的请求的示例,请参阅WebClient文档。
在调用exchange()
之后,WebTestClient
与WebClient
不同,而是继续执行验证响应的工作流。
要断言响应状态和标头,请使用以下命令:
client.get().uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON);
如果希望即使其中一个失败也断言所有预期,则可以使用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内容解码为byte[]
或为空正文。
并对生成的更高级别对象执行断言:
client.get().uri("/persons")
.exchange()
.expectStatus().isOk()
.expectBodyList(Person.class).hasSize(3).contains(person);
如果内置断言不足,您可以改为使用对象并执行任何其他断言:
client.get().uri("/persons/1") .exchange() .expectStatus().isOk() .expectBody(Person.class) .consumeWith(result -> { // custom assertions (e.g. AssertJ)... });
或者,您可以退出工作流并获取EntityExchangeResult
:
EntityExchangeResult<Person> result = client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody(Person.class)
.returnResult();
When you need to decode to a target type with generics, look for the overloaded methods that accept ParameterizedTypeReference instead of Class<T> . |
No Content
如果预期响应不包含内容,则可以按如下方式断言:
client.post().uri("/persons")
.body(personMono, Person.class)
.exchange()
.expectStatus().isCreated()
.expectBody().isEmpty();
如果您想忽略响应内容,下面的代码将不带任何断言地释放该内容:
client.get().uri("/persons/123")
.exchange()
.expectStatus().isNotFound()
.expectBody(Void.class);
JSON Content
您可以使用不带目标类型的expectBody()
对原始内容执行断言,而不是通过更高级别的对象。
要使用JSONAssert验证完整的JSON内容:
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");
Streaming Responses
要测试可能无限的流,如“Text/Event-stream”
或“app/x-ndjson”
,请首先验证响应状态和Header,然后获得FlosExchangeResult
:
FluxExchangeResult<MyEvent> result = client.get().uri("/events")
.accept(TEXT_EVENT_STREAM)
.exchange()
.expectStatus().isOk()
.returnResult(MyEvent.class);
现在,您已经准备好使用反应器测试
中的StepVerier
使用响应流:
Flux<Event> eventFlux = result.getResponseBody();
StepVerifier.create(eventFlux)
.expectNext(person)
.expectNextCount(4)
.consumeNextWith(p -> ...)
.thenCancel()
.verify();
MockMvc Assertions
WebTestClient
是一个HTTP客户端,因此它只能验证客户端响应中的内容,包括状态、标头和正文。
在使用MockMvc服务器设置测试Spring MVC应用程序时,您可以选择对服务器响应执行进一步的断言。为此,首先在断言Body之后获取ExchangeResult
:
// For a response with a body
EntityExchangeResult<Person> result = client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody(Person.class)
.returnResult();
// For a response without a body
EntityExchangeResult<Void> result = client.get().uri("/path")
.exchange()
.expectBody().isEmpty();
然后切换到MockMvc服务器响应断言:
MockMvcWebTestClient.resultActionsFor(result)
.andExpect(model().attribute("integer", 3))
.andExpect(model().attribute("string", "a string value"));
3.7. MockMvc
Spring MVC测试框架,也称为MockMvc,为测试Spring MVC应用程序提供支持。它执行完整的Spring MVC请求处理,但通过模拟请求和响应对象,而不是运行的服务器。
MockMvc可以单独用于执行请求和验证响应。也可以通过WebTestClient使用MockMvc作为服务器来处理请求。WebTestClient
的优势是可以选择使用更高级别的对象而不是原始数据,并且能够切换到针对活动服务器的完整端到端HTTP测试并使用相同的测试API。
3.7.1. Overview
您可以通过实例化控制器、向其注入依赖项并调用其方法来为Spring MVC编写普通单元测试。然而,这样的测试不验证请求映射、数据绑定、消息转换、类型转换、验证,也不涉及任何支持@InitBinder
、@ModelAttribute
或@ExceptionHandler
方法。
Spring MVC测试框架,也称为MockMvc
,旨在为无需运行服务器的Spring MVC控制器提供更完整的测试。它通过调用DispatcherServlet
并从Spring-test
模块传递Servlet API的“mock”实现,该模块复制了完整的Spring MVC请求处理,而无需运行服务器。
MockMvc是一个服务器端测试框架,它允许您使用轻量级和有针对性的测试来验证Spring MVC应用程序的大部分功能。您可以单独使用它来执行请求和验证响应,也可以通过插入MockMvc作为服务器来处理请求的WebTestClientAPI使用它。
Static Imports
当直接使用MockMvc执行请求时,您将需要静态导入:
-
MockMvcBuilders。*
-
MockMvcRequestBuilders。*
-
MockMvcResultMatcher。*
-
MockMvcResultHandler。*
记住这一点的一个简单方法是搜索MockMvc*
。如果使用的是Eclipse,请确保还将上面的成员添加为“最喜欢的静态成员”。
当通过WebTestClient使用MockMvc时,您不需要静态导入。WebTestClient
提供了无需静态导入的流畅API。
Setup Choices
MockMvc可以通过以下两种方式之一进行设置。一种方法是直接指向您想要测试并以编程方式配置Spring MVC基础设施的控制器。第二个是指向包含了Spring MVC和控制器基础设施的Spring配置。
要设置MockMvc以测试特定控制器,请使用以下命令:
class MyWebTests {
MockMvc mockMvc;
@BeforeEach
void setup() {
this.mockMvc = MockMvcBuilders.standaloneSetup(new AccountController()).build();
}
// ...
}
或者,您也可以在通过WebTestClient进行测试时使用此设置,它委托给相同的构建器,如上所示。
要通过Spring配置设置MockMvc,请使用以下方法:
@SpringJUnitWebConfig(locations = "my-servlet-context.xml")
class MyWebTests {
MockMvc mockMvc;
@BeforeEach
void setup(WebApplicationContext wac) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}
// ...
}
或者,您也可以在通过WebTestClient进行测试时使用此设置,它委托给相同的构建器,如上所示。
您应该使用哪个设置选项?
webAppConextSetup
加载您的实际Spring MVC配置,从而得到更完整的集成测试。由于TestContext框架缓存了加载的Spring配置,因此它有助于保持测试快速运行,即使您在测试套件中引入了更多测试。此外,您可以通过Spring配置将模拟服务注入控制器,以保持专注于测试Web层。以下示例使用Mockito声明模拟服务:
<bean id="accountService" class="org.mockito.Mockito" factory-method="mock">
<constructor-arg value="org.example.AccountService"/>
</bean>
然后,您可以将模拟服务注入到测试中,以设置和验证您的预期,如下例所示:
@SpringJUnitWebConfig(locations = "test-servlet-context.xml")
class AccountTests {
@Autowired
AccountService accountService;
MockMvc mockMvc;
@BeforeEach
void setup(WebApplicationContext wac) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}
// ...
}
而StandaloneSetup
则更接近于单元测试。它一次测试一个控制器。您可以手动注入带有模拟依赖项的控制器,并且不涉及加载Spring配置。这类测试更侧重于样式,可以更容易地看到正在测试的控制器、是否需要任何特定的Spring MVC配置才能工作,等等。StandaloneSetup
也是编写特殊测试以验证特定行为或调试问题的一种非常方便的方法。
与大多数“集成与单元测试”的争论一样,没有正确或错误的答案。然而,使用StandaloneSetup
确实意味着需要额外的webAppConextSetup
测试来验证您的Spring MVC配置。或者,您可以使用webAppConextSetup
编写所有测试,以便始终针对实际的Spring MVC配置进行测试。
Setup Features
无论您使用哪个MockMvc构建器,所有MockMvcBuilder
实现都提供了一些常见且非常有用的功能。例如,您可以为所有请求声明Accept
头,并在所有响应中预期状态为200和Content-Type
头,如下所示:
// static import of MockMvcBuilders.standaloneSetup
MockMvc mockMvc = standaloneSetup(new MusicController())
.defaultRequest(get("/").accept(MediaType.APPLICATION_JSON))
.alwaysExpect(status().isOk())
.alwaysExpect(content().contentType("application/json;charset=UTF-8"))
.build();
此外,第三方框架(和应用程序)可以预先打包安装指令,例如MockMvcConfigurer
中的指令。Spring框架有一个这样的内置实现,可以帮助跨请求保存和重用HTTP会话。您可以按如下方式使用它:
// static import of SharedHttpSessionConfigurer.sharedHttpSession
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new TestController())
.apply(sharedHttpSession())
.build();
// Use mockMvc to perform requests...
参见ConfigurableMockMvcBuilder
的javadoc获取所有MockMvc构建器特性的列表,或者使用集成开发环境来探索可用的选项。
Performing Requests
本节介绍如何单独使用MockMvc执行请求和验证响应。如果通过WebTestClient
使用MockMvc,请参阅编写测试中的相应部分。
执行使用任何HTTP方法的请求,如下例所示:
// static import of MockMvcRequestBuilders.*
mockMvc.perform(post("/hotels/{id}", 42).accept(MediaType.APPLICATION_JSON));
您还可以执行内部使用MockMultipartHttpServletRequest
的文件上传请求,这样就不需要对分块请求进行实际解析。相反,您必须将其设置为类似于以下示例:
mockMvc.perform(multipart("/doc").file("a1", "ABC".getBytes("UTF-8")));
您可以使用URI模板样式指定查询参数,如下例所示:
mockMvc.perform(get("/hotels?thing={thing}", "somewhere"));
您还可以添加表示查询或表单参数的Servlet请求参数,如下例所示:
mockMvc.perform(get("/hotels").param("thing", "somewhere"));
如果应用程序代码依赖于Servlet请求参数,并且不显式检查查询字符串(这是最常见的情况),则使用哪个选项并不重要。但是,请记住,URI模板提供查询参数在请求通过参数(…)提供的参数时被解码)方法应已解码。
在大多数情况下,最好将上下文路径和Servlet路径排除在请求URI之外。如果您必须使用完整的请求URI进行测试,请确保相应地设置上下文路径
和servletPath
,以便请求映射工作,如下例所示:
mockMvc.perform(get("/app/main/hotels/{id}").contextPath("/app").servletPath("/main"))
在前面的示例中,为每个执行的请求设置上下文路径
和servletPath
会很麻烦。相反,您可以设置默认请求属性,如下例所示:
class MyWebTests {
MockMvc mockMvc;
@BeforeEach
void setup() {
mockMvc = standaloneSetup(new AccountController())
.defaultRequest(get("/")
.contextPath("/app").servletPath("/main")
.accept(MediaType.APPLICATION_JSON)).build();
}
}
上述属性影响通过MockMvc
实例执行的每个请求。如果在给定请求上也指定了相同的属性,则它将覆盖缺省值。这就是为什么默认请求中的HTTP方法和URI无关紧要,因为它们必须在每个请求上指定。
Defining Expectations
您可以通过在执行请求后附加一个或多个和Expect(..)
调用来定义预期,如下面的示例所示。一旦一个预期落空,就不会再断言其他预期。
// static import of MockMvcRequestBuilders.* and MockMvcResultMatchers.*
mockMvc.perform(get("/accounts/1")).andExpect(status().isOk());
您可以通过在执行请求后添加和ExspectAll(..)
来定义多个预期,如下例所示。与andExpect(..)
相反,andExspectAll(..)
保证所有提供的预期都将被断言,并且所有失败都将被跟踪和报告。
// static import of MockMvcRequestBuilders.* and MockMvcResultMatchers.*
mockMvc.perform(get("/accounts/1")).andExpectAll(
status().isOk(),
content().contentType("application/json;charset=UTF-8"));
MockMvcResultMatcher.*
提供了一些预期,其中一些进一步嵌套了更详细的预期。
预期分为两大类。第一类断言验证响应的属性(例如,响应状态、标头和内容)。这些是需要断言的最重要的结果。
第二类断言超越了回应。这些断言允许您检查Spring MVC特定的方面,例如哪个控制器方法处理了请求、是否引发并处理了异常、模型的内容是什么、选择了什么视图、添加了哪些闪存属性等等。它们还允许您检查特定于Servlet的方面,例如请求和会话属性。
以下测试断言绑定或验证失败:
mockMvc.perform(post("/persons"))
.andExpect(status().isOk())
.andExpect(model().attributeHasErrors("person"));
很多时候,在编写测试时,转储已执行请求的结果是很有用的。您可以这样做,其中print()
是来自MockMvcResultHandters
的静态导入:
mockMvc.perform(post("/persons"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(model().attributeHasErrors("person"));
只要请求处理不会导致未处理的异常,print()
方法就会将所有可用的结果数据打印到System.out
。还有一个log()
方法和另外两个print()
方法,一个接受OutputStream
,另一个接受Writer
。例如,调用print(System.err)
将结果数据打印到System.err
,而调用print(MyWriter)
将结果数据打印到自定义编写器。如果希望记录结果数据而不是打印结果数据,可以调用<org.springframework.test.web.servlet.result
>log()方法,该方法将结果数据作为单个调试
消息记录在日志记录类别下。
在某些情况下,您可能希望直接访问结果并验证以其他方式无法验证的内容。这可以通过在所有其他预期之后追加.andReturn()
来实现,如下面的示例所示:
MvcResult mvcResult = mockMvc.perform(post("/persons")).andExpect(status().isOk()).andReturn();
// ...
如果所有测试都重复相同的预期,则可以在构建MockMvc
实例时设置一次通用预期,如下例所示:
standaloneSetup(new SimpleController())
.alwaysExpect(status().isOk())
.alwaysExpect(content().contentType("application/json;charset=UTF-8"))
.build()
请注意,如果不创建单独的MockMvc
实例,则始终应用通用预期,并且无法覆盖这些预期。
当JSON响应内容包含使用Spring HATEOAS创建的超媒体链接时,您可以使用JsonPath表达式验证生成的链接,如下例所示:
mockMvc.perform(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"));
Async Requests
本节介绍如何单独使用MockMvc来测试异步请求处理。如果通过WebTestClient使用MockMvc,则无需特殊操作即可使异步请求工作,因为WebTestClient
会自动执行本节所述的操作。
在Spring MVC测试中,可以通过以下方式测试异步请求:首先断言生成的异步值,然后手动执行异步调度,最后验证响应。以下是对返回DeferredResult
、Callable
或反应性类型(如反应器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 | Check response status is still unchanged |
2 | Async processing must have started |
3 | Wait and assert the async result |
4 | Manually perform an ASYNC dispatch (as there is no running container) |
5 | Verify the final response |
Streaming Responses
测试流响应(如服务器发送的事件)的最佳方式是通过WebTestClient,它可以用作测试客户端,连接到MockMvc
实例,在没有运行服务器的情况下在Spring MVC控制器上执行测试。例如:
WebTestClient client = MockMvcWebTestClient.bindToController(new SseController()).build();
FluxExchangeResult<Person> exchangeResult = client.get()
.uri("/persons")
.exchange()
.expectStatus().isOk()
.expectHeader().contentType("text/event-stream")
.returnResult(Person.class);
// Use StepVerifier from Project Reactor to test the streaming response
StepVerifier.create(exchangeResult.getResponseBody())
.expectNext(new Person("N0"), new Person("N1"), new Person("N2"))
.expectNextCount(4)
.consumeNextWith(person -> assertThat(person.getName()).endsWith("7"))
.thenCancel()
.verify();
WebTestClient
还可以连接到直播服务器并执行完全端到端集成测试。在Spring Boot中也支持这一点,您可以测试正在运行的服务器。
Filter Registrations
设置MockMvc
实例时,可以注册一个或多个ServletFilter
实例,如下例所示:
mockMvc = standaloneSetup(new PersonController()).addFilters(new CharacterEncodingFilter()).build();
注册的筛选器通过Spring-test
中的MockFilterChain
调用,最后一个筛选器委托给DispatcherServlet
。
MockMvc vs End-to-End Tests
MockMVc构建在来自Spring-test
模块的Servlet API模拟实现之上,并且不依赖于运行的容器。因此,与运行实际客户端和活动服务器的完全端到端集成测试相比,存在一些差异。
考虑这一点的最简单方法是从空白MockHttpServletRequest
开始。无论您添加什么内容,请求都会变成这样。可能会让您大吃一惊的是,默认情况下没有上下文路径;没有jessionid
cookie;没有转发、错误或异步分派;因此,没有实际的JSP呈现。相反,“转发”和“重定向”的URL保存在MockHttpServletResponse
中,并且可以按照预期进行断言。
这意味着,如果您使用的是JSPs,您可以验证请求被转发到的JSP页,但不会呈现任何HTML。换句话说,不会调用该JSP。但是,请注意,所有其他不依赖于转发的呈现技术,如Thymeleaf和Freemarker,都会按照预期将HTML呈现给响应体。通过@ResponseBody
方法呈现JSON、XML和其他格式也是如此。
或者,您可以考虑通过@SpringBootTest
从Spring Boot获得完整的端到端集成测试支持。请参阅Spring Boot参考指南。
每种方法都有利弊。Spring MVC测试提供了从经典单元测试到完全集成测试的不同级别的选项。可以肯定的是,Spring MVC测试中的所有选项都不属于经典单元测试的范畴,但它们更接近经典单元测试。例如,您可以通过将模拟服务注入控制器来隔离Web层,在这种情况下,您仅通过DispatcherServlet
使用实际的Spring配置来测试Web层,因为您可能会将数据访问层与其上面的层隔离测试。此外,您还可以使用独立设置,一次只关注一个控制器,然后手动提供使其正常工作所需的配置。
使用Spring MVC测试时的另一个重要区别是,从概念上讲,这样的测试是在服务器端进行的,因此您可以检查使用了哪个处理程序、异常是否由HandlerExceptionResolver处理、模型的内容是什么、有哪些绑定错误,以及其他细节。这意味着更容易编写预期,因为服务器不像通过实际的HTTP客户端进行测试时那样是一个不透明的盒子。这通常是经典单元测试的优势:它更容易编写、推理和调试,但不会取代完全集成测试的需要。与此同时,重要的是不要忽视这样一个事实,即反应是最重要的检查。简而言之,即使在同一个项目中,也有多种测试样式和策略的空间。
Further Examples
该框架自己的测试包括许多样本测试旨在展示如何单独或通过WebTestClient使用MockMvc。浏览这些示例以了解更多想法。
3.7.2. HtmlUnit Integration
MockMvc works with templating technologies that do not rely on a Servlet Container (for example, Thymeleaf, FreeMarker, and others), but it does not work with JSPs, since they rely on the Servlet container. |
Why HtmlUnit Integration?
我脑海中浮现的最明显的问题是“我为什么需要这个?”最好的答案是通过研究一个非常基本的样例应用程序来找到。假设您有一个Spring MVC web应用程序,它支持对消息
对象的CRUD操作。该应用程序还支持对所有消息进行寻呼。你将如何着手测试它?
使用Spring MVC测试,我们可以轻松地测试是否能够创建消息
,如下所示:
MockHttpServletRequestBuilder createMessage = post("/messages/")
.param("summary", "Spring Rocks")
.param("text", "In case you didn't know, Spring Rocks!");
mockMvc.perform(createMessage)
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/messages/123"));
如果我们想要测试允许我们创建消息的表单视图,该怎么办?例如,假设我们的表单如下所示:
<form id="messageForm" action="/messages/" method="post">
<div class="pull-right"><a href="/messages/">Messages</a></div>
<label for="summary">Summary</label>
<input type="text" class="required" id="summary" name="summary" value="" />
<label for="text">Message</label>
<textarea id="text" name="text"></textarea>
<div class="form-actions">
<input type="submit" value="Create" />
</div>
</form>
如何确保我们的表单生成创建新消息的正确请求?一次天真的尝试可能如下所示:
mockMvc.perform(get("/messages/form"))
.andExpect(xpath("//input[@name='summary']").exists())
.andExpect(xpath("//textarea[@name='text']").exists());
这项测试有一些明显的缺陷。如果我们更新控制器以使用参数消息
而不是文本
,我们的表单测试将继续通过,即使HTML表单与控制器不同步。为了解决这个问题,我们可以组合我们的两个测试,如下所示:
String summaryParamName = "summary";
String textParamName = "text";
mockMvc.perform(get("/messages/form"))
.andExpect(xpath("//input[@name='" + summaryParamName + "']").exists())
.andExpect(xpath("//textarea[@name='" + textParamName + "']").exists());
MockHttpServletRequestBuilder createMessage = post("/messages/")
.param(summaryParamName, "Spring Rocks")
.param(textParamName, "In case you didn't know, Spring Rocks!");
mockMvc.perform(createMessage)
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/messages/123"));
这将降低我们的测试错误通过的风险,但仍然存在一些问题:
-
如果我们的页面上有多个表单,该怎么办?诚然,我们可以更新我们的XPath表达式,但当我们考虑更多因素时,它们会变得更加复杂:字段的类型是否正确?这些字段是否已启用?诸若此类。
-
另一个问题是,我们正在做的工作是我们预期的两倍。我们必须首先验证视图,然后使用刚才验证的相同参数提交视图。理想情况下,这可以一次完成。
-
最后,我们仍然无法解释一些事情。例如,如果表单具有我们也希望测试的JavaScript验证,该怎么办?
总体问题是,测试网页并不涉及单一的交互。相反,它是用户如何与网页交互以及该网页如何与其他资源交互的组合。例如,表单视图的结果用作用户创建消息的输入。此外,我们的表单视图可以潜在地使用影响页面行为的其他资源,例如JavaScript验证。
Integration Testing to the Rescue?
要解决前面提到的问题,我们可以执行端到端集成测试,但这有一些缺点。考虑测试允许我们浏览消息的视图。我们可能需要以下测试:
-
当消息为空时,我们的页面是否向用户显示通知,指示没有可用的结果?
-
我们的页面是否正确显示一条消息?
-
我们的页面是否正确支持分页?
要设置这些测试,我们需要确保我们的数据库包含正确的消息。这导致了一些额外的挑战:
-
确保数据库中有适当的消息可能会很繁琐。(考虑外键约束。)
-
测试可能会变得很慢,因为每次测试都需要确保数据库处于正确的状态。
-
因为我们的数据库需要处于特定的状态,所以我们不能并行运行测试。
-
对自动生成的ID、时间戳等项执行断言可能很困难。
这些挑战并不意味着我们应该完全放弃端到端集成测试。相反,我们可以通过重构详细的测试以使用运行更快、更可靠且没有副作用的模拟服务来减少端到端集成测试的数量。然后,我们可以实施少量真正的端到端集成测试,以验证简单的工作流,以确保一切都能正常工作。
HtmlUnit Integration Options
当您想要将MockMvc与HtmlUnit集成时,您有许多选择:
-
MockMvc and HtmlUnit:如果希望使用原始HtmlUnit库,请使用此选项。
-
MockMvc和WebDriver:使用此选项可以简化集成和端到端测试之间的开发和代码重用。
-
MockMvc and Geb:如果希望使用Groovy进行测试、简化开发并在集成和端到端测试之间重用代码,请使用此选项。
MockMvc and HtmlUnit
本节介绍如何集成MockMvc和HtmlUnit。如果要使用原始HtmlUnit库,请使用此选项。
MockMvc and HtmlUnit Setup
首先,确保您已经包含了对net.sourceforge.htmlunit:htmlunit
.的测试依赖项为了在ApacheHttpComponents 4.5+中使用HtmlUnit,您需要使用HtmlUnit2.18或更高版本。
我们可以使用MockMvcWebClientBuilder
轻松创建与MockMvc集成的HtmlUnitWebClient
,如下所示:
WebClient webClient;
@BeforeEach
void setup(WebApplicationContext context) {
webClient = MockMvcWebClientBuilder
.webAppContextSetup(context)
.build();
}
This is a simple example of using MockMvcWebClientBuilder . For advanced usage, see Advanced MockMvcWebClientBuilder . |
这确保了引用本地主机
作为服务器的任何URL都被定向到我们的MockMvc
实例,而不需要真正的HTTP连接。正常情况下,任何其他URL都是通过使用网络连接请求的。这让我们可以很容易地测试CDN的使用。
MockMvc and HtmlUnit Usage
现在,我们可以像往常一样使用HtmlUnit,但不需要将应用程序部署到Servlet容器。例如,我们可以请求视图创建一条具有以下内容的消息:
HtmlPage createMsgFormPage = webClient.getPage("http://localhost/messages/form");
The default context path is "" . Alternatively, we can specify the context path, as described in Advanced MockMvcWebClientBuilder . |
一旦我们有了对HtmlPage
的引用,我们就可以填写表单并提交它来创建一条消息,如下面的示例所示:
HtmlForm form = createMsgFormPage.getHtmlElementById("messageForm");
HtmlTextInput summaryInput = createMsgFormPage.getHtmlElementById("summary");
summaryInput.setValueAttribute("Spring Rocks");
HtmlTextArea textInput = createMsgFormPage.getHtmlElementById("text");
textInput.setText("In case you didn't know, Spring Rocks!");
HtmlSubmitInput submit = form.getOneHtmlElementByAttribute("input", "type", "submit");
HtmlPage newMessagePage = submit.click();
最后,我们可以验证是否成功创建了一条新消息。以下断言使用AssertJ库:
assertThat(newMessagePage.getUrl().toString()).endsWith("/messages/123");
String id = newMessagePage.getHtmlElementById("id").getTextContent();
assertThat(id).isEqualTo("123");
String summary = newMessagePage.getHtmlElementById("summary").getTextContent();
assertThat(summary).isEqualTo("Spring Rocks");
String text = newMessagePage.getHtmlElementById("text").getTextContent();
assertThat(text).isEqualTo("In case you didn't know, Spring Rocks!");
前面的代码在许多方面改进了MockMvc测试。首先,我们不再需要显式地验证我们的表单,然后创建一个看起来像表单的请求。取而代之的是,我们请求表单、填写并提交表单,从而显著减少了开销。
另一个重要因素是HtmlUnit使用Mozilla Rhino引擎来评估JavaScript。这意味着我们还可以测试页面中的JavaScript行为。
有关使用HtmlUnit的其他信息,请参阅HtmlUnit文档。
Advanced MockMvcWebClientBuilder
在到目前为止的示例中,我们已经以最简单的方式使用了MockMvcWebClientBuilder
,方法是基于Spring TestContext框架为我们加载的WebApplicationContext
构建一个WebClient
。此方法在下面的示例中重复:
WebClient webClient;
@BeforeEach
void setup(WebApplicationContext context) {
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();
}
或者,我们可以通过单独配置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();
这更加繁琐,但是,通过使用MockMvc
实例构建WebClient
,我们可以在指尖掌握MockMvc的全部功能。
For additional information on creating a MockMvc instance, see Setup Choices. |
MockMvc and WebDriver
在前面的小节中,我们了解了如何将MockMvc与原始HtmlUnitAPI结合使用。在这一节中,我们使用SelifyWebDriver中的其他抽象来使事情变得更加简单。
Why WebDriver and MockMvc?
我们已经可以使用HtmlUnit和MockMvc了,那么为什么还要使用WebDriver呢?Selify WebDiverer提供了一个非常优雅的API,使我们可以轻松地组织代码。为了更好地展示它是如何工作的,我们在本节中探索一个示例。
Despite being a part of Selenium, WebDriver does not require a Selenium Server to run your tests. |
假设我们需要确保正确创建一条消息。这些测试包括查找、填充这些输入元素,然后进行各种断言。
这种方法会导致许多单独的测试,因为我们还想测试错误条件。例如,我们希望确保如果只填写表单的一部分,就会出现错误。如果我们填写整个表单,随后应该会显示新创建的消息。
如果其中一个字段被命名为“SUMMART”,我们可能会在测试中的多个位置重复类似以下内容:
HtmlTextInput 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);
}
这样做可以确保在更改UI时不必更新所有测试。
我们甚至可以更进一步,将此逻辑放在表示我们当前所在的HtmlPage
的对象
中,如下面的示例所示:
public class CreateMessagePage {
final HtmlPage currentPage;
final HtmlTextInput summaryInput;
final HtmlSubmitInput submit;
public CreateMessagePage(HtmlPage currentPage) {
this.currentPage = currentPage;
this.summaryInput = currentPage.getHtmlElementById("summary");
this.submit = currentPage.getHtmlElementById("submit");
}
public <T> T createMessage(String summary, String text) throws Exception {
setSummary(summary);
HtmlPage result = submit.click();
boolean error = CreateMessagePage.at(result);
return (T) (error ? new CreateMessagePage(result) : new ViewMessagePage(result));
}
public void setSummary(String summary) throws Exception {
summaryInput.setValueAttribute(summary);
}
public static boolean at(HtmlPage page) {
return "Create Message".equals(page.getTitleText());
}
}
以前,这种模式称为Page Object模式。虽然我们当然可以使用HtmlUnit来实现这一点,但WebDriver提供了一些工具,我们将在下面几节中对这些工具进行探索,以使该模式更易于实现。
MockMvc and WebDriver Setup
要在Spring MVC测试框架中使用Selify WebDriver,请确保您的项目包含对org.seleniumhq.selenium:selenium-htmlunit-driver
.的测试依赖
通过使用MockMvcHtmlUnitDriverBuilder
,我们可以轻松创建一个与MockMvc集成的Selify WebDriver,如下例所示:
WebDriver driver;
@BeforeEach
void setup(WebApplicationContext context) {
driver = MockMvcHtmlUnitDriverBuilder
.webAppContextSetup(context)
.build();
}
This is a simple example of using MockMvcHtmlUnitDriverBuilder . For more advanced usage, see Advanced MockMvcHtmlUnitDriverBuilder . |
前面的示例确保将localhost
作为服务器引用的任何URL都被定向到我们的MockMvc
实例,而不需要真正的HTTP连接。正常情况下,任何其他URL都是通过使用网络连接请求的。这让我们可以很容易地测试CDN的使用。
MockMvc and WebDriver Usage
现在,我们可以像往常一样使用WebDriver,但不需要将应用程序部署到Servlet容器。例如,我们可以请求视图创建一条具有以下内容的消息:
CreateMessagePage page = CreateMessagePage.to(driver);
1 | CreateMessagePage extends the AbstractPage . We do not go over the details of AbstractPage , but, in summary, it contains common functionality for all of our pages. For example, if our application has a navigational bar, global error messages, and other features, we can place this logic in a shared location. |
2 | We have a member variable for each of the parts of the HTML page in which we are interested. These are of type WebElement . WebDriver’s PageFactory lets us remove a lot of code from the HtmlUnit version of CreateMessagePage by automatically resolving each WebElement . The PageFactory#initElements(WebDriver,Class<T>) method automatically resolves each WebElement by using the field name and looking it up by the id or name of the element within the HTML page. |
3 | We can use the @FindBy annotation to override the default lookup behavior. Our example shows how to use the @FindBy annotation to look up our submit button with a css selector (input[type=submit]). |
然后,我们可以填写表单并提交以创建一条消息,如下所示:
ViewMessagePage viewMessagePage = page.createMessage(ViewMessagePage.class, expectedSummary, expectedText);
1 | CreateMessagePage extends the AbstractPage . We do not go over the details of AbstractPage , but, in summary, it contains common functionality for all of our pages. For example, if our application has a navigational bar, global error messages, and other features, we can place this logic in a shared location. |
2 | We have a member variable for each of the parts of the HTML page in which we are interested. These are of type WebElement . WebDriver’s PageFactory lets us remove a lot of code from the HtmlUnit version of CreateMessagePage by automatically resolving each WebElement . The PageFactory#initElements(WebDriver,Class<T>) method automatically resolves each WebElement by using the field name and looking it up by the id or name of the element within the HTML page. |
3 | We can use the @FindBy annotation to override the default lookup behavior. Our example shows how to use the @FindBy annotation to look up our submit button with a css selector (input[type=submit]). |
这通过利用Page对象模式改进了HtmlUnit测试的设计。正如我们在为什么要使用WebDriver和MockMvc?中提到的,我们可以将Page对象模式与HtmlUnit一起使用,但使用WebDriver要容易得多。考虑以下CreateMessagePage
实现:
public class CreateMessagePage extends AbstractPage { (1)
(2)
private WebElement summary;
private WebElement text;
(3)
@FindBy(css = "input[type=submit]")
private WebElement submit;
public CreateMessagePage(WebDriver driver) {
super(driver);
}
public <T> T createMessage(Class<T> resultPage, String summary, String details) {
this.summary.sendKeys(summary);
this.text.sendKeys(details);
this.submit.click();
return PageFactory.initElements(driver, resultPage);
}
public static CreateMessagePage to(WebDriver driver) {
driver.get("http://localhost:9990/mail/messages/form");
return PageFactory.initElements(driver, CreateMessagePage.class);
}
}
最后,我们可以验证是否成功创建了一条新消息。以下断言使用AssertJ断言库:
assertThat(viewMessagePage.getMessage()).isEqualTo(expectedMessage);
assertThat(viewMessagePage.getSuccess()).isEqualTo("Successfully created a new message");
我们可以看到,ViewMessagePage
允许我们与自定义域模型交互。例如,它公开了一个返回消息
对象的方法:
public Message getMessage() throws ParseException {
Message message = new Message();
message.setId(getId());
message.setCreated(getCreated());
message.setSummary(getSummary());
message.setText(getText());
return message;
}
然后,我们可以在断言中使用富领域对象。
最后,我们不要忘记在测试完成后关闭WebDriver
实例,如下所示:
@AfterEach
void destroy() {
if (driver != null) {
driver.close();
}
}
有关使用WebDriver的其他信息,请参阅SelensWebDriver文档。
Advanced MockMvcHtmlUnitDriverBuilder
在到目前为止的示例中,我们已经以最简单的方式使用了MockMvcHtmlUnitDriverBuilder
,方法是基于Spring TestContext框架为我们加载的WebApplicationContext
构建WebDriver
。此方法在此处重复,如下所示:
WebDriver driver;
@BeforeEach
void setup(WebApplicationContext context) {
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();
}
或者,我们可以通过单独配置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();
这更加繁琐,但是,通过使用MockMvc
实例构建WebDriver
,我们可以在指尖掌握MockMvc的全部功能。
For additional information on creating a MockMvc instance, see Setup Choices. |
MockMvc and Geb
在上一节中,我们了解了如何在WebDriver中使用MockMvc。在本节中,我们使用geb来使我们的测试更加Groovy-er。
Why Geb and MockMvc?
GEB是由WebDriver支持的,因此它提供了许多我们从WebDriver获得的相同的好处。然而,Geb为我们处理了一些样板代码,从而使事情变得更加容易。
MockMvc and Geb Setup
我们可以很容易地使用使用MockMvc的Selify WebDriver来初始化一个Geb浏览器
,如下所示:
def setup() {
browser.driver = MockMvcHtmlUnitDriverBuilder
.webAppContextSetup(context)
.build()
}
This is a simple example of using MockMvcHtmlUnitDriverBuilder . For more advanced usage, see Advanced MockMvcHtmlUnitDriverBuilder . |
这确保了引用本地主机
作为服务器的任何URL都被定向到我们的MockMvc
实例,而不需要真正的HTTP连接。任何其他URL都是通过正常使用网络连接请求的。这让我们可以很容易地测试CDN的使用。
MockMvc and Geb Usage
现在,我们可以像往常一样使用Geb,但不需要将应用程序部署到Servlet容器。例如,我们可以请求视图创建一条具有以下内容的消息:
to CreateMessagePage
然后,我们可以填写表单并提交以创建一条消息,如下所示:
when:
form.summary = expectedSummary
form.text = expectedMessage
submit.click(ViewMessagePage)
任何未找到的无法识别的方法调用、属性访问或引用都将转发到当前页对象。这消除了我们在直接使用WebDriver时所需的大量样板代码。
与直接使用WebDriver一样,这通过使用Page对象模式改进了HtmlUnit测试的设计。如前所述,我们可以将Page Object模式与HtmlUnit和WebDriver一起使用,但使用Geb更容易。考虑一下我们新的基于Groovy的CreateMessagePage
实现:
class CreateMessagePage extends Page {
static url = 'messages/form'
static at = { assert title == 'Messages : Create'; true }
static content = {
submit { $('input[type=submit]') }
form { $('form') }
errors(required:false) { $('label.error, .alert-error')?.text() }
}
}
我们的CreateMessagePage
扩展了Page
。我们不详细介绍Page
的细节,但总而言之,它包含所有页面的公共功能。我们定义了一个URL,可以在其中找到该页面。这使我们可以导航到该页面,如下所示:
to CreateMessagePage
我们还有一个at
闭包,它确定我们是否在指定的页面上。如果我们在正确的页面上,它应该返回True
。这就是为什么我们可以断言我们的想法是正确的,如下所示:
then:
at CreateMessagePage
errors.contains('This field is required.')
We use an assertion in the closure so that we can determine where things went wrong if we were at the wrong page. |
接下来,我们创建一个内容
闭包,它指定页面中所有感兴趣的区域。我们可以使用jQuery-ish Navigator API选择我们感兴趣的内容。
最后,我们可以验证是否成功创建了一条新消息,如下所示:
then:
at ViewMessagePage
success == 'Successfully created a new message'
id
date
summary == expectedSummary
message == expectedMessage
有关如何充分利用Geb的更多详细信息,请参阅《Geb手册》用户手册。
3.8. Testing Client Applications
您可以使用客户端测试来测试在内部使用RestTemplate
的代码。其思想是声明预期的请求并提供“存根”响应,以便您可以专注于隔离测试代码(即,无需运行服务器)。以下示例显示了如何执行此操作:
RestTemplate restTemplate = new RestTemplate();
MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build();
mockServer.expect(requestTo("/greeting")).andRespond(withSuccess());
// Test code that uses the above RestTemplate ...
mockServer.verify();
在前面的示例中,MockRestServiceServer
(客户端REST测试的中心类)使用定制的ClientHttpRequestFactory
配置RestTemplate
,该客户端根据预期断言实际请求并返回“存根”响应。在本例中,我们期待一个对/greting
的请求,并希望返回一个包含文本/纯文本
内容的200响应。我们可以根据需要定义额外的预期请求和存根响应。当我们定义预期的请求和存根响应时,可以照常在客户端代码中使用RestTemplate
。在测试结束时,可以使用mock Server.Verify()
来验证是否满足了所有预期。
默认情况下,请求的预期顺序是声明期望的顺序。您可以在构建服务器时设置IGNOREEXPectOrder
选项,在这种情况下,将检查所有预期(以便)找到与给定请求匹配的项。这意味着允许以任何顺序发送请求。下面的示例使用IGNORREEXPECtOrder
:
server = MockRestServiceServer.bindTo(restTemplate).ignoreExpectOrder(true).build();
即使在默认情况下,对于无序请求,每个请求也只能运行一次。Expect
方法提供了一个重载变量,该变量接受指定计数范围的ExspectedCount
参数(例如,一次
、多次
、最大
、最小
、介于
等等)。下面的示例使用次
:
RestTemplate restTemplate = new RestTemplate();
MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build();
mockServer.expect(times(2), requestTo("/something")).andRespond(withSuccess());
mockServer.expect(times(3), requestTo("/somewhere")).andRespond(withSuccess());
// ...
mockServer.verify();
请注意,如果未设置IgnreExspectOrder
(默认设置),因此请求应按声明顺序进行,则该顺序仅适用于任何预期请求中的第一个。例如,如果“/某事”应该出现两次,然后是“/某地”三次,那么在请求“/某地”之前,应该有一个对“/某事”的请求,但是,除了随后的“/某事”和“/某地”之外,请求可以随时到来。
作为上述所有功能的替代方案,客户端测试支持还提供了ClientHttpRequestFactory
实现,您可以将其配置到RestTemplate
中,以将其绑定到MockMvc
实例。这允许使用实际的服务器端逻辑处理请求,而无需运行服务器。以下示例显示了如何执行此操作:
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
this.restTemplate = new RestTemplate(new MockMvcClientHttpRequestFactory(mockMvc));
// Test code that uses the above RestTemplate ...
3.8.1. Static Imports
与服务器端测试一样,客户端测试的流畅API需要一些静态导入。通过搜索MockRest*
可以很容易地找到它们。→用户应该将<代码>MockRestRequestMatcher.*
和<代码>MockRestResponseCreator.*
添加为“最喜欢的静态成员”,它们位于Java→编辑器→Content Assister→Favorites下。这允许在键入静态方法名称的第一个字符后使用内容助手。其他IDE(如IntelliJ)可能不需要任何额外的配置。检查对静态成员的代码完成支持。
3.8.2. Further Examples of Client-side REST Tests
Spring MVC测试自己的测试包括客户端REST测试的示例测试。
4. Further Resources
有关测试的更多信息,请参阅以下资源:
-
JUnit:“面向Java的程序员友好测试框架”。由Spring框架在其测试套件中使用,并在Spring TestContext框架中受支持。
-
TestNG:一个受JUnit启发的测试框架,增加了对测试组、数据驱动测试、分布式测试和其他特性的支持。受Spring TestContext框架支持
-
AssertJ:“流畅的Java断言”,包括对Java 8 lambdas、Streams和其他特性的支持。
-
模拟对象:维基百科上的文章。
-
MockObjects.com:专门用于模拟对象的网站,这是一种用于改进测试驱动开发中的代码设计的技术。
-
EasyMock:Java库“为接口(和通过类扩展的对象)提供模拟对象,方法是使用Java的代理机制动态生成它们。”
-
JMock:支持带有模拟对象的Java代码的测试驱动开发的库。
-
DbUnit:JUnit扩展(也可与Ant和Maven一起使用),其目标是数据库驱动的项目,并在测试运行之间将数据库置于已知状态。
-
TestContainers:支持JUnit测试的Java库,提供常见数据库的轻量级、一次性实例、SelengeWeb浏览器或任何可以在Docker容器中运行的东西。
-
the Grinder:Java负载测试框架。
-
SpringMockK:支持使用MockK而不是Mockito用Kotlin编写的Spring Boot集成测试。