0%

SpringFramework官方文档翻译-测试

测试

测试是企业软件开发不可或缺的一部分。本章重点介绍 IoC 原则为单元测试添加的 value 以及 Spring Framework 对整合测试的支持的好处。 (对企业中的测试进行全面处理超出了本次参考的范围 manual.)

单元测试

与传统的 Java EE 开发相比,依赖注入应该使 code 对容器的依赖性降低。构成 application 的 POJO 应该在 JUnit 或 TestNG 测试中可测试,使用new operator 实例化 object,不使用 Spring 或任何其他容器。您可以使用mock objects(与其他有价值的测试技术结合使用)来单独测试您的 code。如果您遵循 Spring 的 architecture 建议,那么代码库的干净分层和组件化便于单元测试。例如,您可以通过存根或 mocking DAO 或 repository 接口来测试服务层 objects,而无需在 running 单元测试时访问持久数据。

True 单元测试通常非常快速地运行,因为没有要设置的运行时基础结构。强调 true 单元测试作为开发方法的一部分可以提高您的工作效率。您可能不需要测试章节的这一部分来帮助您为 IoC-based applications 编写有效的单元测试。但是,对于某些单元测试场景,Spring Framework 提供了 mock objects 和测试支持 classes,本章对此进行了描述。

MockObjects

Spring 包含许多专用于 mocking 的包:

环境

org.springframework.mock.env包包含EnvironmentPropertySource抽象的 mock implementations(参见Bean 定义 ProfilesPropertySource 抽象)。 MockEnvironmentMockPropertySource对于 code 开发 out-of-container 测试很有用,因为 code 取决于 environment-specific properties。

JNDI

org.springframework.mock.jndi包中包含 JNDI SPI 的 实现,您可以使用它为测试套件或 stand-alone applications 设置简单的 JNDI 环境。例如,如果 JDBC DataSource实例在 test code 中绑定到与 Java EE 容器中相同的 JNDI 名称,则可以在测试方案中重复使用 application code 和 configuration 而无需修改。

ServletAPI

org.springframework.mock.web包中包含一组全面的 Servlet API mock objects,可用于测试 web 上下文,控制器和过滤器。这些 mock objects 的目标是使用 Spring 的 Web MVC framework,并且通常比动态 mock objects(例如EasyMock)或替代 Servlet API mock objects(例如MockObjects)更方便使用。

从 Spring Framework 5.0 开始,org.springframework.mock.web中的 mock objects 基于 Servlet 4.0 API。

Spring MVC Test framework 构建在 mock Servlet API objects 之上,为 Spring MVC 提供 integration 测试 framework。见Spring MVC Test Framework

SpringWebReactive

org.springframework.mock.http.server.reactive包中包含_m mplemplements of ServerHttpRequestServerHttpResponse,用于 WebFlux applications。 org.springframework.mock.web.server包中包含 mock ServerWebExchange,它取决于那些 mock 请求和响应 objects。

MockServerHttpRequestMockServerHttpResponse都从与 server-specific __mplementations 相同的抽象 base classes 扩展,并与它们共享行为。对于 example,mock 请求一旦创建就是不可变的,但您可以使用ServerHttpRequest中的mutate()方法创建修改后的实例。

在 mock 响应的 order 中正确实现 write contract 和 return 一个写完成句柄(即Mono<Void>),它默认使用Fluxcache().then(),它缓冲数据并使其可用于测试中的断言。 Applications 可以设置自定义写入 function(对于 example,以测试无限流)。

WebTestClient构建在 mock 请求和响应之上,以支持在没有 HTTP 服务器的情况下测试 WebFlux applications。 client 也可用于 running 服务器的 end-to-end 测试。

单元测试支持Classes

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

一般测试实用程序

org.springframework.test.util包中包含几个用于单元和 integration 测试的通用实用程序。

ReflectionTestUtils是 reflection-based 实用程序方法的集合。您可以在测试需要更改常量的 value,设置非public字段,调用非public setter 方法,或在测试 application code 时调用非public configuration 或生命周期回调方法时使用这些方法用例如下:

  • ORM 框架(例如 JPA 和 Hibernate)容忍privateprotected字段访问,而不是域实体中 properties 的public setter 方法。
  • Spring 支持 annotations(例如@Autowired@Inject@Resource),它们为privateprotected字段,setter 方法和 configuration 方法提供依赖注入。
  • 使用_an和@PreDestroy这样的 annotations 进行生命周期回调方法。

AopTestUtils是 AOP-related 实用程序方法的集合。您可以使用这些方法获取隐藏在一个或多个 Spring 代理后面的基础目标 object 的 reference。例如,如果您使用 library(例如 EasyMock 或 Mockito)将 bean 配置为动态 mock,并且 mock 包含在 Spring 代理中,则可能需要直接访问底层 mock 以配置对它的期望并执行验证。对于 Spring 的核心 AOP 实用程序,请参阅AopUtilsAopProxyUtils

SpringMVC测试实用程序

org.springframework.test.web包中包含ModelAndViewAssert,您可以将其与 JUnit,TestNG 或任何其他测试 framework 结合使用,以进行处理 Spring MVC ModelAndView objects 的单元测试。

单元测试 Spring MVC 控制器 要将 Spring MVC Controller classes 作为 POJO 进行单元测试,请在 Spring 的Servlet API 模拟中使用ModelAndViewAssertMockHttpServletRequestMockHttpSession等结合使用。要对 Spring MVC 和 REST Controller classes 与 Spring MVC 的WebApplicationContext configuration 一起进行彻底的 integration 测试,请改用Spring MVC Test Framework

集成测试

本节(本章的大部分内容)涵盖了 Spring applications 的 integration 测试。它包括以下主题:

概述

能够执行某些 integration 测试非常重要,无需部署到 application 服务器或连接到其他企业基础结构。这样做可以让您测试以下内容:

  • Spring IoC 容器上下文的正确连线。
  • 使用 JDBC 或 ORM 工具进行数据访问。这可以包括诸如 SQL statements,Hibernate 查询,JPA 实体映射等的正确性。

Spring Framework 为spring-test模块中的 integration 测试提供 first-class 支持。实际的 JAR 文件的 name 可能包含 release version,也可能是 long org.springframework.test形式,具体取决于你从哪里得到它(参见关于依赖管理的部分的解释)。这个 library 包含org.springframework.test包,其中包含用于 Spring 容器的 integration 测试的有价值的 classes。此测试不依赖于 application 服务器或其他部署环境。这些测试比单元测试慢得多,但比依赖部署到 application 服务器的等效 Selenium 测试或 remote 测试要快得多。

在 Spring 2.5 及更高版本中,单元和 integration 测试支持以 annotation-driven Spring TestContext Framework的形式提供。 TestContext framework 与所使用的实际测试 framework 无关,它允许在各种环境中检测测试,包括 JUnit,TestNG 等。

集成测试的目标

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

接下来的几节将介绍每个目标,并提供指向 implementation 和 configuration 详细信息的链接。

上下文管理和缓存

Spring TestContext Framework 提供 Spring ApplicationContext实例和WebApplicationContext实例的一致 loading 以及这些上下文的缓存。支持缓存加载的上下文非常重要,因为启动 time 可能会成为一个问题 - 不是因为 Spring 本身的开销,而是因为 Spring 容器实例化的 objects 需要 time 来实例化。例如,具有 50 到 100 个 Hibernate 映射 files 的项目可能需要 10 到 20 秒来加载映射 files,并且在每个测试夹具中运行每个测试之前产生该成本会导致整体测试运行速度变慢,从而降低开发人员的工作效率。

Test classes 通常声明 XML 的资源位置的 array 或 Groovy configuration 元数据 - 通常在 classpath 中 - 或者用于配置 application 的带注释的 class 的 array。这些位置或 classes 与web.xml或 production 部署的其他 configuration files 中指定的位置或 classes 相同或相似。

默认情况下,一旦加载,配置的ApplicationContext将被重用于每个测试。因此,每个测试套件仅产生一次设置成本,并且后续测试执行要快得多。在此 context 中,术语“测试套件”表示在同一 JVM 中运行所有测试 - 例如,对于给定项目或模块,所有测试都来自 Ant,Maven 或 Gradle build。在不太可能的情况下,测试会破坏 application context 并需要重新加载(例如,通过修改 bean 定义或 application object 的 state),TestContext framework 可以配置为重新加载 configuration 并重建 application context,然后再执行下一个测试。

请参阅带有 TestContext framework 的Context ManagementContext 缓存

测试fixture实例的依赖注入

当 TestContext framework 加载 application context 时,它可以选择使用依赖注入来配置 test classes 的实例。这提供了一种方便的机制,通过使用 application context 中的预配置 beans 来设置测试装置。这里的一个强大好处是您可以在各种测试场景中重用 application 上下文(例如,用于配置 Spring-managed object 图,transactional proxies,DataSource实例等),从而避免了为单个测试用例复制复杂测试夹具设置的需要。

作为一个例子,考虑一个我们有一个 class(HibernateTitleRepository)实现Title域实体的数据访问逻辑的场景。我们想编写测试以下方面的 integration 测试:

  • Spring configuration:基本上,与HibernateTitleRepository bean 的 configuration 相关的所有内容是否正确并且存在?
  • Hibernate 映射文件 configuration:是否所有映射都正确并且是否正确设置了 lazy-loading 设置?
  • HibernateTitleRepository的逻辑:此 class 的已配置实例是否按预期执行?

请参阅使用TestContext framework依赖注入测试夹具。

TransactionManagement

访问真实数据库的测试中的一个常见问题是它们对持久性 store 的 state 的影响。即使您使用开发数据库,对 state 的更改也可能会影响将来的测试。此外,许多操作(例如插入或修改持久数据)无法在 transaction 之外执行(或验证)。

TestContext framework 解决了这个问题。默认情况下,framework 为每个测试创建并回滚 transaction。您可以编写可以假设存在 transaction 的 code。如果在测试中调用事务代理的 objects,则根据其配置的 transactional 语义,它们的行为正确。此外,如果测试方法在为测试管理的 transaction 中运行时删除所选表的内容,则 transaction 默认回滚,并且数据库在执行测试之前返回其 state。通过使用测试的 application context 中定义的PlatformTransactionManager bean 为测试提供 Transactional 支持。

如果您希望 transaction 提交(异常,但在您希望特定测试填充或修改数据库时偶尔有用),您可以通过使用@Commit annotation 告诉 TestContext framework 使 transaction 提交而不是回滚。

请参阅TestContext framework的transaction management。

集成测试的支持类

Spring TestContext Framework 提供了几个abstract支持 classes,简化了 integration 测试的编写。这些基本测试 classes 为测试 framework 提供了 well-defined 钩子以及方便的实例变量和方法,使您可以访问:

  • ApplicationContext,用于执行显式 bean 查找或测试 context 的 state 作为整体。
  • 一个JdbcTemplate,用于执行 SQL statements 来查询数据库。您可以在执行 database-related application code 之前和之后使用此类查询来确认数据库 state,并且 Spring 确保此类查询在与 application code 相同的 transaction 范围内运行。与 ORM 工具结合使用时,请务必避免使用误报

此外,您可能希望使用特定于项目的实例变量和方法创建自己的自定义 application-wide 超类。

请参阅TestContext framework的支持 classes。

JDBC测试支持

org.springframework.test.jdbc包中包含JdbcTestUtils,它是 JDBC-related 实用程序函数的集合,旨在简化标准数据库测试方案。具体来说,JdbcTestUtils提供以下静态实用程序方法。

  • countRowsInTable(..):计算给定 table 中的行数。
  • countRowsInTableWhere(..):使用提供的WHERE子句计算给定 table 中的行数。
  • deleteFromTables(..):删除指定表中的所有行。
  • deleteFromTableWhere(..):使用提供的WHERE子句删除给定 table 中的行。
  • dropTables(..):删除指定的表。

AbstractTransactionalJUnit4SpringContextTestsAbstractTransactionalTestNGSpringContextTests提供了方便的方法,委托JdbcTestUtils中的上述方法。

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

Annotations

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

SpringTestingAnnotations

Spring Framework 提供了以下一组 Spring-specific注解,您可以在单元和集成测试中结合 TestContext framework 使用它们。有关详细信息,请参阅相应的 javadoc,包括默认属性值,属性别名和其他详细信息。

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

BootstrapWith注解

@BootstrapWith是一个 class-level 注释,可用于配置 Spring TestContext Framework 的引导方式。具体来说,您可以使用@BootstrapWith指定自定义TestContextBootstrapper。有关详细信息,请参阅引导 TestContext framework部分。

ContextConfiguration注解

@ContextConfiguration定义 class-level 元数据,用于确定如何加载和配置ApplicationContext for integration 测试。具体来说,@ContextConfiguration声明 application context 资源locations或用于加载 context 的带注释的classes

资源位置通常是位于 classpath 中的 XML configuration files 或 Groovy 脚本,而带注释的 classes 通常是@Configuration classes。但是,资源位置也可以引用文件系统中的 files 和脚本,带注释的 classes 可以是 component classes,依此类推。

以下 example 显示引用 XML 文件的@ContextConfiguration annotation:

1
2
3
4
@ContextConfiguration("/test-config.xml") (1)
public class XmlApplicationContextTests {
// class body...
}
1 参考 XML 文件。

以下 example 显示引用 class 的@ContextConfiguration annotation:

1
2
3
4
@ContextConfiguration(classes = TestConfig.class) (1)
public class ConfigClassApplicationContextTests {
// class body...
}
1 参考 class。

作为声明资源位置或带注释的 classes 的替代或补充,您可以使用@ContextConfiguration来声明ApplicationContextInitializer classes。以下 example 显示了这样的情况:

1
2
3
4
@ContextConfiguration(initializers = CustomContextIntializer.class) (1)
public class ContextInitializerTests {
// class body...
}
1 声明初始化程序 class。

您也可以选择使用@ContextConfiguration来声明ContextLoader策略。但请注意,您通常不需要显式配置加载器,因为默认加载器支持initializers和资源locations或带注释的classes

以下 example 使用位置和加载器:

1
2
3
4
@ContextConfiguration(locations = "/test-context.xml", loader = CustomContextLoader.class) (1)
public class CustomLoaderXmlApplicationContextTests {
// class body...
}
1 配置位置和自定义加载程序。

@ContextConfiguration支持继承资源位置或 configuration classes 以及超类声明的 context 初始值设定项。

有关详细信息,请参阅Context Management@ContextConfiguration javadocs。

WebAppConfiguration注解

@WebAppConfiguration是一个 class-level annotation,可以用来声明为 integration 测试加载的ApplicationContext应该是WebApplicationContext。仅仅在测试 class 上存在@WebAppConfiguration就可以确保为测试加载WebApplicationContext,使用"file:src/main/webapp"的默认 value 作为 web application 根目录的路径(即资源库路径)。在幕后使用资源基本路径来创建MockServletContext,其用作测试WebApplicationContextServletContext

以下 example 显示了如何使用@WebAppConfiguration annotation:

1
2
3
4
5
@ContextConfiguration
@WebAppConfiguration (1)
public class WebAppTests {
// class body...
}
1 @WebAppConfiguration annotation。

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

1
2
3
4
5
@ContextConfiguration
@WebAppConfiguration("classpath:test-web-resources") (1)
public class WebAppTests {
// class body...
}
1 指定 classpath 资源。

请注意,@WebAppConfiguration必须与@ContextConfiguration结合使用,可以在单个测试 class 中,也可以在 test class 层次结构中使用。有关更多详细信息,请参阅@WebAppConfiguration javadoc。

ContextHierarchy注解

@ContextHierarchy是一个 class-level annotation,用于为 integration 测试定义ApplicationContext实例的层次结构。 @ContextHierarchy应该使用一个或多个@ContextConfiguration实例的列表声明,每个实例在 context 层次结构中定义 level。以下示例演示了在单个测试 class 中使用@ContextHierarchy(@ContextHierarchy也可以在测试 class 层次结构中使用):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@ContextHierarchy({
@ContextConfiguration("/parent-config.xml"),
@ContextConfiguration("/child-config.xml")
})
public class ContextHierarchyTests {
// class body...
}
@WebAppConfiguration
@ContextHierarchy({
@ContextConfiguration(classes = AppConfig.class),
@ContextConfiguration(classes = WebConfig.class)
})
public class WebIntegrationTests {
// class body...
}

如果需要合并或覆盖 test class 层次结构中 context 层次结构的给定 level 的 configuration,则必须通过向 class 层次结构中每个对应 level 的@ContextConfiguration中的name属性提供相同的 value 来显式 name 该 level。有关更多示例,请参阅Context Hierarchies@ContextHierarchy javadoc。

ActiveProfiles注解

@ActiveProfiles是一个 class-level annotation,用于声明

以下 example 表明dev profile 应该是 active:

1
2
3
4
5
@ContextConfiguration
@ActiveProfiles("dev") (1)
public class DeveloperTests {
// class body...
}
1 指示dev profile 应为 active。

以下 example 表示devintegration profiles 都应该是 active:

1
2
3
4
5
@ContextConfiguration
@ActiveProfiles({"dev", "integration"}) (1)
public class DeveloperIntegrationTests {
// class body...
}
1 指示devintegration profiles 应为 active。

@ActiveProfiles默认提供对继承超类声明的 active bean 定义 profiles 的支持。您还可以通过实现自定义ActiveProfilesResolver并使用@ActiveProfilesresolver属性进行注册来以编程方式解析 active bean definition profiles。

有关示例和更多详细信息,请参阅Context Configuration with Environment Profiles@ActiveProfiles javadoc。

TestPropertySource注解

@TestPropertySource是一个 class-level 注释,您可以使用它来配置 properties files 和内联 properties 的位置,以便将Environment添加到EnvironmentPropertySources中,以便为 integration 测试加载ApplicationContext

Test property 源的优先级高于从操作系统环境或 Java 系统 properties 加载的源以及 application 以声明方式通过@PropertySource或以编程方式添加的 property 源。因此,test property 源可用于有选择地覆盖 system 和 application property 源中定义的 properties。此外,内联 properties 的优先级高于从资源位置加载的 properties。

以下 example 演示了如何从 classpath 声明 properties 文件:

1
2
3
4
5
@ContextConfiguration
@TestPropertySource("/test.properties") (1)
public class MyIntegrationTests {
// class body...
}
1 从 classpath 的根目录中的test.properties获取 properties。

以下 example 演示了如何声明内联 properties:

1
2
3
4
5
@ContextConfiguration
@TestPropertySource(properties = { "timezone = GMT", "port: 4242" }) (1)
public class MyIntegrationTests {
// class body...
}
1 声明timezoneport properties。
DirtiesContext注解

@DirtiesContext表示在执行测试期间底层 Spring ApplicationContext已被弄脏(即,测试以某种方式修改或损坏它 - 例如,通过更改 singleton bean 的 state)并且应该关闭。当 application context 被标记为脏时,它将从测试 framework 的缓存中删除并关闭。因此,对于需要具有相同 configuration 元数据的 context 的任何后续测试,都会重建基础 Spring 容器。

您可以在同一 class 或 class 层次结构中使用@DirtiesContext作为 class-level 和 method-level annotation。在这种情况下,ApplicationContext在任何此类带注释的方法之前或之后以及当前测试 class 之前或之后被标记为脏,具体取决于配置的methodModeclassMode

以下示例说明 context 何时会因各种 configuration 方案而变脏:

  • 在当前测试 class 之前,当 class 上的 class 模式设置为BEFORE_CLASS时声明。
1
2
3
4
@DirtiesContext(classMode = BEFORE_CLASS) (1)
public class FreshContextTests {
// some tests that require a new Spring container
}
1 在当前测试 class 之前弄脏了 context。
  • 在当前测试 class 之后,在 class 上声明 class 模式时设置为AFTER_CLASS(i.e.,默认为 class 模式)。
1
2
3
4
@DirtiesContext (1)
public class ContextDirtyingTests {
// some tests that result in the Spring container being dirtied
}
1 在当前测试 class 之后弄脏了 context。
  • 在当前测试 class 中的每个测试方法之前,当 class 上的 class 模式设置为BEFORE_EACH_TEST_METHOD.时声明
1
2
3
4
@DirtiesContext(classMode = BEFORE_EACH_TEST_METHOD) (1)
public class FreshContextTests {
// some tests that require a new Spring container
}
1 在每个测试方法之前弄脏 context。
  • 在当前测试 class 中的每个测试方法之后,当 class 上的 class 模式设置为AFTER_EACH_TEST_METHOD.时声明
1
2
3
4
@DirtiesContext(classMode = AFTER_EACH_TEST_METHOD) (1)
public class ContextDirtyingTests {
// some tests that result in the Spring container being dirtied
}
1 在每个测试方法之后弄脏 context。
  • 在当前测试之前,在方法模式设置为BEFORE_METHOD的方法上声明。
