Spring Framework 中文文档

4.3.21.RELEASE

15. Integration 测试

15.1 概述

能够执行某些 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 等。

15.2 Integration Testing 的目标

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

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

15.2.1 Context management and caching

Spring TestContext Framework 提供 Spring ApplicationContextWebApplicationContext的一致 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 中,术语测试套件意味着所有测试 run 在同一个 JVM 中 - 对于 example,所有测试 run 来自 Ant,Maven 或 Gradle build,用于给定的项目或模块。在不太可能的情况下,测试会破坏 application context 并需要重新加载 - 例如,通过修改 bean 定义或 application object 的 state - 可以将 TestContext framework 配置为重新加载 configuration 并重建 application context,然后再执行下一个测试。

请参阅带有 TestContext framework 的第 15.5.4 节,“Context management”名为“Context caching”的部分

15.2.2 依赖注入测试夹具

当 TestContext framework 加载 application context 时,它可以选择通过依赖注入配置 test classes 的实例。这为使用 application context 中预配置的 beans 设置测试夹具提供了一种便捷的机制。这里的一个强大好处是,您可以在各种测试场景中重用 application 上下文(e.g. ,用于配置 Spring-managedobject 图,transactional proxies,DataSources,etc.),因此无需为各个测试用例复制复杂的测试夹具设置。

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

  • Spring configuration:基本上,与HibernateTitleRepository bean 的 configuration 相关的一切是否正确和存在?

  • Hibernate 映射文件 configuration:是否正确映射了所有内容,并且是正确的 lazy-loading 设置?

  • HibernateTitleRepository的逻辑:这个 class 的配置实例是否按预期执行?

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

15.2.3 Transaction management

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

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

如果你想要一个 transaction 提交 - 不寻常,但偶尔有用,当你想要一个特定的测试来填充或修改数据库时 - 可以指示 TestContext framework 使 transaction 提交而不是通过@Commit annotation 回滚。

请参阅带的 transaction management。

15.2.4 支持 classes 进行 integration 测试

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。

15.3 JDBC 测试支持

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

  • countRowsInTable(..):计算给定 table 中的行数

  • countRowsInTableWhere(..):使用提供的WHERE子句计算给定 table 中的行数

  • deleteFromTables(..):删除指定表中的所有行

  • deleteFromTableWhere(..):使用提供的WHERE子句删除给定 table 中的行

  • dropTables(..):删除指定的表

请注意,AbstractTransactionalJUnit4SpringContextTestsAbstractTransactionalTestNGSpringContextTests提供了方便的方法,它们委托JdbcTestUtils中的上述方法。

spring-jdbc模块支持配置和启动嵌入式数据库,该数据库可用于与数据库交互的 integration 测试。有关详细信息,请参阅第 19.8 节,“嵌入式数据库支持”第 19.8.5 节,“使用嵌入式数据库测试数据访问逻辑”

15.4 注释

15.4.1 Spring Testing Annotations

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

@BootstrapWith

@BootstrapWith是一个 class-level annotation,用于配置 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 等。

@ContextConfiguration("/test-config.xml")
public class XmlApplicationContextTests {
    // class body...
}
@ContextConfiguration(classes = TestConfig.class)
public class ConfigClassApplicationContextTests {
    // class body...
}

作为声明资源位置或带注释的 classes 的替代或补充,@ContextConfiguration可用于声明ApplicationContextInitializer classes。

@ContextConfiguration(initializers = CustomContextIntializer.class)
public class ContextInitializerTests {
    // class body...
}

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

@ContextConfiguration(locations = "/test-context.xml", loader = CustomContextLoader.class)
public class CustomLoaderXmlApplicationContextTests {
    // class body...
}

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

有关详细信息,请参阅第 15.5.4 节,“Context management”@ContextConfiguration javadocs。

@WebAppConfiguration

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

@ContextConfiguration
@WebAppConfiguration
public class WebAppTests {
    // class body...
}

要覆盖默认值,请通过隐式value属性指定不同的基本资源路径。支持classpath:file:资源前缀。如果未提供资源前缀,则假定路径是文件系统资源。

@ContextConfiguration
@WebAppConfiguration("classpath:test-web-resources")
public class WebAppTests {
    // class body...
}

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

@ContextHierarchy

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

@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 hierarchyies”的部分@ContextHierarchy javadocs。

@ActiveProfiles

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

@ContextConfiguration
@ActiveProfiles("dev")
public class DeveloperTests {
    // class body...
}
@ContextConfiguration
@ActiveProfiles({"dev", "integration"})
public class DeveloperIntegrationTests {
    // class body...
}

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

有关示例和更多详细信息,请参阅名为“Context configuration with environment profiles”的部分@ActiveProfiles javadocs。

@TestPropertySource

@TestPropertySource是一个 class-level annotation,用于配置 properties files 和 inlined properties 的位置,以便在Environment中添加ApplicationContextPropertySources集合以进行 integration 测试。

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

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

@ContextConfiguration
@TestPropertySource("/test.properties")
public class MyIntegrationTests {
    // class body...
}

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

@ContextConfiguration
@TestPropertySource(properties = { "timezone = GMT", "port: 4242" })
public class MyIntegrationTests {
    // class body...
}

@DirtiesContext

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

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

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

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

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

@ContextHierarchy({
    @ContextConfiguration("/parent-config.xml"),
    @ContextConfiguration("/child-config.xml")
})
public class BaseTests {
    // class body...
}

public class ExtendedTests extends BaseTests {

    @Test
    @DirtiesContext(hierarchyMode = CURRENT_LEVEL)
    public void test() {
        // some logic that results in the child context being dirtied
    }
}

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

@TestExecutionListeners

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

@ContextConfiguration
@TestExecutionListeners({CustomTestExecutionListener.class, AnotherTestExecutionListener.class})
public class CustomTestExecutionListenerTests {
    // class body...
}

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

@Commit

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

@Commit
@Test
public void testProcessWithoutRollback() {
    // ...
}

@Rollback

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

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

@Rollback(false)
@Test
public void testProcessWithoutRollback() {
    // ...
}

@BeforeTransaction

@BeforeTransaction表示在通过 Spring 的@Transactional annotation 在 transaction 中配置为 run 的测试方法启动 transaction 之前,应该执行带注释的void方法。从 Spring Framework 4.3 开始,@BeforeTransaction方法不需要public,并且可以在基于 Java 8 的接口默认方法上声明。

@BeforeTransaction
void beforeTransaction() {
    // logic to be executed before a transaction is started
}

@AfterTransaction

@AfterTransaction表示在 transaction 结束后,对于通过 Spring 的@Transactional annotation 在 transaction 中配置为 run 的测试方法,应该执行带注释的void方法。从 Spring Framework 4.3 开始,@AfterTransaction方法不需要public,并且可以在基于 Java 8 的接口默认方法上声明。