1
2
3
4
5
@DirtiesContext(methodMode = BEFORE_METHOD) (1)
@Test
public void testProcessWhichRequiresFreshAppCtx() {
// some logic that requires a new Spring container
}
1 在当前测试方法之前弄脏 context。
  • 在当前测试之后,在方法模式设置为AFTER_METHOD(i.e.,默认方法模式)的方法上声明。
1
2
3
4
5
@DirtiesContext (1)
@Test
public void testProcessWhichDirtiesAppCtx() {
// some logic that results in the Spring container being dirtied
}
1 在当前测试方法之后弄脏 context。

如果在 context 配置为带@ContextHierarchy的 context 层次结构的一部分的测试中使用@DirtiesContext,则可以使用hierarchyMode flag 来控制 context 缓存的清除方式。默认情况下,使用详尽的算法来清除 context 缓存,不仅包括当前 level,还包括与当前测试共享祖先 context common 的所有其他 context 层次结构。驻留在 common 祖先 context 的 sub-hierarchy 中的所有ApplicationContext实例将从 context 缓存中删除并关闭。如果穷举算法对于特定用例而言过度,则可以指定更简单的当前 level 算法,如下面的 example 所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@ContextHierarchy({
@ContextConfiguration("/parent-config.xml"),
@ContextConfiguration("/child-config.xml")
})
public class BaseTests {
// class body...
}

public class ExtendedTests extends BaseTests {

@Test
@DirtiesContext(hierarchyMode = CURRENT_LEVEL) (1)
public void test() {
// some logic that results in the child context being dirtied
}
}
1 使用 current-level 算法。

有关EXHAUSTIVECURRENT_LEVEL算法的更多详细信息,请参阅DirtiesContext.HierarchyMode javadoc。

TestExecutionListeners注解

@TestExecutionListeners定义了 class-level 元数据,用于配置应该使用TestContextManager注册的TestExecutionListener __mplement。通常,@TestExecutionListeners@ContextConfiguration一起使用。

以下 example 显示了如何注册两个TestExecutionListener implementations:

1
2
3
4
5
@ContextConfiguration
@TestExecutionListeners({CustomTestExecutionListener.class, AnotherTestExecutionListener.class}) (1)
public class CustomTestExecutionListenerTests {
// class body...
}
1 注册两个TestExecutionListener implementations。

默认情况下,@TestExecutionListeners支持继承的 listeners。有关 example 和更多详细信息,请参阅javadoc

Commit注解

@Commit表示应在测试方法完成后提交 transactional 测试方法的 transaction。您可以使用@Commit作为@Rollback(false)的直接替换来更明确地传达 code 的意图。类似于@Rollback@Commit也可以声明为 class-level 或 method-level 注释。

以下 example 显示了如何使用@Commit annotation:

1
2
3
4
5
@Commit (1)
@Test
public void testProcessWithoutRollback() {
// ...
}
1 将测试结果提交给数据库。
Rollback注解

@Rollback表示在测试方法完成后是否应回滚 transactional 测试方法的 transaction。如果是true,transaction 将被回滚。否则,提交 transaction(另请参见@Commit)。即使没有显式声明@Rollback,Spring TestContext Framework 中的 integration 测试的回滚默认为true

声明为 class-level annotation 时,@Rollback定义 test class 层次结构中所有测试方法的默认回滚语义。当声明为 method-level annotation 时,@Rollback定义特定测试方法的回滚语义,可能会覆盖 class-level @Rollback@Commit语义。

以下 example 导致测试方法的结果不被回滚(即,结果被提交到数据库):

1
2
3
4
5
@Rollback(false) (1)
@Test
public void testProcessWithoutRollback() {
// ...
}
1 不要回滚结果。
BeforeTransaction注解

@BeforeTransaction表示在 transaction 启动之前_11方法应该是 run,对于已经使用 Spring 的@Transactional annotation 在 transaction 中配置为 run 的测试方法。从 Spring Framework 4.3 开始,@BeforeTransaction方法不需要public,可以在 Java 8-based 接口默认方法中声明。

以下 example 显示了如何使用@BeforeTransaction annotation:

1
2
3
4
@BeforeTransaction (1)
void beforeTransaction() {
// logic to be executed before a transaction is started
}
1 在 transaction 之前运行此方法。
AfterTransaction注解

@AfterTransaction表示在 transaction 结束后,对于已在 transaction 中使用 Spring 的@Transactional annotation 配置为 run 的测试方法,应该 run 注释void方法。从 Spring Framework 4.3 开始,@AfterTransaction方法不需要public,可以在 Java 8-based 接口默认方法中声明。

1
2
3
4
@AfterTransaction (1)
void afterTransaction() {
// logic to be executed after a transaction has ended
}
1 在 transaction 之后运行此方法。
Sql注解

@Sql用于注释测试 class 或测试方法,以便在 integration 测试期间将 SQL 脚本配置为针对给定数据库运行。以下 example 显示了如何使用它:

1
2
3
4
5
@Test
@Sql({"/test-schema.sql", "/test-user-data.sql"}) (1)
public void userTest {
// execute code that relies on the test schema and test data
}
1 运行此测试的两个脚本。

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

SqlConfig注解

@SqlConfig定义元数据,用于确定如何解析和运行使用@Sql annotation 配置的 SQL 脚本。以下 example 显示了如何使用它:

1
2
3
4
5
6
7
8
@Test
@Sql(
scripts = "/test-user-data.sql",
config = @SqlConfig(commentPrefix = "`", separator = "@@") (1)
)
public void userTest {
// execute code that relies on the test data
}
1 在 SQL 脚本中设置 comment 前缀和分隔符。
SqlGroup注解

@SqlGroup是一个容器 annotation,它聚合了几个@Sql 注释。您可以使用@SqlGroup本机声明几个嵌套的@Sql 注释,或者可以将它与 Java 8 对可重复注释的支持结合使用,其中@Sql可以在同一个 class 或方法上多次声明,隐式生成此容器 annotation。以下 example 显示了如何声明 SQL group:

1
2
3
4
5
6
7
8
@Test
@SqlGroup({ (1)
@Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`")),
@Sql("/test-user-data.sql")
)}
public void userTest {
// execute code that uses the test schema and test data
}
1 声明一组 SQL 脚本。

标准注解支持

对于 Spring TestContext Framework 的所有配置,标准语义支持以下注释。请注意,这些注释并非特定于测试,可以在 Spring Framework 中的任何位置使用。

  • @Autowired
  • @Qualifier
  • 如果 JSR-250 存在,@Resource(javax.annotation)
  • 如果 JSR-250 存在,@ManagedBean(javax.annotation)
  • 如果 JSR-330 存在,@Inject(javax.inject)
  • 如果 JSR-330 存在,@Named(javax.inject)
  • 如果存在 JPA,@PersistenceContext(javax.persistence)
  • 如果存在 JPA,@PersistenceUnit(javax.persistence)
  • @Required
  • @Transactional

JSR-250 生命周期注释

在 Spring TestContext Framework 中,您可以在ApplicationContext中配置的任何 application 组件上使用@PostConstruct@PreDestroy标准语义。但是,这些生命周期注释在实际测试 class 中的使用受到限制。

如果 test class 中的方法使用@PostConstruct注释,则该方法在基础测试 framework 的任何 before 方法之前运行(对于 example,使用 JUnit Jupiter 的@BeforeEach注释的方法),并且这适用于 test class 中的每个测试方法。另一方面,如果测试 class 中的方法使用@PreDestroy注释,则该方法永远不会运行。因此,在 test class 中,我们建议您使用基础测试 framework 而不是@PostConstruct@PreDestroy的测试生命周期回调。

SpringJUnit4测试注解

仅当与SpringRunnerSpring 的 JUnit 4 规则Spring 的 JUnit 4 支持 classes一起使用时,才支持以下注解:

IfProfileValue注解

@IfProfileValue表示为特定测试环境启用了带注释的测试。如果配置的ProfileValueSource为提供的name返回匹配的value,则启用测试。否则,测试被禁用,并且有效地被忽略。

您可以在 class level,方法 level 或两者中应用@IfProfileValue。对于 class 或其子类中的任何方法,@IfProfileValue的使用优先于 method-level 用法。具体来说,如果在 class level 和 level 方法中都启用了测试,则启用测试。缺少@IfProfileValue意味着隐式启用了测试。这类似于 JUnit 4 的@Ignore annotation 的语义,除了@Ignore的存在总是禁用测试。

以下 example 显示了具有@IfProfileValue annotation 的测试:

1
2
3
4
5
@IfProfileValue(name="java.vendor", value="Oracle Corporation") (1)
@Test
public void testProcessWhichRunsOnlyOnOracleJvm() {
// some logic that should run only on Java VMs from Oracle Corporation
}
1 仅在 Java 供应商为“Oracle Corporation”时运行此测试。

或者,您可以使用values(带OR语义)列表配置@IfProfileValue,以便在 JUnit 4 环境中实现对测试组的 TestNG-like 支持。考虑以下 example:

1
2
3
4
5
@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 运行此测试以进行单元测试和 integration 测试。
ProfileValueSourceConfiguration注解

@ProfileValueSourceConfiguration是一个 class-level annotation,它指定在检索通过@IfProfileValue annotation 配置的 profile 值时要使用的ProfileValueSource类型。如果未为测试声明@ProfileValueSourceConfiguration,则默认使用SystemProfileValueSource。以下 example 显示了如何使用@ProfileValueSourceConfiguration

1
2
3
4
@ProfileValueSourceConfiguration(CustomProfileValueSource.class) (1)
public class CustomProfileValueSourceTests {
// class body...
}
1 使用自定义 profile value 源。
Timed注解

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

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

1
2
3
4
@Timed(millis = 1000) (1)
public void testProcessWithOneSecondTimeout() {
// some logic that should not take longer than 1 second to execute
}
1 将测试的 time 周期设置为一秒。

Spring 的@Timed annotation 具有与 JUnit 4 的@Test(timeout=…)支持不同的语义。具体来说,由于 JUnit 4 处理测试执行超时的方式(即,通过在单独的Thread中执行测试方法),如果测试花费太长,则@Test(timeout=…)抢先未通过测试。另一方面,Spring 的@Timed并没有先发制人地失败,而是在失败之前等待测试完成。

Repeat注解

@Repeat表示注释的测试方法必须重复 run。在 annotation 中指定执行测试方法的次数。

要重复的执行范围包括执行测试方法本身以及测试夹具的任何设置或拆除。以下 example 显示了如何使用@Repeat annotation:

1
2
3
4
5
@Repeat(10) (1)
@Test
public void testProcessRepeatedly() {
// ...
}
1 重复此测试十次。

SpringJUnitJupiterTestingAnnotations

仅当与SpringExtension和 JUnit Jupiter(即 JUnit 5 中的编程 model)一起使用时,才支持以下注释:

SpringJUnitConfig注解

@SpringJUnitConfig是一个组合的 annotation,它将 JUnit Jupiter 中的@ExtendWith(SpringExtension.class)与 Spring TestContext Framework 中的@ContextConfiguration组合在一起。它可以在 class level 中用作@ContextConfiguration的 drop-in 替换。关于 configuration 选项,@ContextConfiguration@SpringJUnitConfig之间的唯一区别是可以使用@SpringJUnitConfig中的value属性声明带注释的 classes。

以下 example 显示了如何使用@SpringJUnitConfig annotation 指定 configuration class:

1
2
3
4
@SpringJUnitConfig(TestConfig.class) (1)
class ConfigurationClassJUnitJupiterSpringTests {
// class body...
}
1 指定 configuration class。

以下 example 显示了如何使用@SpringJUnitConfig annotation 指定 configuration 文件的位置:

1
2
3
4
@SpringJUnitConfig(locations = "/test-config.xml") (1)
class XmlJUnitJupiterSpringTests {
// class body...
}
1 指定 configuration 文件的位置。

有关详细信息,请参阅Context Management以及@SpringJUnitConfig@ContextConfiguration的 javadoc。

SpringJUnitWebConfig注解

@SpringJUnitWebConfig是一个组合注释,它将 JUnit Jupiter 中的@ExtendWith(SpringExtension.class)与来自 Spring TestContext Framework 的@ContextConfiguration@WebAppConfiguration组合在一起。您可以在 class level 中将其用作@ContextConfiguration@WebAppConfiguration的 drop-in 替换。关于 configuration 选项,@ContextConfiguration@SpringJUnitWebConfig之间的唯一区别是您可以使用@SpringJUnitWebConfig中的value属性声明带注释的 classes bu。此外,只能使用@SpringJUnitWebConfig中的resourcePath属性覆盖value中的value属性。

以下 example 显示了如何使用@SpringJUnitWebConfig annotation 指定 configuration class:

1
2
3
4
@SpringJUnitWebConfig(TestConfig.class) (1)
class ConfigurationClassJUnitJupiterSpringWebTests {
// class body...
}
1 指定 configuration class。

以下 example 显示了如何使用@SpringJUnitWebConfig annotation 指定 configuration 文件的位置:

1
2
3
4
@SpringJUnitWebConfig(locations = "/test-config.xml") (1)
class XmlJUnitJupiterSpringWebTests {
// class body...
}
1 指定 configuration 文件的位置。

有关详细信息,请参阅Context Management以及@SpringJUnitWebConfig@ContextConfiguration@WebAppConfiguration的 javadoc。

EnabledIf注解

@EnabledIf用于表示已注释的 JUnit Jupiter 测试 class 或测试方法已启用,如果提供的expression求值为true,则应为 run。具体来说,如果表达式求值为Boolean.TRUEString等于true(忽略大小写),则启用测试。当应用于 class level 时,默认情况下也会自动启用该 class 中的所有测试方法。

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

  • Spring 表达语言(SpEL)表达式。对于 example:@EnabledIf("#{systemProperties['os.name'].toLowerCase().contains('mac')}")
  • Spring 环境中可用 property 的占位符。对于 example:@EnabledIf("${smoke.tests.enabled}")
  • 文字文字。对于 example:@EnabledIf("true")

但请注意,不是 property 占位符动态解析结果的文本文字是零实用 value,因为@EnabledIf("false")等同于@Disabled@EnabledIf("true")在逻辑上没有意义。

您可以使用@EnabledIf作为 meta-annotation 来创建自定义组合注释。对于 example,您可以创建自定义@EnabledOnMac annotation,如下所示:

1
2
3
4
5
6
7
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@EnabledIf(
expression = "#{systemProperties['os.name'].toLowerCase().contains('mac')}",
reason = "Enabled on Mac OS"
)
public @interface EnabledOnMac {}
DisabledIf注解

@DisabledIf用于表示已禁用带注释的 JUnit Jupiter 测试 class 或测试方法,如果提供的expression求值为true,则不应执行。具体来说,如果表达式求值为Boolean.TRUEString等于true(忽略大小写),则禁用测试。当应用于 class level 时,该 class 中的所有测试方法也会自动禁用。

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

  • Spring 表达语言(SpEL)表达式。对于 example:@DisabledIf("#{systemProperties['os.name'].toLowerCase().contains('mac')}")
  • Spring 环境中可用 property 的占位符。对于 example:@DisabledIf("${smoke.tests.disabled}")
  • 文字文字。对于 example:@DisabledIf("true")

但请注意,不是 property 占位符动态解析结果的文本文字是零实用 value,因为@DisabledIf("true")等同于@Disabled@DisabledIf("false")在逻辑上没有意义。

您可以使用@DisabledIf作为 meta-annotation 来创建自定义组合注释。对于 example,您可以创建自定义@DisabledOnMac annotation,如下所示:

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

Meta-Annotation支持测试

您可以使用大多数 test-related annotations 作为meta-annotations来创建自定义组合注释并减少测试套件中的配置重复。

您可以将以下各项与Spring TestContext Framework一起用作 meta-annotation。

  • @BootstrapWith
  • @ContextConfiguration
  • @ContextHierarchy
  • @ActiveProfiles
  • @TestPropertySource
  • @DirtiesContext
  • @WebAppConfiguration
  • @TestExecutionListeners
  • @Transactional
  • @BeforeTransaction
  • @AfterTransaction
  • @Commit
  • @Rollback
  • @Sql
  • @SqlConfig
  • @SqlGroup
  • @Repeat(仅在 JUnit 4 上受支持)
  • @Timed(仅在 JUnit 4 上受支持)
  • @IfProfileValue(仅在 JUnit 4 上受支持)
  • @ProfileValueSourceConfiguration(仅在 JUnit 4 上受支持)
  • @SpringJUnitConfig(仅在 JUnit Jupiter 上受支持)
  • @SpringJUnitWebConfig(仅在 JUnit Jupiter 上受支持)
  • @EnabledIf(仅在 JUnit Jupiter 上受支持)
  • @DisabledIf(仅在 JUnit Jupiter 上受支持)

考虑以下 example:

1
2
3
4
5
6
7
8
9
10
11
@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 { }

如果我们发现我们在 JUnit 4-based 测试套件中重复了前面的 configuration,我们可以通过引入一个自定义组合 annotation 来集中 Spring 的 common test configuration 来减少重复,如下所示:

1
2
3
4
5
6
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
public @interface TransactionalDevTestConfig { }

然后我们可以使用我们的自定义@TransactionalDevTestConfig annotation 来简化基于 JUnit 4 的各个 test classes 的 configuration 配置,如下所示:

1
2
3
4
5
6
7
@RunWith(SpringRunner.class)
@TransactionalDevTestConfig
public class OrderRepositoryTests { }

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

如果我们编写使用 JUnit Jupiter 的测试,我们可以进一步减少 code 重复,因为 JUnit 5 中的 annotations 也可以用作 meta-annotations。考虑以下 example:

1
2
3
4
5
6
7
8
9
10
11
@ExtendWith(SpringExtension.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
class OrderRepositoryTests { }

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

如果我们发现我们在 JUnit Jupiter-based 测试套件中重复了前面的 configuration,我们可以通过引入一个自定义组合的 annotation 来集中 Spring 和 JUnit Jupiter 的 common test configuration 来减少重复,如下所示:

1
2
3
4
5
6
7
@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 annotation 来简化基于 JUnit Jupiter 的各个 test classes 的 configuration 配置,如下所示:

1
2
3
4
5
@TransactionalDevTestConfig
class OrderRepositoryTests { }

@TransactionalDevTestConfig
class UserRepositoryTests { }

由于 JUnit Jupiter 支持使用@Test@RepeatedTestParameterizedTest和其他作为 meta-annotations,因此您还可以在测试方法 level 中创建自定义组合注释。例如,如果我们希望创建一个组合的 annotation,它将来自 JUnit Jupiter 的@Test@Tag 注释与来自 Spring 的@Transactional annotation 结合起来,我们可以创建@TransactionalIntegrationTest annotation,如下所示:

1
2
3
4
5
6
@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 annotation 来简化基于 JUnit Jupiter 的测试方法的配置,如下所示:

1
2
3
4
5
@TransactionalIntegrationTest
void saveOrder() { }

@TransactionalIntegrationTest
void deleteOrder() { }

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

SpringTestContext框架

Spring TestContext Framework(位于org.springframework.test.context包中)提供了通用的 annotation-driven 单元和 integration 测试支持,它与正在使用的测试 framework 无关。 TestContext framework 在 configuration 上的约定上也非常重要,你可以通过 annotation-based configuration 覆盖合理的默认值。

除了通用测试基础结构之外,TestContext framework 还为 JUnit 4,JUnit Jupiter(AKA JUnit 5)和 TestNG 提供了显式支持。对于 JUnit 4 和 TestNG,Spring 提供abstract支持 classes。此外,Spring 为 JUnit 4 提供了一个自定义 JUnit Runner和自定义 JUnit Rules,为 JUnit Jupiter 提供了一个自定义Extension,可以让你编写 so-called POJO test classes。 POJO test classes 不需要扩展特定的 class 层次结构,例如abstract support classes。

以下部分概述了 TestContext framework 的内部结构。如果您只对使用 framework 感兴趣并且不想使用自己的自定义 listeners 或自定义加载器扩展它,请随意直接转到 configuration(context management依赖注入transaction management),支持 classesannotation 支持部分。

Key抽象

framework 的核心由TestContextManager class 和TestContextTestExecutionListenerSmartContextLoader接口组成。为每个测试 class 创建TestContextManager(对于 example,用于执行 JUnit Jupiter 中单个测试 class 中的所有测试方法)。反过来,TestContextManager管理一个TestContext,它保存当前测试的 context。 TestContextManager还会在测试进行时更新TestContext的 state,并委托TestExecutionListener implementations,它通过提供依赖注入,管理 transactions 等来检测实际的测试执行情况。 SmartContextLoader负责为给定的测试 class 加载ApplicationContext。有关各种 implementations 的更多信息和示例,请参阅javadoc和 Spring 测试套件。

TestContext

TestContext封装了执行测试的 context(与使用中的实际测试 framework 无关),并为其负责的测试实例提供 context management 和缓存支持。如果请求,TestContext还委托SmartContextLoader加载ApplicationContext

TestContextManager

TestContextManager是 Spring TestContext Framework 的主要入口点,负责在 well-defined 测试执行点管理每个注册TestExecutionListener的单个TestContext和信令 events:

  • 在特定测试 framework 之前的任何“before class”或“before all”方法之前。
  • 测试实例 post-processing。
  • 在特定测试 framework 之前的任何“之前”或“之前”方法之前。
  • 在执行测试方法之前,但在测试设置之后。
  • 在执行测试方法之后但在测试之前立即拆除。
  • 在特定测试 framework 之后的任何“之后”或“之后”方法之后。
  • 经过特定测试 framework 的任何“after class”或“after all”方法之后。
TestExecutionListener

TestExecutionListener定义 API 以响应_l发布的TestContextManager发布的 test-execution events。见TestExecutionListener Configuration

Context加载器

ContextLoader是 Spring 2.5 中引入的策略接口,用于为 Spring TestContext Framework 管理的 integration 测试加载ApplicationContext。您应该实现SmartContextLoader而不是此接口,以提供对带注释的 classes,active bean definition profiles,test property sources,context 层次结构和WebApplicationContext支持的支持。

SmartContextLoader是 Spring 3.1 中引入的ContextLoader接口的扩展。 SmartContextLoader SPI 取代 Spring 2.5 中引入的ContextLoader SPI。具体来说,SmartContextLoader可以选择 process 资源位置,带注释的 classes 或 context 初始值设定项。此外,SmartContextLoader可以在其加载的 context 中设置 active bean definition profiles 和 test property sources。

Spring 提供以下 implementations:

  • DelegatingSmartContextLoader:两个默认加载器之一,它内部委托给AnnotationConfigContextLoaderGenericXmlContextLoaderGenericGroovyXmlContextLoader,这取决于为 test class 声明的 configuration 或默认位置或默认 configuration classes 的存在。仅当 Groovy 位于 classpath 时才启用 Groovy 支持。
  • WebDelegatingSmartContextLoader:两个默认加载器之一,它内部委托给AnnotationConfigWebContextLoaderGenericXmlWebContextLoaderGenericGroovyXmlWebContextLoader,这取决于为 test class 声明的 configuration 或默认位置或默认 configuration classes 的存在。仅当 Class 上存在@WebAppConfiguration时才使用 web ContextLoader。仅当 Groovy 位于 classpath 时才启用 Groovy 支持。
  • AnnotationConfigContextLoader:从带注释的 classes 中加载标准ApplicationContext
  • AnnotationConfigWebContextLoader:从带注释的 classes 中加载WebApplicationContext
  • GenericGroovyXmlContextLoader:从 Groovy 脚本或 XML configuration files 的资源位置加载标准ApplicationContext
  • GenericGroovyXmlWebContextLoader:从 Groovy 脚本或 XML configuration files 的资源位置加载WebApplicationContext
  • GenericXmlContextLoader:从 XML 资源位置加载标准ApplicationContext
  • GenericXmlWebContextLoader:从 XML 资源位置加载WebApplicationContext
  • GenericPropertiesContextLoader:从 Java properties files 加载标准ApplicationContext

引导TestContextFramework

Spring TestContext Framework 内部的默认 configuration 足以用于所有 common 用例。但是,有时开发团队或第三方 framework 要更改默认ContextLoader,实现自定义TestContextContextCache,增加ContextCustomizerFactoryTestExecutionListener 实现的默认 sets,依此类推。对于 TestContext framework 如何操作的 low-level 控制,Spring 提供了一个自举策略。

TestContextBootstrapper定义 SPI 以引导 TestContext framework。 TestContextManager使用TestContextBootstrapper来加载当前测试的TestExecutionListener 实现并build它管理的TestContext。您可以使用@BootstrapWith直接或作为 meta-annotation 为 test class(或 test class 层次结构)配置自定义引导策略。如果未使用@BootstrapWith显式配置引导程序,则使用DefaultTestContextBootstrapperWebTestContextBootstrapper,具体取决于是否存在@WebAppConfiguration

由于TestContextBootstrapper SPI 将来可能会发生变化(以适应新的要求),因此我们强烈建议实施者不要直接实现此接口,而应扩展AbstractTestContextBootstrapper或其中一个具体的子类。

TestExecutionListener配置

Spring 提供以下默认注册的TestExecutionListener __mplementations,完全在以下 order 中:

  • ServletTestExecutionListener:为WebApplicationContext配置 Servlet API 模拟。
  • DirtiesContextBeforeModesTestExecutionListener:处理“之前”模式的@DirtiesContext 注释。
  • DependencyInjectionTestExecutionListener:为测试实例提供依赖注入。
  • DirtiesContextTestExecutionListener:处理“后”模式的@DirtiesContext 注释。
  • TransactionalTestExecutionListener:使用默认回滚语义提供 transactional 测试执行。
  • SqlScriptsTestExecutionListener:运行使用@Sql annotation 配置的 SQL 脚本。
注册TestExecutionListener实现

您可以使用@TestExecutionListeners annotation 为 test class 及其子类注册自定义TestExecutionListener __mplement。有关详细信息和示例,请参阅annotation 支持@TestExecutionListeners的 javadoc。

自动发现默认的TestExecutionListener实现

使用@TestExecutionListeners注册自定义TestExecutionListener 实现适用于在有限测试场景中使用的自定义 listener。但是,如果需要在测试套件中使用自定义 listener,则会变得很麻烦。自 Spring Framework 4.1 以来,通过支持通过SpringFactoriesLoader机制自动发现默认TestExecutionListener 实现来解决此问题。

具体来说,spring-test模块在org.springframework.test.context下声明所有核心的默认TestExecutionListener实现。TestExecutionListener键的META-INF/spring.factories属性文件。第三方框架和开发人员可以通过他们自己的META-INF/spring.factories以同样的方式将他们自己的TestExecutionListener实现贡献给默认侦听器列表。工厂的属性文件。

OrderingTestExecutionListener实现

当的 TestContext framework 通过前述 SpringFactoriesLoader机制发现默认TestExecutionListener 实现,实例化 listeners 通过使用 Spring 的AnnotationAwareOrderComparator,里面供奉 Spring 的Ordered接口和@Order annotation 为 ordering 排序。 AbstractTestExecutionListener和 Spring 提供的所有默认TestExecutionListener 实现将使用适当的值实现Ordered。因此,Third-party 框架和开发人员应确保通过实现Ordered或声明@Order来确保其默认的TestExecutionListener 实现在正确的 order 中注册。有关为每个核心 listener 分配的值的详细信息,请参阅 javadoc 以获取核心缺省TestExecutionListener 实现的getOrder()方法。

合并TestExecutionListener实现

如果通过@TestExecutionListeners注册了自定义TestExecutionListener,则不会注册默认的 listener。在大多数 common 测试场景中,这有效地迫使开发人员除了任何自定义 listener 之外还手动声明所有默认 listener。以下清单演示了这种 configuration 方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
@ContextConfiguration
@TestExecutionListeners({
MyCustomTestExecutionListener.class,
ServletTestExecutionListener.class,
DirtiesContextBeforeModesTestExecutionListener.class,
DependencyInjectionTestExecutionListener.class,
DirtiesContextTestExecutionListener.class,
TransactionalTestExecutionListener.class,
SqlScriptsTestExecutionListener.class
})
public class MyTest {
// class body...
}

这种方法的挑战在于它要求开发人员确切地知道默认情况下注册了哪些 listeners。此外,默认 listeners 的集合可以在发行版之间更改 - 例如,在 Spring Framework 4.1 中引入,而DirtiesContextBeforeModesTestExecutionListener在 Spring Framework 4.2 中引入。此外,像 Spring Security 这样的 third-party 框架通过使用前面提到的自动发现机制来注册它们自己的默认TestExecutionListener 实现。

为避免必须知道和 re-declare 所有默认 listeners,您可以将@TestExecutionListenersmergeMode属性设置为MergeMode.MERGE_WITH_DEFAULTSMERGE_WITH_DEFAULTS表示本地声明的 listeners 应该与默认的 listeners 合并。合并算法确保从列表中删除重复项,并根据AnnotationAwareOrderComparator的语义对生成的合并 listeners 组进行排序,如Ordering TestExecutionListener Implementations中所述。如果 listener 实现Ordered或使用@Order注解,它可以影响它与默认值合并的位置。否则,在合并时,本地声明的 listeners 将附加到默认 listeners 列表中。

对于 example,如果MyCustomTestExecutionListener class 在先前 example 配置其order value(对于 example,500)至小于ServletTestExecutionListener的 order(这恰好是1000),则MyCustomTestExecutionListener然后可以自动地与默认列表合并在ServletTestExecutionListener前面,前一个 example 可以替换为以下内容:

1
2
3
4
5
6
7
8
@ContextConfiguration
@TestExecutionListeners(
listeners = MyCustomTestExecutionListener.class,
mergeMode = MERGE_WITH_DEFAULTS
)
public class MyTest {
// class body...
}

Context管理

每个TestContext为其负责的测试实例提供 context management 和缓存支持。测试实例不会自动接收对已配置的ApplicationContext的访问权限。但是,如果测试 class 实现了ApplicationContextAware接口,则会向测试实例提供ApplicationContext的 reference。请注意AbstractJUnit4SpringContextTestsAbstractTestNGSpringContextTests实现了ApplicationContextAware,因此可以自动提供对ApplicationContext的访问。

@Autowired ApplicationContext

作为实现ApplicationContextAware接口的替代方法,您可以通过字段或 setter 方法上的@Autowired annotation 为您的 test class 注入 application context,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
@RunWith(SpringRunner.class)
@ContextConfiguration
public class MyTest {

@Autowired (1)
private ApplicationContext applicationContext;

// class body...
}
1 注入ApplicationContext

同样,如果您的测试配置为加载WebApplicationContext,您可以 inject web application context 进入测试,如下所示:

1
2
3
4
5
6
7
8
9
10
@RunWith(SpringRunner.class)
@WebAppConfiguration (1)
@ContextConfiguration
public class MyWebAppTest {

@Autowired (2)
private WebApplicationContext wac;

// class body...
}
1 配置WebApplicationContext
2 注入WebApplicationContext

使用@Autowired的依赖注入由DependencyInjectionTestExecutionListener提供,默认配置为DependencyInjectionTestExecutionListener(参见测试夹具的依赖注入)。

使用 TestContext framework 的 Test classes 不需要扩展任何特定的 class 或实现特定的接口来配置它们的 application context。相反,configuration 是通过在 class level 处声明@ContextConfiguration annotation 来实现的。如果 test class 没有显式声明 application context 资源位置或带注释的 classes,则配置的ContextLoader确定如何从默认位置加载 context 或默认 configuration classes。除了 context 资源位置和带注释的 classes 之外,还可以通过 application context 初始化程序配置 application context。

以下部分说明如何使用 Spring 的@ContextConfiguration annotation 通过使用 XML configuration files,Groovy 脚本,带注释的 classes(通常为@Configuration classes)或 context 初始化程序来配置测试ApplicationContext。或者,您可以为高级用例实现和配置自己的自定义SmartContextLoader

使用XML资源配置Context

要使用 XML configuration files 为测试加载ApplicationContext,请使用@ContextConfiguration注释 test class,并使用包含 XML configuration 元数据的资源位置的 array 配置locations属性。普通路径或相对路径(对于 example,context.xml)被视为 classpath 资源,该资源相对于定义 test class 的包。以斜杠开头的路径被视为绝对 classpath 位置(对于 example,/org/example/config.xml)。表示资源 URL 的路径(i.e.,前缀为classpath:file:http:,etc.)的路径按原样使用。

1
2
3
4
5
6
7
@RunWith(SpringRunner.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)
public class MyTest {
// class body...
}
1 将 locations 属性设置为 XML files 列表。

@ContextConfiguration通过标准 Java value属性支持locations属性的别名。因此,如果您不需要在@ContextConfiguration中声明其他属性,则可以省略locations属性 name 的声明,并使用以下 example 中演示的缩写格式声明资源位置:

1
2
3
4
5
@RunWith(SpringRunner.class)
@ContextConfiguration({"/app-config.xml", "/test-config.xml"}) (1)
public class MyTest {
// class body...
}
1 在不使用location属性的情况下指定 XML files。

如果省略@ContextConfiguration annotation 中的locationsvalue属性,则 TestContext framework 会尝试检测默认的 XML 资源位置。具体来说,GenericXmlContextLoaderGenericXmlWebContextLoader根据 test class 的 name 检测默认位置。如果 class 被命名为com.example.MyTest,则GenericXmlContextLoader"classpath:com/example/MyTest-context.xml"加载 application context。以下 example 显示了如何执行此操作:

1
2
3
4
5
6
7
8
9
package com.example;

@RunWith(SpringRunner.class)
// ApplicationContext will be loaded from
// "classpath:com/example/MyTest-context.xml"
@ContextConfiguration (1)
public class MyTest {
// class body...
}
1 从默认位置加载 configuration。
使用Groovy脚本配置Context

要使用使用Groovy Bean 定义 DSL的 Groovy 脚本为测试加载ApplicationContext,可以使用@ContextConfiguration注释 test class,并使用包含 Groovy 脚本资源位置的 array 配置locationsvalue属性。 Groovy 脚本的资源查找语义与XML configuration files中描述的相同。

启用 Groovy 脚本支持 如果 Groovy 位于 classpath 上,则支持使用 Groovy 脚本加载 Spring TestContext Framework 中的ApplicationContext

以下 example 显示了如何指定 Groovy configuration files:

1
2
3
4
5
6
7
@RunWith(SpringRunner.class)
// ApplicationContext will be loaded from "/AppConfig.groovy" and
// "/TestConfig.groovy" in the root of the classpath
@ContextConfiguration({"/AppConfig.groovy", "/TestConfig.Groovy"}) (1)
public class MyTest {
// class body...
}
1 指定 Groovy configuration files 的位置。

如果省略@ContextConfiguration annotation 中的locationsvalue属性,则 TestContext framework 会尝试检测默认的 Groovy 脚本。具体来说,GenericGroovyXmlContextLoaderGenericGroovyXmlWebContextLoader根据 test class 的 name 检测默认位置。如果您的 class 名为com.example.MyTest,则 Groovy context 加载程序会从"classpath:com/example/MyTestContext.groovy"加载 application context。以下 example 显示了如何使用默认值:

1
2
3
4
5
6
7
8
9
package com.example;

@RunWith(SpringRunner.class)
// ApplicationContext will be loaded from
// "classpath:com/example/MyTestContext.groovy"
@ContextConfiguration (1)
public class MyTest {
// class body...
}
1 从默认位置加载 configuration。

同时声明 XML configuration 和 Groovy 脚本

您可以使用@ContextConfigurationlocationsvalue属性同时声明 XML configuration files 和 Groovy 脚本。如果配置的资源位置的路径_end 为.xml,则使用XmlBeanDefinitionReader加载。否则,使用GroovyBeanDefinitionReader加载它。

以下清单显示了如何在 integration 测试中组合两者:

1
2
3
4
5
6
7
@RunWith(SpringRunner.class)
// ApplicationContext will be loaded from
// "/app-config.xml" and "/TestConfig.groovy"
@ContextConfiguration({ "/app-config.xml", "/TestConfig.groovy" })
public class MyTest {
// class body...
}
使用Annotated类配置Context

要使用带注释的 classes(请参阅Java-based container configuration)为测试加载ApplicationContext,可以使用@ContextConfiguration注释 test class,并使用 array 配置包含引用 classes 的 references 的_ar属性。以下 example 显示了如何执行此操作:

1
2
3
4
5
6
@RunWith(SpringRunner.class)
// ApplicationContext will be loaded from AppConfig and TestConfig
@ContextConfiguration(classes = {AppConfig.class, TestConfig.class}) (1)
public class MyTest {
// class body...
}
1 指定带注释的 classes。

带注释的 Classes

术语“带注释的 class”可以指以下任何一种:

  • @Configuration注释的 class。
  • 一个 component(即 class 用@Component@Service@Repository或其他构造型 annotations 注释)。
  • 一个符合 JSR-330 的 class,用javax.inject annotations 注释。
  • 包含@Bean方法的任何其他 class。

有关注释 classes 的 configuration 和语义的更多信息,请参阅@Configuration@Bean的 javadoc,特别注意@Bean Lite 模式的讨论。

如果省略@ContextConfiguration annotation 中的classes属性,则 TestContext framework 会尝试检测是否存在 default configuration classes。具体来说,AnnotationConfigContextLoaderAnnotationConfigWebContextLoader检测符合@Configuration javadoc 中指定的 configuration class implementations 要求的 test class 的所有static嵌套 classes。请注意 configuration class 的 name 是任意的。此外,如果需要,test class 可以包含多个static嵌套 configuration class。在下面的示例中,OrderServiceTest class 声明一个名为Configstatic嵌套 configuration class,它自动用于加载测试 class 的ApplicationContext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@RunWith(SpringRunner.class)
// ApplicationContext will be loaded from the
// static nested Config class
@ContextConfiguration (1)
public class OrderServiceTest {

@Configuration
static class Config {

// this bean will be injected into the OrderServiceTest class
@Bean
public OrderService orderService() {
OrderService orderService = new OrderServiceImpl();
// set properties, etc.
return orderService;
}
}

@Autowired
private OrderService orderService;

@Test
public void testOrderService() {
// test the orderService
}

}
1 _Lading 来自嵌套 class 的 configuration 信息。
混合XML,Groovy脚本和带注解的Classes

有时可能需要混合使用 XML configuration files,Groovy 脚本和带注释的 classes(通常为@Configuration classes)来为测试配置ApplicationContext。例如,如果在 production 中使用 XML configuration,则可能决定要使用@Configuration classes 为测试配置特定的 Spring-managed 组件,反之亦然。

此外,一些 third-party 框架(例如 Spring Boot)提供 first-class 支持同时从不同类型的资源中加载ApplicationContext(例如,XML configuration files,Groovy 脚本和@Configuration classes)。历史上,Spring Framework 对标准部署不支持此功能。因此,Spring Framework 在spring-test模块中提供的大多数SmartContextLoader 实现只支持每个测试 context 的一种资源类型。但是,这并不意味着您不能同时使用两者。一般规则的一个例外是GenericGroovyXmlContextLoaderGenericGroovyXmlWebContextLoader同时支持 XML configuration files 和 Groovy 脚本。此外,third-party 框架可以选择支持locationsclasses@ContextConfiguration的声明,并且,通过 TestContext framework 中的标准测试支持,您有以下选项。

如果要使用资源位置(对于 example,XML 或 Groovy)和@Configuration classes 来配置测试,则必须选择一个作为入口点,并且必须包含或 import 另一个。例如,在 XML 或 Groovy 脚本中,您可以通过使用 component 扫描包含@Configuration classes 或将它们定义为普通的 Spring beans,而在@Configuration class 中,您可以使用@ImportResource来 import XML configuration files 或 Groovy 脚本。请注意,此行为在语义上等同于在 production 中配置 application 的方式:在 production configuration 中,您可以定义一组 XML 或 Groovy 资源位置,或者从中加载 production ApplicationContext的一组@Configuration classes,但是您仍然拥有自由包含或 import 其他类型的 configuration。

使用Context初始化器配置Context

要使用 context 初始化程序为测试配置ApplicationContext,请使用@ContextConfiguration注释 test class,并使用包含 references 的 array 配置initializers属性以实现ApplicationContextInitializer。然后使用声明的 context 初始值设定项初始化为测试加载的ConfigurableApplicationContext。请注意,每个声明的初始化程序支持的具体ConfigurableApplicationContext类型必须与正在使用的SmartContextLoader(通常为GenericApplicationContext)创建的ApplicationContext类型兼容。此外,调用初始值设定项的 order 取决于它们是实现 Spring 的Ordered接口还是使用 Spring 的@Order annotation 或标准@Priority annotation 注释。以下 example 显示了如何使用初始值设定项:

1
2
3
4
5
6
7
8
9
@RunWith(SpringRunner.class)
// ApplicationContext will be loaded from TestConfig
// and initialized by TestAppCtxInitializer
@ContextConfiguration(
classes = TestConfig.class,
initializers = TestAppCtxInitializer.class) (1)
public class MyTest {
// class body...
}
1 使用 configuration class 和初始化程序指定 configuration。

你也可以在@ContextConfiguration中完全省略 XML configuration files,Groovy 脚本或带注释的 classes 的声明,而只是声明ApplicationContextInitializer classes,它们负责在 context 中注册 beans - for example,通过从 XML files 编程 loading bean 定义或 configuration classes。以下 example 显示了如何执行此操作:

1
2
3
4
5
6
7
@RunWith(SpringRunner.class)
// ApplicationContext will be initialized by EntireAppInitializer
// which presumably registers beans in the context
@ContextConfiguration(initializers = EntireAppInitializer.class) (1)
public class MyTest {
// class body...
}
1 仅使用初始化程序指定 configuration。
Context配置继承

@ContextConfiguration支持 boolean inheritLocationsinheritInitializers属性,这些属性表示是否应继承资源位置或由超类声明的带注释的 classes 和 context 初始值设定项。两个标志的默认 value 是true。这意味着 test class 继承资源位置或带注释的 classes 以及任何超类声明的 context 初始值设定项。具体而言,test class 的资源位置或带注释的 classes 将附加到资源位置列表或超类声明的带注释的 classes。类似地,给定测试 class 的初始化器被添加到由测试超类定义的初始化器集合中。因此,子类可以选择扩展资源位置,带注释的 classes 或 context 初始值设定项。

如果@ContextConfiguration中的inheritLocationsinheritInitializers属性设置为false,则资源位置或带注释的 classes 和 context 初始值分别为 test class 阴影,并有效地替换超类定义的 configuration。

在下一个使用 XML 资源位置的 example 中,ApplicationContext for ExtendedTest是从base-config.xmlextended-config.xml加载到该 order 中的。因此,在extended-config.xml中定义的 Beans 可以覆盖(即替换)base-config.xml中定义的那些。以下 example 显示了一个 class 如何扩展另一个 class 并使用它自己的 configuration 文件和超类的 configuration 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RunWith(SpringRunner.class)
// ApplicationContext will be loaded from "/base-config.xml"
// in the root of the classpath
@ContextConfiguration("/base-config.xml") (1)
public 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)
public class ExtendedTest extends BaseTest {
// class body...
}
1 超类中定义的配置文件。
2 子类中定义的配置文件。

类似地,在使用带注释的 classes 的下一个 example 中,ApplicationContext for ExtendedTest是从BaseConfigExtendedConfig classes 中加载的,在 order 中。因此,在ExtendedConfig中定义的 Beans 可以覆盖(即替换)BaseConfig中定义的那些。以下 example 显示了一个 class 如何扩展另一个 class 并使用它自己的 configuration class 和超类的 configuration class:

1
2
3
4
5
6
7
8
9
10
11
12
@RunWith(SpringRunner.class)
// ApplicationContext will be loaded from BaseConfig
@ContextConfiguration(classes = BaseConfig.class) (1)
public class BaseTest {
// class body...
}

// ApplicationContext will be loaded from BaseConfig and ExtendedConfig
@ContextConfiguration(classes = ExtendedConfig.class) (2)
public class ExtendedTest extends BaseTest {
// class body...
}
1 Configuration class 在超类中定义。
2 Configuration class 在子类中定义。

在使用 context 初始值设定项的下一个 example 中,使用BaseInitializerExtendedInitializer初始化ApplicationContext for ExtendedTest。但请注意,调用初始值设定项的 order 取决于它们是实现 Spring 的Ordered接口还是使用 Spring 的@Order annotation 或标准@Priority annotation 注释。下面的 example 显示了一个 class 如何扩展另一个 class 并使用它自己的初始化器和超类的初始化器:

1
2
3
4
5
6
7
8
9
10
11
12
13
@RunWith(SpringRunner.class)
// ApplicationContext will be initialized by BaseInitializer
@ContextConfiguration(initializers = BaseInitializer.class) (1)
public class BaseTest {
// class body...
}

// ApplicationContext will be initialized by BaseInitializer
// and ExtendedInitializer
@ContextConfiguration(initializers = ExtendedInitializer.class) (2)
public class ExtendedTest extends BaseTest {
// class body...
}
1 初始化程序在超类中定义。
2 初始化器在子类中定义。
使用环境配置文件进行上下文配置

Spring 3.1 在 framework 中引入了 first-class 支持环境概念和 profiles(AKA“ bean definition profiles”),并且 integration 测试可以配置为激活特定的 bean 定义 profiles 用于各种测试场景。这是通过使用@ActiveProfiles annotation 注释 test class 并提供_ _ofofiles 列表来实现的,这些 profiles 列表应该在为测试加载时激活。

您可以将@ActiveProfiles用于新SmartContextLoader SPI 的任何 implementation,但ContextLoader SPI 的 implementations 不支持@ActiveProfiles

考虑使用 XML configuration 和@Configuration classes 的两个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<!-- 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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.bank.service;

@RunWith(SpringRunner.class)
// ApplicationContext will be loaded from "classpath:/app-config.xml"
@ContextConfiguration("/app-config.xml")
@ActiveProfiles("dev")
public class TransferServiceTest {

@Autowired
private TransferService transferService;

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

TransferServiceTest是 run 时,它的ApplicationContext从 classpath 的根目录中的app-config.xml configuration 文件加载。如果检查app-config.xml,则可以看到accountRepository bean 依赖于dataSource bean。但是,dataSource未定义为 top-level bean。相反,dataSource被定义三次:在production profile 中,在dev profile 中,以及default profile 中。

通过使用@ActiveProfiles("dev")注释TransferServiceTest,我们指示 Spring TestContext Framework 加载ApplicationContext并将 active profiles 设置为{"dev"}。因此,创建嵌入式数据库并使用测试数据填充,accountRepository bean 与_re开发DataSource连接。这可能是我们在 integration 测试中想要的。

将 beans 分配给default profile 有时很有用。仅当没有专门激活其他 profile 时,才会包含默认 profile 中的 Beans。您可以使用它来定义要在 application 的默认 state 中使用的“fallback”beans。对于 example,您可以显式提供devproduction profiles 的数据源,但如果这两个数据源都不是 active,则将 in-memory 数据源定义为默认值。

以下 code 列表演示了如何使用@Configuration classes 而不是 XML 实现相同的 configuration 和 integration 测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
@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();
}

}
package com.bank.service;

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {
TransferServiceConfig.class,
StandaloneDataConfig.class,
JndiDataConfig.class,
DefaultDataConfig.class})
@ActiveProfiles("dev")
public class TransferServiceTest {

@Autowired
private TransferService transferService;

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

在此变体中,我们将 XML configuration 拆分为四个独立的@Configuration classes:

  • TransferServiceConfig:使用@Autowired通过依赖注入获取dataSource
  • StandaloneDataConfig:为适合开发人员测试的嵌入式数据库定义dataSource
  • JndiDataConfig:定义在 production 环境中从 JNDI 检索的dataSource
  • DefaultDataConfig:如果没有 profile 是 active,则为默认的嵌入式数据库定义dataSource

与 XML-based configuration example 一样,我们仍然使用@ActiveProfiles("dev")注释TransferServiceTest,但是这个 time 我们使用@ContextConfiguration annotation 指定所有四个 configuration classes。测试 class 本身保持完全不变。

通常情况下,在给定项目中的多个 test classes 中使用一组 profiles。因此,为了避免@ActiveProfiles annotation 的重复声明,您可以在 base class 上声明@ActiveProfiles一次,并且子类自动从 base class 继承@ActiveProfiles configuration。在下面的示例中,@ActiveProfiles(以及其他 annotations)的声明已被移动到抽象超类AbstractIntegrationTest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.bank.service;

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {
TransferServiceConfig.class,
StandaloneDataConfig.class,
JndiDataConfig.class,
DefaultDataConfig.class})
@ActiveProfiles("dev")
public abstract class AbstractIntegrationTest {
}
package com.bank.service;

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

@Autowired
private TransferService transferService;

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

@ActiveProfiles还支持可用于禁用 active profiles 继承的inheritProfiles属性,如下面的 example 所示:

1
2
3
4
5
6
7
package com.bank.service;

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

此外,有时需要以编程方式而不是声明性地为测试解析 active profiles - 例如,基于:

  • 目前的操作系统。
  • 是否在连续的 integration build 服务器上执行测试。
  • 存在某些环境变量。
  • 自定义 class-level 注释的存在。
  • 其他问题。

要以编程方式解析 active bean definition profiles,您可以实现自定义ActiveProfilesResolver并使用@ActiveProfilesresolver属性进行注册。有关详细信息,请参阅相应的javadoc。以下 example 演示了如何实现和注册自定义OperatingSystemActiveProfilesResolver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.bank.service;

// "dev" profile overridden programmatically via a custom resolver
@ActiveProfiles(
resolver = OperatingSystemActiveProfilesResolver.class,
inheritProfiles = false)
public class TransferServiceTest extends AbstractIntegrationTest {
// test body
}
package com.bank.service.test;

public class OperatingSystemActiveProfilesResolver implements ActiveProfilesResolver {

@Override
String[] resolve(Class<?> testClass) {
String profile = ...;
// determine the value of profile based on the operating system
return new String[] {profile};
}
}
使用测试Property资源配置Context

Spring 3.1 在 framework 中引入了 first-class 支持,用于具有 property 源层次结构的环境的概念。从 Spring 4.1 开始,您可以使用 test-specific property 源配置 integration 测试。与@Configuration classes 上使用的@PropertySource annotation 相比,您可以在 test class 上声明@TestPropertySource annotation,以声明 test properties files 或 inlined properties 的资源位置。这些测试 property 源被添加到EnvironmentPropertySources的集合中,用于为注释的 integration 测试加载的ApplicationContext

您可以将@TestPropertySourceSmartContextLoader SPI 的任何 implementation 一起使用,但ContextLoader SPI 的 implementations 不支持@TestPropertySource

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

声明测试属性来源

您可以使用@TestPropertySourcelocationsvalue属性配置 test properties files。

支持传统和 XML-based properties 文件格式 - 例如,"classpath:/com/example/test.properties""file:///path/to/file.xml"

每个路径都被解释为 Spring Resource。普通路径(对于 example,"test.properties")被视为 classpath 资源,该资源相对于定义 test class 的包。以斜杠开头的路径被视为绝对 classpath 资源(对于 example:"/org/example/test.xml")。使用指定的资源协议加载 references URL(对于 example,前缀为classpath:file:http:的路径)的路径。不允许使用资源位置通配符(例如*/.properties):每个位置必须仅评估一个.properties.xml资源。

以下 example 使用 test properties 文件:

1
2
3
4
5
@ContextConfiguration
@TestPropertySource("/test.properties") (1)
public class MyIntegrationTests {
// class body...
}
1 使用绝对路径指定 properties 文件。

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

key-value 对支持的语法与为 Java properties 文件中的条目定义的语法相同:

  • key=value
  • key:value
  • key value

以下 example sets 两个内联 properties:

1
2
3
4
5
@ContextConfiguration
@TestPropertySource(properties = {"timezone = GMT", "port: 4242"}) (1)
public class MyIntegrationTests {
// class body...
}
1 使用 key-value 语法的两个变体设置两个 properties。
默认Properties文件检测

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

优先

测试 property 源的优先级高于从操作系统环境加载的源,Java 系统 properties 或_appperty 源由 application 以声明方式使用@PropertySource或以编程方式添加。因此,test property 源可用于有选择地覆盖 system 和 application property 源中定义的 properties。此外,内联 properties 的优先级高于从资源位置加载的 properties。

在下一个示例中,"/test.properties"port properties 以及"/test.properties"中定义的任何 properties 都会覆盖在 system 和 application property 源中定义的同一 name 的任何 properties。此外,如果"/test.properties"文件定义了timezoneport properties 的条目,那么这些条目将被使用properties属性声明的内联 properties 覆盖。以下 example 显示了如何在文件和内联中指定 properties:

1
2
3
4
5
6
7
8
@ContextConfiguration
@TestPropertySource(
locations = "/test.properties",
properties = {"timezone = GMT", "port: 4242"}
)
public class MyIntegrationTests {
// class body...
}
继承和覆盖测试PropertySources

@TestPropertySource支持 boolean inheritLocationsinheritProperties属性,表示是否应继承 properties files 和超类声明的内联 properties 的资源位置。两个标志的默认 value 是true。这意味着 test class 继承了任何超类声明的位置和内联 properties。具体来说,test class 的位置和内联 properties 将附加到超类声明的位置和内联 properties。因此,子类可以选择扩展位置和内联 properties。请注意,后面出现的 properties 会影响(即覆盖)之前出现的同一 name 的 properties。此外,上述优先规则也适用于继承的 test property 源。

如果@TestPropertySource中的inheritLocationsinheritProperties属性设置为false,则分别为 test class 阴影的位置或内联 properties,并有效地替换超类定义的 configuration。

在下一个 example 中,BaseTest for BaseTest是仅使用base.properties文件作为 test property 源加载的。相反,使用base.propertiesextended.properties files 作为 test property 源位置来加载ApplicationContext for ExtendedTest。以下 example 显示了如何使用properties files 在子类及其超类中定义 properties:

1
2
3
4
5
6
7
8
9
10
11
@TestPropertySource("base.properties")
@ContextConfiguration
public class BaseTest {
// ...
}

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

在下一个 example 中,BaseTest for BaseTest仅使用内联的key1 property 加载。相反,ApplicationContext for ExtendedTest是使用内联key1key2 properties 加载的。以下 example 显示了如何使用内联 properties 在子类及其超类中定义 properties:

1
2
3
4
5
6
7
8
9
10
11
@TestPropertySource(properties = "key1 = value1")
@ContextConfiguration
public class BaseTest {
// ...
}

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

Spring 3.2 在 integration 测试中引入了的支持。要指示 TestContext framework 加载WebApplicationContext而不是标准ApplicationContext,您可以使用@WebAppConfiguration注释相应的 test class。

测试 class 上存在@WebAppConfiguration指示 TestContext framework(TCF)应该为 integration 测试加载WebApplicationContext(WAC)。在后台,TCF 确保创建MockServletContext并将其提供给测试的 WAC。默认情况下,MockServletContext的基本资源路径设置为src/main/webapp。这被解释为相对于 JVM 根目录的路径(通常是项目的路径)。如果您熟悉 Maven 项目中 web application 的目录结构,则您知道src/main/webapp是 WAR 根目录的默认位置。如果需要覆盖此默认值,则可以提供@WebAppConfiguration annotation 的备用路径(对于 example,@WebAppConfiguration("src/test/webapp"))。如果您希望从 classpath 而不是文件系统引用基本资源路径,则可以使用 Spring 的classpath:前缀。

请注意,Spring 对WebApplicationContext implementations 的测试支持与其对标准ApplicationContext implementations 的支持相同。使用WebApplicationContext进行测试时,可以使用@ContextConfiguration自由声明 XML configuration files,Groovy 脚本或@Configuration classes。您也可以自由使用任何其他测试注释,例如@ActiveProfiles@TestExecutionListeners@Sql@Rollback等。

本节中的其余示例显示了的各种 configuration 选项。以下 example 显示了 TestContext framework 对_coniguration 约定的支持:

例 1.公约

1
2
3
4
5
6
7
8
9
10
11
12
@RunWith(SpringRunner.class)

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

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

public class WacTests {
//...
}

如果使用@WebAppConfiguration注释测试 class 而未指定资源基路径,则资源路径有效地默认为file:src/main/webapp。同样,如果在未指定资源locations,带注释的classes或 context initializers的情况下声明@ContextConfiguration,则 Spring 会尝试使用约定(即与WacTests class 或静态嵌套@Configuration classes 相同的包中的WacTests-context.xml)检测 configuration 的存在。

以下 example 显示了如何使用@WebAppConfiguration显式声明资源基本路径以及使用@ContextConfiguration显示 XML 资源位置:

Example 2.默认资源语义

1
2
3
4
5
6
7
8
9
10
11
@RunWith(SpringRunner.class)

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

// classpath resource
@ContextConfiguration("/spring/test-servlet-config.xml")

public class WacTests {
//...
}

这里要注意的重要一点是_path 与这两个注释的不同语义。默认情况下,@WebAppConfiguration resource paths 是基于文件系统的,而@ContextConfiguration资源位置是基于 classpath 的。

以下 example 显示我们可以通过指定 Spring 资源前缀来覆盖两个 annotations 的默认资源语义:

Example 3.显式资源语义

1
2
3
4
5
6
7
8
9
10
11
@RunWith(SpringRunner.class)

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

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

public class WacTests {
//...
}

将此 example 中的 comments 与前一个 example 对比。

使用 Web Mocks

为了提供全面的 web 测试支持,Spring 3.2 引入了默认启用的ServletTestExecutionListener。在针对WebApplicationContext进行测试时,在每个测试方法之前使用 Spring Web 的RequestContextHolder来设置TestExecutionListener state 默认 thread-local state,并根据使用@WebAppConfiguration配置的基本资源路径创建MockHttpServletRequestMockHttpServletResponseServletWebRequestServletTestExecutionListener还确保可以将MockHttpServletResponseServletWebRequest注入到测试实例中,并且一旦测试完成,它就会清除 thread-local state。

一旦为测试加载WebApplicationContext,您可能会发现需要与 web 模拟进行交互 - 例如,设置测试夹具或在调用 web component 后执行断言。以下 example 显示哪些模拟可以自动连接到您的测试实例中。请注意,WebApplicationContextMockServletContext都在测试套件中缓存,而其他模拟由ServletTestExecutionListener按照测试方法进行管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@SpringJUnitWebConfig
class WacTests {

@Autowired
WebApplicationContext wac; // cached

@Autowired
MockServletContext servletContext; // cached

@Autowired
MockHttpSession session;

@Autowired
MockHttpServletRequest request;

@Autowired
MockHttpServletResponse response;

@Autowired
ServletWebRequest webRequest;

//...
}
Context缓存

一旦 TestContext framework 为测试加载ApplicationContext(或WebApplicationContext),该 context 就会被缓存并重用于在同一测试套件中声明相同唯一 context configuration 的所有后续测试。要了解缓存的工作原理,了解“独特”和“测试套件”的含义非常重要。

可以通过用于加载它的 configuration 参数的组合来唯一标识。因此,configuration 参数的唯一组合用于生成 key,高速缓存 context。 TestContext framework 使用以下 configuration 参数来 build context cache key:

  • locations(来自@ContextConfiguration)
  • classes(来自@ContextConfiguration)
  • contextInitializerClasses(来自@ContextConfiguration)
  • contextCustomizers(来自ContextCustomizerFactory)
  • contextLoader(来自@ContextConfiguration)
  • parent(来自@ContextHierarchy)
  • activeProfiles(来自@ActiveProfiles)
  • propertySourceLocations(来自@TestPropertySource)
  • propertySourceProperties(来自@TestPropertySource)
  • resourceBasePath(来自@WebAppConfiguration)

例如,如果TestClassA@ContextConfigurationlocations(或value)属性指定{"app-config.xml", "test-config.xml"},则 TestContext framework 将相应的ApplicationContext和 store 加载到仅基于这些位置的 key 下的static context 缓存中。因此,如果TestClassB也为其位置定义了{"app-config.xml", "test-config.xml"}(通过继承显式或隐式)但没有定义@WebAppConfiguration,不同的ContextLoader,不同的 active profiles,不同的 context 初始化器,不同的 test property 源或不同的 parent context,那么相同ApplicationContext由两个 test classes 共享。这意味着 loading application context 的设置成本仅产生一次(每个测试套件),后续测试执行速度要快得多。

测试套件和分叉流程

Spring TestContext framework stores application 上下文在静态缓存中。这意味着 context 实际上存储在static变量中。换句话说,如果测试在单独的进程中执行,则在每次测试执行之间清除静态高速缓存,这有效地禁用了高速缓存机制。

要从缓存机制中受益,所有测试必须在同一个 process 或测试套件中运行。这可以通过在 IDE 中作为 group 执行所有测试来实现。类似地,当使用_bum framework(例如 Ant,Maven 或 Gradle)执行测试时,确保 build framework 不会在测试之间进行分配是很重要的。例如,如果 Maven Surefire plug-in 的forkMode设置为alwayspertest,则 TestContext framework 无法在 test classes 之间缓存 application 上下文,因此 build process 的运行速度明显更慢。

从 Spring Framework 4.3 开始,context 缓存的大小受限于默认的最大大小 32.每当达到最大大小时,最近最少使用(LRU)驱逐 policy 用于驱逐和关闭陈旧的上下文。您可以通过设置名为spring.test.context.cache.maxSize的 JVM 系统 property,从命令 line 或 build 脚本配置最大大小。或者,您可以使用SpringProperties API 以编程方式设置相同的 property。

由于在给定的测试套件中加载了大量的 application 上下文会导致套件执行不必要的 long time 执行,因此确切地知道已经加载和缓存了多少个上下文通常是有益的。要查看基础 context 缓存的统计信息,可以将org.springframework.test.context.cache logging 类别的 log level 设置为DEBUG

在不太可能的情况下,测试会破坏 application context 并需要重新加载(例如,通过修改 bean 定义或 application object 的 state),您可以使用@DirtiesContext注解 test class 或 test 方法(请参阅@DirtiesContext的讨论) Spring Testing Annotations)。这指示 Spring 从缓存中删除 context 并在 running 下一个测试之前重建 application context。请注意,DirtiesContextBeforeModesTestExecutionListenerDirtiesContextTestExecutionListener提供了对@DirtiesContext annotation 的支持,这些默认情况下已启用。

Context层次结构

在编写依赖于加载的 Spring ApplicationContext的 integration 测试时,通常可以对单个 context 进行测试。但是,有时候对ApplicationContext实例的层次结构进行测试是有益的,甚至是必要的。例如,如果你正在开发一个 Spring MVC web application,你通常会有一个由 Spring 的ContextLoaderListener加载的WebApplicationContext和一个由 Spring 的DispatcherServlet加载的 child WebApplicationContext。这会产生 parent-child context 层次结构,其中共享组件和基础结构 configuration 在根 context 中声明,并在 child context 中由 web-specific 组件使用。另一个用例可以在 Spring Batch applications 中找到,其中您经常有一个 parent context 为共享批处理基础结构提供 configuration,而 child context 用于 configuration 特定批处理 job。

从 Spring Framework 3.2.2 开始,您可以通过在单个 test class 或 test class 层次结构中使用@ContextHierarchy annotation 声明 context configuration 来编写使用 context 层次结构的 integration 测试。如果在 test class 层次结构中的多个 classes 上声明 context 层次结构,则还可以合并或覆盖 context 层次结构中特定的名为 level 的 context configuration。合并层次结构中给定 level 的 configuration 时,configuration 资源类型(即 XML configuration files 或 annotated classes)必须一致。否则,在使用不同资源类型配置的 context 层次结构中具有不同的级别是完全可以接受的。

本节中剩余的 JUnit 4-based 示例显示了需要使用 context 层次结构的 integration 测试的 common configuration 方案。

使用 context 层次结构单个测试 class

ControllerIntegrationTests表示 Spring MVC web application 的典型 integration 测试场景,声明 context 层次结构包含两个级别,一个用于根WebApplicationContext(使用TestAppConfig @Configuration class 加载),另一个用于调度程序 servlet WebApplicationContext(通过使用WebConfig @Configuration class)。自动装入测试实例的WebApplicationContext是 child context(即层次结构中最低的 context)的WebApplicationContext。以下清单显示了此 configuration 方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
@RunWith(SpringRunner.class)
@WebAppConfiguration
@ContextHierarchy({
@ContextConfiguration(classes = TestAppConfig.class),
@ContextConfiguration(classes = WebConfig.class)
})
public class ControllerIntegrationTests {

@Autowired
private WebApplicationContext wac;

// ...
}

具有隐式 parent context 的 Class 层次结构

此 example 中的测试 classes 在 test class 层次结构中定义 context 层次结构。 AbstractWebTests在 Spring-powered web application 中声明了根WebApplicationContext的 configuration。但请注意,AbstractWebTests未声明@ContextHierarchy。因此,AbstractWebTests的子类可以选择参与 context 层次结构或遵循@ContextConfiguration的标准语义。 SoapWebServiceTestsRestWebServiceTests都扩展AbstractWebTests并使用@ContextHierarchy定义 context 层次结构。结果是加载了三个 application 上下文(每个@ContextConfiguration声明一个),并且基于AbstractWebTests中的 configuration 加载的 application context 被设置为为具体子类加载的每个上下文的 parent context。以下清单显示了此 configuration 方案:

1
2
3
4
5
6
7
8
9
10
@RunWith(SpringRunner.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 {}

具有合并 context 层次结构 configuration 的 Class 层次结构

此 example 中的 classes 显示在 order 中使用命名层次结构级别来合并 context 层次结构中特定级别的 configuration。 BaseTests在层次结构中定义了两个级别parentchildExtendedTests扩展BaseTests并指示 Spring TestContext Framework 合并child层次 level 的 context configuration,方法是确保@ContextConfigurationname属性中声明的名称都是child。结果是加载了三个 application 上下文:一个用于/app-config.xml,一个用于/user-config.xml,一个用于{"/user-config.xml", "/order-config.xml"}。与前面的 example 一样,从/app-config.xml加载的 application context 被设置为从/user-config.xml{"/user-config.xml", "/order-config.xml"}加载的上下文的 parent context。以下清单显示了此 configuration 方案:

1
2
3
4
5
6
7
8
9
10
11
@RunWith(SpringRunner.class)
@ContextHierarchy({
@ContextConfiguration(name = "parent", locations = "/app-config.xml"),
@ContextConfiguration(name = "child", locations = "/user-config.xml")
})
public class BaseTests {}

@ContextHierarchy(
@ContextConfiguration(name = "child", locations = "/order-config.xml")
)
public class ExtendedTests extends BaseTests {}

具有重写 context 层次结构 configuration 的 Class 层次结构

与前面的示例相比,此 example 演示了如何通过将@ContextConfiguration中的inheritLocations flag 设置为false来覆盖 context 层次结构中给定命名 level 的 configuration。因此,ExtendedTests的 application context 仅从/test-user-config.xml加载,并且 parent 设置为从/app-config.xml加载的 context。以下清单显示了此 configuration 方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RunWith(SpringRunner.class)
@ContextHierarchy({
@ContextConfiguration(name = "parent", locations = "/app-config.xml"),
@ContextConfiguration(name = "child", locations = "/user-config.xml")
})
public class BaseTests {}

@ContextHierarchy(
@ContextConfiguration(
name = "child",
locations = "/test-user-config.xml",
inheritLocations = false
))
public class ExtendedTests extends BaseTests {}

在 context 层次结构中消除 context 如果在 context 配置为 context 层次结构的一部分的测试中使用@DirtiesContext,则可以使用hierarchyMode flag 来控制 context 缓存的清除方式。有关更多详细信息,请参阅Spring Testing Annotations中的@DirtiesContext@DirtiesContext javadoc 的讨论。

测试fixture的依赖注入

当您使用DependencyInjectionTestExecutionListener(默认配置)时,测试实例的依赖项将从您使用@ContextConfiguration配置的 application context 中的 beans 注入。您可以使用 setter injection,field injection 或两者,具体取决于您选择的注释以及是否将它们放在 setter 方法或字段上。为了与 Spring 2.5 和 3.0 中引入的 annotation 支持保持一致,您可以使用 Spring 的@Autowired annotation 或 JSR 330 中的@Inject annotation。

TestContext framework 没有检测实例化测试实例的方式。因此,对构造函数使用@Autowired@Inject对 test classes 没有影响。

因为@Autowired用于执行按类型自动装配,如果您有多个相同类型的 bean 定义,则不能依赖此方法来处理这些特定的 beans。在这种情况下,您可以将@Autowired@Qualifier结合使用。从 Spring 3.0 开始,您也可以选择将@Inject@Named结合使用。或者,如果您的 test class 可以访问其ApplicationContext,则可以使用(for example)调用applicationContext.getBean("titleRepository")来执行显式查找。

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

考虑测试HibernateTitleRepository class 的场景,如目标部分所述。接下来的两个 code 列表演示了在字段和 setter 方法上使用@Autowired。 application context configuration 在所有 sample code 列表之后显示。

以下 code 列表中的依赖项注入行为并非特定于 JUnit 4.相同的 DI 技术可以与任何测试 framework 一起使用。

以下示例将 calls 设置为静态断言方法,例如assertNotNull(),但不使用Assert预先调用。在这种情况下,假设该方法是通过_示例中未显示的import static声明正确导入的。

第一个 code 列表显示了一个基于 JUnit 4 的_Ilass 实现,它使用@Autowired进行字段注入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RunWith(SpringRunner.class)
// specifies the Spring configuration to load for this test fixture
@ContextConfiguration("repository-config.xml")
public class HibernateTitleRepositoryTests {

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RunWith(SpringRunner.class)
// specifies the Spring configuration to load for this test fixture
@ContextConfiguration("repository-config.xml")
public class HibernateTitleRepositoryTests {

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

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

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

前面的 code 列表使用@ContextConfiguration annotation 引用的相同 XML context 文件(即repository-config.xml)。以下显示了此 configuration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?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
http://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>

如果从正在其中一个 setter 方法上使用@Autowired的 Spring-provided test base class 进行扩展,则可能在 application context 中定义了多个受影响类型的 beans(对于 example,多个DataSource beans)。在这种情况下,您可以覆盖 setter 方法并使用@Qualifier annotation 来指示特定的目标 bean,如下所示(但请确保也委托给超类中的重写方法):

1
2
3
4
5
6
7
8
9
// ...

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

// ...

指定的限定符 value 表示特定的DataSource bean 到 inject,将类型匹配集缩小到特定的 bean。它的 value 与相应<bean>定义中的<qualifier>声明匹配。 bean name 用作回退限定符 value,因此您还可以有效地指向 name by name(如前所示,假设myDataSource是 bean id)。

测试Request和Session-scopedBeans

Spring 从早年就开始支持请求 - 和 session-scoped beans。从 Spring 3.2 开始,您可以按照以下步骤测试 request-scoped 和 session-scoped beans:

  • 通过使用@WebAppConfiguration注释 test class,确保为测试加载了WebApplicationContext
  • 将 mock 请求或 session 注入测试实例并根据需要准备测试夹具。
  • 调用从配置的WebApplicationContext中检索到的 web component(带依赖注入)。
  • 对模拟执行断言。

下一个 code 代码段显示了登录用例的 XML configuration。请注意,userService bean 依赖于 request-scoped loginAction bean。此外,LoginAction通过使用SpEL 表达式来实例化,该SpEL 表达式从当前 HTTP 请求中检索用户名和密码。在我们的测试中,我们希望通过 TestContext framework 管理的 mock 配置这些请求参数。以下清单显示了此用例的 configuration:

Example 5. Request-scoped bean configuration

1
2
3
4
5
6
7
8
9
10
11
12
13
<beans>

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

<bean id="loginAction" class="com.example.LoginAction"
c:username="#{request.getParameter('user')}"
c:password="#{request.getParameter('pswd')}"
scope="request">
<aop:scoped-proxy/>
</bean>

</beans>

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

Example 6. Request-scoped bean test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RunWith(SpringRunner.class)
@ContextConfiguration
@WebAppConfiguration
public class RequestScopedBeanTests {

@Autowired UserService userService;
@Autowired MockHttpServletRequest request;

@Test
public void requestScope() {
request.setParameter("user", "enigma");
request.setParameter("pswd", "$pr!ng");

LoginResults results = userService.loginUser();
// assert results
}
}

以下 code 代码段类似于我们之前为 request-scoped bean 看到的代码片段。但是,这个 time,userService bean 依赖于 session-scoped userPreferences bean。请注意,UserPreferences bean 是使用 SpEL 表达式实例化的,该表达式从当前 HTTP session 中检索主题。在我们的测试中,我们需要在 TestContext framework 管理的 mock session 中配置主题。以下 example 显示了如何执行此操作:

例 7. Session-scoped bean configuration

1
2
3
4
5
6
7
8
9
10
11
12
<beans>

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

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

</beans>

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

例 8. Session-scoped bean 测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RunWith(SpringRunner.class)
@ContextConfiguration
@WebAppConfiguration
public class SessionScopedBeanTests {

@Autowired UserService userService;
@Autowired MockHttpSession session;

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

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

事务管理

在 TestContext framework 中,transactions 由TransactionalTestExecutionListener管理,默认情况下配置TransactionalTestExecutionListener,即使您没有在 test class 上显式声明@TestExecutionListeners。但是,要启用对 transactions 的支持,必须在ApplicationContext中配置PlatformTransactionManager bean,并加载@ContextConfiguration语义(稍后会提供更多详细信息)。此外,您必须在 class 或方法 level 中为测试声明 Spring 的@Transactional annotation。

Test-managed事物

Test-managed transactions 是 transactions,通过使用TransactionalTestExecutionListener以编程方式管理,或者使用TestTransaction(稍后描述)以编程方式管理。你不应该将这样的 transactions 与 Spring-managed transactions(由ApplicationContext加载的 Spring 直接管理的测试)或 application-managed transactions(那些由测试调用的 application code 中以编程方式管理的那些)混淆。 Spring-managed 和 application-managed transactions 通常参与 test-managed transactions。但是,如果 Spring-managed 或 application-managed transactions 配置了除REQUIREDSUPPORTS之外的任何传播类型,则应谨慎使用(有关详细信息,请参阅有关transaction 传播的讨论)。

启用和禁用Transactions

使用@Transactional注释测试方法会导致测试在 transaction 中运行,默认情况下,在完成测试后会自动回滚。如果使用@Transactional注释了测试 class,则该 class 层次结构中的每个测试方法都在 transaction 中运行。未使用@Transactional(在 class 或方法 level 上)注释的测试方法在 transaction 中不是 run。此外,使用@Transactional注释但propagation类型设置为NOT_SUPPORTED的测试在 transaction 中不是 run。

请注意,AbstractTransactionalJUnit4SpringContextTestsAbstractTransactionalTestNGSpringContextTests已预先配置为 class level 的 transactional 支持。

以下 example 演示了为 Hibernate-based UserRepository编写 integration 测试的 common 场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = TestConfig.class)
@Transactional
public class HibernateUserRepositoryTests {

@Autowired
HibernateUserRepository repository;

@Autowired
SessionFactory sessionFactory;

JdbcTemplate jdbcTemplate;

@Autowired
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}

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

protected int countRowsInTable(String tableName) {
return JdbcTestUtils.countRowsInTable(this.jdbcTemplate, tableName);
}

protected void assertNumUsers(int expected) {
assertEquals("Number of rows in the [user] table.", expected, countRowsInTable("user"));
}
}

Transaction 回滚和提交行为中所述,在运行createUser()方法后无需清理数据库,因为对数据库所做的任何更改都会由TransactionalTestExecutionListener自动回滚。

Transaction回滚和提交行为

默认情况下,test transactions 将在测试完成后自动回滚;但是,transactional commit 和 rollback behavior 可以通过@Commit@Rollback注解以声明方式配置。有关详细信息,请参阅annotation 支持部分中的相应条目。

编程式Transaction管理

从 Spring Framework 4.1 开始,您可以使用TestTransaction中的静态方法以编程方式与 test-managed transactions 进行交互。例如,您可以在测试方法中使用TestTransaction,在方法之前,在方法之后使用TestTransaction来启动或结束当前 test-managed transaction,或者为回滚或提交配置当前 test-managed transaction。只要启用TransactionalTestExecutionListener,就会自动提供对TestTransaction的支持。

以下 example 演示了TestTransaction的一些 features。有关更多详细信息,请参阅 javadoc 以获取TestTransaction

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@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"));
}
}
运行Code外的事物

有时,您可能需要在 transactional 测试方法之前或之后执行某些 code,但在 transactional context 之外 - 为 example,在运行测试之前验证初始数据库 state 或在测试运行后验证预期的 transactional 提交行为(如果 test 配置为提交 transaction)。对于这种情况,TransactionalTestExecutionListener支持@BeforeTransaction@AfterTransaction 注释。您可以使用其中一个注释在 test class 或测试接口中的任何void default 方法中注释任何void方法,并且TransactionalTestExecutionListener确保您的 before transaction 方法或 transaction 方法在适当的 time 运行。

之前的任何方法(例如使用 JUnit Jupiter 的@BeforeEach注释的方法)和任何后续方法(例如使用 JUnit Jupiter 的@AfterEach注释的方法)都在 transaction 中运行。此外,对于未在 transaction 中配置为 run 的测试方法,@BeforeTransaction@AfterTransaction注释的方法不是 run。

配置事务管理

TransactionalTestExecutionListener期望在 Spring ApplicationContext中定义PlatformTransactionManager bean 进行测试。如果在测试的ApplicationContext中有多个PlatformTransactionManager实例,则可以使用@Transactional("myTxMgr")@Transactional(transactionManager = "myTxMgr")声明限定符,或者TransactionManagementConfigurer可以由@Configuration class 实现。有关用于在测试ApplicationContext中查找 transaction manager 的算法的详细信息,请参阅javadoc for TestContextTransactionUtils.retrieveTransactionManager()

所有事物相关的Annotations的演示

以下基于 JUnit 4 的 example 显示了一个虚构的 integration 测试场景,它突出显示了所有 transaction-related 注释。 example 并非用于演示最佳实践,而是用于演示如何使用这些注释。有关更多信息和 configuration 示例,请参见annotation 支持部分。 Transaction management for @Sql包含一个额外的 example,它使用@Sql进行声明性 SQL 脚本执行,并使用默认的 transaction 回滚语义。以下 example 以粗体显示相关注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@RunWith(SpringRunner.class)
@ContextConfiguration
@Transactional(transactionManager = "txMgr")
@Commit
public class FictitiousTransactionalTest {

@BeforeTransaction
void verifyInitialDatabaseState() {
// logic to verify the initial state before a transaction is started
}

@Before
public void setUpTestDataWithinTransaction() {
// set up test data within the transaction
}

@Test
// overrides the class-level @Commit setting
@Rollback
public void modifyDatabaseWithinTransaction() {
// logic which uses the test data and modifies database state
}

@After
public void tearDownWithinTransaction() {
// execute "tear down" logic within the transaction
}

@AfterTransaction
void verifyFinalDatabaseState() {
// logic to verify the final state after transaction has rolled back
}

}

在测试 ORM code 时避免 false 肯定

当您测试操作 Hibernate session 或 JPA 持久性 context 的 state 的 application code 时,请确保在 run 该 code 的测试方法中刷新基础工作单元。未能刷新基础工作单元可能会产生 false 肯定:您的测试通过,但相同的 code 会在实时的 production 环境中抛出 exception。请注意,这适用于维护 in-memory 工作单元的任何 ORM framework。在下面的 Hibernate-based example 测试用例中,一个方法演示 false 正面,另一个方法正确公开了刷新 session 的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ...

@Autowired
SessionFactory sessionFactory;

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

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

// ...

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ...

@PersistenceContext
EntityManager entityManager;

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

@Transactional
@Test(expected = ...)
public void updateWithEntityManagerFlush() {
updateEntityInJpaPersistenceContext();
// Manual flush is required to avoid false positive in test
entityManager.flush();
}

// ...

执行SQL脚本

在针对关系数据库编写 integration 测试时,执行 SQL 脚本以将数据库 schema 或 insert 测试数据修改为表通常是有益的。 spring-jdbc模块通过在加载 Spring ApplicationContext时执行 SQL 脚本来支持初始化嵌入或现有数据库。有关详细信息,请参阅嵌入式数据库支持使用嵌入式数据库测试数据访问逻辑

虽然在加载ApplicationContext时初始化数据库进行一次测试非常有用,但有时在 integration 测试期间能够修改数据库是很重要的。以下部分说明如何在 integration 测试期间以编程方式和声明方式执行 SQL 脚本。

以编程方式执行SQL脚本

Spring 提供了以下选项,用于在 integration 测试方法中以编程方式执行 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 脚本的静态实用程序方法,主要供 framework 内部使用。但是,如果您需要完全控制 SQL 脚本的解析和执行方式,ScriptUtils可能比后面描述的其他一些替代方案更适合您的需求。有关详细信息,请参阅中中的各个方法。

ResourceDatabasePopulator提供 object-based API,用于通过使用外部资源中定义的 SQL 脚本以编程方式填充,初始化或清理数据库。 ResourceDatabasePopulator提供了用于配置解析和运行脚本时使用的字符编码,语句分隔符,comment 分隔符和错误处理标志的选项。每个 configuration 选项都有一个合理的默认值 value。有关默认值的详细信息,请参阅javadoc。要 run 在ResourceDatabasePopulator中配置的脚本,您可以调用populate(Connection)方法来执行针对java.sql.Connection的填充程序或execute(DataSource)方法来针对javax.sql.DataSource执行填充程序。以下 example 指定测试 schema 和测试数据的 SQL 脚本,将语句分隔符设置为@@,并针对DataSource执行脚本:

1
2
3
4
5
6
7
8
9
10
@Test
public void databaseTest {
ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
populator.addScripts(
new ClassPathResource("test-schema.sql"),
new ClassPathResource("test-data.sql"));
populator.setSeparator("@@");
populator.execute(this.dataSource);
// execute code that uses the test schema and data
}

请注意,ResourceDatabasePopulator内部委托ScriptUtils进行解析和 running SQL 脚本。同样,AbstractTransactionalJUnit4SpringContextTestsAbstractTransactionalTestNGSpringContextTests中的executeSqlScript(..)方法在内部使用ResourceDatabasePopulator到 run SQL 脚本。有关更多详细信息,请参阅 javadoc 以获取各种executeSqlScript(..)方法。

以Sql注解声明性地执行SQL脚本

除了前面提到的以编程方式运行 SQL 脚本的机制之外,您还可以在 Spring TestContext Framework 中以声明方式配置 SQL 脚本。具体来说,您可以在 test class 或 test 方法上声明@Sql annotation,以将资源_path 配置为在 integration 测试方法之前或之后应该针对给定数据库 run 的 SQL 脚本。请注意,method-level 声明覆盖 class-level 声明,并且SqlScriptsTestExecutionListener支持@Sql,默认情况下启用。

路径资源语义

每个路径都被解释为 Spring Resource。普通路径(对于 example,"schema.sql")被视为 classpath 资源,该资源相对于定义 test class 的包。以斜杠开头的路径被视为绝对 classpath 资源(对于 example,"/org/example/schema.sql")。使用指定的资源协议加载 references URL(对于 example,前缀为classpath:file:http:的路径)的路径。

以下 example 显示了如何在 class level 和基于 JUnit Jupiter 的 integration test class 中的方法 level 中使用@Sql

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@SpringJUnitConfig
@Sql("/test-schema.sql")
class DatabaseTests {

@Test
void emptySchemaTest {
// execute code that uses the test schema without any test data
}

@Test
@Sql({"/test-schema.sql", "/test-user-data.sql"})
void userTest {
// execute code that uses the test schema and test data
}
}
默认脚本检测

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

  • Class-level 声明:如果带注释的测试 class 为com.example.MyTest,则相应的默认脚本为classpath:com/example/MyTest.sql
  • Method-level 声明:如果带注释的测试方法名为testMethod()并且在 class com.example.MyTest中定义,则相应的默认脚本为classpath:com/example/MyTest.testMethod.sql
声明多个Sql注解Sets

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

以下 example 显示了如何在 Java 8 中将@Sql用作可重复的 annotation:

1
2
3
4
5
6
@Test
@Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`"))
@Sql("/test-user-data.sql")
public void userTest {
// execute code that uses the test schema and test data
}

在前面的 example 中显示的场景中,test-schema.sql脚本对 single-line comments 使用不同的语法。

以下 example 与前面的 example 相同,只是@Sql声明在@SqlGroup中组合在一起,以便与 Java 6 和 Java 7 兼容。

1
2
3
4
5
6
7
8
@Test
@SqlGroup({
@Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`")),
@Sql("/test-user-data.sql")
)}
public void userTest {
// execute code that uses the test schema and test data
}
脚本执行阶段

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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
)
public void userTest {
// execute code that needs the test data to be committed
// to the database outside of the test's transaction
}

请注意,ISOLATEDAFTER_TEST_METHOD分别从Sql.TransactionModeSql.ExecutionPhase静态导入。

使用SqlConfig注解配置脚本

您可以使用@SqlConfig annotation 配置脚本解析和错误处理。在 integration test class 上声明为 class-level annotation 时,@SqlConfig用作 test class 层次结构中所有 SQL 脚本的 global configuration。当使用@Sql annotation 的config属性直接声明时,@SqlConfig用作封闭@Sql annotation 中声明的 SQL 脚本的本地 configuration。 @SqlConfig中的每个属性都有一个隐式的默认值 value,它记录在相应属性的 javadoc 中。由于在 Java 语言规范中为 annotation 属性定义了规则,遗憾的是,不可能将null的 value 赋给 annotation 属性。因此,为了支持继承的 global configuration 的覆盖,@SqlConfig属性具有""(对于 Strings)或DEFAULT(对于枚举)的显式缺省 value。这种方法允许@SqlConfig的局部声明通过提供除""DEFAULT之外的 value 来有选择地覆盖@SqlConfig的 global 声明中的各个属性。只要本地@SqlConfig属性不提供""DEFAULT以外的显式 value,就会继承 Global @SqlConfig属性。因此,显式本地配置会覆盖 global configuration。

@Sql@SqlConfig提供的 configuration 选项等同于ScriptUtilsResourceDatabasePopulator支持的选项,但它们是<jdbc:initialize-database/> XML 名称空间元素提供的选项的超集。有关详细信息,请参阅@Sql@SqlConfig中各个属性的 javadoc。

事物管理@Sql

默认情况下,SqlScriptsTestExecutionListener为使用@Sql配置的脚本推断出所需的 transaction 语义。具体来说,SQL 脚本是 run 而没有 transaction,在现有的 Spring-managed transaction 中(对于 example,由TransactionalTestExecutionListener管理的 transaction 用于使用@Transactional注释的测试),或者在单独的 transaction 中,具体取决于@SqlConfigtransactionMode属性的配置 value 并且在测试的ApplicationContext中存在PlatformTransactionManager。但是,作为最低限度,测试的ApplicationContext中必须存在javax.sql.DataSource

如果SqlScriptsTestExecutionListener用于检测DataSourcePlatformTransactionManager并推断 transaction 语义的算法不符合您的需要,则可以通过设置@SqlConfigdataSourcetransactionManager属性来指定显式名称。此外,您可以通过设置@SqlConfigtransactionMode属性来控制 transaction 传播行为(对于 example,脚本是否应该在隔离的 transaction 中运行)。虽然对@Sql的 transaction management 的所有支持选项的详尽讨论超出了本 reference 手册的范围,但@SqlConfigSqlScriptsTestExecutionListener的 javadoc 提供了详细信息,以下 example 显示了使用 JUnit Jupiter 和 transactional 测试的典型测试场景@Sql

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@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);
// execute 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()方法为 run 之后无需清理数据库,因为对数据库所做的任何更改(在测试方法内或在/test-data.sql脚本中)都会被TransactionalTestExecutionListener自动回滚(有关详细信息,请参阅transaction management) 。

使用@SqlMergeMode合并和重写配置

在Spring Framework 5.2中,可以将方法级的@Sql声明与类级声明合并。例如,这允许您为每个测试类提供一次数据库模式或一些公共测试数据的配置,然后为每个测试方法提供附加的、特定于用例的测试数据。要启用@Sql合并,请使用@SqlMergeMode(MERGE)注释您的测试类或测试方法。要禁用特定测试方法(或特定测试子类)的合并,可以通过@SqlMergeMode(OVERRIDE)切换回默认模式。查阅@SqlMergeMode注解文档小节以获得示例和更多细节。

Parallel测试执行

Spring Framework 5.0 引入了在使用 Spring TestContext Framework 时在单个 JVM 中执行 parallel 测试的基本支持。通常,这意味着大多数测试类或测试方法可以在 parallel 中执行,而无需对 test code 或 configuration 进行任何更改。

有关如何设置 parallel 测试执行的详细信息,请参阅测试 framework,build 工具或 IDE 的文档。

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

如果测试,请不要在 parallel 中执行测试:

  • 使用 Spring 的@DirtiesContext支持。
  • 使用 JUnit 4 的@FixMethodOrder支持或任何测试 framework feature,旨在确保测试方法 run 在特定的 order 中。但请注意,如果在 parallel 中执行整个测试类,则不适用。
  • 更改共享服务或系统的 state,例如数据库,消息 broker,文件系统等。这适用于 in-memory 和外部系统。

如果 parallel 测试执行失败,并且 exception 声明当前测试的ApplicationContext不再是 active,则这通常意味着ApplicationContext已从另一个线程中的ContextCache中删除。

这可能是由于使用@DirtiesContext或由于ContextCache的自动驱逐。如果@DirtiesContext是罪魁祸首,您需要找到一种方法来避免使用@DirtiesContext或从 parallel 执行中排除此类测试。如果已超出ContextCache的最大大小,则可以增加高速缓存的最大大小。有关详细信息,请参阅context 缓存上的讨论。

Spring TestContext Framework 中的 Parallel 测试执行只有在底层TestContext implementation 提供复制构造函数时才有可能,如TestContext的 javadoc 中所述。 Spring 中使用的DefaultTestContext提供了这样的构造函数。但是,如果使用提供自定义TestContext implementation 的 third-party library,则需要验证它是否适合 parallel 测试执行。

TestContext框架支持Classes

本节介绍支持 Spring TestContext Framework 的各种 classes。

SpringJUnit4Runner

Spring TestContext Framework 通过自定义运行器(在 JUnit 4.12 或更高版本上支持)提供与 JUnit 4 的完整 integration。通过使用@RunWith(SpringJUnit4ClassRunner.class)或更短的@RunWith(SpringRunner.class)变体注释 test classes,开发人员可以实现标准的 JUnit 4-based 单元和 integration 测试,同时获得 TestContext framework 的好处,例如支持 loading application 上下文,依赖注入测试实例,transactional 测试方法执行, 等等。如果您想将 Spring TestContext Framework 与替代跑步者(例如 JUnit 4 的Parameterized跑步者)或third-party跑步者(例如MockitoJUnitRunner)一起使用,您可以选择使用Spring 支持 JUnit 规则

以下 code 列表显示了使用自定义 Spring Runner将 test class 配置为 run 的最低要求:

1
2
3
4
5
6
7
8
9
@RunWith(SpringRunner.class)
@TestExecutionListeners({})
public class SimpleTest {

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

在前面的 example 中,@TestExecutionListeners配置了一个空列表,以禁用默认的 listeners,否则需要通过@ContextConfiguration配置ApplicationContext

SpringJunit4规则

org.springframework.test.context.junit4.rules包提供以下 JUnit 4 规则(JUnit 4.12 或更高版本支持):

  • SpringClassRule
  • SpringMethodRule

SpringClassRule是一个 JUnit TestRule,它支持 Spring TestContext Framework 的 class-level features,而SpringMethodRule是一个 JUnit MethodRule,它支持 Spring TestContext Framework 的 instance-level 和 method-levelfeatures。

SpringRunner相比,Spring 的 rule-based JUnit 支持具有独立于任何org.junit.runner.Runner implementation 的优点,因此可以与现有的替代运行程序(例如 JUnit 4 的Parameterized)或 third-party 运行程序(例如MockitoJUnitRunner)组合。

要支持 TestContext framework 的完整功能,必须将SpringClassRuleSpringMethodRule组合在一起。以下 example 显示了在 integration 测试中声明这些规则的正确方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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() {
// execute test logic...
}
}
JUnit4支持Classes

org.springframework.test.context.junit4包为 JUnit 4-based 测试用例提供以下支持 classes(在 JUnit 4.12 或更高版本上受支持):

  • AbstractJUnit4SpringContextTests
  • AbstractTransactionalJUnit4SpringContextTests

AbstractJUnit4SpringContextTests是一个抽象基础测试 class,它将 Spring TestContext Framework 与 JUnit 4 环境中的显式ApplicationContext测试支持集成在一起。扩展AbstractJUnit4SpringContextTests时,可以访问protected applicationContext实例变量,可用于执行显式 bean 查找或测试 context 的 state 作为整体。

AbstractTransactionalJUnit4SpringContextTestsAbstractJUnit4SpringContextTests的抽象 transactional 扩展,为 JDBC 访问添加了一些便利功能。这个 class 期望在ApplicationContext中定义javax.sql.DataSource bean 和PlatformTransactionManager bean。当您扩展AbstractTransactionalJUnit4SpringContextTests时,您可以访问protected jdbcTemplate实例变量,您可以使用该变量来运行 SQL statements 来查询数据库。您可以在 running database-related application code 之前和之后使用此类查询来确认数据库 state,并且 Spring 确保此类查询在与 application code 相同的 transaction 范围内运行。与 ORM 工具结合使用时,请务必避免使用误报。如JDBC 测试支持中所述,AbstractTransactionalJUnit4SpringContextTests还提供了方便的方法,通过使用前面提到的jdbcTemplate来委托JdbcTestUtils中的方法。此外,AbstractTransactionalJUnit4SpringContextTests提供了一个executeSqlScript(..)方法,用于针对配置的DataSource运行 SQL 脚本。

这些 classes 是扩展的便利。如果您不希望将 test classes 绑定到 Spring-specific class 层次结构,则可以使用@RunWith(SpringRunner.class)Spring 的 JUnit 规则配置自己的自定义测试 classes。

JUnitJupiter的SpringExtension

Spring TestContext Framework 使用 JUnit 5 中引入的 JUnit Jupiter 测试 framework 提供完整的 integration。通过使用@ExtendWith(SpringExtension.class)注释 test classes,您可以实现标准的 JUnit Jupiter-based 单元和 integration 测试,同时获得 TestContext framework 的好处,例如支持 loading application 上下文,依赖注入测试实例,transactional 测试方法执行等。

此外,由于 JUnit Jupiter 中的丰富扩展 API,Spring 可以在 Spring 支持 JUnit 4 和 TestNG 的 feature 集之上提供以下 features:

  • 测试构造函数,测试方法和测试生命周期回调方法的依赖注入。有关详细信息,请参阅SpringExtension 的依赖注入
  • 基于 SpEL 表达式,环境变量,系统 properties 等对条件测试执行的强大支持。有关更多详细信息和示例,请参阅Spring JUnit Jupiter Testing Annotations@EnabledIf@DisabledIf的文档。
  • 自定义组合的注释,它们组合了来自 Spring 和 JUnit Jupiter 的 annotations。有关详细信息,请参阅Meta-Annotation 支持测试中的@TransactionalDevTestConfig@TransactionalIntegrationTest示例。

以下 code 列表显示了如何配置 test class 以将SpringExtension@ContextConfiguration结合使用:

1
2
3
4
5
6
7
8
9
10
11
// 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() {
// execute test logic...
}
}

由于您也可以在 JUnit 5 中使用 annotations 作为 meta-annotations,Spring 可以提供@SpringJUnitConfig@SpringJUnitWebConfig组合注释来简化测试ApplicationContext和 JUnit Jupiter 的 configuration。

以下 example 使用@SpringJUnitConfig来减少前一个 example 中使用的 configuration 数量:

1
2
3
4
5
6
7
8
9
10
// Instructs Spring to register the SpringExtension with JUnit
// Jupiter and load an ApplicationContext from TestConfig.class
@SpringJUnitConfig(TestConfig.class)
class SimpleTests {

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

类似地,以下 example 使用@SpringJUnitWebConfig创建WebApplicationContext以与 JUnit Jupiter 一起使用:

1
2
3
4
5
6
7
8
9
10
// Instructs Spring to register the SpringExtension with JUnit
// Jupiter and load a WebApplicationContext from TestWebConfig.class
@SpringJUnitWebConfig(TestWebConfig.class)
class SimpleWebTests {

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

有关详细信息,请参阅Spring JUnit Jupiter Testing Annotations@SpringJUnitConfig@SpringJUnitWebConfig的文档。

SpringExtension的依赖注入

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

具体来说,SpringExtension可以将测试的ApplicationContext中的依赖项注入到使用@BeforeAll@AfterAll@BeforeEach@AfterEach@Test@RepeatedTest@ParameterizedTest等注释的测试构造函数和方法中。

构造函数注入

如果 JUnit Jupiter 测试 class 的构造函数中的参数类型为ApplicationContext(或 sub-type)或带注释或带有@Autowired@Qualifier@Value的 meta-annotated,则 Spring 会使用测试中的相应 bean 为该特定参数注入 value ApplicationContext。如果所有参数都应由 Spring 提供,您还可以使用@Autowired直接注释测试构造函数。

如果 test class 的构造函数本身用@Autowired注释,则 Spring 承担解析构造函数中所有参数的责任。因此,JUnit Jupiter 中没有注册的其他ParameterResolver可以解析此类构造函数的参数。

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

1
2
3
4
5
6
7
8
9
10
11
12
@SpringJUnitConfig(TestConfig.class)
class OrderServiceIntegrationTests {

private final OrderService orderService;

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

// tests that use the injected OrderService
}

请注意,此 feature 允许测试依赖项为final,因此不可变。

方法注入

如果 JUnit Jupiter 测试方法或测试生命周期回调方法中的参数类型为ApplicationContext(或 sub-type)或带注释或 meta-annotated 带有@Autowired@Qualifier@Value,则 Spring 会使用相应的 bean 从该特定参数中注入 value 考试的ApplicationContext

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

1
2
3
4
5
6
7
8
@SpringJUnitConfig(TestConfig.class)
class OrderServiceIntegrationTests {

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

由于 JUnit Jupiter 中ParameterResolver支持的健壮性,您还可以将多个依赖项注入单个方法,不仅来自 Spring,还来自 JUnit Jupiter 本身或其他 third-party extensions。

以下 example 显示了如何同时将 Spring 和 JUnit Jupiter inject 依赖项同时包含在placeOrderRepeatedly()测试方法中。

1
2
3
4
5
6
7
8
9
10
11
@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可以让测试方法获得对RepetitionInfo的访问权限。

TestNG支持Classes

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

  • AbstractTestNGSpringContextTests
  • AbstractTransactionalTestNGSpringContextTests

AbstractTestNGSpringContextTests是一个抽象基础测试 class,它将 Spring TestContext Framework 与 TestNG 环境中的显式ApplicationContext测试支持集成在一起。扩展AbstractTestNGSpringContextTests时,可以访问protected applicationContext实例变量,可用于执行显式 bean 查找或测试 context 的 state 作为整体。

AbstractTransactionalTestNGSpringContextTestsAbstractTestNGSpringContextTests的抽象 transactional 扩展,为 JDBC 访问添加了一些便利功能。这个 class 期望在ApplicationContext中定义javax.sql.DataSource bean 和PlatformTransactionManager bean。扩展AbstractTransactionalTestNGSpringContextTests时,可以访问可用于执行 SQL statements 以查询数据库的protected jdbcTemplate实例变量。您可以在 running database-related application code 之前和之后使用此类查询来确认数据库 state,并且 Spring 确保此类查询在与 application code 相同的 transaction 范围内运行。与 ORM 工具结合使用时,请务必避免使用误报。如JDBC 测试支持中所述,AbstractTransactionalTestNGSpringContextTests还提供了方便的方法,通过使用前面提到的jdbcTemplate来委托JdbcTestUtils中的方法。此外,AbstractTransactionalTestNGSpringContextTests提供了一个executeSqlScript(..)方法,用于针对配置的DataSource运行 SQL 脚本。

这些 classes 是扩展的便利。如果您不希望将 test classes 绑定到 Spring-specific class 层次结构,则可以使用@ContextConfiguration@TestExecutionListeners等配置自己的自定义测试 classes,并使用TestContextManager手动检测 test class。有关如何检测测试 class 的示例,请参阅AbstractTestNGSpringContextTests的 source code。

SpringMVC测试框架

Spring MVC Test framework 为使用 fluent API 测试 Spring MVC code 提供了第一个 class 支持,您可以将其与 JUnit,TestNG 或任何其他测试 framework 一起使用。它建立在spring-test模块的Servlet API mock objects上,因此不使用 running Servlet 容器。它使用DispatcherServlet提供完整的 Spring MVC 运行时行为,并且除了独立模式之外,还支持使用 TestContext framework 加载实际的 Spring configuration,在独立模式下,您可以手动实例化控制器并在 time 时测试它们。

Spring MVC Test 还为测试使用RestTemplate的 code 提供 client-side 支持。 Client-side tests mock 服务器响应,也不使用 running 服务器。

Spring Boot 提供了一个选项来编写包含 running 服务器的完整的 end-to-end integration 测试。如果这是您的目标,请参阅Spring Boot reference 页面。有关 out-of-container 和 end-to-end integration 测试之间差异的更多信息,请参阅Out-of-Container 和 End-to-End Integration 测试之间的差异

Server-Side测试

您可以使用 JUnit 或 TestNG 为 Spring MVC 控制器编写普通单元测试。为此,实例化控制器,使用模拟或存根依赖项对其进行注入,并调用其方法(根据需要传递MockHttpServletRequestMockHttpServletResponse和其他)。但是,在编写这样的单元测试时,仍有许多未经测试:例如,请求映射,数据绑定,类型转换,验证等等。此外,还可以调用其他控制器方法(如@InitBinder@ModelAttribute@ExceptionHandler)作为请求处理生命周期的一部分。

Spring MVC Test 的目标是通过执行请求并通过实际DispatcherServlet生成响应来提供测试控制器的有效方法。

Spring MVC Test 建立在spring-test模块中熟悉的“mock”实现 Servlet API上。这允许执行请求并生成响应,而无需在 Servlet 容器中运行 running。在大多数情况下,一切都应该像在运行时那样使用一些值得注意的 exceptions,如Out-of-Container 和 End-to-End Integration 测试之间的差异中所述。以下 JUnit Jupiter-based example 使用 Spring MVC Test:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@SpringJUnitWebConfig(locations = "test-servlet-context.xml")
class ExampleTests {

private MockMvc mockMvc;

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

@Test
void getAccount() throws Exception {
this.mockMvc.perform(get("/accounts/1")
.accept(MediaType.parseMediaType("application/json;charset=UTF-8")))
.andExpect(status().isOk())
.andExpect(content().contentType("application/json"))
.andExpect(jsonPath("$.name").value("Lee"));
}
}

上述测试依赖于 TestContext framework 的WebApplicationContext支持,以从与 test class 相同的包中的 XML configuration 文件加载 Spring configuration,但也支持 Java-based 和 Groovy-based configuration。看到这些sample 测试

MockMvc实例用于对/accounts/1执行GET请求,并验证生成的响应是否为状态 200,content type 是application/json,并且响应正文具有名为name且带有 value Lee的 JSON property。 Jayway JsonPath 项目支持jsonPath语法。验证执行请求结果的许多其他选项将在本文档的后面部分讨论。

静态进口

来自前一节的 example 中的 fluent API 需要一些静态导入,例如MockMvcRequestBuilders.*MockMvcResultMatchers.*MockMvcBuilders.*。查找这些 classes 的简单方法是搜索 match MockMvc*的类型。如果您使用 Eclipse 或 Eclipse-based Spring 工具套件,请确保在 Java→编辑器→内容辅助→收藏夹下的 Eclipse 首选项中将它们添加为“最喜欢的静态成员”。这样做可以在 typing 静态方法 name 的第一个字符后使用内容辅助。其他 IDE(例如 IntelliJ)可能不需要任何其他 configuration。检查静态成员的 code 完成支持。

设置选择

您有两个主要选项来创建MockMvc的实例。第一种是通过 TestContext framework 加载 Spring MVC configuration,它加载 Spring configuration 并将WebApplicationContext注入测试以用于 build MockMvc实例。以下 example 显示了如何执行此操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RunWith(SpringRunner.class)
@WebAppConfiguration
@ContextConfiguration("my-servlet-context.xml")
public class MyWebTests {

@Autowired
private WebApplicationContext wac;

private MockMvc mockMvc;

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

// ...

}

第二个选项是手动创建一个控制器实例,而不是 loading Spring configuration。相反,将自动创建与 MVC JavaConfig 或 MVC 名称空间大致相当的基本默认 configuration。您可以在一定程度上自定义它。以下 example 显示了如何执行此操作:

1
2
3
4
5
6
7
8
9
10
11
12
public class MyWebTests {

private MockMvc mockMvc;

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

// ...

}

你应该使用哪种设置选项?

webAppContextSetup加载您实际的 Spring MVC configuration,从而产生更完整的 integration 测试。由于 TestContext framework 缓存了加载的 Spring configuration,因此即使您在测试套件中引入了更多测试,它也有助于保持测试快速运行。此外,您可以通过 Spring configuration 将 mock 服务注入控制器,以继续专注于测试 web 层。以下 example 使用 Mockito 声明 mock 服务:

1
2
3
<bean id="accountService" class="org.mockito.Mockito" factory-method="mock">
<constructor-arg value="org.example.AccountService"/>
</bean>

然后,您可以将 mock 服务注入测试以设置和验证您的期望,如下面的示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RunWith(SpringRunner.class)
@WebAppConfiguration
@ContextConfiguration("test-servlet-context.xml")
public class AccountTests {

@Autowired
private WebApplicationContext wac;

private MockMvc mockMvc;

@Autowired
private AccountService accountService;

// ...

}

另一方面,standaloneSetup更接近单元测试。它在 time 测试一个控制器。您可以使用 mock 依赖项手动 inject 控制器,它不涉及 loading Spring configuration。这些测试更侧重于样式,并且更容易看到正在测试哪个控制器,是否需要任何特定的 Spring MVC configuration 才能工作,等等。 standaloneSetup也是编写 ad-hoc 测试以验证特定行为或调试问题的一种非常方便的方法。

与大多数“整合与单元测试”辩论一样,没有正确或错误的答案。但是,使用standaloneSetup确实意味着需要在 order 中进行额外的webAppContextSetup测试以验证您的 Spring MVC configuration。或者,您可以使用webAppContextSetup编写所有测试,在 order 中始终针对您的实际 Spring MVC configuration 进行测试。

设置Features

无论您使用哪个 MockMvc 构建器,所有MockMvcBuilder implementations 都提供了一些 common 和非常有用的 features。对于 example,您可以为所有请求声明Accept标头,并期望在所有响应中状态为 200 以及Content-Type标头,如下所示:

1
2
3
4
5
6
7
// 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();

另外,third-party frameworks(和 applications)可以 pre-package 设置指令,例如MockMvcConfigurer中的指令。 Spring Framework 有一个这样的 built-in implementation,它有助于跨请求保存和 re-use HTTP session。您可以按如下方式使用它:

1
2
3
4
5
6
7
// static import of SharedHttpSessionConfigurer.sharedHttpSession

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

// Use mockMvc to perform requests...

有关ConfigurableMockMvcBuilder的 javadoc,请参阅所有 MockMvc 构建器 features 的列表,或使用 IDE 浏览可用选项。

执行请求

您可以执行使用任何 HTTP 方法的请求,如以下 example 所示:

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

您还可以执行内部使用MockMultipartHttpServletRequest的文件上载请求,以便不会实际解析 multipart 请求。相反,您必须将其设置为类似于以下 example:

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

您可以在 URI 模板样式中指定查询参数,如以下 example 所示:

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

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

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

如果 application code 依赖于 Servlet 请求参数,并且没有显式检查查询 string(最常见的情况),那么使用哪个选项并不重要。但请记住,对 URI 模板提供的查询参数进行解码,而通过param(…)方法提供的请求参数预计已经被解码。

在大多数情况下,最好将 context 路径和 Servlet 路径保留在请求 URI 之外。如果必须使用完整请求 URI 进行测试,请确保相应地设置contextPathservletPath以使请求映射有效,如下面的 example 所示:

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

在前面的示例中,对每个执行的请求设置contextPathservletPath将是麻烦的。相反,您可以设置默认请求 properties,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
public class MyWebTests {

private MockMvc mockMvc;

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

前面的 properties 会影响通过MockMvc实例执行的每个请求。如果在给定请求上也指定了相同的 property,它将覆盖默认的 value。这就是默认请求中的 HTTP 方法和 URI 无关紧要的原因,因为必须在每个请求中指定它们。

定义期望

您可以在执行请求后通过附加一个或多个.andExpect(..) _call 来定义期望,如下面的 example 所示:

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

MockMvcResultMatchers.*提供了许多期望,其中一些期望与更详细的期望进一步嵌套。

期望分为两大类。第一类断言验证响应的 properties(例如,响应状态,headers 和内容)。这些是断言最重要的结果。

第二类断言超出了回应范围。这些断言允许您检查 Spring MVC 特定方面,例如处理请求的控制器方法,是否引发和处理 exception,model 的内容是什么,选择了哪个视图,添加了哪些 flash 属性等等。它们还允许您检查 Servlet 特定方面,例如 request 和 session 属性。

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

1
2
3
mockMvc.perform(post("/persons"))
.andExpect(status().isOk())
.andExpect(model().attributeHasErrors("person"));

很多时候,在编写测试时,转储执行的请求的结果很有用。您可以按如下方式执行此操作,其中print()是来自MockMvcResultHandlers的静态 import:

1
2
3
4
mockMvc.perform(post("/persons"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(model().attributeHasErrors("person"));

由于 long 因为请求处理不会导致未处理的 exception,print()方法会将所有可用的结果数据打印到System.out。 Spring Framework 4.2 引入了log()方法和print()方法的另外两个变体,一个接受OutputStream,另一个接受Writer。对于 example,调用print(System.err)将结果数据打印到System.err,而调用print(myWriter)则将结果数据打印到自定义 writer。如果要记录结果数据而不是打印结果数据,可以调用log()方法,该方法将结果数据记录为org.springframework.test.web.servlet.result logging 类别下的单个DEBUG消息。

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

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

如果所有测试重复相同的期望,则可以在 building MockMvc实例时设置 common 期望值,如下面的 example 所示:

1
2
3
4
standaloneSetup(new SimpleController())
.alwaysExpect(status().isOk())
.alwaysExpect(content().contentType("application/json;charset=UTF-8"))
.build()

请注意,如果没有 creating 单独的MockMvc实例,则始终会应用 common 期望值并且无法覆盖。

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

1
2
mockMvc.perform(get("/people").accept(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.links[?(@.rel == 'self')].href").value("http://localhost:8080/people"));

当 XML 响应内容包含使用Spring HATEOAS创建的超媒体链接时,您可以使用 XPath 表达式验证生成的链接:

1
2
3
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"));
过滤器注册

设置MockMvc实例时,可以注册一个或多个 Servlet Filter实例,如下面的 example 所示:

1
mockMvc = standaloneSetup(new PersonController()).addFilters(new CharacterEncodingFilter()).build();

已注册的过滤器通过spring-testspring-test调用,最后一个过滤器委托给DispatcherServlet

Out-of-Container和End-to-End集成测试之间的差异

如前所述 Spring MVC Test 构建在spring-test模块的 Servlet API mock objects 之上,并且不使用 running Servlet 容器。因此,与使用实际 client 和 server running 的完整 end-to-end integration 测试相比,存在一些重要差异。

考虑这一点的最简单方法是从空白MockHttpServletRequest开始。无论你添加什么是请求变成什么。令你惊讶的是,默认情况下没有 context 路径;没有jsessionid cookie;没有转发,错误或异步调度;因此,没有实际的 JSP 渲染。相反,“转发”和“重定向”的 URL 保存在MockHttpServletResponse中,并且可以满足期望。

这意味着,如果使用 JSP,则可以验证请求转发到的 JSP 页面,但不呈现 HTML。换句话说,不调用 JSP。但请注意,所有其他不依赖转发的呈现技术(如 Thymeleaf 和 Freemarker)都会按预期将 HTML 呈现给响应主体。通过@ResponseBody方法呈现 JSON,XML 和其他格式也是如此。

或者,您可以考虑 Spring Boot 与@WebIntegrationTest的完整 end-to-end integration 测试支持。见Spring Boot Reference Guide

每种方法都有利弊。 Spring MVC Test 中提供的选项在从经典单元测试到完整整合测试的规模上是不同的停止。确切地说,Spring MVC Test 中的所有选项都属于经典单元测试类别,但它们更接近它。例如,您可以通过将模拟服务注入控制器来隔离 web 层,在这种情况下,您只通过DispatcherServlet测试 web 层,但使用实际的 Spring configuration,因为您可以独立于上面的层测试数据访问层。此外,您可以使用 stand-alone 设置,在 time 时关注一个控制器并手动提供使其工作所需的 configuration。

使用 Spring MVC Test 时的另一个重要区别是,从概念上讲,这样的测试是 server-side,所以你可以检查使用了什么处理程序,是否使用 HandlerExceptionResolver 处理 exception,model 的内容是什么,binding 错误是什么等细节。这意味着编写期望更容易,因为服务器不是黑盒子,就像通过实际的 HTTP client 测试它一样。这通常是经典单元测试的一个优点:它更容易编写,推理和调试,但不能取代完整整合测试的需要。在同一时间,重要的是不要忽视响应是最重要的检查事实。简而言之,即使在同一个项目中,也存在多种样式和测试策略的空间。

进一步Server-Side测试实例

framework 自己的测试包括许多 sample 测试旨在展示如何使用 Spring MVC Test。您可以浏览这些示例以获取更多想法。此外,spring-mvc-showcase项目具有基于 Spring MVC 测试的完整测试覆盖率。

HtmlUnit集成

Spring 在MockMvcHtmlUnit之间提供 integration。这简化了使用 HTML-based 视图时执行 end-to-end 测试的过程。这个 integration 让你:

  • 使用HtmlUnitwebdriver创业板等工具轻松测试 HTML 页面,而无需部署到 Servlet 容器。
  • 在页面中测试 JavaScript。
  • (可选)使用 mock 服务进行测试以加快测试速度。
  • 在 in-container end-to-end 测试和 out-of-container integration 测试之间共享逻辑。

MockMvc 使用不依赖于 Servlet 容器的模板技术(对于 example,Thymeleaf,FreeMarker 等),但它不适用于 JSP,因为它们依赖于 Servlet 容器。

为什么HtmlUnit集成?

想到的最明显的问题是“为什么我需要这个?”通过探索一个非常基本的 sample application 可以找到答案。假设您有 Spring MVC web application,它支持Message object 上的 CRUD 操作。 application 还支持对所有消息进行分页。你会怎么做测试呢?

使用 Spring MVC Test,我们可以轻松测试是否能够创建Message,如下所示:

1
2
3
4
5
6
7
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"));

如果我们想测试允许我们创建消息的表单视图,该怎么办?对于 example,假设我们的表单看起来像以下代码段:

1
2
3
4
5
6
7
8
9
10
11
12
13
<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>

我们如何确保我们的表单产生正确的请求以创建新消息?天真的尝试可能类似于以下内容:

1
2
3
mockMvc.perform(get("/messages/form"))
.andExpect(xpath("//input[@name='summary']").exists())
.andExpect(xpath("//textarea[@name='text']").exists());

该测试有一些明显的缺点。如果我们更新控制器以使用参数message而不是text,我们的表单测试将继续通过,即使 HTML 表单与控制器不同步。要解决这个问题,我们可以结合两个测试,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
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 验证,该怎么办?

总体问题是测试 web 页面不涉及单个交互。相反,它是用户如何与 web 页面交互以及 web 页面如何与其他资源交互的组合。对于 example,表单视图的结果用作用户输入创建消息的输入。此外,我们的表单视图可能会使用影响页面行为的其他资源,例如 JavaScript 验证。

Integration测试救援?

要解决前面提到的问题,我们可以执行 end-to-end integration 测试,但这有一些缺点。考虑测试让我们浏览消息的视图。我们可能需要以下测试:

  • 我们的页面是否向用户显示通知,指示消息为空时没有结果可用?
  • 我们的页面是否正确显示单个消息?
  • 我们的页面是否正确支持分页?

要设置这些测试,我们需要确保我们的数据库包含正确的消息。这导致了许多额外的挑战:

  • 确保数据库中存在正确的消息可能很繁琐。 (考虑外国 key constraints.)
  • 测试可能会变慢,因为每个测试都需要确保数据库处于正确的 state 状态。
  • 由于我们的数据库需要在特定的 state 中,因此我们无法在 parallel 中运行测试。
  • 对诸如 auto-generatedid,时间戳等项目执行断言可能很困难。

这些挑战并不意味着我们应该完全放弃 end-to-end 整合测试。相反,我们可以通过重构我们的详细测试来减少 end-to-end 整合测试的数量,以便使用 mock 服务,这些服务运行速度更快,更可靠,没有副作用。然后,我们可以实现少量 true end-to-end integration 测试,以验证简单的工作流程,以确保一切正常工作。

输入HtmlUnit集成

那么我们如何才能在测试页面交互之间取得平衡,并在测试套件中保持良好的性能?答案是:“通过将 MockMvc 与 HtmlUnit 集成。”

HtmlUnit集成选项

当您想要将 MockMvc 与 HtmlUnit 集成时,您有许多选项:

  • MockMvc 和 HtmlUnit:如果要使用原始 HtmlUnit libraries,请使用此选项。
  • MockMvc 和 WebDriver:使用此选项可以简化 integration 和 end-to-end 测试之间的开发和重用 code。
  • MockMvc 和 Geb:如果要使用 Groovy 进行测试,简化开发并在 integration 和 end-to-end testing 之间重用 code,请使用此选项。
MockMvc和HtmlUnit

本节介绍如何集成 MockMvc 和 HtmlUnit。如果要使用原始 HtmlUnit libraries,请使用此选项。

MockMvc和HtmlUnit设置

首先,确保您已在net.sourceforge.htmlunit:htmlunit中包含测试依赖项。为了使用 HtmlUnit 和 Apache HttpComponents 4.5,你需要使用 HtmlUnit 2.18 或更高版本。

我们可以使用MockMvcWebClientBuilder轻松创建一个与 MockMvc 集成的 HtmlUnit WebClient,如下所示:

1
2
3
4
5
6
7
8
9
10
11
@Autowired
WebApplicationContext context;

WebClient webClient;

@Before
public void setup() {
webClient = MockMvcWebClientBuilder
.webAppContextSetup(context)
.build();
}

这是使用MockMvcWebClientBuilder的简单示例。有关高级用法,请参阅高级 MockMvcWebClientBuilder

这确保了 references localhost作为服务器的任何 URL 都定向到我们的MockMvc实例,而无需真正的 HTTP 连接。正常情况下,使用网络连接请求任何其他 URL。这让我们可以轻松测试 CDN 的使用。

MockMvc和HtmlUnit用法

现在我们可以像往常一样使用 HtmlUnit,但不需要将 application 部署到 Servlet 容器。对于 example,我们可以请求视图创建包含以下内容的消息:

1
HtmlPage createMsgFormPage = webClient.getPage("http://localhost/messages/form");

默认的 context 路径是""。或者,我们可以指定 context 路径,如高级 MockMvcWebClientBuilder中所述。

一旦我们对HtmlPage进行了 reference,我们就可以填写表单并提交它以创建一条消息,如下面的 example 所示:

1
2
3
4
5
6
7
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 library:

1
2
3
4
5
6
7
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!");

前面的 code 以多种方式改进了我们的MockMvc 测试。首先,我们不再需要显式验证我们的表单,然后创建一个看起来像表单的请求。相反,我们请求表单,填写表单并提交表单,从而显着减少开销。

另一个重要因素是HtmlUnit 使用 Mozilla Rhino 引擎来评估 JavaScript。这意味着我们还可以在页面中测试 JavaScript 的行为。

有关使用 HtmlUnit 的其他信息,请参阅HtmlUnit 文档

高级MockMvcWebClientBuilder

在到目前为止的示例中,我们以最简单的方式使用MockMvcWebClientBuilder,通过基于 Spring TestContext Framework 为我们加载的WebApplicationContext构建WebClient。在以下 example 中重复此方法:

1
2
3
4
5
6
7
8
9
10
11
@Autowired
WebApplicationContext context;

WebClient webClient;

@Before
public void setup() {
webClient = MockMvcWebClientBuilder
.webAppContextSetup(context)
.build();
}

我们还可以指定其他 configuration 选项,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
WebClient webClient;

@Before
public 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来执行完全相同的设置,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
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 的全部功能。

有关创建MockMvc实例的其他信息,请参阅设置选择

MockMvc和WebDriver

在前面的部分中,我们已经了解了如何将 MockMvc 与原始 HtmlUnit API 结合使用。在本节中,我们使用 Selenium webdriver中的其他抽象来使事情变得更加容易。

为什么选择WebDriver和MockMvc?

我们已经可以使用 HtmlUnit 和 MockMvc,那么我们为什么要使用 WebDriver 呢? Selenium WebDriver 提供了一个非常优雅的 API,可以让我们轻松地组织我们的 code。为了更好地展示它的工作原理,我们将在本节中探讨一个 example。

尽管是Selenium的一部分,但 WebDriver 并不需要 Selenium Server 来运行您的测试。

假设我们需要确保正确创建消息。测试涉及查找 HTML 表单输入元素,填写它们以及进行各种断言。

这种方法导致许多单独的测试,因为我们也想测试错误条件。例如,如果我们只填写表单的一部分,我们希望确保收到错误。如果我们填写整个表格,则应在之后显示新创建的消息。

如果其中一个字段被命名为“summary”,我们可能会在我们的测试中有多个类似于以下重复的内容:

1
2
HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary");
summaryInput.setValueAttribute(summary);

那么如果我们将id更改为smmry会发生什么?这样做会迫使我们更新所有测试以包含此更改。这违反了 DRY 原则,因此我们理想情况下应将此 code 提取到自己的方法中,如下所示:

1
2
3
4
5
6
7
8
9
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 时不必更新所有测试。

我们甚至可以进一步将这个逻辑放在一个Object中,代表我们当前所在的HtmlPage,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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());
}
}

以前,这个 pattern 被称为Page Object Pattern。虽然我们当然可以使用 HtmlUnit 执行此操作,但 WebDriver 提供了一些我们在以下部分中探讨的工具,以使此 pattern 更容易实现。

MockMvc和WebDriver设置

要将 Selenium WebDriver 与 Spring MVC Test framework 一起使用,请确保您的项目包含对org.seleniumhq.selenium:selenium-htmlunit-driver的测试依赖项。

我们可以使用MockMvcHtmlUnitDriverBuilder轻松创建与 MockMvc 集成的 Selenium WebDriver,如以下 example 所示:

1
2
3
4
5
6
7
8
9
10
11
@Autowired
WebApplicationContext context;

WebDriver driver;

@Before
public void setup() {
driver = MockMvcHtmlUnitDriverBuilder
.webAppContextSetup(context)
.build();
}

这是使用MockMvcHtmlUnitDriverBuilder的简单示例。有关更高级的用法,请参阅高级 MockMvcHtmlUnitDriverBuilder

前面的 example 确保将_re 作为服务器的任何 URL 定向到我们的MockMvc实例,而无需真正的 HTTP 连接。正常情况下,使用网络连接请求任何其他 URL。这让我们可以轻松测试 CDN 的使用。

MockMvc和WebDriver用法

现在我们可以像往常一样使用 WebDriver,但不需要将 application 部署到 Servlet 容器。对于 example,我们可以请求视图创建包含以下内容的消息:

1
CreateMessagePage page = CreateMessagePage.to(driver);

然后我们可以填写表单并提交它以创建消息,如下所示:

1
2
ViewMessagePage viewMessagePage =
page.createMessage(ViewMessagePage.class, expectedSummary, expectedText);

这通过利用 Page Object Pattern 改进了HtmlUnit 测试的设计。正如我们在为什么选择 WebDriver 和 MockMvc?中提到的,我们可以使用 Page Object Pattern 和 HtmlUnit,但使用 WebDriver 会更容易。考虑以下CreateMessagePage implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class CreateMessagePage
extends AbstractPage { (1)

(2)
private WebElement summary;
private WebElement text;

(3)
@FindBy(css = "input[type=submit]")
private WebElement submit;

public CreateMessagePage(WebDriver driver) {
super(driver);
}

public <T> T createMessage(Class<T> resultPage, String summary, String details) {
this.summary.sendKeys(summary);
this.text.sendKeys(details);
this.submit.click();
return PageFactory.initElements(driver, resultPage);
}

public static CreateMessagePage to(WebDriver driver) {
driver.get("http://localhost:9990/mail/messages/form");
return PageFactory.initElements(driver, CreateMessagePage.class);
}
}
1 CreateMessagePage扩展AbstractPage。我们不会详细介绍AbstractPage的细节,但总的来说,它包含了我们所有页面的 common 功能。例如,如果我们的 application 具有导航栏,global 错误消息和其他 features,我们可以将此逻辑放在共享位置。
2 我们有一个成员变量,用于我们感兴趣的 HTML 页面的每个部分。这些是WebElement类型。 WebDriver 的PageFactory允许我们通过自动解析每个WebElementCreateMessagePage的 HtmlUnit version 中删除大量 code。 PageFactory#initElements(WebDriver,Class)方法通过使用字段 name 自动解析每个WebElement,并通过 HTML 页面中元素的idname查找它。
3 我们可以使用@FindBy annotation来覆盖默认的查找行为。我们的 example 显示了如何使用@FindBy annotation 查找带有css选择器的提交按钮(输入[2083])。

最后,我们可以验证是否已成功创建新消息。以下断言使用AssertJ断言 library:

1
2
assertThat(viewMessagePage.getMessage()).isEqualTo(expectedMessage);
assertThat(viewMessagePage.getSuccess()).isEqualTo("Successfully created a new message");

我们可以看到我们的ViewMessagePage允许我们与自定义域 model 进行交互。对于 example,它公开了一个返回Message object 的方法:

1
2
3
4
5
6
7
8
public Message getMessage() throws ParseException {
Message message = new Message();
message.setId(getId());
message.setCreated(getCreated());
message.setSummary(getSummary());
message.setText(getText());
return message;
}

然后我们可以在断言中使用富域 objects。

最后,我们不要忘记在测试完成时关闭WebDriver实例,如下所示:

1
2
3
4
5
6
@After
public void destroy() {
if (driver != null) {
driver.close();
}
}

有关使用 WebDriver 的其他信息,请参阅 Selenium WebDriver 文档

高级MockMvcHtmlUnitDriverBuilder

在到目前为止的示例中,我们以最简单的方式使用MockMvcHtmlUnitDriverBuilder,通过基于 Spring TestContext Framework 为我们加载的WebApplicationContext构建WebDriver。这里重复这种方法,如下:

1
2
3
4
5
6
7
8
9
10
11
@Autowired
WebApplicationContext context;

WebDriver driver;

@Before
public void setup() {
driver = MockMvcHtmlUnitDriverBuilder
.webAppContextSetup(context)
.build();
}

我们还可以指定其他 configuration 选项,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
WebDriver driver;

@Before
public 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来执行完全相同的设置,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
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 的全部功能。

有关创建MockMvc实例的其他信息,请参阅设置选择

MockMvc和Geb

在上一节中,我们了解了如何将 MockMvc 与 WebDriver 一起使用。在本节中,我们使用创业板使我们的测试甚至 Groovy-er。

为什么选择Geb和MockMvc?

Geb 由 WebDriver 支持,因此它提供了许多我们从 WebDriver 获得的同样的好处。但是,通过为我们处理一些样板 code,Geb 使事情变得更加容易。

MockMvc和Geb设置

我们可以使用使用 MockMvc 的 Selenium WebDriver 轻松初始化 Geb Browser,如下所示:

1
2
3
4
5
def setup() {
browser.driver = MockMvcHtmlUnitDriverBuilder
.webAppContextSetup(context)
.build()
}

这是使用MockMvcHtmlUnitDriverBuilder的简单示例。有关更高级的用法,请参阅高级 MockMvcHtmlUnitDriverBuilder

这可确保任何引用localhost作为服务器的 URL 都定向到我们的MockMvc实例,而无需真正的 HTTP 连接。正常使用网络连接请求任何其他 URL。这让我们可以轻松测试 CDN 的使用。

MockMvc和Geb用法

现在我们可以像往常一样使用 Geb,但不需要将 application 部署到 Servlet 容器。对于 example,我们可以请求视图创建包含以下内容的消息:

1
to CreateMessagePage

然后我们可以填写表单并提交它以创建消息,如下所示:

1
2
3
4
when:
form.summary = expectedSummary
form.text = expectedMessage
submit.click(ViewMessagePage)

任何未识别的方法 calls 或 property 访问或未找到的 references 将被转发到当前页面 object。这删除了直接使用 WebDriver 时我们需要的许多样板 code。

与直接使用 WebDriver 一样,这通过使用 Page Object Pattern 改进了HtmlUnit 测试的设计。如前所述,我们可以使用 Page Object Pattern 和 HtmlUnit 以及 WebDriver,但使用 Geb 更容易。考虑我们新的 Groovy-based CreateMessagePage implementation:

1
2
3
4
5
6
7
8
9
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的细节,但总的来说,它包含了我们所有页面的 common 功能。我们定义了一个可以在其中找到此页面的 URL。这让我们可以导航到页面,如下所示:

1
to CreateMessagePage

我们还有一个at闭包,用于确定我们是否在指定页面。如果我们在正确的页面上,它应该_ret。这就是为什么我们可以断言我们在正确的页面上,如下所示:

1
2
3
then:
at CreateMessagePage
errors.contains('This field is required.')

我们在闭包中使用断言,以便在错误的页面中我们可以确定出错的地方。

接下来,我们创建一个content闭包,指定页面中所有感兴趣的区域。我们可以使用jQuery-ish Navigator API来选择我们感兴趣的内容。

最后,我们可以验证是否已成功创建新消息,如下所示:

1
2
3
4
5
6
7
then:
at ViewMessagePage
success == 'Successfully created a new message'
id
date
summary == expectedSummary
message == expectedMessage

有关如何充分利用 Geb 的更多详细信息,请参阅The Geb用户手册。

客户端REST测试

您可以使用 client-side 测试来测试内部使用RestTemplate的 code。 idea 用于声明预期的请求并提供“存根”响应,以便您可以专注于单独测试 code(即,无需运行服务器)。以下 example 显示了如何执行此操作:

1
2
3
4
5
6
7
8
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(client-side REST 测试的中央 class)使用自定义ClientHttpRequestFactory配置RestTemplate,该自定义ClientHttpRequestFactory根据预期断言实际请求并返回“存根”响应。在这种情况下,我们期望/greeting的请求并且想要返回带有text/plain内容的 200 响应。我们可以根据需要定义其他预期请求和存根响应。当我们定义期望的请求和存根响应时,RestTemplate可以像往常一样在 client-side code 中使用。在测试结束时,mockServer.verify()可用于验证是否已满足所有期望。

默认情况下,请求在声明期望的 order 中。您可以在 building 服务器时设置ignoreExpectOrder选项,在这种情况下,检查所有期望(在 order 中)以查找给定请求的 match。这意味着允许请求进入任何 order。以下 example 使用ignoreExpectOrder

1
server = MockRestServiceServer.bindTo(restTemplate).ignoreExpectOrder(true).build();

即使默认情况下无序请求,每个请求也只允许执行一次。 expect方法提供了一个重载变量,它接受一个指定计数范围的ExpectedCount参数(对于 example,oncemanyTimesmaxminbetween等)。以下 example 使用times

1
2
3
4
5
6
7
8
9
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();

请注意,如果未设置ignoreExpectOrder(默认值),并且因此请求在 order 声明中,则该 order 仅适用于任何预期请求中的第一个。对于 example,如果预期“/something”两次,然后是“/somewhere”三次,那么在请求“/somewhere”之前应该有“/something”的请求,但是,除了后续的“/something”和“/somewhere”之外“,请求可以在任何时间到来。

作为上述所有选项的替代方案,client-side 测试支持还提供ClientHttpRequestFactory implementation,您可以将其配置为RestTemplate以将其绑定到MockMvc实例。这允许使用实际 server-side 逻辑处理请求但不运行服务器。以下 example 显示了如何执行此操作:

1
2
3
4
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
this.restTemplate = new RestTemplate(new MockMvcClientHttpRequestFactory(mockMvc));

// Test code that uses the above RestTemplate ...
静态进口

与 server-side 测试一样,client-side 测试的 fluent API 需要一些静态导入。通过搜索MockRest*很容易找到。 Eclipse 用户应该在 Java→编辑器→内容辅助→收藏夹下的 Eclipse 首选项中添加MockRestRequestMatchers.*MockRestResponseCreators.*作为“最喜欢的静态成员”。这允许在 typing 静态方法 name 的第一个字符后使用内容辅助。其他 IDE(例如 IntelliJ)可能不需要任何额外的 configuration。检查静态成员对 code 完成的支持。

客户端REST测试的更多示例

Spring MVC Test 自己的测试包括example 测试 client-side REST 测试。

WebTestClient

WebTestClient是一个围绕Web 客户端的瘦 shell,用它来执行请求并公开一个专用的,流畅的 API 来验证响应。 WebTestClient使用mock 请求和响应绑定到 WebFlux application,或者它可以通过 HTTP 连接测试任何 web 服务器。

Kotlin 用户:请参阅这个部分与使用WebTestClient相关。

建立

要创建WebTestClient,您必须选择多个服务器设置选项之一。实际上,您要么将 WebFlux application 配置为绑定,要么使用 URL 连接到运行服务器。

绑定到控制器

以下 example 显示了如何创建服务器设置以在 time 测试一个@Controller

1
client = WebTestClient.bindToController(new TestController()).build();

前面的 example 加载WebFlux Java configuration并注册给定的控制器。通过使用 mock 请求和响应 objects,在没有 HTTP 服务器的情况下测试生成的 WebFlux application。构建器上有更多方法可以自定义默认的 WebFlux Java configuration。

绑定到RouterFunction

以下 example 显示了如何从RouterFunction设置服务器:

1
2
RouterFunction<?> route = ...
client = WebTestClient.bindToRouterFunction(route).build();

在内部,configuration 传递给RouterFunctions.toWebHandler。通过使用 mock 请求和响应 objects,在没有 HTTP 服务器的情况下测试生成的 WebFlux application。

绑定到ApplicationContext

以下 example 显示了如何从 application 的 Spring configuration 或其某个子集设置服务器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = WebConfig.class) (1)
public class MyTests {

@Autowired
private ApplicationContext context; (2)

private WebTestClient client;

@Before
public void setUp() {
client = WebTestClient.bindToApplicationContext(context).build(); (3)
}
}
1 指定要加载的 configuration
2 Inject the configuration
3 创建WebTestClient

在内部,configuration 被传递给WebHttpHandlerBuilder以设置请求处理链。有关详细信息,请参阅WebHandler API。通过使用 mock 请求和响应 objects,在没有 HTTP 服务器的情况下测试生成的 WebFlux application。

绑定到服务器

以下服务器设置选项允许您连接到运行服务器:

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

除了前面描述的服务器设置选项之外,您还可以配置 client 选项,包括基本 URL,默认 headers,client 过滤器等。这些选项在bindToServer之后随时可用。对于所有其他人,您需要使用configureClient()从服务器转换到 client configuration,如下所示:

1
2
3
4
client = WebTestClient.bindToController(new TestController())
.configureClient()
.baseUrl("/test")
.build();

写测试

WebTestClient提供与Web 客户端相同的 API,直到使用exchange()执行请求为止。 exchange()之后的内容是用于验证响应的链式 API 工作流程。

通常,您首先声明响应状态和 headers,如下所示:

1
2
3
4
5
6
client.get().uri("/persons/1")
.accept(MediaType.APPLICATION_JSON_UTF8)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
// ...

然后指定如何解码和使用响应正文:

  • expectBody(Class<T>):解码为单个 object。
  • expectBodyList(Class<T>):将 objects 解码并收集到List<T>
  • expectBody():为JSON 内容或空体解码为byte[]

然后你可以使用 built-in 断言为身体。以下 example 显示了一种方法:

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

您还可以超越 built-in 断言并创建自己的断言,如下面的示例所示:

1
2
3
4
5
6
7
client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody(Person.class)
.consumeWith(result -> {
// custom assertions (e.g. AssertJ)...
});

您也可以退出工作流程并获得结果,如下所示:

1
2
3
4
5
EntityExchangeResult<Person> result = client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody(Person.class)
.returnResult();

当您需要使用泛型解码为目标类型时,请查找接受ParameterizedTypeReference而不是Class<T>的重载方法。

没有内容

如果响应没有内容(或者您不关心它),请使用Void.class,这可确保释放资源。以下 example 显示了如何执行此操作:

1
2
3
4
client.get().uri("/persons/123")
.exchange()
.expectStatus().isNotFound()
.expectBody(Void.class);

或者,如果要断言没有响应内容,可以使用类似于以下内容的 code:

1
2
3
4
5
client.post().uri("/persons")
.body(personMono, Person.class)
.exchange()
.expectStatus().isCreated()
.expectBody().isEmpty();
JSON内容

使用expectBody()时,响应将作为byte[]使用。这对原始内容断言很有用。对于 example,您可以使用JSONAssert来验证 JSON 内容,如下所示:

1
2
3
4
5
client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody()
.json("{\"name\":\"Jane\"}")

您还可以使用JSONPath表达式,如下所示:

1
2
3
4
5
6
client.get().uri("/persons")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$[0].name").isEqualTo("Jane")
.jsonPath("$[1].name").isEqualTo("Jason");
Streaming响应

要测试无限流(对于 example,"text/event-stream""application/stream+json"),您需要在响应状态和标头断言之后立即退出链式 API(使用returnResult),如下面的 example 所示:

1
2
3
4
5
FluxExchangeResult<MyEvent> result = client.get().uri("/events")
.accept(TEXT_EVENT_STREAM)
.exchange()
.expectStatus().isOk()
.returnResult(MyEvent.class);

现在你可以使用Flux<T>,断言解码后的 objects,然后在满足测试目标时取消。我们建议使用reactor-test模块中的StepVerifier来执行此操作,如下面的 example 所示:

1
2
3
4
5
6
7
8
Flux<Event> eventFux = result.getResponseBody();

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

当涉及 building 请求时,WebTestClient提供与WebClient相同的 API,implementation 主要是一个简单的 pass-through。有关如何使用正文准备请求的示例,请参阅WebClient 文档,包括提交表单数据,Multipart 请求等。

PetClinic例子

GitHub 上上可用的 PetClinic application 显示了 JUnit 4 环境中 Spring TestContext Framework 的几个 features。大多数测试功能都包含在AbstractClinicTests中,其中部分列表如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import static org.junit.Assert.assertEquals;
// import ...

@ContextConfiguration (1)
public abstract class AbstractClinicTests extends AbstractTransactionalJUnit4SpringContextTests { (2)

@Autowired (3)
protected Clinic clinic;

@Test
public void getVets() {
Collection<Vet> vets = this.clinic.getVets();
assertEquals("JDBC query must show the same number of vets",
super.countRowsInTable("VETS"), vets.size()); (4)
Vet v1 = EntityUtils.getById(vets, Vet.class, 2);
assertEquals("Leary", v1.getLastName());
assertEquals(1, v1.getNrOfSpecialties());
assertEquals("radiology", (v1.getSpecialties().get(0)).getName());
// ...
}

// ...
}
1 从默认位置加载 application context:AbstractClinicTests-context.xml
2 这个测试用例扩展了AbstractTransactionalJUnit4SpringContextTests class,它继承了依赖注入的 configuration(通过DependencyInjectionTestExecutionListener)和 transactional 行为(通过TransactionalTestExecutionListener)。
3 clinic实例变量(正在测试的 application object)由依赖注入通过@Autowired语义设置。
4 getVets()方法显示了如何使用继承的countRowsInTable()方法轻松验证给定 table 中的行数,从而验证正在测试的 application code 的正确行为。这允许更强的测试并减少对精确测试数据的依赖性。例如,您可以在数据库中添加其他行而不会破坏测试。

与使用数据库的许多 integration 测试一样,AbstractClinicTests中的大多数测试依赖于测试用例 run 之前数据库中已存在的最小数据量。或者,您可以在测试用例的测试夹具设置中填充数据库(同样,在与测试相同的 transaction 中)。

PetClinic application 支持三种数据访问技术:JDBC,Hibernate 和 JPA。通过在没有任何特定资源位置的情况下声明@ContextConfigurationAbstractClinicTests class 从默认位置AbstractClinicTests-context.xml加载了 application context,它声明 common DataSource。子类指定必须声明PlatformTransactionManager的其他 context 位置和Clinic的具体 implementation。

例如,PetClinic 测试的 Hibernate implementation 包含以下 implementation。对于此 example,HibernateClinicTests不包含 code 的 line。我们只需声明@ContextConfiguration,测试继承自AbstractClinicTests。因为声明@ContextConfiguration没有任何特定的资源位置,Spring TestContext Framework 从AbstractClinicTests-context.xml中定义的所有 beans(即继承的位置)和HibernateClinicTests-context.xml加载 application context,可能覆盖AbstractClinicTests-context.xml中定义的 beans。以下清单显示了HibernateClinicTests class 的定义:

1
2
@ContextConfiguration (1)
public class HibernateClinicTests extends AbstractClinicTests { }
1 AbstractClinicTests-context.xmlHibernateClinicTests-context.xml加载 application context。

在 large-scale application 中,Spring configuration 通常分为多个 files。因此,configuration 位置通常在 common base class 中为所有 application-specific integration 测试指定。这样的 base class 也可以添加有用的实例变量(自然地由依赖注入填充),例如在使用 Hibernate 的 application 的情况下SessionFactory

您应尽可能在 integration 测试中使用与部署环境中完全相同的 Spring configuration files。一个可能的差异点涉及数据库连接池和 transaction 基础结构。如果要部署到 full-blown application 服务器,则可能使用其连接池(可通过 JNDI 获得)和 JTA implementation。因此,在 production 中,您可以使用JndiObjectFactoryBean<jee:jndi-lookup>作为DataSourceJtaTransactionManager。 JNDI 和 JTA 在 out-of-container integration 测试中不可用,因此您应该使用 Commons _DBCP BasicDataSourceDataSourceTransactionManagerHibernateTransactionManager这样的组合。您可以将此变体行为分解为单个 XML 文件,可以选择 application 服务器和与所有其他 configuration 分离的“本地”configuration,这在 test 和 production 环境之间不会有所不同。此外,我们建议您使用 properties files 进行连接设置。有关 example,请参阅 PetClinic application。

更多资源

有关测试的更多信息,请参阅以下资源:

  • JUnit:“A programmer-oriented 测试 framework for Java”。由 Spring Framework 在其测试套件中使用。
  • TestNG:受 JUnit 启发的测试 framework,增加了对 annotations,测试组,data-driven 测试,分布式测试和其他 features 的支持。
  • AssertJ:“Fluent 断言为 Java”,包括对 Java 8 lambdas,流和其他 features 的支持。
  • Mock Objects:维基百科中的文章。
  • MockObjects.com:Web 网站致力于 mock objects,这是一种在 test-driven 开发中改进 code 设计的技术。
  • Mockito:Java mock library 基于Test Spy pattern。
  • EasyMock:Java library“通过使用 Java 的代理机制动态生成它们,为接口提供 Mock Objects(以及通过 class 扩展提交 objects)。”由 Spring Framework 在其测试套件中使用。
  • JMock:Library 支持使用 mock objects 开发 Java code test-driven。
  • DbUnit:针对 database-driven 项目的 JUnit 扩展(也可用于 Ant 和 Maven),除其他外,将数据库置于测试运行之间的已知 state 中。
  • 磨床:Java 负载测试 framework。