@AfterTransaction
void afterTransaction() {
    // logic to be executed after a transaction has ended
}

@Sql

@Sql用于注释测试 class 或测试方法,以配置在 integration 测试期间针对给定数据库执行的 SQL 脚本。

@Test
@Sql({"/test-schema.sql", "/test-user-data.sql"})
public void userTest {
    // execute code that relies on the test schema and test data
}

有关详细信息,请参阅“@Sql 以声明方式执行 SQL 脚本”一节

@SqlConfig

@SqlConfig定义元数据,用于确定如何解析和执行通过@Sql annotation 配置的 SQL 脚本。

@Test
@Sql(
    scripts = "/test-user-data.sql",
    config = @SqlConfig(commentPrefix = "`", separator = "@@")
)
public void userTest {
    // execute code that relies on the test data
}

@SqlGroup

@SqlGroup是一个容器 annotation,它聚合了几个@Sql 注释。 @SqlGroup可以原生使用,声明几个嵌套的@Sql 注释,或者它可以与 Java 8 对可重复注释的支持结合使用,其中@Sql可以简单地在同一个 class 或方法上多次声明,隐式生成此容器 annotation。

@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
}

15.4.2 标准 Annotation 支持

对于 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

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

如果测试 class 中的方法用@PostConstruct注释,那么该方法将在基础测试 framework(e.g. ,使用 JUnit 4 的@Before注释的方法)的任何 before 方法之前执行,并且将适用于 test class 中的每个测试方法。另一方面,如果测试 class 中的方法使用@PreDestroy注释,则永远不会执行该方法。因此,在测试 class 中,建议使用基础测试 framework 而不是@PostConstruct@PreDestroy的测试生命周期回调。

15.4.3 Spring JUnit 4 测试注释

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

@IfProfileValue

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

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

@IfProfileValue(name="java.vendor", value="Oracle Corporation")
@Test
public void testProcessWhichRunsOnlyOnOracleJvm() {
    // some logic that should run only on Java VMs from Oracle Corporation
}

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

@IfProfileValue(name="test-groups", values={"unit-tests", "integration-tests"})
@Test
public void testProcessWhichRunsForUnitOrIntegrationTestGroups() {
    // some logic that should run only for unit and integration test groups
}

@ProfileValueSourceConfiguration

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

@ProfileValueSourceConfiguration(CustomProfileValueSource.class)
public class CustomProfileValueSourceTests {
    // class body...
}

@Timed

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

time 时间段包括测试方法本身的执行,测试的任何重复(参见@Repeat),以及测试夹具的任何设置或拆除。

@Timed(millis=1000)
public void testProcessWithOneSecondTimeout() {
    // some logic that should not take longer than 1 second to execute
}

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

@Repeat

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

要重复的执行范围包括执行测试方法本身以及测试夹具的任何设置或拆除。

@Repeat(10)
@Test
public void testProcessRepeatedly() {
    // ...
}

15.4.4 Meta-Annotation 支持测试

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

以下各项可以与TestContext framework一起用作 meta-annotations。

  • @BootstrapWith

  • @ContextConfiguration

  • @ContextHierarchy

  • @ActiveProfiles

  • @TestPropertySource

  • @DirtiesContext

  • @WebAppConfiguration

  • @TestExecutionListeners

  • @Transactional

  • @BeforeTransaction

  • @AfterTransaction

  • @Commit

  • @Rollback

  • @Sql

  • @SqlConfig

  • @SqlGroup

  • @Repeat

  • @Timed

  • @IfProfileValue

  • @ProfileValueSourceConfiguration

例如,如果我们发现我们在基于 JUnit 4 的测试套件中重复以下 configuration ...

@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 { }

我们可以通过引入一个自定义组合的 annotation 来集中 common test configuration,从而减少上述重复,如下所示:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
public @interface TransactionalDevTest { }

然后我们可以使用我们的自定义@TransactionalDevTest annotation 来简化各个 test classes 的 configuration,如下所示:

@RunWith(SpringRunner.class)
@TransactionalDevTest
public class OrderRepositoryTests { }

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

有关详细信息,请参阅Spring Annotation Programming Model

15.5 Spring TestContext Framework

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

除了通用测试基础结构之外,TestContext framework 还以abstract support classes 的形式为 JUnit 4 和 TestNG 提供显式支持。对于 JUnit 4,Spring 还提供了一个自定义 JUnit Runner和自定义 JUnit Rules,允许用户编写 so-called POJO 测试 classes。 POJO 测试类不需要扩展特定的 class 层次结构。

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

15.5.1 Key 抽象

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

TestContext

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

TestContextManager

TestContextManager是 Spring TestContext Framework 的主要入口点,它管理单个TestContext并向 well-defined 测试执行点处的每个已注册的TestExecutionListener发送 events:

  • 在 class 之前或之前的任何特定测试 framework 的所有方法之前

  • 测试实例 post-processing

  • 在特定测试 framework 的每种方法之前或之前的任何之前

  • 在特定测试 framework 的每种方法之后或之后

  • 在任何 class 之后或者特定测试 framework 的所有方法之后

TestExecutionListener

TestExecutionListener定义了用于对_l发布的_l发布的测试执行 events 做出反应的 API。见第 15.5.3 节,“TestExecutionListener configuration”

Context 加载器

ContextLoader是 Spring 2.5 中引入的策略接口,用于为 Spring TestContext Framework 管理的 integration 测试加载ApplicationContext。在 order 中实现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 resource locations,annotated classes或 context initializers。此外,SmartContextLoader可以在其加载的 context 中设置 active bean 定义 profiles 和 test property 源。

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

15.5.2 引导 TestContext framework

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

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

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

15.5.3 TestExecutionListener configuration

Spring 提供以下TestExecutionListener implementations,默认情况下已注册,完全在此 order 中。

  • ServletTestExecutionListener:为WebApplicationContext配置 Servlet API 模拟

  • DirtiesContextBeforeModesTestExecutionListener:处理之前模式的@DirtiesContext annotation

  • DependencyInjectionTestExecutionListener:为测试实例提供依赖注入

  • DirtiesContextTestExecutionListener:处理后模式的@DirtiesContext annotation

  • TransactionalTestExecutionListener:使用默认回滚语义提供 transactional 测试执行

  • SqlScriptsTestExecutionListener:执行通过@Sql annotation 配置的 SQL 脚本

注册自定义 TestExecutionListeners

可以通过@TestExecutionListeners annotation 为 test class 及其子类注册自定义TestExecutionListener s。有关详细信息和示例,请参阅annotation 支持@TestExecutionListeners的 javadocs。

自动发现默认的 TestExecutionListeners

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

具体来说,spring-test模块在META-INF/spring.factories properties 文件中的org.springframework.test.context.TestExecutionListener key 下声明所有核心缺省TestExecutionListener s。 Third-party 框架和开发人员可以通过他们自己的META-INF/spring.factories properties 文件以相同的方式将自己的TestExecutionListener提供给默认 listener 列表。

Ordering TestExecutionListeners

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

合并 TestExecutionListeners

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

@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 合并。合并算法确保从列表中删除重复项,并根据名为“Ordering TestExecutionListeners”的部分的语义对的语义进行排序,如名为“Ordering TestExecutionListeners”的部分中所述。如果 listener 实现Ordered或用@Order注释,它可以影响它与默认值合并的位置;否则,本地声明的 listeners 将在合并时简单地附加到默认 listeners 列表中。

例如,如果前一个 example 中的MyCustomTestExecutionListener class 将其order value(对于 example,500)配置为小于ServletTestExecutionListener的 order(恰好是1000),那么MyCustomTestExecutionListener可以自动与默认列表合并在ServletTestExecutionListener前面,前一个 example 可以替换为以下内容。

@ContextConfiguration
@TestExecutionListeners(
    listeners = MyCustomTestExecutionListener.class,
    mergeMode = MERGE_WITH_DEFAULTS
)
public class MyTest {
    // class body...
}

15.5.4 Context management

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

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

@RunWith(SpringRunner.class)
@ContextConfiguration
public class MyTest {

    @Autowired
    private ApplicationContext applicationContext;

    // class body...
}

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

@RunWith(SpringRunner.class)
@WebAppConfiguration
@ContextConfiguration
public class MyWebAppTest {
    @Autowired
    private WebApplicationContext wac;

    // class body...
}

通过@Autowired的依赖注入由DependencyInjectionTestExecutionListener提供,默认配置为DependencyInjectionTestExecutionListener(参见第 15.5.5 节,“测试夹具的依赖注入”)。

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

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

Context configuration with XML resources

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

@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"})
public class MyTest {
    // class body...
}

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

@RunWith(SpringRunner.class)
@ContextConfiguration({"/app-config.xml", "/test-config.xml"})
public class MyTest {
    // class body...
}

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

package com.example;

@RunWith(SpringRunner.class)
// ApplicationContext will be loaded from
// "classpath:com/example/MyTest-context.xml"
@ContextConfiguration
public class MyTest {
    // class body...
}

使用 Groovy 脚本 Context configuration

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

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

@RunWith(SpringRunner.class)
// ApplicationContext will be loaded from "/AppConfig.groovy" and
// "/TestConfig.groovy" in the root of the classpath
@ContextConfiguration({"/AppConfig.groovy", "/TestConfig.Groovy"})
public class MyTest {
    // class body...
}

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

package com.example;

@RunWith(SpringRunner.class)
// ApplicationContext will be loaded from
// "classpath:com/example/MyTestContext.groovy"
@ContextConfiguration
public class MyTest {
    // class body...
}

XML configuration files 和 Groovy 脚本都可以通过@ContextConfigurationlocationsvalue属性同时声明。如果配置的资源位置_end 与.xml的路径将使用XmlBeanDefinitionReader加载;否则将使用GroovyBeanDefinitionReader加载。

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

@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...
}

Context configuration with annotated classes

要使用带注释的 classes(请参阅第 7.12 节,“Java-based 容器配置”)为测试加载ApplicationContext,请使用@ContextConfiguration注释 test class,并使用 array 配置包含引用 classes 的 references 的_ar属性。

@RunWith(SpringRunner.class)
// ApplicationContext will be loaded from AppConfig and TestConfig
@ContextConfiguration(classes = {AppConfig.class, TestConfig.class})
public class MyTest {
    // class body...
}

术语注释 class 可以指以下任何一种。

  • @Configuration注释的 class

  • 一个 component(i.e.,一个用@Component@Service@Repository,etc.)注释的 class

  • 一个符合 JSR-330 的 class,用javax.inject annotations 注释

  • 包含@Bean -methods 的任何其他 class

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

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

@RunWith(SpringRunner.class)
// ApplicationContext will be loaded from the
// static nested Config class
@ContextConfiguration
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
    }

}

混合 XML,Groovy 脚本和带注释的 classes

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

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

如果要使用资源位置(e.g. ,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 资源位置或一组classes,您的 production ApplicationContext将从中加载,但您仍然有包含或 import 其他类型的 configuration 的自由。

Context configuration with context initializers

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

@RunWith(SpringRunner.class)
// ApplicationContext will be loaded from TestConfig
// and initialized by TestAppCtxInitializer
@ContextConfiguration(
    classes = TestConfig.class,
    initializers = TestAppCtxInitializer.class)
public class MyTest {
    // class body...
}

也可以在@ContextConfiguration中完全省略 XML configuration files,Groovy 脚本或带注释的 classes 的声明,而只是声明ApplicationContextInitializer classes 然后负责在 context 中注册 beans - for example,通过从 XML files 编程 loading bean 定义或 configuration classes。

@RunWith(SpringRunner.class)
// ApplicationContext will be initialized by EntireAppInitializer
// which presumably registers beans in the context
@ContextConfiguration(initializers = EntireAppInitializer.class)
public class MyTest {
    // class body...
}

Context configuration 继承

@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将从该 order 中的“base-config.xml”和“extended-config.xml”加载。因此,“extended-config.xml”中定义的 Beans 可能会覆盖(i.e.,替换)“base-config.xml”中定义的那些。

@RunWith(SpringRunner.class)
// ApplicationContext will be loaded from "/base-config.xml"
// in the root of the classpath
@ContextConfiguration("/base-config.xml")
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")
public class ExtendedTest extends BaseTest {
    // class body...
}

类似地,在下面的使用带注释的 classes 的 example 中,ExtendedTest for ExtendedTest将从BaseConfigExtendedConfig classes 中加载到该 order 中。 中定义的 Beans 因此可以覆盖(i.e.,替换)BaseConfig中定义的那些。

@RunWith(SpringRunner.class)
// ApplicationContext will be loaded from BaseConfig
@ContextConfiguration(classes = BaseConfig.class)
public class BaseTest {
    // class body...
}

// ApplicationContext will be loaded from BaseConfig and ExtendedConfig
@ContextConfiguration(classes = ExtendedConfig.class)
public class ExtendedTest extends BaseTest {
    // class body...
}

在下面使用 context 初始值设定项的 example 中,ExtendedTest for ExtendedTest将使用BaseInitializerExtendedInitializer进行初始化。但请注意,调用初始值设定项的 order 取决于它们是实现 Spring 的Ordered接口还是使用 Spring 的@Order annotation 或标准@Priority annotation 注释。

@RunWith(SpringRunner.class)
// ApplicationContext will be initialized by BaseInitializer
@ContextConfiguration(initializers = BaseInitializer.class)
public class BaseTest {
    // class body...
}

// ApplicationContext will be initialized by BaseInitializer
// and ExtendedInitializer
@ContextConfiguration(initializers = ExtendedInitializer.class)
public class ExtendedTest extends BaseTest {
    // class body...
}

Context configuration with environment profiles

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

@ActiveProfiles可以与新的SmartContextLoader SPI 的任何 implementation 一起使用,但ContextLoader SPI 的 implementations 不支持@ActiveProfiles

让我们看一下 XML configuration 和@Configuration classes 的一些例子。

<!-- 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>
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 和默认的 profile。

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

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

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

@Configuration
@Profile("dev")
public class StandaloneDataConfig {

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("classpath:com/bank/config/sql/schema.sql")
            .addScript("classpath:com/bank/config/sql/test-data.sql")
            .build();
    }
}
@Configuration
@Profile("production")
public class JndiDataConfig {

    @Bean(destroyMethod="")
    public DataSource dataSource() throws Exception {
        Context ctx = new InitialContext();
        return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
    }
}
@Configuration
@Profile("default")
public class DefaultDataConfig {

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("classpath:com/bank/config/sql/schema.sql")
            .build();
    }
}
@Configuration
public class TransferServiceConfig {

    @Autowired DataSource dataSource;

    @Bean
    public TransferService transferService() {
        return new DefaultTransferService(accountRepository(), feePolicy());
    }

    @Bean
    public AccountRepository accountRepository() {
        return new JdbcAccountRepository(dataSource);
    }

    @Bean
    public FeePolicy feePolicy() {
        return new ZeroFeePolicy();
    }

}
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 via 依赖注入

  • 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

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还支持inheritProfiles属性,该属性可用于禁用 active profiles 的继承。

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属性注册它。以下 example 演示了如何实现和注册自定义OperatingSystemActiveProfilesResolver。有关详细信息,请参阅相应的 javadoc。

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};
    }
}

Context configuration with test property sources

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

@TestPropertySource可以与SmartContextLoader SPI 的任何 implementation 一起使用,但@TestPropertySource SPI 的 implementations 不支持@TestPropertySource

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

声明测试 property 来源

可以通过@TestPropertySourcelocationsvalue属性配置测试 properties files,如下面的 example 所示。

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

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

@ContextConfiguration
@TestPropertySource("/test.properties")
public class MyIntegrationTests {
    // class body...
}

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

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

  • "key=value"

  • "key:value"

  • "key value"

@ContextConfiguration
@TestPropertySource(properties = {"timezone = GMT", "port: 4242"})
public class MyIntegrationTests {
    // class body...
}

默认 properties 文件检测

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

****优先

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

在下面的示例中,timezoneport properties 以及"/test.properties"中定义的任何 properties 将覆盖在 system 和 application property 源中定义的同一 name 的任何 properties。此外,如果"/test.properties"文件定义了timezoneport properties 的条目,那么这些条目将被通过properties属性声明的内联 properties 覆盖。

@ContextConfiguration
@TestPropertySource(
    locations = "/test.properties",
    properties = {"timezone = GMT", "port: 4242"}
)
public class MyIntegrationTests {
    // class body...
}

继承和覆盖测试 property 来源

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

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

在下面的示例中,BaseTest for BaseTest将仅使用"base.properties"文件作为 test property 源加载。相反,ApplicationContext for ExtendedTest将使用"base.properties" "extended.properties" files 作为 test property 源位置加载。

@TestPropertySource("base.properties")
@ContextConfiguration
public class BaseTest {
    // ...
}

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

在下面的示例中,BaseTest for BaseTest将仅使用内联key1 property 加载。相反,ApplicationContext for ExtendedTest将使用内联的key1key2 properties 加载。

@TestPropertySource(properties = "key1 = value1")
@ContextConfiguration
public class BaseTest {
    // ...
}

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

Loading 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 根目录的路径(i.e.,通常是项目的路径)。如果您熟悉 Maven 项目中 web application 的目录结构,您将知道“src/main/webapp”是 WAR 根目录的默认位置。如果需要覆盖此默认值,只需提供@WebAppConfiguration annotation(e.g. ,@WebAppConfiguration("src/test/webapp"))的备用路径。如果您希望从 classpath 而不是文件系统引用基本资源路径,只需使用 Spring 的 classpath:前缀。

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

以下示例演示了的各种 configuration 选项。

约定.

@RunWith(SpringRunner.class)

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

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

public class WacTests {
    //...
}

上面的示例演示了 TestContext framework 对_coniguration 约定的支持。如果使用@WebAppConfiguration注释测试 class 而未指定资源基路径,则资源路径将有效地默认为“file:src/main/webapp”。同样,如果声明@ContextConfiguration而不指定资源locations,带注释的classes或 context initializers,Spring 将尝试使用约定检测 configuration 的存在(i.e.,“WacTests-context.xml”与WacTests class 或静态嵌套@Configuration classes 在同一个包中)。

默认资源语义.

@RunWith(SpringRunner.class)

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

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

public class WacTests {
    //...
}

此 example 演示了如何使用@WebAppConfiguration显式声明资源基路径以及使用@ContextConfiguration显示 XML 资源位置。这里要注意的重要一点是_path 与这两个注释的不同语义。默认情况下,@WebAppConfiguration resource paths 是基于文件系统的;而@ContextConfiguration资源位置是基于 classpath 的。

显式资源语义.

@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 {
    //...
}

在第三个例子中,我们看到我们可以通过指定 Spring 资源前缀来覆盖两个_notnotations 的默认资源语义。将此 example 中的 comments 与前一个 example 对比。

为了提供全面的 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按照测试方法进行管理。

注射嘲笑.

@WebAppConfiguration
@ContextConfiguration
public 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 的所有后续测试。要了解缓存的工作原理,了解独特和测试套件的含义非常重要。

ApplicationContext可以通过用于加载它的 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并将其存储在仅基于这些位置的 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 将显着减慢 run。

从 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 方法(参见第 15.4.1 节,“弹簧测试注释”@DirtiesContext的讨论) )。这指示 Spring 从缓存中删除 context 并在执行下一个测试之前重建 application context。请注意,对@DirtiesContext annotation 的支持由DirtiesContextBeforeModesTestExecutionListenerDirtiesContextTestExecutionListener提供,默认情况下已启用。

Context 层次结构

在编写依赖于加载的 Spring ApplicationContext的 integration 测试时,通常可以对单个 context 进行测试;但是,有时候对ApplicationContext s 的层次结构进行测试是有益的,甚至是必要的。例如,如果您正在开发 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 用于特定批处理 job 的 configuration。

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

以下基于 JUnit 4 的示例演示了需要使用 context 层次结构的 integration 测试的 common configuration 方案。

ControllerIntegrationTests表示 Spring MVC web application 的典型 integration 测试场景,声明 context 层次结构包含两个级别,一个用于根 WebApplicationContext(使用TestAppConfig @Configuration class 加载),另一个用于调度程序 servlet WebApplicationContext(使用WebConfig @Configuration class 加载) )。自动连接到测试实例中的WebApplicationContext是 child context(i.e.,层次结构中最低的 context)的WebApplicationContext

@RunWith(SpringRunner.class)
@WebAppConfiguration
@ContextHierarchy({
    @ContextConfiguration(classes = TestAppConfig.class),
    @ContextConfiguration(classes = WebConfig.class)
})
public class ControllerIntegrationTests {

    @Autowired
    private WebApplicationContext wac;

    // ...
}

以下测试 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。

@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 {}

以下 classes 演示如何在 order 中使用命名层次结构级别来合并 context 层次结构中特定级别的 configuration。 BaseTests在层次结构中定义了两个级别parentchildExtendedTests扩展BaseTests并指示 Spring TestContext Framework 合并child层次 level 的 context configuration,只需确保通过@ContextConfiguration中的name属性声明的名称都是"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。

@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 {}

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

@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 层次结构的一部分的测试中使用@DirtiesContexthierarchyMode flag 可用于控制 context 缓存的清除方式。有关详细信息,请参阅Spring Testing Annotations中的@DirtiesContext@DirtiesContext javadocs 的讨论。

15.5.5 测试夹具的依赖注入

当您使用DependencyInjectionTestExecutionListener(默认配置)时,测试实例的依赖关系是从_be 中配置的 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进行字段注入。

@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 注入,如下所示。

@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(即repository-config.xml)引用的相同 XML context 文件,如下所示:

<?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:for example,multiple DataSource beans。在这种情况下,您可以覆盖 setter 方法并使用@Qualifier annotation 来指示特定的目标 bean,如下所示,但也要确保委托给超类中的重写方法。

// ...

    @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)。

15.5.6 测试请求和 session 范围 beans

自早年开始就受到 Spring 的支持,自 Spring 3.2 以来,通过以下步骤测试你的 request-scoped 和 session-scoped beans 是一件轻而易举的事。

  • 通过使用@WebAppConfiguration注释 test class,确保为测试加载了WebApplicationContext

  • 将 mock 请求或 session 注入测试实例并根据需要准备测试夹具。

  • 调用从配置的WebApplicationContext(i.e.,通过依赖注入)检索到的 web component。

  • 对模拟执行断言。

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

Request-scoped bean configuration.

<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(i.e.,被测试的主体)和MockHttpServletRequest注入我们的测试实例中。在我们的requestScope()测试方法中,我们通过在提供的MockHttpServletRequest中设置请求参数来设置我们的测试夹具。当我们在userService上调用loginUser()方法时,我们确信用户服务可以访问当前MockHttpServletRequest的 request-scoped loginAction(i.e.,我们刚刚设置的参数)。然后,我们可以根据用户名和密码的已知输入对结果执行断言。

Request-scoped bean 测试.

@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;但是,请注意,使用 SpEL 表达式实例化UserPreferences bean,该表达式从当前 HTTP session 中检索主题。在我们的测试中,我们需要在 TestContext framework 管理的 mock session 中配置一个主题。

Session-scoped bean configuration.

<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,并且我们可以根据配置的主题对结果执行断言。

Session-scoped bean 测试.

@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
    }
}

15.5.7 Transaction management

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

Test-managed transactions

Test-managed transactions 是 transactions,通过TransactionalTestExecutionListener以声明方式管理或通过TestTransaction以编程方式管理(见下文)。这样的 transactions 不应该与 Spring-managed transactions(i.e.,@ Spring-managed 和 application-managed transactions 通常会参与 test-managed transactions;但是,如果 Spring-managed 或 application-managed transactions 配置了除REQUIREDSUPPORTS以外的任何传播类型,则应该小心(有关详细信息,请参阅有关transaction 传播的讨论)。

启用和禁用 transactions

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

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

以下 example 演示了为 Hibernate-based UserRepository编写 integration 测试的 common 场景。如名为“Transaction rollback and commit behavior”的部分中所述,执行createUser()方法后无需清理数据库,因为对数据库所做的任何更改都将由TransactionalTestExecutionListener自动回滚。有关其他 example,请参阅第 15.7 节,“PetClinic Example”

@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 回滚和提交行为

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

Programmatic transaction management

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

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

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

在 transaction 之外执行 code

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

之前的任何方法(例如使用 JUnit 4 的@Before注释的方法)和任何后续方法(例如使用 JUnit 4 的@After注释的方法)都在 transaction 中执行。此外,对于未在 transaction 中配置为 run 的测试方法,自然不会执行使用@BeforeTransaction@AfterTransaction注释的方法。

配置 transaction manager

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

演示所有 transaction-related 注释

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

@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
    }

}

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

// ...

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

// ...

或者对于 JPA:

// ...

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

// ...

15.5.8 执行 SQL 脚本

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

虽然在加载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可能比下面描述的其他一些替代方案更适合您的需求。有关详细信息,请参阅ScriptUtils中各个方法的 javadocs。

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

@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来解析和执行 SQL 脚本。同样,AbstractTransactionalJUnit4SpringContextTestsAbstractTransactionalTestNGSpringContextTests中的executeSqlScript(..)方法在内部使用ResourceDatabasePopulator来执行 SQL 脚本。有关更多详细信息,请参阅 javadocs 了解各种executeSqlScript(..)方法。

以 @Sql 声明性地执行 SQL 脚本

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

路径资源语义

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

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

@RunWith(SpringRunner.class)
@ContextConfiguration
@Sql("/test-schema.sql")
public class DatabaseTests {

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

    @Test
    @Sql({"/test-schema.sql", "/test-user-data.sql"})
    public void userTest {
        // execute code that uses the test schema and test data
    }
}

默认脚本检测

如果未指定 SQL 脚本,则将尝试根据@Sql声明的位置检测default脚本。如果无法检测到默认值,则会抛出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。在此方案中,test-schema.sql脚本对 single-line comments 使用不同的语法。

@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
}

以下示例与上述示例相同,只是@Sql声明在@SqlGroup中组合在一起,以便与 Java 6 和 Java 7 兼容。

@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 脚本将在相应的测试方法之前执行。但是,如果需要在测试方法之后执行一组特定的脚本 - 例如,要清理数据库 state - 可以使用@Sql中的executionPhase属性,如下面的 example 所示。请注意,ISOLATEDAFTER_TEST_METHOD分别从Sql.TransactionModeSql.ExecutionPhase静态导入。

@Test
@Sql(
    scripts = "create-test-data.sql",
    config = @SqlConfig(transactionMode = ISOLATED)
)
@Sql(
    scripts = "delete-test-data.sql",
    config = @SqlConfig(transactionMode = ISOLATED),
    executionPhase = AFTER_TEST_METHOD
)
public void userTest {
    // execute code that needs the test data to be committed
    // to the database outside of the test's transaction
}

带@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属性具有_S的""或 Enums 的DEFAULT的显式默认 value。这种方法允许@SqlConfig的局部声明通过提供除""DEFAULT之外的 value 来有选择地覆盖@SqlConfig的 global 声明中的各个属性。只要本地@SqlConfig属性不提供""DEFAULT以外的显式 value,就会继承 Global @SqlConfig属性。因此,显式本地配置会覆盖 global configuration。

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


交易管理@Sql

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

如果SqlScriptsTestExecutionListener用于检测DataSourcePlatformTransactionManager并推断 transaction 语义的算法不符合您的需要,您可以通过@SqlConfigdataSourcetransactionManager属性指定显式名称。此外,transaction 传播行为可以通过@SqlConfigtransactionMode属性进行控制 - 例如,如果脚本应该在隔离的 transaction 中执行。虽然对_tra_actionmanagement with @Sql的所有支持选项的详尽讨论超出了本 reference 手册的范围,但@SqlConfigSqlScriptsTestExecutionListener的 javadoc 提供了详细信息,以下 example 演示了使用 JUnit 4 和 transactional 测试@Sql的典型测试场景。请注意,执行usersTest()方法后无需清理数据库,因为对数据库所做的任何更改(在测试方法内或在/test-data.sql脚本中)都将由TransactionalTestExecutionListener自动回滚(有关详细信息,请参阅transaction management) 。

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = TestDatabaseConfig.class)
@Transactional
public class TransactionalSqlScriptsTests {

    protected JdbcTemplate jdbcTemplate;

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

    @Test
    @Sql("/test-data.sql")
    public void usersTest() {
        // verify state in test database:
        assertNumUsers(2);
        // execute code that uses the test data...
    }

    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"));
    }
}

15.5.9 TestContext Framework 支持 classes

Spring JUnit 4 Runner

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

以下 code 列表显示了使用自定义 Spring Runner将 test class 配置为 run 的最低要求。 @TestExecutionListeners在 order 中配置了一个空列表以禁用默认的 listeners,否则需要通过@ContextConfiguration配置ApplicationContext

@RunWith(SpringRunner.class)
@TestExecutionListeners({})
public class SimpleTest {

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

Spring JUnit 4 规则

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)组合使用。

在 order 中,为了支持 TestContext framework 的全部功能,SpringClassRule必须与SpringMethodRule组合。以下 example 演示了在 integration 测试中声明这些规则的正确方法。

// 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...
    }
}

JUnit 4 支持 classes

org.springframework.test.context.junit4包为基于 JUnit 4 的测试用例提供以下支持 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 以查询数据库。此类查询可用于在执行 database-related application code 之前和之后确认数据库 state,并且 Spring 确保此类查询在与 application code 相同的 transaction 范围内运行。与 ORM 工具结合使用时,请务必避免使用误报。如第 15.3 节,“JDBC 测试支持”中所述,AbstractTransactionalJUnit4SpringContextTests还提供了方便的方法,使用前面提到的jdbcTemplate委托给JdbcTestUtils中的方法。此外,AbstractTransactionalJUnit4SpringContextTests提供了一个executeSqlScript(..)方法,用于针对配置的DataSource执行 SQL 脚本。

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

JUnit 5 支持

Spring Framework 5.0 提供与 JUnit 5 中引入的 JUnit Jupiter 测试 framework 的完整 integration。因此鼓励开发人员升级到 Spring 5.x 以完全受益于 Spring 对 JUnit 5 的支持。但是,如果你的项目由于某种原因还无法升级到 Spring 5.x ,您可能有兴趣将spring-test-junit5项目用作临时解决方案,以帮助您在仍使用 Spring Framework 4.3.x 的同时升级到 JUnit 5。

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时,可以访问protected jdbcTemplate实例变量,该变量可用于执行 SQL statements 以查询数据库。此类查询可用于在执行 database-related application code 之前和之后确认数据库 state,并且 Spring 确保此类查询在与 application code 相同的 transaction 范围内运行。与 ORM 工具结合使用时,请务必避免使用误报。如第 15.3 节,“JDBC 测试支持”中所述,AbstractTransactionalTestNGSpringContextTests还提供了方便的方法,使用前面提到的jdbcTemplate委托给JdbcTestUtils中的方法。此外,AbstractTransactionalTestNGSpringContextTests提供了一个executeSqlScript(..)方法,用于针对配置的DataSource执行 SQL 脚本。

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

15.6 Spring MVC Test Framework

Spring MVC Test framework 为使用 fluent API 测试 Spring MVC code 提供了第一个 class 支持,该 API 可以与 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 测试之间的差异”的部分

15.6.1 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 4 的使用 Spring MVC 测试的示例:

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@RunWith(SpringRunner.class)
@WebAppConfiguration
@ContextConfiguration("test-servlet-context.xml")
public class ExampleTests {

    @Autowired
    private WebApplicationContext wac;

    private MockMvc mockMvc;

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

    @Test
    public 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 的简单方法是搜索与“MockMvc *”匹配的类型。如果使用 Eclipse,请确保在 Java→编辑器→内容辅助→收藏夹下的 Eclipse 首选项中将它们添加为“最喜欢的静态成员”。这将允许在 typing 静态方法 name 的第一个字符后使用内容辅助。其他 IDE(e.g. IntelliJ)可能不需要任何其他 configuration。只需检查静态成员对 code 完成的支持。

设置选择

创建MockMvc的实例有两个主要选项。第一种是通过 TestContext framework 加载 Spring MVC configuration,它加载 Spring configuration 并将WebApplicationContext注入测试以用于 build MockMvc实例:

@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。相反,基本的默认 configuration 与 MVC JavaConfig 或 MVC 命名空间的大致相当,是自动创建的,并且可以在一定程度上进行自定义:

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,因此即使您在测试套件中引入了更多测试,它也有助于保持测试快速运行。此外,您可以通过 order 中的 Spring configuration 将 mock 服务注入控制器,以继续专注于测试 web 层。这是一个用 Mockito 声明 mock 服务的示例:

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

然后,您可以在 order 设置中将 mock 服务注入测试并验证期望:

@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 依赖项,并且它不涉及 loading Spring configuration。这些测试更侧重于样式,并且更容易看到正在测试哪个控制器,是否需要任何特定的 Spring MVC configuration 才能工作,等等。 “standaloneSetup”也是编写 ad-hoc 测试以验证特定行为或调试问题的一种非常方便的方法。

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

执行请求

使用任何 HTTP 方法执行请求都很容易:

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

您还可以执行内部使用MockMultipartHttpServletRequest的文件上载请求,这样就不会对 multipart 请求进行实际解析,而是必须进行设置:

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

您可以在 URI 模板样式中指定查询参数:

mockMvc.perform(get("/hotels?foo={foo}", "bar"));

或者您可以添加表示表单参数查询的 Servlet 请求参数:

mockMvc.perform(get("/hotels").param("foo", "bar"));

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

在大多数情况下,最好从请求 URI 中省略 context 路径和 Servlet 路径。如果必须使用完整请求 URI 进行测试,请确保相应地设置contextPathservletPath,以便请求映射可以正常工作:

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

查看上面的示例,使用每个执行的请求设置 contextPath 和 servletPath 是很麻烦的。相反,您可以设置默认请求 properties:

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(..) calls 来定义期望:

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

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

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

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

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

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

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

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()方法,该方法将结果数据 log 记录为org.springframework.test.web.servlet.result logging 类别下的单个DEBUG消息。

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

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

如果所有测试都重复相同的期望,那么在构建MockMvc实例时可以设置 common 期望:

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

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

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

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

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

Map<String, String> ns = Collections.singletonMap("ns", "http://www.w3.org/2005/Atom");
mockMvc.perform(get("/handle").accept(MediaType.APPLICATION_XML))
    .andExpect(xpath("/person/ns:link[@rel='self']/@href", ns).string("http://localhost:8080/people"));

过滤器注册

设置MockMvc实例时,可以注册一个或多个 Servlet Filter实例:

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

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

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

如前所述 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 和 Velocity)都会按预期将 HTML 呈现给响应主体。通过@ResponseBody方法渲染 JSON,XML 和其他格式也是如此。

或者,您可以考虑通过@WebIntegrationTest从 Spring Boot 获得完整的 end-to-end integration 测试支持。见Spring Boot reference

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

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

进一步 Server-Side 测试实例

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

15.6.2 HtmlUnit Integration

Spring 在MockMvcHtmlUnit之间提供 integration。这简化了在使用基于 HTML 的视图时执行 end-to-end 测试。这个 integration 使开发人员能够:

  • 使用HtmlUnitwebdriver创业板等工具轻松测试 HTML 页面,而无需部署到 Servlet 容器

  • 在页面中测试 JavaScript

  • (可选)使用 mock 服务进行测试以加快测试速度

  • 在 in-container end-to-end 测试和 out-of-container integration 测试之间共享逻辑

MockMvc适用于不依赖于 Servlet 容器的模板技术(e.g. ,Thymeleaf,Freemarker,Velocity,etc.),但它不能与 JSP 一起使用,因为它们依赖于 Servlet 容器。

为什么 HtmlUnit Integration?

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

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

MockHttpServletRequestBuilder createMessage = post("/messages/")
        .param("summary", "Spring Rocks")
        .param("text", "In case you didn't know, Spring Rocks!");

mockMvc.perform(createMessage)
        .andExpect(status().is3xxRedirection())
        .andExpect(redirectedUrl("/messages/123"));

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

<form id="messageForm" action="/messages/" method="post">
    <div class="pull-right"><a href="/messages/">Messages</a></div>

    <label for="summary">Summary</label>
    <input type="text" class="required" id="summary" name="summary" value="" />

    <label for="text">Message</label>
    <textarea id="text" name="text"></textarea>

    <div class="form-actions">
        <input type="submit" value="Create" />
    </div>
</form>

我们如何确保我们的表单能够产生正确的创建新消息的请求?天真的尝试看起来像这样:

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

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

String summaryParamName = "summary";
String textParamName = "text";
mockMvc.perform(get("/messages/form"))
        .andExpect(xpath("//input[@name='" + summaryParamName + "']").exists())
        .andExpect(xpath("//textarea[@name='" + textParamName + "']").exists());

MockHttpServletRequestBuilder createMessage = post("/messages/")
        .param(summaryParamName, "Spring Rocks")
        .param(textParamName, "In case you didn't know, Spring Rocks!");

mockMvc.perform(createMessage)
        .andExpect(status().is3xxRedirection())
        .andExpect(redirectedUrl("/messages/123"));

这样可以降低我们的测试错误传递的风险,但仍然存在一些问题。

  • 如果我们的页面上有多个表单怎么办?不可否认,我们可以更新我们的 xpath 表达式,但是我们考虑的因素越多,它们就越复杂(字段的类型是否正确?字段是否已启用?etc.)。

  • 另一个问题是我们正在做我们期望的工作。我们必须首先验证视图,然后我们使用我们刚刚验证的相同参数提交视图。理想情况下,这可以一次完成。

  • 最后,还有一些我们仍然无法解释的事情。例如,如果表单具有我们希望测试的 JavaScript 验证,该怎么办?

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

整合测试救援?

要解决上述问题,我们可以执行 end-to-end integration 测试,但这有一些明显的缺点。考虑测试允许我们翻阅消息的视图。我们可能需要以下测试。

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

  • 我们的页面是否正确显示单个消息?

  • 我们的页面是否正确支持分页?

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

  • 确保数据库中存在正确的消息可能很乏味;考虑外国 key 约束。

  • 测试可能会变慢,因为每个测试都需要确保数据库处于正确的 state 状态。

  • 由于我们的数据库需要在特定的 state 中,因此我们无法在 parallel 中运行测试。

  • 对 auto-generatedid,时间戳等事物执行断言可能很困难。

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

输入 HtmlUnit Integration

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

HtmlUnit Integration 选项

有很多方法可以将MockMvc与 HtmlUnit 集成。

  • MockMvc 和 HtmlUnit:如果要使用原始 HtmlUnit libraries,请使用此选项。

  • MockMvc 和 WebDriver:使用此选项可以简化 integration 和 end-to-end 测试之间的开发和重用 code。

  • MockMvc 和 Geb:如果您希望使用 Groovy 进行测试,简化开发并在 integration 和 end-to-end 测试之间重用 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,如下所示。

@Autowired
WebApplicationContext context;

WebClient webClient;

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

这是使用MockMvcWebClientBuilder的简单示例。有关高级用法,请参阅名为“Advanced MockMvcWebClientBuilder”的部分

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

MockMvc 和 HtmlUnit 用法

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

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

默认的 context 路径是""。或者,我们可以指定 context 路径,如名为“Advanced MockMvcWebClientBuilder”的部分所示。

一旦我们对HtmlPage进行了 reference,我们就可以填写表单并提交它以创建一条消息。

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。

assertThat(newMessagePage.getUrl().toString()).endsWith("/messages/123");
String id = newMessagePage.getHtmlElementById("id").getTextContent();
assertThat(id).isEqualTo("123");
String summary = newMessagePage.getHtmlElementById("summary").getTextContent();
assertThat(summary).isEqualTo("Spring Rocks");
String text = newMessagePage.getHtmlElementById("text").getTextContent();
assertThat(text).isEqualTo("In case you didn't know, Spring Rocks!");

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

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

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

高级 MockMvcWebClientBuilder

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

@Autowired
WebApplicationContext context;

WebClient webClient;

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

我们还可以指定其他 configuration 选项。

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

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的全部功能。

有关创建MockMvc实例的其他信息,请参阅名为“设置选择”的部分

MockMvc 和 WebDriver

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

为什么选择 WebDriver 和 MockMvc?

我们已经可以使用 HtmlUnit 和MockMvc,为什么我们要使用WebDriver? Selenium WebDriver提供了一个非常优雅的 API,允许我们轻松地组织我们的 code。为了更好地理解,让我们探索一个 example。

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

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

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

如果其中一个字段被命名为“summary”,那么我们可能会在测试中的多个位置重复以下内容。

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

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

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

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,如下所示。

@Autowired
	WebApplicationContext context;

	WebDriver driver;

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

这是使用MockMvcHtmlUnitDriverBuilder的简单示例。有关更高级的用法,请参阅名为“Advanced MockMvcHtmlUnitDriverBuilder”的部分

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

MockMvc 和 WebDriver 用法

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

CreateMessagePage page = CreateMessagePage.to(driver);

然后我们可以填写表单并提交它以创建消息。

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

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

public class CreateMessagePage
        extends AbstractPage { 

    
    private WebElement summary;
    private WebElement text;

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

你会注意到的第一件事是CreateMessagePage延伸AbstractPage。我们不会详细介绍AbstractPage的细节,但总的来说它包含了我们所有页面的 common 功能。例如,如果我们的 application 具有导航栏,global 错误消息等,则可以将此逻辑放在共享位置。

接下来你要注意的是我们有一个成员变量用于我们感兴趣的 HTML 页面的每个部分。这些部分的类型是WebElementWebDriverPageFactory允许我们通过自动解析每个WebElementCreateMessagePage的 HtmlUnit version 中删除大量 code。 PageFactory#initElements(WebDriver,Class<T>)方法将使用字段 name 自动解析每个WebElement,并通过 HTML 页面中元素的idname查找它。

我们可以使用@FindBy annotation来覆盖默认的查找行为。我们的 example 演示了如何使用@FindBy annotation 使用 css 选择器查找我们的提交按钮,输入[1613]
最后,我们可以验证是否已成功创建新消息。以下断言使用FEST 断言 library

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

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

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实例。

@After
public void destroy() {
    if (driver != null) {
        driver.close();
    }
}

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

高级 MockMvcHtmlUnitDriverBuilder

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

@Autowired
WebApplicationContext context;

WebDriver driver;

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

我们还可以指定其他 configuration 选项。

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

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的全部功能。

有关创建MockMvc实例的其他信息,请参阅名为“设置选择”的部分

MockMvc 和 Geb

在上一节中,我们了解了如何将MockMvcWebDriver一起使用。在本节中,我们将使用创业板来进行测试 Groovy-er。

为什么选择 Geb 和 MockMvc?

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

MockMvc 和 Geb 设置

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

def setup() {
	browser.driver = MockMvcHtmlUnitDriverBuilder
		.webAppContextSetup(context)
		.build()
}

这是使用MockMvcHtmlUnitDriverBuilder的简单示例。有关更高级的用法,请参阅名为“Advanced MockMvcHtmlUnitDriverBuilder”的部分

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

MockMvc 和 Geb 用法

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

to CreateMessagePage

然后我们可以填写表单并提交它以创建消息。

when:
form.summary = expectedSummary
form.text = expectedMessage
submit.click(ViewMessagePage)

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

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

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。这允许我们如下导航到页面。

to CreateMessagePage

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

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

我们在闭包中使用断言,这样我们就可以确定如果我们在错误的页面上出错了。

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

最后,我们可以验证是否已成功创建新消息。

then:
at ViewMessagePage
success == 'Successfully created a new message'
id
date
summary == expectedSummary
message == expectedMessage

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

15.6.3 Client-Side REST 测试

Client-side tests 可用于测试内部使用RestTemplate的 code。 idea 是声明预期的请求并提供“存根”响应,以便您可以专注于单独测试 code,i.e。没有 running 服务器。这是一个 example:

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”,并希望 return 回复 200 回复“text/plain”内容。我们可以根据需要定义额外的预期请求和存根响应。当定义了预期的请求和存根响应时,RestTemplate可以像往常一样在 client-side code 中使用。在测试结束时mockServer.verify()可用于验证是否已满足所有期望。

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

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

即使是默认的无序请求,每个请求也只允许执行一次。 expect方法提供了一个重载变量,它接受指定计数范围 e.g 的ExpectedCount参数。 oncemanyTimesmaxminbetween等。这是一个 example:

RestTemplate restTemplate = new RestTemplate();

MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build();
mockServer.expect(times(2), requestTo("/foo")).andRespond(withSuccess());
mockServer.expect(times(3), requestTo("/bar")).andRespond(withSuccess());

// ...

mockServer.verify();

请注意,如果未设置ignoreExpectOrder(默认值),因此请求声明在 order 中,那么 order 仅适用于任何预期请求中的第一个。例如,如果“/foo”预期 2 次,然后“/bar”3 次,那么在请求“/bar”之前应该有“/foo”请求但是除了后续的“/foo”和“/bar”请求之外任何时候都可以来。

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

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

// Test code that uses the above RestTemplate ...

mockServer.verify();

静态进口

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

Client-side REST 测试的更多示例

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

15.7 PetClinic Example

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

import static org.junit.Assert.assertEquals;
// import ...

@ContextConfiguration
public abstract class AbstractClinicTests extends AbstractTransactionalJUnit4SpringContextTests {

    @Autowired
    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());
        Vet v1 = EntityUtils.getById(vets, Vet.class, 2);
        assertEquals("Leary", v1.getLastName());
        assertEquals(1, v1.getNrOfSpecialties());
        assertEquals("radiology", (v1.getSpecialties().get(0)).getName());
        // ...
    }

    // ...
}

笔记:

  • 这个测试用例扩展了AbstractTransactionalJUnit4SpringContextTests class,它继承了依赖注入的 configuration(通过DependencyInjectionTestExecutionListener)和 transactional 行为(通过TransactionalTestExecutionListener)。

  • clinic实例变量 - 正在测试的 application object - 由依赖注入通过@Autowired语义设置。

  • getVets()方法说明了如何使用继承的countRowsInTable()方法轻松验证给定 table 中的行数,从而验证正在测试的 application code 的正确行为。这允许更强的测试并减少对精确测试数据的依赖性。例如,您可以在数据库中添加其他行而不会破坏测试。

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

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

例如,PetClinic 测试的 Hibernate implementation 包含以下 implementation。对于这个例子,HibernateClinicTests不包含 code 的 line:我们只需要声明@ContextConfiguration,并且测试继承自AbstractClinicTests。因为声明@ContextConfiguration没有任何特定的资源位置,Spring TestContext Framework 从AbstractClinicTests-context.xml(i.e.,继承的位置)和HibernateClinicTests-context.xml中定义的所有 beans 加载 application context,可能覆盖AbstractClinicTests-context.xml中定义的 beans。

@ContextConfiguration
public class HibernateClinicTests extends AbstractClinicTests { }

在 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。

Updated at: 5 months ago
14.2.2. Spring MVCTable of content16. 更多资源