15. Integration Testing

15.1 Overview

能够执行一些集成测试而无需部署到应用程序服务器或连接到其他企业基础结构,这一点很重要。这将使您能够测试诸如:

Spring 框架为spring-test模块中的集成测试提供了一流的支持。实际的 JAR 文件的名称可能包括发行版,也可能采用长org.springframework.test格式,具体取决于从何处获取(说明请参见依赖 Management 部分)。该库包含org.springframework.test软件包,该软件包包含用于与 Spring 容器进行集成测试的有价值的类。此测试不依赖于应用程序服务器或其他部署环境。此类测试的运行速度比单元测试慢,但比依赖于部署到应用程序服务器的等效 Selenium 测试或远程测试快得多。

在 Spring 2.5 和更高版本中,以 Comments 驱动的Spring TestContext 框架形式提供了单元和集成测试支持。 TestContext 框架与实际使用的测试框架无关,因此可以在各种环境(包括 JUnit,TestNG 等)中对测试进行检测。

15.2 集成测试的目标

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

接下来的几节描述了每个目标,并提供了有关实现和配置详细信息的链接。

15.2.1 上下文 Management 和缓存

Spring TestContext Framework 提供了 Spring ApplicationContextWebApplicationContext的一致加载以及这些上下文的缓存。支持加载上下文的缓存很重要,因为启动时间可能会成为一个问题-不是因为 Spring 本身的开销,而是因为 Spring 容器实例化的对象需要时间才能实例化。例如,具有 50 到 100 个 HibernateMap 文件的项目可能需要 10 到 20 秒钟来加载 Map 文件,并且在每个测试夹具中运行每个测试之前要承担该费用,这会导致整体测试运行速度变慢,从而降低开发人员的工作效率。

测试类通常声明 XML 的“资源位置”数组或 Groovy 配置元数据(通常在 Classpath 中)或用于配置应用程序的带 Comments 的类的数组。这些位置或类与web.xml或其他用于生产部署的配置文件中指定的位置或类相同或相似。

默认情况下,配置的ApplicationContext加载后将重新用于每个测试。因此,每个测试套件仅产生一次安装成本,并且随后的测试执行要快得多。在这种情况下,术语“测试套件”表示所有测试都在同一 JVM 中运行,例如,所有测试都从给定项目或模块的 Ant,Maven 或 Gradle 构建运行。在不太可能的情况下,测试破坏了应用程序上下文并需要重新加载(例如,通过修改 bean 定义或应用程序对象的状态),可以将 TestContext 框架配置为重新加载配置并重建应用程序上下文,然后再执行下一个测试。

参见第 15.5.4 节“上下文 Management”称为“上下文缓存”的部分与 TestContext 框架。

15.2.2 测试夹具的依赖注入

当 TestContext 框架加载您的应用程序上下文时,可以选择通过 Dependency Injection 配置您的测试类的实例。这提供了一种方便的机制,可以使用来自应用程序上下文的预配置 bean 来设置测试装置。这里的一个强大好处是,您可以在各种测试场景中重用应用程序上下文(例如,用于配置 SpringManagement 的对象图,事务代理,DataSource s 等),从而避免了为单个测试用例复制复杂的测试夹具设置的需要。

举个例子,考虑一下我们有一个HibernateTitleRepository类,该类为Title域实体实现数据访问逻辑。我们要编写集成测试来测试以下方面:

请参阅TestContext framework依赖测试夹具的依赖项注入。

15.2.3TransactionManagement

访问真实数据库的测试中的一个常见问题是它们对持久性存储状态的影响。即使使用开发数据库,对状态的更改也可能会影响以后的测试。另外,无法在事务外部执行(或验证)许多操作(例如,插入或修改持久数据)。

TestContext 框架解决了这个问题。默认情况下,框架将为每个测试创建并回滚事务。您只需编写可以假定事务存在的代码。如果在测试中调用事务代理对象,则对象将根据其配置的事务语义正确运行。此外,如果测试方法在测试 Management 的事务中运行时删除选定表的内容,则该事务将默认回滚,并且数据库将返回到执行测试之前的状态。通过在测试的应用程序上下文中定义的PlatformTransactionManager bean 为测试提供事务支持。

如果您要提交事务,这是不寻常的,但是在希望特定测试填充或修改数据库时偶尔有用,可以指示 TestContext 框架使事务提交,而不是通过@CommitComments 回滚。

请参阅使用TestContext framework进行 TransactionManagement。

15.2.4 集成测试的支持类

Spring TestContext Framework 提供了几个abstract支持类,这些类简化了集成测试的编写。这些基础测试类为测试框架提供了定义明确的钩子,以及方便的实例变量和方法,使您可以访问:

此外,您可能希望使用针对项目的实例变量和方法来创建自己的自定义,应用程序级超类。

请参阅TestContext framework的支持类。

15.3 JDBC 测试支持

org.springframework.test.jdbc软件包包含JdbcTestUtils,这是 JDBC 相关 Util 功能的集合,旨在简化标准数据库测试方案。具体地说,JdbcTestUtils提供了以下静态 Util 方法。

请注意AbstractTransactionalJUnit4SpringContextTestsAbstractTransactionalTestNGSpringContextTests提供了方便的方法,这些方法委派给JdbcTestUtils中的上述方法.

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

15.4 Annotations

15.4.1Spring 测试 Comments

Spring 框架提供了以下* Spring 特定*Comments 集,您可以在单元测试和集成测试中将它们与 TestContext 框架结合使用。有关更多信息,请参见相应的 javadocs,包括默认属性值,属性别名等。

@BootstrapWith

@BootstrapWith是一个类级别的 Comments,用于配置如何引导* Spring TestContext Framework *。具体来说,@BootstrapWith用于指定自定义TestContextBootstrapper。有关更多详细信息,请参见引导 TestContext 框架部分。

@ContextConfiguration

@ContextConfiguration定义了类级别的元数据,用于确定如何加载和配置ApplicationContext以进行集成测试。具体地说,@ContextConfiguration声明了将用于加载上下文的应用程序上下文资源locations或带 Comments 的classes

资源位置通常是位于 Classpath 中的 XML 配置文件或 Groovy 脚本。而带 Comments 的类通常是@Configuration个类。但是,资源位置也可以引用文件系统中的文件和脚本,带 Comments 的类可以是组件类,等等。

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

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

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

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

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

Note

@ContextConfiguration默认情况下支持继承资源位置或配置类以及超类声明的上下文初始化器。

有关更多详细信息,请参见第 15.5.4 节“上下文 Management”@ContextConfiguration javadocs。

@WebAppConfiguration

@WebAppConfiguration是类级别的 Comments,用于声明为集成测试加载的ApplicationContext应该是WebApplicationContext。在测试类上仅存在@WebAppConfiguration可以确保将WebApplicationContext加载到测试中,并使用默认值"file:src/main/webapp"作为 Web 应用程序根目录的路径(即* resource base path *)。资源基础路径用于在后台创建MockServletContext,该MockServletContext用作测试WebApplicationContextServletContext

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

要覆盖默认值,请通过* implicit * value属性指定其他基础资源路径。 classpath:file:资源前缀均受支持。如果未提供资源前缀,则假定该路径是文件系统资源。

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

请注意,在单个测试类中或在测试类层次结构中,@WebAppConfiguration必须与@ContextConfiguration结合使用。有关更多详细信息,请参见@WebAppConfiguration javadocs。

@ContextHierarchy

@ContextHierarchy是类级别的注解,用于为集成测试定义ApplicationContext的层次结构。 @ContextHierarchy应该用一个或多个@ContextConfiguration实例的列表声明,每个实例定义上下文层次结构中的一个级别。以下示例演示了在单个测试类中使用@ContextHierarchy的情况;但是,@ContextHierarchy也可以在测试类层次结构中使用。

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

如果需要合并或覆盖测试类层次结构中给定级别的上下文层次结构的配置,则必须通过在类层次结构中每个相应级别上为@ContextConfiguration中的name属性提供相同的值来显式命名该级别。有关更多示例,请参见称为“上下文层次结构”的部分@ContextHierarchy javadocs。

@ActiveProfiles

@ActiveProfiles是类级别的注解,用于声明在加载ApplicationContext进行集成测试时应启用哪些* bean 定义配置文件*。

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

Note

@ActiveProfiles默认情况下为“继承”超类声明的活动 bean 定义配置文件提供支持。还可以通过实现自定义ActiveProfilesResolver并通过@ActiveProfilesresolver属性对其进行注册,从而以编程方式解析活动 bean 定义概要文件。

有关示例和更多详细信息,请参见名为“使用环境配置文件进行上下文配置”的部分@ActiveProfiles javadocs。

@TestPropertySource

@TestPropertySource是类级别的 Comments,用于配置属性文件和内联属性的位置,这些属性将被添加到Environment中的PropertySources集合中,以便为集成测试加载ApplicationContext

测试属性源的优先级高于从 os 环境或 Java 系统属性以及应用程序通过@PropertySource或以编程方式声明性添加的属性源加载的优先级。因此,测试属性源可用于选择性覆盖系统和应用程序属性源中定义的属性。此外,内联属性比从资源位置加载的属性具有更高的优先级。

下面的示例演示如何从 Classpath 声明属性文件。

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

下面的示例演示如何声明内联属性。

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

@DirtiesContext

@DirtiesContext表示底层 Spring ApplicationContext已在测试执行期间被“脏污”(即,以某种方式修改或损坏了,例如通过更改单例 bean 的状态),因此应该关闭。当应用程序上下文标记为* dirty *时,会将其从测试框架的缓存中删除并关闭。因此,将为需要上下文具有相同配置元数据的任何后续测试重建基础 Spring 容器。

@DirtiesContext可以用作同一类或类层次结构中的类级和方法级 Comments。在这种情况下,取决于配置的methodModeclassMode,在任何此类带 Comments 的方法之前或之后以及在当前测试类之前或之后,ApplicationContext被标记为* dirty *。

以下示例说明了在各种配置情况下何时弄脏上下文:

@DirtiesContext(classMode = BEFORE_CLASS)
public class FreshContextTests {
    // some tests that require a new Spring container
}
@DirtiesContext
public class ContextDirtyingTests {
    // some tests that result in the Spring container being dirtied
}
@DirtiesContext(classMode = BEFORE_EACH_TEST_METHOD)
public class FreshContextTests {
    // some tests that require a new Spring container
}
@DirtiesContext(classMode = AFTER_EACH_TEST_METHOD)
public class ContextDirtyingTests {
    // some tests that result in the Spring container being dirtied
}
@DirtiesContext(methodMode = BEFORE_METHOD)
@Test
public void testProcessWhichRequiresFreshAppCtx() {
    // some logic that requires a new Spring container
}
@DirtiesContext
@Test
public void testProcessWhichDirtiesAppCtx() {
    // some logic that results in the Spring container being dirtied
}

如果在通过@ContextHierarchy将上下文配置为上下文层次结构一部分的测试中使用@DirtiesContext,则hierarchyMode标志可用于控制如何清除上下文缓存。默认情况下,将使用“穷举”算法清除上下文缓存,不仅包括当前级别,还包括共享当前测试共有的祖先上下文的所有其他上下文层次结构;驻留在公共祖先上下文的子层次结构中的所有ApplicationContext将从上下文缓存中删除并关闭。如果对于特定的用例,“穷举”算法是过大的,则可以指定更简单的“当前级别”算法,如下所示。

@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定义了用于配置TestExecutionListener实现的类级元数据,该实现应向TestContextManager注册。通常,@TestExecutionListeners@ContextConfiguration结合使用。

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

@TestExecutionListeners默认支持继承的侦听器。有关示例和更多详细信息,请参见 javadocs。

@Commit

@Commit表示应在测试方法完成后“提交” *事务测试方法的事务。 @Commit可用作@Rollback(false)的直接替代品,以便更明确地传达代码的意图。类似于@Rollback,也可以将@Commit声明为类级别或方法级别的 Comments。

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

@Rollback

@Rollback指示是否应在测试方法完成后“回滚”Transaction 测试方法的事务。如果true,则事务回滚;否则,事务回滚。否则,将提交事务(另请参见@Commit)。即使未明确声明@Rollback,Spring TestContext Framework 中集成测试的回滚语义也默认为true

当声明为类级别的 Comments 时,@Rollback定义测试类层次结构中所有测试方法的默认回滚语义。当声明为方法级别的 Comments 时,@Rollback定义特定测试方法的回滚语义,可能会覆盖类级别的@Rollback@Commit语义。

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

@BeforeTransaction

@BeforeTransaction指示 Comments 的void方法应在 Transaction 开始之前执行,该 Transaction 应针对配置为通过 Spring 的@TransactionalComments 在事务内运行的测试方法启动。从 Spring Framework 4.3 开始,@BeforeTransaction方法不需要为public,并且可以在基于 Java 8 的接口默认方法中声明。

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

@AfterTransaction

@AfterTransaction表示 Comments 的void方法应在事务结束后针对配置为通过 Spring 的@TransactionalComments 在事务中运行的测试方法执行。从 Spring Framework 4.3 开始,@AfterTransaction方法不需要为public,并且可以在基于 Java 8 的接口默认方法中声明。

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

@Sql

@Sql用于 Comments 测试类或测试方法,以配置要在集成测试期间针对给定数据库执行的 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定义用于确定如何解析和执行通过@SqlComments 配置的 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是一个容器 Comments,它聚合了几个@SqlComments。 @SqlGroup可以在本地使用,声明多个嵌套的@Sql注解,也可以与 Java 8 对可重复注解的支持结合使用,其中@Sql可以在同一类或方法上简单地声明多次,从而隐式生成此容器注解。

@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 标准 Comments 支持

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

Note

在 Spring TestContext Framework 中,@PostConstruct@PreDestroy可以与在ApplicationContext中配置的任何应用程序组件上的标准语义一起使用;但是,这些生命周期 Comments 在实际测试类中的使用受到限制。

如果测试类中的方法用@PostConstructComments,则该方法将在基础测试框架的任何* before *方法之前执行(例如,用 JUnit 4 的@BeforeComments 的方法),并且该方法将适用于测试中的每个测试方法类。另一方面,如果测试类中的方法用@PreDestroyComments,则该方法将“永远不会”执行。因此,在测试类中,建议使用来自底层测试框架的测试生命周期回调,而不是@PostConstruct@PreDestroy

15.4.3 Spring JUnit 4 测试 Comments

SpringRunnerSpring 的 JUnit 规则Spring 的 JUnit 4 支持类结合使用时,支持以下 Comments。

@IfProfileValue

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

@IfProfileValue可以在类级别,方法级别或两者上应用。对于该类或其子类中的任何方法,@IfProfileValue的类级用法优先于方法级用法。具体来说,如果同时在类级别在方法级别启用了测试,则将启用该测试。缺少@IfProfileValue意味着测试已隐式启用。这类似于 JUnit 4 的@IgnoreComments 的语义,除了@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 环境中实现对 test groups *的类似于 TestNG 的支持。考虑以下示例:

@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是类级别的 Comments,它指定在检索通过@IfProfileValueComments 配置的“配置文件值”时要使用的ProfileValueSource类型。如果未为测试声明@ProfileValueSourceConfiguration,则默认使用SystemProfileValueSource

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

@Timed

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

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

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

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

@Repeat

@Repeat表示必须重复执行带 Comments 的测试方法。Comments 中指定了要执行测试方法的次数。

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

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

15.4.4 测试的元 Comments 支持

可以将大多数与测试相关的 Comments 用作meta-annotations,以创建自定义的“组成的 Comments”并减少整个测试套件中的配置重复。

以下各项均可以与TestContext framework一起用作元 Comments。

例如,如果发现在基于 JUnit 4 的测试套件中重复以下配置,则……

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

我们可以通过引入自定义的“组合 Comments”来减少上述重复,该 Comments 将如下所示的通用测试配置集中化:

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

然后,我们可以使用我们的自定义@TransactionalDevTestComments 来简化各个测试类的配置,如下所示:

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

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

有关更多详细信息,请咨询SpringComments 编程模型

15.5 Spring TestContext 框架

除了通用测试基础结构之外,TestContext 框架还以abstract支持类的形式提供对 JUnit 4 和 TestNG 的显式支持。对于 JUnit 4,Spring 还提供了一个自定义 JUnit Runner和自定义 JUnit Rules,它们允许编写所谓的* POJO 测试类*。不需要 POJO 测试类来扩展特定的类层次结构。

下一节概述了 TestContext 框架的内部。如果您仅对使用框架感兴趣,而不必对使用自己的自定义侦听器或自定义加载程序对其进行扩展,则可以直接进入配置(context managementdependency injectiontransaction management),support classesannotation support部分。

15.5.1 密钥抽象

该框架的核心由TestContextManager类和TestContextTestExecutionListenerSmartContextLoader接口组成。每个测试类都会创建一个TestContextManager(例如,用于在 JUnit 4 中的单个测试类中执行所有测试方法)。 TestContextManager依次 Management 一个TestContext,该TestContext保留当前测试的上下文。 TestContextManager还随着测试的进行并更新TestContext的状态并更新TestContext的状态,这些实现通过提供依赖项注入,Management 事务等来指示实际的测试执行。 SmartContextLoader负责为给定的测试类加载ApplicationContext。有关更多信息和各种实现的示例,请查阅 javadocs 和 Spring 测试套件。

TestContext

TestContext封装了要在其中执行测试的上下文,而与使用中的实际测试框架无关,并为其负责的测试实例提供了上下文 Management 和缓存支持。 TestContext还委托SmartContextLoader来加载ApplicationContext(如果有要求)。

TestContextManager

TestContextManager是* Spring TestContext Framework *的主要入口点,该框架 Management 一个单独的TestContext,并在定义良好的测试执行点向每个注册的TestExecutionListener发送事件 signal:

TestExecutionListener

TestExecutionListener定义了对注册侦听器的TestContextManager发布的测试执行事件作出反应的 API。参见第 15.5.3 节“ TestExecutionListener 配置”

Context Loaders

ContextLoader是 Spring 2.5 中引入的策略接口,用于加载ApplicationContext以便由 Spring TestContext FrameworkManagement 的集成测试。实现SmartContextLoader而不是此接口,以便为带 Comments 的类,活动的 bean 定义配置文件,测试属性源,上下文层次结构和WebApplicationContext提供支持。

SmartContextLoader是 Spring 3.1 中引入的ContextLoader接口的扩展。 SmartContextLoader SPI 取代了 Spring 2.5 中引入的ContextLoader SPI。具体来说,SmartContextLoader可以选择处理资源locations,带 Comments 的classes或上下文initializers。此外,SmartContextLoader可以在其加载的上下文中设置活动 bean 定义概要文件并测试属性源。

Spring 提供了以下实现:

15.5.2 引导 TestContext 框架

Spring TestContext Framework 内部的默认配置足以满足所有常见用例。但是,有时开发团队或第三方框架想要更改默认的ContextLoader,实现自定义的TestContextContextCache,扩展ContextCustomizerFactoryTestExecutionListener的默认设置,等等。对于 TestContext 框架的这种低级控制运行时,Spring 提供了自举策略。

TestContextBootstrapper为“引导” TestContext 框架定义了 SPI。 TestContextManager使用TestContextBootstrapper来加载当前测试的TestExecutionListener实现并构建其 Management 的TestContext。可以通过@BootstrapWith直接或作为元 Comments 为测试类(或测试类层次结构)配置自定义引导策略。如果没有通过@BootstrapWith显式配置引导程序,则取决于@WebAppConfiguration的存在,将使用DefaultTestContextBootstrapperWebTestContextBootstrapper

由于将来TestContextBootstrapper SPI 可能会更改以适应新要求,因此强烈建议实现人员不要直接实现此接口,而应扩展AbstractTestContextBootstrapper或其具体子类之一。

15.5.3 TestExecutionListener 配置

Spring 提供了以下TestExecutionListener实现,这些实现默认情况下按此 Sequences 注册。

注册自定义 TestExecutionListener

可以通过@TestExecutionListeners注解为测试类及其子类注册自定义TestExecutionListener。有关详细信息和示例,请参见annotation support@TestExecutionListeners的 javadocs。

自动发现默认的 TestExecutionListener

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

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

Ordering TestExecutionListeners

当 TestContext 框架通过上述SpringFactoriesLoader机制发现默认的TestExecutionListener时,将使用 Spring 的AnnotationAwareOrderComparator对实例化的侦听器进行排序,该AnnotationAwareOrderComparator遵循 Spring 的Ordered接口和@OrderComments 进行排序。 AbstractTestExecutionListener和 Spring 提供的所有默认TestExecutionListener都以适当的值实现Ordered。因此,第三方框架和开发人员应通过实现Ordered或声明@Order来确保以正确的 Sequences 注册其* default * TestExecutionListener。有关核心默认值TestExecutionListenergetOrder()方法的信息,请查阅 javadocs,以获取有关为每个核心侦听器分配哪些值的详细信息。

Merging TestExecutionListeners

如果自定义TestExecutionListener通过@TestExecutionListeners注册,则不会注册* default *侦听器。在大多数常见的测试方案中,这有效地迫使开发人员手动声明除任何自定义侦听器之外的所有默认侦听器。下面的 Lists 演示了这种配置样式。

@ContextConfiguration
@TestExecutionListeners({
    MyCustomTestExecutionListener.class,
    ServletTestExecutionListener.class,
    DirtiesContextBeforeModesTestExecutionListener.class,
    DependencyInjectionTestExecutionListener.class,
    DirtiesContextTestExecutionListener.class,
    TransactionalTestExecutionListener.class,
    SqlScriptsTestExecutionListener.class
})
public class MyTest {
    // class body...
}

这种方法的挑战在于,它要求开发人员确切地知道默认情况下已注册了哪些侦听器。此外,默认的侦听器集可以在各个发行版之间进行更改,例如,在 Spring Framework 4.1 中引入了SqlScriptsTestExecutionListener,在 Spring Framework 4.2 中引入了DirtiesContextBeforeModesTestExecutionListener。此外,诸如 Spring Security 之类的第三方框架通过上述自动发现机制注册了自己的默认TestExecutionListener

为了避免必须意识到并重新声明 all * default *侦听器,可以将@TestExecutionListenersmergeMode属性设置为MergeMode.MERGE_WITH_DEFAULTSMERGE_WITH_DEFAULTS表示应将本地声明的侦听器与默认侦听器合并。合并算法可确保从列表中删除重复项,并确保根据AnnotationAwareOrderComparator的语义对合并的侦听器集进行排序,如名为“OrderTestExecutionListeners”的部分中所述。如果侦听器实现Ordered或带有@Order注解,则可以影响将其与默认值合并的位置。否则,在合并时,本地声明的侦听器将简单地添加到默认侦听器列表中。

例如,如果上一个示例中的MyCustomTestExecutionListener类将其order值(例如500)配置为小于ServletTestExecutionListener(恰好是1000)的 Sequences,那么MyCustomTestExecutionListener可以自动与默认列表合并在ServletTestExecutionListener的*前面,可以用以下示例替换前面的示例。

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

15.5.4 上下文 Management

每个TestContext为其负责的测试实例提供上下文 Management 和缓存支持。测试实例不会自动获得对配置的ApplicationContext的访问权限。但是,如果测试类实现ApplicationContextAware接口,则将对ApplicationContext的引用提供给测试实例。请注意,AbstractJUnit4SpringContextTestsAbstractTestNGSpringContextTests实现ApplicationContextAware,因此自动提供对ApplicationContext的访问。

Tip

作为实现ApplicationContextAware接口的替代方法,您可以通过字段或 setter 方法上的@Autowired注解为测试类注入应用程序上下文。例如:

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

@Autowired
private ApplicationContext applicationContext;

// class body...
}

同样,如果将测试配置为加载WebApplicationContext,则可以按以下方式将 Web 应用程序上下文注入到测试中:

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

// class body...
}

默认情况下配置的DependencyInjectionTestExecutionListener提供了通过@Autowired进行依赖注入(请参见第 15.5.5 节“测试夹具的依赖注入”)。

使用 TestContext 框架的测试类不需要扩展任何特定的类或实现特定的接口来配置其应用程序上下文。相反,只需在类级别声明@ContextConfiguration注解即可实现配置。如果您的测试类未明确声明应用程序上下文资源locations或带 Comments 的classes,则已配置的ContextLoader将确定如何从默认位置或默认配置类加载上下文。除了上下文资源locations和带 Comments 的classes之外,还可以通过应用程序上下文initializers配置应用程序上下文。

以下各节说明如何使用 XML 的@ContextConfigurationComments 通过 XML 配置文件,Groovy 脚本,带 Comments 的类(通常为@Configuration类)或上下文初始化器来配置ApplicationContext。另外,您可以针对高级用例实现并配置自己的自定义SmartContextLoader

使用 XML 资源进行上下文配置

要使用 XML 配置文件为测试加载ApplicationContext,请使用@ContextConfigurationComments 测试类,并使用包含 XML 配置元数据的资源位置的数组配置locations属性。普通路径或相对路径(例如"context.xml")将被视为相对于定义测试类的程序包的 Classpath 资源。以斜杠开头的路径被视为绝对 Classpath 位置,例如"/org/example/config.xml"。表示资源 URL 的路径(即以classpath:file:http:等为前缀的路径)将被“按原样”使用。

@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属性名称的声明并声明资源位置。

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

如果您从@ContextConfiguration注解中省略了locationsvalue属性,则 TestContext 框架将尝试检测默认的 XML 资源位置。具体来说,GenericXmlContextLoaderGenericXmlWebContextLoader根据测试类的名称检测默认位置。如果您的类名为com.example.MyTest,则GenericXmlContextLoader"classpath:com/example/MyTest-context.xml"加载您的应用程序上下文。

package com.example;

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

使用 Groovy 脚本进行上下文配置

要使用使用Groovy Bean 定义 DSL的 Groovy 脚本为测试加载ApplicationContext,请使用@ContextConfigurationComments 测试类,并使用包含 Groovy 脚本资源位置的数组配置locationsvalue属性。 Groovy 脚本的资源查找语义与针对XML 配置文件描述的语义相同。

Tip

如果 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注解中省略了locationsvalue属性,则 TestContext 框架将尝试检测默认的 Groovy 脚本。具体来说,GenericGroovyXmlContextLoaderGenericGroovyXmlWebContextLoader根据测试类的名称检测默认位置。如果您的类名为com.example.MyTest,那么 Groovy 上下文加载器将从"classpath:com/example/MyTestContext.groovy"加载您的应用程序上下文。

package com.example;

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

Tip

可以通过@ContextConfigurationlocationsvalue属性同时声明 XML 配置文件和 Groovy 脚本。如果到已配置资源位置的路径以.xml结尾,它将使用XmlBeanDefinitionReader加载;否则将使用GroovyBeanDefinitionReader加载。

下面的 Lists 演示了如何在集成测试中将两者结合在一起。

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

带 Comments 类的上下文配置

要使用带 Comments 的类*(请参见第 7.12 节“基于 Java 的容器配置”)为测试加载ApplicationContext,请用@ContextConfigurationComments 测试类,并使用包含对带 Comments 的类的引用的数组来配置classes属性。

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

Tip

术语“带 Comments 的类”可以指以下任何一种。

  • 带有@ConfigurationComments 的类

  • 组件(即带有@Component@Service@Repository等 Comments 的类)

  • 带有javax.inject注解的 JSR-330 兼容类

  • 包含@Bean-方法的任何其他类

有关 Comments 类的配置和语义的更多信息,请查阅@Configuration@Bean的 javadocs,尤其要注意 Bean @ Lite 模式的讨论。

如果从@ContextConfiguration注解中省略classes属性,则 TestContext 框架将尝试检测默认配置类的存在。具体来说,AnnotationConfigContextLoaderAnnotationConfigWebContextLoader将检测满足@Configuration javadocs 中指定的配置类实现要求的测试类的所有static嵌套类。在以下示例中,OrderServiceTest类声明了一个名为Configstatic嵌套配置类,该配置类将自动用于为测试类加载ApplicationContext。请注意,配置类的名称是任意的。此外,如果需要,测试类可以包含多个static嵌套配置类。

@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 脚本和带 Comments 的类

有时可能需要混合使用 XML 配置文件,Groovy 脚本和带 Comments 的类(即通常为@Configuration个类)来为测试配置ApplicationContext。例如,如果在 Producing 使用 XML 配置,则可以决定要使用@Configuration类为测试配置特定的 Spring 托管组件,反之亦然。

此外,某些第三方框架(例如 Spring Boot)提供了一流的支持,可同时从不同类型的资源(例如 XML 配置文件,Groovy 脚本和@Configuration类)中加载ApplicationContext。过去,Spring 框架不支持此标准部署。因此,Spring 框架在spring-test模块中提供的大多数SmartContextLoader实现仅在每个测试上下文中仅支持一种资源类型。但是,这并不意味着您不能同时使用两者。一般规则的一个 exception 是GenericGroovyXmlContextLoaderGenericGroovyXmlWebContextLoader同时支持 XML 配置文件和 Groovy 脚本。此外,第三方框架可以选择通过@ContextConfiguration支持locationsclasses的声明,并且通过 TestContext 框架中的标准测试支持,您可以选择以下选项。

如果要使用资源位置(例如 XML 或 Groovy) @Configuration类来配置测试,则必须选择一个作为入口点,并且其中一个必须包含或导入另一个。例如,在 XML 或 Groovy 脚本中,您可以通过组件扫描来包括@Configuration类,或者将它们定义为普通的 Spring bean。相反,在@Configuration类中,您可以使用@ImportResource导入 XML 配置文件或 Groovy 脚本。请注意,此行为在语义上等同于您在生产环境中配置应用程序的方式:在生产配置中,您将定义一组 XML 或 Groovy 资源位置或一组@Configuration类,将从中加载您的生产ApplicationContext,但您仍然包含或导入其他类型配置的自由。

使用上下文初始化程序进行上下文配置

要使用上下文初始化程序为测试配置ApplicationContext,请使用@ContextConfigurationComments 测试类,并使用包含对实现ApplicationContextInitializer的类的引用的数组来配置initializers属性。然后,已声明的上下文初始化器将用于初始化为测试加载的ConfigurableApplicationContext。请注意,每个声明的初始化程序支持的具体ConfigurableApplicationContext类型必须与使用中的SmartContextLoader创建的ApplicationContext类型兼容(即通常为GenericApplicationContext)。此外,初始化程序的调用 Sequences 取决于它们是实现 Spring 的Ordered接口还是用 Spring 的@OrderComments 或标准@PriorityComments 进行 Comments。

@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 配置文件,Groovy 脚本或带 Comments 的类的声明,而仅声明ApplicationContextInitializer类,这些类随后负责在上下文中注册 Bean(例如,通过编程方式从 XML 文件加载 Bean 定义)或配置类。

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

上下文配置继承

@ContextConfiguration支持布尔值inheritLocationsinheritInitializers,这些属性表示是否应该继承继承资源位置或由超类声明的带 Comments 的类和上下文初始化器。这两个标志的默认值为true。这意味着测试类将继承资源位置或带 Comments 的类以及任何超类声明的上下文初始化器。具体地说,将测试类的资源位置或带 Comments 的类追加到超类声明的资源位置或带 Comments 的类的列表中。同样,给定测试类的初始化程序将添加到由测试超类定义的初始化程序集。因此,子类可以选择“扩展”资源位置,带 Comments 的类或上下文初始化器。

如果@ContextConfiguration中的inheritLocationsinheritInitializers属性设置为false,则测试类* shadow *的资源位置或带 Comments 的类以及上下文初始化器分别有效地替换了超类定义的配置。

在以下使用 XML 资源位置的示例中,ExtendedTestApplicationContext将按此 Sequences 从*“ base-config.xml” * extended-config.xml“ 加载。因此,在“ extended-config.xml” 中定义的 Bean 可以覆盖*(即替换)在“” 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...
}

同样,在以下使用带 Comments 的类的示例中,将从BaseConfig ExtendedConfig类中按此 Sequences 加载ExtendedTestApplicationContext。因此,ExtendedConfig中定义的 Bean 可以覆盖(即替换)BaseConfig中定义的 Bean。

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

在以下使用上下文初始化器的示例中,将使用BaseInitializer ExtendedInitializer初始化ExtendedTestApplicationContext。但是请注意,初始化程序的调用 Sequences 取决于它们是实现 Spring 的Ordered接口还是用 Spring 的@OrderComments 或标准@PriorityComments 进行 Comments。

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

使用环境配置文件进行上下文配置

Spring 3.1 在框架中引入了对环境和配置文件(也称为* bean 定义配置文件*)概念的一流支持,并且可以配置集成测试以针对各种测试场景激活特定的 bean 定义配置文件。这可以通过用@ActiveProfilesComments 对测试类进行 Comments 并提供在加载ApplicationContext进行测试时应激活的配置文件列表来实现。

Note

@ActiveProfiles可以与新SmartContextLoader SPI 的任何实现一起使用,但是较旧的ContextLoader SPI 的实现不支持@ActiveProfiles

让我们看一些 XML 配置和@Configuration类的示例。

<!-- app-config.xml -->
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    xmlns:jee="http://www.springframework.org/schema/jee"
    xsi:schemaLocation="...">

    <bean id="transferService"
            class="com.bank.service.internal.DefaultTransferService">
        <constructor-arg ref="accountRepository"/>
        <constructor-arg ref="feePolicy"/>
    </bean>

    <bean id="accountRepository"
            class="com.bank.repository.internal.JdbcAccountRepository">
        <constructor-arg ref="dataSource"/>
    </bean>

    <bean id="feePolicy"
        class="com.bank.service.internal.ZeroFeePolicy"/>

    <beans profile="dev">
        <jdbc:embedded-database id="dataSource">
            <jdbc:script
                location="classpath:com/bank/config/sql/schema.sql"/>
            <jdbc:script
                location="classpath:com/bank/config/sql/test-data.sql"/>
        </jdbc:embedded-database>
    </beans>

    <beans profile="production">
        <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
    </beans>

    <beans profile="default">
        <jdbc:embedded-database id="dataSource">
            <jdbc:script
                location="classpath:com/bank/config/sql/schema.sql"/>
        </jdbc:embedded-database>
    </beans>

</beans>
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时,其ApplicationContext将从 Classpath 根目录中的app-config.xml配置文件中加载。如果检查app-config.xml,您会注意到accountRepository bean 对dataSource bean 有依赖性;但是,dataSource没有定义为顶级 bean。而是,dataSource被定义了 3 次:在* production 配置文件, dev 配置文件和 default *配置文件中。

通过用@ActiveProfiles("dev")CommentsTransferServiceTest,我们指示 Spring TestContext Framework 加载ApplicationContext并将活动配置文件设置为{"dev"}。结果,将创建一个嵌入式数据库并将测试数据填充到该数据库中,并将accountRepository bean 与对开发DataSource的引用进行连接。而这可能正是我们在集成测试中想要的。

将 bean 分配给default概要文件有时很有用。仅当没有专门激活其他配置文件时,才包含默认配置文件中的 Bean。这可以用来定义在应用程序的默认状态下使用的* fallback * bean。例如,您可以显式提供devproduction配置文件的数据源,但是当两者都不处于活动状态时,将内存中数据源定义为默认数据源。

以下代码 Lists 演示了如何使用@Configuration类而不是 XML 来实现相同的配置和集成测试。

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

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

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

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

    @Autowired DataSource dataSource;

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

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

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

}
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类:

与基于 XML 的配置示例一样,我们仍然使用@ActiveProfiles("dev")CommentsTransferServiceTest,但是这次我们通过@ContextConfigurationComments 指定所有四个配置类。测试类的主体本身保持完全不变。

通常在给定项目中跨多个测试类使用一组概要文件。因此,为避免重复声明@ActiveProfilesComments,可以在 Base Class 上声明一次@ActiveProfiles,并且子类将自动从 Base Class 继承@ActiveProfiles配置。在以下示例中,@ActiveProfiles的声明(以及其他 Comments)已移至抽象超类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属性,该属性可用于禁用活动配置文件的继承。

package com.bank.service;

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

此外,有时有必要以编程方式而非声明方式来解析测试的活动配置文件-例如,基于:

要以编程方式解析活动 bean 定义概要文件,只需实现自定义ActiveProfilesResolver并通过@ActiveProfilesresolver属性对其进行注册。下面的示例演示如何实现和注册自定义OperatingSystemActiveProfilesResolver。有关更多信息,请参考相应的 javadocs。

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

具有测试属性源的上下文配置

Spring 3.1 在框架中引入了对具有“属性源”层次结构的环境的概念的一流支持,并且由于 Spring 4.1 集成测试可以使用特定于测试的属性源进行配置。与@Configuration类上使用的@PropertySourceComments 相反,可以在测试类上声明@TestPropertySourceComments,以声明测试属性文件或“内联”属性的资源位置。这些测试属性源将被添加到Environment中的PropertySources集合中,以供为带 Comments 的集成测试加载的ApplicationContext

Note

@TestPropertySource可用于SmartContextLoader SPI 的任何实现,但较旧的ContextLoader SPI 的实现不支持@TestPropertySource

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

声明测试属性来源

可以通过@TestPropertySourcelocationsvalue属性来配置测试属性文件,如以下示例所示。

支持传统属性文件格式和基于 XML 的属性文件格式,例如"classpath:/com/example/test.properties""file:///path/to/file.xml"

每个路径将被解释为 Spring Resource。普通路径(例如"test.properties")将被视为与定义测试类的包“相对”的 Classpath 资源。以斜杠开头的路径将被视为绝对Classpath 资源,例如:"/org/example/test.xml"。将使用指定的资源协议加载引用 URL 的路径(例如,以classpath:file:http:等为前缀的路径)。不允许使用资源位置通配符(例如*/.properties):每个位置的值必须恰好等于一个.properties.xml资源。

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

可以通过@TestPropertySourceproperties属性配置键值对形式的* Inlined *属性,如以下示例所示。所有键值对都将作为具有最高优先级的单个测试PropertySource添加到封闭的Environment中。

键值对支持的语法与为 Java 属性文件中的条目定义的语法相同:

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

默认属性文件检测

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

Precedence

测试属性源的优先级高于从 os 环境或 Java 系统属性以及应用程序通过@PropertySource或以编程方式声明性添加的属性源加载的优先级。因此,测试属性源可用于选择性覆盖系统和应用程序属性源中定义的属性。此外,内联属性比从资源位置加载的属性具有更高的优先级。

在以下示例中,timezoneport属性以及"/test.properties"中定义的任何属性都将覆盖系统和应用程序属性源中定义的同名属性。此外,如果"/test.properties"文件定义了timezoneport属性的条目,则这些条目将被通过properties属性声明的* inlined *属性覆盖。

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

继承和覆盖测试属性源

@TestPropertySource支持布尔值inheritLocationsinheritProperties,它们表示是否应继承超类声明的属性文件和内联属性的资源位置。这两个标志的默认值为true。这意味着测试类将继承任何超类声明的位置和内联属性。具体来说,将测试类的位置和内联属性附加到超类声明的位置和内联属性中。因此,子类可以选择“扩展”位置和内联属性。请注意,稍后出现的属性会shade(即覆盖)之前出现的同名属性。此外,上述优先规则也适用于继承的测试属性源。

如果@TestPropertySource中的inheritLocationsinheritProperties属性设置为false,则测试类* shadow *的位置或内联属性分别有效地替换超类定义的配置。

在以下示例中,将仅使用"base.properties"文件作为测试属性源来加载ApplicationContext for BaseTest。相反,将使用"base.properties" "extended.properties"文件作为测试属性源位置来加载_5 的ApplicationContext

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

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

在以下示例中,将仅使用* inlined * key1属性来加载ApplicationContext for BaseTest。相反,ExtendedTestApplicationContext将使用* inlined * key1key2属性加载。

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

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

加载 WebApplicationContext

Spring 3.2 引入了对在集成测试中加载WebApplicationContext的支持。要指示 TestContext 框架加载WebApplicationContext而不是标准ApplicationContext,只需用@WebAppConfigurationComments 相应的测试类。

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

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

以下示例演示了用于加载WebApplicationContext的各种配置选项。

Conventions.

@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 框架对“约定超越配置”的支持。如果使用@WebAppConfigurationComments 测试类而未指定资源基本路径,则资源路径将有效地默认为*“ file:src/main/webapp” 。同样,如果您声明@ContextConfiguration但未指定资源locations,带 Comments 的classes或上下文initializers,则 Spring 会尝试使用约定(即“ WacTests-context.xml” *与WacTests类放在同一包中)检测配置的存在。或静态嵌套的@Configuration类)。

默认资源语义.

@RunWith(SpringRunner.class)

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

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

public class WacTests {
    //...
}

此示例演示如何使用@WebAppConfiguration显式声明资源基础路径和使用@ContextConfiguration声明 XML 资源位置。这里要注意的重要一点是具有这两个 Comments 的路径的语义不同。默认情况下,@WebAppConfiguration资源路径是基于文件系统的;而@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 资源前缀来覆盖这两个 Comments 的默认资源语义。将本示例中的 Comments 与上一个示例进行对比。

为了提供全面的 Web 测试支持,Spring 3.2 引入了默认情况下启用的ServletTestExecutionListener。在针对WebApplicationContext进行测试时,此TestExecutionListener在每种测试方法之前通过 Spring Web 的RequestContextHolder设置默认的线程本地状态,并根据通过@WebAppConfiguration配置的基本资源路径创建MockHttpServletRequestMockHttpServletResponseServletWebRequestServletTestExecutionListener还确保可以将MockHttpServletResponseServletWebRequest注入到测试实例中,并且一旦测试完成,它将清除线程本地状态。

加载WebApplicationContext进行测试后,您可能会发现您需要与网络模拟进行交互,例如,在调用 Web 组件后设置测试装置或执行声明。以下示例演示了可以将哪些模拟自动连接到您的测试实例。注意,WebApplicationContextMockServletContext都缓存在整个测试套件中。而其他模拟则由ServletTestExecutionListener按测试方法进行 Management。

Injecting mocks.

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

一旦 TestContext 框架为测试加载ApplicationContext(或WebApplicationContext),该上下文将被缓存并为所有后续测试重用,这些测试在同一测试套件中声明了相同的唯一上下文配置。要了解缓存的工作原理,重要的是要了解* unique test suite *的含义。

ApplicationContext可以通过用于加载它的配置参数的组合来唯一地标识。因此,使用配置参数的唯一组合来生成一个* key *,在该键下缓存上下文。 TestContext 框架使用以下配置参数来构建上下文缓存键:

例如,如果TestClassA@ContextConfigurationlocations(或value)属性指定{"app-config.xml", "test-config.xml"},则 TestContext 框架将加载相应的ApplicationContext并将其存储在static上下文缓存中,该缓存位于仅基于那些位置的键下。因此,如果TestClassB也为其位置(通过继承显式或隐式)定义{"app-config.xml", "test-config.xml"},但未定义@WebAppConfiguration,不同的ContextLoader,不同的活动配置文件,不同的上下文初始化程序,不同的测试属性源或不同的父上下文,则相同的ApplicationContext将由两个测试类共享。这意味着加载应用程序上下文的设置成本仅发生一次(每个测试套件),并且随后的测试执行要快得多。

Note

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

要从缓存机制中受益,所有测试必须在同一进程或测试套件中运行。这可以通过在 IDE 中以组的形式执行所有测试来实现。同样,在使用 Ant,Maven 或 Gradle 之类的构建框架执行测试时,务必确保该构建框架不会在测试之间“分叉”。例如,如果将 Maven Surefire 插件的forkMode设置为alwayspertest,则 TestContext 框架将无法在测试类之间缓存应用程序上下文,结果,构建过程将大大降低运行速度。

从 Spring Framework 4.3 开始,上下文缓存的大小以默认的最大大小 32 为界。每当达到最大大小时,就会使用最近最少使用(LRU)驱逐策略来驱逐和关闭陈旧的上下文。可以从命令行或构建脚本通过设置名为spring.test.context.cache.maxSize的 JVM 系统属性来配置最大大小。另外,也可以通过SpringProperties API 以编程方式设置相同的属性。

由于在给定的测试套件中加载大量应用程序上下文会导致该套件花费不必要的长时间执行,因此准确地知道已加载和缓存了多少个上下文通常是有益的。要查看基础上下文缓存的统计信息,只需将org.springframework.test.context.cache日志记录类别的日志级别设置为DEBUG即可。

在不太可能的情况下,测试破坏了应用程序上下文并需要重新加载(例如,通过修改 bean 定义或应用程序对象的状态),您可以使用@DirtiesContextComments 测试类或测试方法(请参见第 15.4.1 节“Spring 测试 Comments”中关于@DirtiesContext的讨论) )。这指示 Spring 在执行下一个测试之前从缓存中删除上下文并重建应用程序上下文。请注意,默认情况下启用的DirtiesContextBeforeModesTestExecutionListenerDirtiesContextTestExecutionListener提供了对@DirtiesContext注解的支持。

Context hierarchies

在编写依赖于已加载的 Spring ApplicationContext的集成测试时,通常足以针对单个上下文进行测试;但是,有时候甚至有必要针对ApplicationContext s 的层次结构进行测试。例如,如果您正在开发 Spring MVC Web 应用程序,则通常会通过 Spring 的ContextLoaderListener加载根WebApplicationContext,而通过 Spring 的DispatcherServlet加载子级WebApplicationContext。这导致父子上下文层次结构,其中共享组件和基础结构配置在根上下文中声明,并在特定于 Web 的组件的子上下文中使用。在 Spring Batch 应用程序中可以找到另一个用例,在该应用程序中,您经常有一个父上下文为共享批处理基础结构提供配置,而子上下文为特定批处理作业的配置提供配置。

从 Spring Framework 3.2.2 开始,可以通过在单个测试类上或在测试类层次结构中通过@ContextHierarchyComments 声明上下文配置来编写使用上下文层次结构的集成测试。如果在测试类层次结构中的多个类上声明了上下文层次结构,则还可以合并或覆盖上下文层次结构中特定命名级别的上下文配置。在合并层次结构中给定级别的配置时,配置资源类型(即 XML 配置文件或带 Comments 的类)必须保持一致;否则,在使用不同资源类型配置的上下文层次结构中具有不同级别是完全可以接受的。

以下基于 JUnit 4 的示例演示了需要使用上下文层次结构进行集成测试的常见配置方案。

ControllerIntegrationTestspass 语句一个上下文层次结构来表示 Spring MVC Web 应用程序的典型集成测试场景,该上下文层次结构包含两个级别,一个层次用于* root * WebApplicationContext(使用TestAppConfig @Configuration类加载),一个层次用于* dispatcher servlet * WebApplicationContext(使用WebConfig @Configuration类)。被自动装配到测试实例中的WebApplicationContext是用于子上下文(即,层次结构中的最低上下文)的那个。

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

    @Autowired
    private WebApplicationContext wac;

    // ...
}

以下测试类在测试类层次结构中定义了上下文层次结构。 AbstractWebTests在 Spring 支持的 Web 应用程序中声明根WebApplicationContext的配置。但是请注意,AbstractWebTests不声明@ContextHierarchy;因此,AbstractWebTests的子类可以选择参与上下文层次结构,或仅遵循@ContextConfiguration的标准语义。 SoapWebServiceTestsRestWebServiceTests都扩展AbstractWebTests并通过@ContextHierarchy定义上下文层次结构。结果是将加载三个应用程序上下文(每个@ContextConfiguration声明一个),并且基于AbstractWebTests中的配置加载的应用程序上下文将被设置为为具体子类加载的每个上下文的父上下文。

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

以下类演示了“命名”层次结构级别的用法,以便“合并”上下文上下文层次结构中特定级别的配置。 BaseTests在层次结构中定义了两个级别parentchildExtendedTests扩展BaseTests并指示 Spring TestContext Framework 合并child层次结构级别的上下文配置,只需确保通过@ContextConfigurationname属性声明的名称都是"child"即可。结果是将加载三个应用程序上下文:一个用于"/app-config.xml",一个用于"/user-config.xml",以及一个{"/user-config.xml", "/order-config.xml"}。与前面的示例一样,将从"/app-config.xml"加载的应用程序上下文设置为从"/user-config.xml"{"/user-config.xml", "/order-config.xml"}加载的上下文的父上下文。

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

与之前的示例相反,此示例演示如何通过将@ContextConfiguration中的inheritLocations标志设置为false来覆盖上下文层次结构中给定命名级别的配置。因此,ExtendedTests的应用程序上下文将仅从"/test-user-config.xml"加载,并将其父级设置为从"/app-config.xml"加载的上下文。

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

Note

如果在上下文配置为上下文层次结构一部分的测试中使用@DirtiesContext,则hierarchyMode标志可用于控制如何清除上下文缓存。有关更多详细信息,请参见Spring 测试 Comments@DirtiesContext javadocs 中有关@DirtiesContext的讨论。

15.5.5 测试夹具的依赖注入

当您使用DependencyInjectionTestExecutionListener(默认情况下配置)时,测试实例的依赖项是从@ContextConfiguration配置的应用程序上下文中的 bean 中“注入”的。您可以使用 setter 注入,字段注入或同时使用这两种方式,这取决于您选择的 Comments 以及是否将它们放置在 setter 方法或字段中。为了与 Spring 2.5 和 3.0 中引入的 Comments 支持保持一致,可以使用 Spring 的@AutowiredComments 或 JSR 330 中的@InjectComments。

Tip

TestContext 框架不检测测试实例的实例化方式。因此,对于构造函数使用@Autowired@Inject对测试类无效。

因为@Autowired用于执行按类型自动布线,所以如果您具有相同类型的多个 bean 定义,那么对于那些特定的 bean,您将不能依靠这种方法。在这种情况下,您可以将@Autowired@Qualifier结合使用。从 Spring 3.0 开始,您还可以选择将@Inject@Named结合使用。另外,如果您的测试类可以访问其ApplicationContext,则可以通过使用(例如)对applicationContext.getBean("titleRepository")的调用来执行显式查找。

如果您不希望将依赖项注入应用于测试实例,只需不要使用@Autowired@InjectComments 字段或设置方法。或者,可以通过用@TestExecutionListeners显式配置您的类并从侦听器列表中省略DependencyInjectionTestExecutionListener.class来完全禁用依赖项注入。

请考虑测试Goals部分中概述的HibernateTitleRepository类的方案。接下来的两个代码 Lists 演示了在字段和 setter 方法上使用@Autowired的方法。在所有示例代码 Lists 之后显示了应用程序上下文配置。

Note

以下代码 Lists 中的依赖项注入行为并非特定于 JUnit4.相同的 DI 技术可以与任何测试框架结合使用。

以下示例调用了诸如assertNotNull()之类的静态 assert 方法,但未在调用前加上Assert。在这种情况下,假定该方法已通过示例中未显示的import static声明正确导入。

第一个代码 Lists 显示了使用@Autowired进行字段注入的测试类的基于 JUnit 4 的实现。

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

或者,您可以将类配置为使用@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
    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);
    }
}

前面的代码 Lists 使用@ContextConfigurationComments(即repository-config.xml)引用的相同 XML 上下文文件,如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        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>

Note

如果您是从 Spring 提供的测试 Base Class 扩展而来的,而该 Base Class 恰巧在其 setter 方法之一上使用@Autowired,则可能在应用程序上下文中定义了多个受影响类型的 Bean:例如,多个DataSource Bean。在这种情况下,您可以重写 setter 方法,并使用@Qualifier注解来指示特定的目标 bean,如下所示,但请确保也委派给超类中的重写方法。

// ...

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

// ...

指定的限定符值指示要注入的特定DataSource bean,从而将类型匹配的范围缩小到特定的 bean。其值与相应<bean>定义中的<qualifier>声明匹配。 Bean 名称用作后备限定符值,因此您也可以在该名称中有效地指向特定的 Bean(如上所示,假设“ myDataSource”是 Bean ID)。

15.5.6 测试请求和会话范围的 Bean

早年以来,Spring 就一直支持请求和会话范围的 bean,而从 Spring 3.2 开始,通过遵循以下步骤来测试您的请求范围和会话范围的 bean 变得轻而易举。

以下代码段显示了登录用例的 XML 配置。注意,userService bean 与请求范围内的loginAction bean 有依赖性。另外,使用SpEL expressions实例化LoginAction,该SpEL expressions从当前 HTTP 请求中检索用户名和密码。在我们的测试中,我们将要通过 TestContext 框架 Management 的模拟来配置这些请求参数。

请求范围的 Bean 配置.

<beans>

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

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

</beans>

RequestScopedBeanTests中,我们将UserService(即被测对象)和MockHttpServletRequest都注入到我们的测试实例中。在requestScope()测试方法中,我们通过在提供的MockHttpServletRequest中设置请求参数来设置测试夹具。当在我们的userService上调用loginUser()方法时,我们可以确保用户服务可以访问当前MockHttpServletRequest的请求范围的loginAction(即,我们刚刚在其中设置参数的方法)。然后,我们可以根据用户名和密码的已知 Importing 对结果进行 assert。

请求范围的 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
    }
}

下面的代码段类似于上面针对请求范围的 Bean 看到的代码段。但是,这次userService bean 与会话范围的userPreferences bean 有依赖关系。注意,UserPreferences bean 是使用 SpEL 表达式实例化的,该表达式从当前 HTTP 会话中检索* theme *。在我们的测试中,我们将需要在由 TestContext 框架 Management 的模拟会话中配置主题。

会话范围的 Bean 配置。

<beans>

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

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

</beans>

SessionScopedBeanTests中,我们将UserServiceMockHttpSession注入到我们的测试实例中。在sessionScope()测试方法中,我们通过在提供的MockHttpSession中设置预期的“ theme”属性来设置测试装置。当在我们的userService上调用processUserPreferences()方法时,我们可以确保用户服务可以访问当前MockHttpSession的会话作用域userPreferences,并且我们可以基于配置的主题对结果进行 assert。

会话范围的 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.7TransactionManagement

在 TestContext 框架中,事务由默认配置的TransactionalTestExecutionListenerManagement,即使您未在测试类上显式声明@TestExecutionListeners也不 exception。但是,要启用对事务的支持,必须在ApplicationContext中配置通过@ContextConfiguration语义加载的PlatformTransactionManager bean(下面提供了更多详细信息)。另外,您必须在类或方法级别为测试声明 Spring 的@Transactional注解。

Test-managed transactions

测试 Management 的 Transaction是通过TransactionalTestExecutionListener以声明方式或通过TestTransaction以编程方式(请参阅下文)进行 Management 的 Transaction。此类事务不应与* SpringManagement 的事务*(即,在ApplicationContext中为测试加载的,由 Spring 直接 Management 的事务)或应用程序 Management 的 Transaction(即,在通过测试调用的应用程序代码中以编程方式 Management 的事务)混淆。 。 SpringManagement 的事务和应用程序 Management 的事务通常将参与测试 Management 的事务。但是,如果将 SpringManagement 的事务或应用程序 Management 的事务配置为REQUIREDSUPPORTS以外的任何* propagation *类型,则应小心(有关详细信息,请参见transaction propagation的讨论)。

启用和禁用 Transaction

使用@TransactionalComments 测试方法将导致测试在事务中运行,默认情况下,该事务将在测试完成后自动回滚。如果用@TransactionalComments 测试类,则该类层次结构中的每个测试方法都将在事务中运行。未用@TransactionalComments 的测试方法(在类或方法级别)将不会在事务中运行。此外,用@TransactionalComments 但propagation类型设置为NOT_SUPPORTED的测试将不会在事务中运行。

请注意,AbstractTransactionalJUnit4SpringContextTestsAbstractTransactionalTestNGSpringContextTests已预先配置为在类级别提供事务支持.

下面的示例演示了为基于 Hibernate 的UserRepository编写集成测试的常见方案。如称为“事务回滚和提交行为”的部分中所述,在执行createUser()方法后无需清理数据库,因为对数据库所做的任何更改都会由TransactionalTestExecutionListener自动回滚。有关其他示例,请参见第 15.7 节“ PetClinic 示例”

@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 将在测试完成后自动回滚;但是,可以通过@Commit@RollbackComments 声明性地配置事务提交和回滚行为。有关更多详细信息,请参见annotation support部分中的相应条目。

程序化 TransactionManagement

从 Spring Framework 4.1 开始,可以通过TestTransaction中的静态方法以编程方式与测试 Management 的事务进行交互。例如,可以在 test 方法, before 方法和 after *方法中使用TestTransaction来启动或结束当前的测试 Management 的事务或配置当前的测试 Management 的事务以进行回滚或提交。启用TransactionalTestExecutionListener时,将自动提供对TestTransaction的支持。

以下示例演示了TestTransaction的某些功能。有关更多详细信息,请查阅 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 之外执行代码

有时,您需要在事务测试方法之前或之后但在事务上下文之外执行某些代码。例如,在执行测试之前验证初始数据库状态,或者在测试执行之后验证预期的事务提交行为(如果测试是配置为提交 Transaction)。 TransactionalTestExecutionListener完全支持此类情况的@BeforeTransaction@AfterTransaction注解。只需使用这些 Comments 之一对测试类中的任何void方法或测试接口中的任何void默认方法进行 Comments,TransactionalTestExecutionListener即可确保您的Transaction 前Transaction 后在适当的时间执行。

Tip

任何* before 方法*(例如,以 JUnit 4 的@BeforeComments 的方法)和任何* after 方法*(例如以 JUnit 4 的@AfterComments 的方法)都在事务内执行。此外,对于未配置为在事务内运行的测试方法,自然不会执行带有@BeforeTransaction@AfterTransactionComments 的方法。

配置事务 Management 器

TransactionalTestExecutionListener期望在 Spring ApplicationContext中定义一个PlatformTransactionManager bean 进行测试。如果测试的ApplicationContext内有PlatformTransactionManager的多个实例,则可以通过@Transactional("myTxMgr")@Transactional(transactionManager = "myTxMgr")声明一个* qualifier *,或者可以由@Configuration类实现TransactionManagementConfigurer。有关用于在测试ApplicationContext中查找事务 Management 器的算法的详细信息,请查阅 javadocs 中的TestContextTransactionUtils.retrieveTransactionManager()

演示所有与 Transaction 相关的 Comments

以下基于 JUnit 4 的示例显示了一个虚拟的集成测试方案,突出显示了所有与事务相关的 Comments。该示例“不是”旨在演示最佳做法,而是演示如何使用这些 Comments。有关更多信息和配置示例,请参考annotation support部分。 @Sql 的事务 Management包含另一个示例,该示例使用@Sql来以默认事务回滚语义执行声明性 SQL 脚本。

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

}

Note

当您测试操纵 Hibernate 会话或 JPA 持久性上下文状态的应用程序代码时,请确保“刷新”执行该代码的测试方法中的基础工作单元。未能刷新基础工作单元可能会产生“误报” :您的测试可能通过,但是相同的代码在实际的生产环境中会引发异常。在下面的基于 Hibernate 的示例测试案例中,一种方法演示了误报,而另一种方法正确地公开了刷新会话的结果。请注意,这适用于任何维护内存工作单元*的 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 脚本

在针对关系数据库编写集成测试时,执行 SQL 脚本来修改数据库架构或将测试数据插入表中通常是有益的。 spring-jdbc模块通过在加载 Spring ApplicationContext时执行 SQL 脚本,为“初始化”嵌入式或现有数据库提供支持。有关详情,请参见第 19.8 节“嵌入式数据库支持”第 19.8.5 节“使用嵌入式数据库测试数据访问逻辑”

尽管在加载ApplicationContext时一次初始化数据库以进行测试非常有用,但是有时在集成测试过程中能够修改数据库是必不可少的。以下各节说明在集成测试期间如何以编程方式和声明方式执行 SQL 脚本。

以编程方式执行 SQL 脚本

Spring 提供了以下选项,用于在集成测试方法中以编程方式执行 SQL 脚本。

ScriptUtils提供了用于处理 SQL 脚本的静态 Util 方法的集合,并且主要供框架内部使用。但是,如果您需要完全控制 SQL 脚本的解析和执行方式,则ScriptUtils可能比下面介绍的其他一些替代方法更适合您的需求。有关更多详细信息,请查阅ScriptUtils中的 Javadocs 以获得单个方法。

ResourceDatabasePopulator提供了一个简单的基于对象的 API,可使用外部资源中定义的 SQL 脚本以编程方式填充,初始化或清理数据库。 ResourceDatabasePopulator提供了用于配置在解析和执行脚本时使用的字符编码,语句分隔符,Comments 定界符和错误处理标志的选项,并且每个配置选项都有一个合理的默认值。请查阅 javadocs 以获取有关默认值的详细信息。要执行ResourceDatabasePopulator中配置的脚本,可以调用populate(Connection)方法以针对java.sql.Connection执行填充程序,也可以调用execute(DataSource)方法以针对javax.sql.DataSource执行填充程序。下面的示例为测试模式和测试数据指定 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 脚本。具体来说,可以在测试类或测试方法上声明@Sql注解,以将 SQL 脚本的资源路径配置为应该在集成测试方法之前或之后针对给定数据库执行的 SQL 脚本。请注意,方法级别的声明将覆盖类级别的声明,并且SqlScriptsTestExecutionListener提供了对@Sql的支持,该默认情况下已启用。

路径资源语义

每个路径将被解释为 Spring Resource。普通路径(例如"schema.sql")将被视为与定义测试类的包“相对”的 Classpath 资源。以斜杠开头的路径将被视为绝对Classpath 资源,例如:"/org/example/schema.sql"。将使用指定的资源协议加载引用 URL 的路径(例如,以classpath:file:http:等为前缀的路径)。

下面的示例演示如何在基于 JUnit 4 的集成测试类中的类级别和方法级别使用@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

声明多个@Sql

如果需要为给定的测试类或测试方法配置多组 SQL 脚本,但使用不同的语法配置,不同的错误处理规则或每组不同的执行阶段,则可以声明@Sql的多个实例。在 Java 8 中,@Sql可用作可重复Comments。否则,@SqlGroupComments 可用作显式容器,用于声明@Sql的多个实例。

下面的示例演示使用 Java 8 将@Sql作为可重复 Comments 的用法。在这种情况下,test-schema.sql脚本对单行 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 脚本将在相应的测试方法之前执行。但是,如果需要在测试方法之后执行特定的脚本集,例如清理数据库状态,则可以使用@Sql中的executionPhase属性,如以下示例所示。请注意,分别从Sql.TransactionModeSql.ExecutionPhase静态导入ISOLATEDAFTER_TEST_METHOD

@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注解配置脚本解析和错误处理的配置。当在集成测试类上声明为类级别的 Comments 时,@SqlConfig充当测试类层次结构中所有 SQL 脚本的* global 配置。当直接通过@SqlComments 的config属性声明时,@SqlConfig用作封闭@SqlComments 中声明的 SQL 脚本的 local 配置。 @SqlConfig中的每个属性都有一个隐式默认值,该默认值记录在相应属性的 javadocs 中。由于 Java 语言规范中为 Comments 属性定义了规则,因此,无法将null的值分配给 Comments 属性。因此,为了支持对继承的全局配置的覆盖,@SqlConfig属性的显式默认值为 String 的""或 Enums 的DEFAULT。此方法允许@SqlConfig的局部声明通过提供""DEFAULT以外的值来选择性地覆盖@SqlConfig的全局声明中的各个属性。只要本地@SqlConfig属性未提供""DEFAULT以外的显式值,就会继承全局@SqlConfig属性。因此,显式 local 配置将覆盖 global *配置。

@Sql@SqlConfig提供的配置选项与ScriptUtilsResourceDatabasePopulator支持的配置选项等效,但是<jdbc:initialize-database/> XML 名称空间元素提供的配置选项的超集。有关详细信息,请查阅@Sql@SqlConfig中各个属性的 javadocs。

@Sql的 TransactionManagement

默认情况下,SqlScriptsTestExecutionListener将为通过@Sql配置的脚本推断所需的事务语义。具体来说,SQL 脚本将在现有的 SpringManagement 的事务中(例如,由TransactionalTestExecutionListenerManagement 的用于用@TransactionalComments 的测试的事务)在隔离事务中执行,而无需事务,这取决于transactionMode属性的配置值在@SqlConfig中,并且在测试的ApplicationContext中存在PlatformTransactionManager。但是,作为最低要求,测试的ApplicationContext中必须存在javax.sql.DataSource

如果SqlScriptsTestExecutionListener用于检测DataSourcePlatformTransactionManager并推断事务语义的算法不符合您的需求,则可以通过@SqlConfigdataSourcetransactionManager属性指定显式名称。此外,例如,如果脚本应在独立的事务中执行,则可以通过@SqlConfigtransactionMode属性来控制事务传播行为。尽管对@Sql事务 Management 的所有受支持选项的详尽讨论不在本参考手册的范围之内,但是@SqlConfigSqlScriptsTestExecutionListener的 javadocs 提供了详细信息,并且以下示例演示了使用 JUnit 4 和@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 支持类

Spring JUnit 4 Runner

以下代码 Lists 显示了配置测试类以与自定义 Spring Runner一起运行的最低要求。 @TestExecutionListeners配置有一个空列表以禁用默认侦听器,否则将需要通过@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是一个 JUnit TestRule,它支持* Spring TestContext Framework class-level 功能;而SpringMethodRule是支持 Spring TestContext Framework *的实例级和方法级功能的 JUnit MethodRule

SpringRunner相比,Spring 的基于规则的 JUnit 支持具有以下优势:它独立于任何org.junit.runner.Runner实现,因此可以与现有的替代运行器(如 JUnit 4 的Parameterized)或第三方运行器(如MockitoJUnitRunner)组合。

为了支持 TestContext 框架的全部功能,必须将SpringClassRuleSpringMethodRule组合在一起。下面的示例演示了在集成测试中声明这些规则的正确方法。

// 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 支持类

org.springframework.test.context.junit4软件包为基于 JUnit 4 的测试用例提供了以下支持类(在 JUnit 4.12 或更高版本上受支持)。

AbstractJUnit4SpringContextTests是抽象的基础测试类,它在 JUnit 4 环境中将* Spring TestContext Framework *与显式的ApplicationContext测试支持集成在一起。扩展AbstractJUnit4SpringContextTests时,您可以访问protected applicationContext实例变量,该实例变量可用于执行显式 bean 查找或测试整个上下文的状态。

AbstractTransactionalJUnit4SpringContextTestsAbstractJUnit4SpringContextTests的抽象事务扩展,为 JDBC 访问添加了一些便利功能。此类期望在ApplicationContext中定义javax.sql.DataSource bean 和PlatformTransactionManager bean。扩展AbstractTransactionalJUnit4SpringContextTests时,可以访问protected jdbcTemplate实例变量,该实例变量可用于执行 SQL 语句来查询数据库。此类查询可用于在与数据库相关的应用程序代码执行之前和之后确认数据库状态,并且 Spring 确保此类查询在与应用程序代码相同的事务范围内运行。与 ORM 工具结合使用时,请务必避免使用false positives。如第 15.3 节“ JDBC 测试支持”中所述,AbstractTransactionalJUnit4SpringContextTests还提供了便捷方法,这些方法使用上述jdbcTemplate委托给JdbcTestUtils中的方法。此外,AbstractTransactionalJUnit4SpringContextTests提供了executeSqlScript(..)方法,用于针对已配置的DataSource执行 SQL 脚本。

Tip

这些类为扩展提供了便利。如果您不希望将测试类绑定到特定于 Spring 的类层次结构,则可以使用@RunWith(SpringRunner.class)Spring 的 JUnit 规则来配置自己的自定义测试类。

JUnit 5 支持

Spring Framework 5.0 提供了与 JUnit 5 中引入的* JUnit Jupiter *测试框架的完全集成,因此,鼓励开发人员升级到 Spring 5.x,以充分利用 Spring 对 JUnit 5 的支持。升级到 Spring 5.x,您可能会对使用spring-test-junit5项目作为临时解决方案感兴趣,以帮助您在仍然使用 Spring Framework 4.3.x 的情况下升级到 JUnit 5.

TestNG 支持类

org.springframework.test.context.testng软件包为基于 TestNG 的测试用例提供以下支持类。

AbstractTestNGSpringContextTests是抽象的基础测试类,该类将* Spring TestContext Framework *与 TestNG 环境中的显式ApplicationContext测试支持集成在一起。扩展AbstractTestNGSpringContextTests时,您可以访问protected applicationContext实例变量,该实例变量可用于执行显式 bean 查找或测试整个上下文的状态。

AbstractTransactionalTestNGSpringContextTestsAbstractTestNGSpringContextTests的抽象事务扩展,为 JDBC 访问添加了一些便利功能。此类期望在ApplicationContext中定义javax.sql.DataSource bean 和PlatformTransactionManager bean。扩展AbstractTransactionalTestNGSpringContextTests时,可以访问protected jdbcTemplate实例变量,该实例变量可用于执行 SQL 语句来查询数据库。此类查询可用于在与数据库相关的应用程序代码执行之前和之后确认数据库状态,并且 Spring 确保此类查询在与应用程序代码相同的事务范围内运行。与 ORM 工具结合使用时,请务必避免使用false positives。如第 15.3 节“ JDBC 测试支持”中所述,AbstractTransactionalTestNGSpringContextTests还提供了便捷方法,这些方法使用上述jdbcTemplate委托给JdbcTestUtils中的方法。此外,AbstractTransactionalTestNGSpringContextTests提供了executeSqlScript(..)方法,用于针对已配置的DataSource执行 SQL 脚本。

Tip

这些类为扩展提供了便利。如果您不希望将测试类绑定到特定于 Spring 的类层次结构,则可以使用@ContextConfiguration@TestExecutionListeners等来配置自己的自定义测试类,并使用TestContextManager手动插入测试类。有关如何检测您的测试类的示例,请参见AbstractTestNGSpringContextTests的源代码。

15.6 Spring MVC 测试框架

Tip

Spring Boot 提供了一个选项,可以编写包括运行中的服务器在内的完整的端到端集成测试。如果这是您的目标,请查看Spring Boot 参考页。有关容器外测试与端到端集成测试之间的区别的更多信息,请参见名为“容器外测试与端到端集成测试之间的差异”的部分

15.6.1 服务器端测试

使用 JUnit 或 TestNG 为 Spring MVC 控制器编写简单的单元测试很容易:只需实例化该控制器,将其注入具有模拟或存根的依赖关系,并根据需要调用其传递MockHttpServletRequestMockHttpServletResponse等的方法。但是,在编写这样的单元测试时,仍有很多未经测试的内容:例如,请求 Map,数据绑定,类型转换,验证等等。此外,也可以调用其他控制器方法(例如@InitBinder@ModelAttribute@ExceptionHandler)作为请求处理生命周期的一部分。

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

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支持,用于从与测试类位于同一包中的 XML 配置文件加载 Spring 配置,但是还支持基于 Java 和基于 Groovy 的配置。参见这些sample tests

MockMvc实例用于执行对"/accounts/1"GET请求,并验证结果响应的状态为 200,Content Type 为"application/json",并且响应主体具有名为“ name”且值为“ Lee”的 JSON 属性。 Jayway JsonPath project支持jsonPath语法。还有许多其他选项可用于验证已执行请求的结果,下面将进行讨论。

Static Imports

上面示例中的流畅 API 需要一些静态导入,例如MockMvcRequestBuilders.*MockMvcResultMatchers.*MockMvcBuilders.*。查找这些类的一种简单方法是搜索与*“ MockMvc 相匹配的类型。如果使用 Eclipse,请确保将它们添加为 Java→编辑器→Content Assist→收藏夹下的 Eclipse 首选项中的“最喜欢的静态成员”。这样可以在键入静态方法名称的第一个字符后使用内容辅助。其他 IDE(例如 IntelliJ)可能不需要任何其他配置。只需检查对静态成员的代码完成支持。

Setup Choices

创建MockMvc实例有两个主要选项。第一种是通过* TestContext framework *加载 Spring MVC 配置,该框架加载 Spring 配置并将WebApplicationContext注入测试中以用于构建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();
    }

    // ...

}

第二个方法是简单地手动创建一个控制器实例,而无需加载 Spring 配置。相反,会自动创建基本的默认配置(可以大致与 MVC JavaConfig 或 MVC 命名空间的配置相媲美),并可在一定程度上进行自定义:

public class MyWebTests {

    private MockMvc mockMvc;

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

    // ...

}

您应该使用哪个设置选项?

*“ webAppContextSetup” 加载实际的 Spring MVC 配置,从而进行更完整的集成测试。由于 TestContext framework *缓存了已加载的 Spring 配置,因此即使您在测试套件中引入更多测试,它也可以帮助保持测试快速运行。此外,您可以通过 Spring 配置将模拟服务注入控制器中,以便 continue 专注于测试 Web 层。这是使用 Mockito 声明模拟服务的示例:

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

然后,您可以将模拟服务注入测试中以进行设置并验证期望:

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

    @Autowired
    private WebApplicationContext wac;

    private MockMvc mockMvc;

    @Autowired
    private AccountService accountService;

    // ...

}

另一方面,“ standaloneSetup” *更接近于单元测试。它一次测试一个控制器:该控制器可以手动注入模拟依赖项,并且不涉及加载 Spring 配置。这样的测试更多地集中在样式上,使查看被测试的控制器,是否需要任何特定的 Spring MVC 配置等工作变得更加容易。 “ standaloneSetup”也是编写临时测试以验证特定行为或调试问题的一种非常方便的方法。

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

Performing Requests

使用任何 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"));

如果应用程序代码依赖 Servlet 请求参数,并且没有显式检查查询字符串(通常是这种情况),那么使用哪个选项都没有关系。但是请记住,随 URI 模板提供的查询参数将被解码,而通过param(…)方法提供的请求参数应已被解码。

在大多数情况下,最好从请求 URI 中忽略上下文路径和 Servlet 路径。如果必须使用完整的请求 URI 进行测试,请确保相应地设置contextPathservletPath,以便请求 Map 起作用:

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

看上面的例子,为每个执行的请求设置 contextPath 和 servletPath 会很麻烦。相反,您可以设置默认请求属性:

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

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

Defining Expectations

可以通过在执行请求后追加一个或多个.andExpect(..)调用来定义期望:

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

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

期望分为两大类。第一类 assert 验证响应的属性:例如,响应状态,Headers 和内容。这些是要 assert 的最重要的结果。

第二类 assert 超出了响应范围。这些 assert 允许检查 Spring MVC 的特定方面,例如哪种控制器方法处理了请求,是否引发和处理了异常,模型的内容是什么,选择了哪种视图,添加了哪些闪存属性,等等。它们还允许人们检查 Servlet 的特定方面,例如请求和会话属性。

以下测试 assert 绑定或验证失败:

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

很多时候编写测试时,“转储”已执行请求的结果很有用。可以按以下步骤完成操作,其中print()是从MockMvcResultHandlers的静态导入:

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

只要请求处理不会导致未处理的异常,print()方法就会将所有可用的结果数据打印到System.out。 Spring Framework 4.2 引入了log()方法和print()方法的两个其他变体,一个变量接受OutputStream,另一个变量接受Writer。例如,调用print(System.err)会将结果数据打印到System.err;而调用print(myWriter)会将结果数据打印到自定义编写器。如果您希望记录而不是打印结果数据,只需调用log()方法,该方法会将结果数据记录为org.springframework.test.web.servlet.result记录类别下的一条DEBUG消息。

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

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

如果所有测试都重复相同的期望,则在构建MockMvc实例时可以一次设置共同的期望:

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

请注意,通常会始终应用共同的期望,并且在不创建单独的MockMvc实例的情况下不能覆盖共同的期望。

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

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

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

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

Filter Registrations

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

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

已注册的过滤器将通过spring-testMockFilterChain调用,最后一个过滤器将委派给DispatcherServlet

容器外测试与端到端集成测试之间的区别

如前所述,* Spring MVC Test *是基于spring-test模块的 Servlet API 模拟对象构建的,并且不使用正在运行的 Servlet 容器。因此,与运行实际 Client 端和服务器的完整端到端集成测试相比,存在一些重要差异。

考虑这一点的最简单方法是从空白MockHttpServletRequest开始。无论您添加到什么内容中,都将是请求。可能令您感到惊讶的是,默认情况下没有上下文路径,没有jsessionid cookie,没有转发,错误或异步调度,因此没有实际的 JSP 呈现。而是将“转发”和“重定向” URL 保存在MockHttpServletResponse中,并且可以按预期进行 assert。

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

另外,您可以考虑通过@WebIntegrationTest从 Spring Boot 获得完整的端到端集成测试支持。参见Spring Boot 参考

每种方法都各有利弊。 Spring MVC Test 中提供的选项从经典的单元测试到全面的集成测试,在规模上都是不同的。可以肯定的是,Spring MVC Test 中的所有选项都不属于经典单元测试的类别,但是它们距离它有点近。例如,您可以通过将模拟服务注入到控制器中来隔离 Web 层,在这种情况下,您仅通过DispatcherServlet来测试 Web 层,但是使用实际的 Spring 配置,就像您可以隔离数据访问层来测试数据访问层一样以上。或者,您可以使用一次只关注一个控制器的独立设置,并手动提供使其工作所需的配置。

使用* Spring MVC Test 时的另一个重要区别是,从概念上讲,此类测试在服务器端的 inside *上,因此您可以检查使用了哪个处理程序(如果使用 HandlerExceptionResolver 处理了异常),模型的内容是,发生了什么绑定错误,等等。这意味着编写期望值更容易,因为服务器不是一个黑盒子,而是通过实际 HTTPClient 端进行测试时的黑盒子。通常,这是经典单元测试的一个优势,它更易于编写,推理和调试,但不能代替完全集成测试的需要。同时,重要的是不要忽略响应是最重要的检查事实。简而言之,即使在同一项目中,这里也存在多种测试样式和测试策略的空间。

其他服务器端测试示例

框架自己的测试包括许多 samples 测试,目的是演示如何使用 Spring MVC Test。浏览这些示例以获取进一步的想法。而且spring-mvc-showcase具有基于 Spring MVC 测试的完整测试范围。

15.6.2 HtmlUnit 集成

Spring 提供了MockMvcHtmlUnit之间的集成。使用基于 HTML 的视图时,这简化了端到端测试的执行。这种集成使开发人员能够:

Note

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

为什么要进行 HtmlUnit 集成?

想到的最明显的问题是:“我为什么需要这个?”。通过探索一个非常基本的示例应用程序,最好找到答案。假设您有一个 Spring MVC Web 应用程序,该应用程序支持对Message对象的 CRUD 操作。该应用程序还支持所有消息的分页。您将如何进行测试?

使用 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"));

如果我们要测试允许我们创建消息的表单视图怎么办?例如,假设我们的表单类似于以下代码段:

<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 表单与控制器不同步,我们的表单测试也将 continue 通过。为了解决这个问题,我们可以结合我们的两个测试。

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

这样可以减少测试不正确通过的风险,但是仍然存在一些问题。

总体问题是,测试网页不涉及单个交互。相反,它是用户如何与网页交互以及该网页与其他资源交互的组合。例如,表单视图的结果用作用户创建消息的 Importing。另外,我们的表单视图可能潜在地利用了影响页面行为的其他资源,例如 JavaScript 验证。

进行集成测试以营救?

为了解决上述问题,我们可以执行端到端集成测试,但这有一些明显的缺点。考虑测试允许我们翻页消息的视图。我们可能需要以下测试。

要设置这些测试,我们需要确保数据库中包含正确的消息。这带来了许多其他挑战。

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

ImportingHtmlUnit 集成

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

HtmlUnit 集成选项

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

MockMvc 和 HtmlUnit

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

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

Note

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

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

MockMvc 和 HtmlUnit 的用法

现在,我们可以像往常一样使用 HtmlUnit,但无需将应用程序部署到 Servlet 容器。例如,我们可以请求视图创建带有以下内容的消息。

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

Note

默认上下文路径为""。或者,我们可以指定上下文路径,如名为“高级 MockMvcWebClientBuilder”的部分所示。

一旦有了对HtmlPage的引用,我们就可以填写表格并将其提交以创建一条消息。

HtmlForm form = createMsgFormPage.getHtmlElementById("messageForm");
HtmlTextInput summaryInput = createMsgFormPage.getHtmlElementById("summary");
summaryInput.setValueAttribute("Spring Rocks");
HtmlTextArea textInput = createMsgFormPage.getHtmlElementById("text");
textInput.setText("In case you didn't know, Spring Rocks!");
HtmlSubmitInput submit = form.getOneHtmlElementByAttribute("input", "type", "submit");
HtmlPage newMessagePage = submit.click();

最后,我们可以验证是否成功创建了新消息。以下 assert 使用AssertJ库。

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

这通过多种方式改善了我们的MockMvc test。首先,我们不再需要显式验证表单,然后创建类似于表单的请求。相反,我们要求表单,将其填写并提交,从而大大减少了开销。

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

有关使用 HtmlUnit 的其他信息,请参考HtmlUnit documentation

Advanced MockMvcWebClientBuilder

在到目前为止的示例中,我们已通过 Spring TestContext Framework 为我们加载的WebApplicationContext构建一个WebClient,从而以最简单的方式使用了MockMvcWebClientBuilder。在此重复此方法。

@Autowired
WebApplicationContext context;

WebClient webClient;

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

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

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实例构建WebClient,我们可以轻松获得MockMvc的全部功能。

Tip

有关创建MockMvc实例的更多信息,请参考称为“设置选择”的部分

MockMvc 和 WebDriver

在前面的部分中,我们已经了解了如何结合使用MockMvc和原始 HtmlUnit API。在本节中,我们将利用 Selenium WebDriver中的其他抽象来简化事情。

为什么要使用 WebDriver 和 MockMvc?

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

Note

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

假设我们需要确保正确创建一条消息。测试涉及找到 HTML 表单 Importing 元素,将其填写并做出各种 assert。

由于我们也要测试错误条件,因此这种方法会导致进行大量单独的测试。例如,如果只填写表格的一部分,我们要确保得到一个错误。如果我们填写整个表格,那么新创建的消息将在之后显示。

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

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

那么,如果我们将id更改为“ smmry”会怎样?这样做将迫使我们更新所有测试以纳入此更改!当然,这违反了* DRY 原则*;因此,理想情况下,我们应按以下方式将此代码提取到其自己的方法中。

public HtmlPage createMessage(HtmlPage currentPage, String summary, String text) {
    setSummary(currentPage, summary);
    // ...
}

public void setSummary(HtmlPage currentPage, String summary) {
    HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary");
    summaryInput.setValueAttribute(summary);
}

这样可以确保在更改 UI 时不必更新所有测试。

我们甚至可以更进一步,将此逻辑放在代表我们当前所在的HtmlPage的对象中。

public class CreateMessagePage {

    final HtmlPage currentPage;

    final HtmlTextInput summaryInput;

    final HtmlSubmitInput submit;

    public CreateMessagePage(HtmlPage currentPage) {
        this.currentPage = currentPage;
        this.summaryInput = currentPage.getHtmlElementById("summary");
        this.submit = currentPage.getHtmlElementById("submit");
    }

    public <T> T createMessage(String summary, String text) throws Exception {
        setSummary(summary);

        HtmlPage result = submit.click();
        boolean error = CreateMessagePage.at(result);

        return (T) (error ? new CreateMessagePage(result) : new ViewMessagePage(result));
    }

    public void setSummary(String summary) throws Exception {
        summaryInput.setValueAttribute(summary);
    }

    public static boolean at(HtmlPage page) {
        return "Create Message".equals(page.getTitleText());
    }
}

以前,此模式称为页面对象模式。虽然我们当然可以使用 HtmlUnit 做到这一点,但 WebDriver 提供了一些工具,我们将在以下部分中进行探索,以使该模式的实现更加容易。

MockMvc 和 WebDriver 设置

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

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

@Autowired
	WebApplicationContext context;

	WebDriver driver;

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

Note

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

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

MockMvc 和 WebDriver 的用法

现在,我们可以像往常一样使用 WebDriver,而无需将应用程序部署到 Servlet 容器。例如,我们可以请求视图创建带有以下内容的消息。

CreateMessagePage page = CreateMessagePage.to(driver);

然后,我们可以填写表格并将其提交以创建一条消息。

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

利用* Page Object Pattern *可以改善HtmlUnit test的设计。正如我们在为何“为什么要使用 WebDriver 和 MockMvc?”部分中提到的,我们可以将 Page Object Pattern 与 HtmlUnit 一起使用,但使用 WebDriver 则容易得多。让我们看一下新的CreateMessagePage实现。

public class CreateMessagePage
        extends AbstractPage { (1)

    (2)
    private WebElement summary;
    private WebElement text;

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

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

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

    public static CreateMessagePage to(WebDriver driver) {
        driver.get("http://localhost:9990/mail/messages/form");
        return PageFactory.initElements(driver, CreateMessagePage.class);
    }
}

最后,我们可以验证是否成功创建了新消息。以下 assert 使用FESTassert 库

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

我们可以看到ViewMessagePage允许我们与自定义域模型进行交互。例如,它公开了一个返回Message对象的方法。

public Message getMessage() throws ParseException {
    Message message = new Message();
    message.setId(getId());
    message.setCreated(getCreated());
    message.setSummary(getSummary());
    message.setText(getText());
    return message;
}

然后,我们可以在声明中利用富域对象。

最后,不要忘记在测试完成后“关闭” WebDriver实例。

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

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

Advanced MockMvcHtmlUnitDriverBuilder

在到目前为止的示例中,我们已通过 Spring TestContext Framework 为我们加载的WebApplicationContext构建一个WebDriver,从而以最简单的方式使用了MockMvcHtmlUnitDriverBuilder。在此重复此方法。

@Autowired
WebApplicationContext context;

WebDriver driver;

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

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

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实例构建WebDriver,我们可以轻松获得MockMvc的全部功能。

Tip

有关创建MockMvc实例的更多信息,请参考称为“设置选择”的部分

MockMvc 和 Geb

在上一节中,我们了解了如何将MockMvcWebDriver结合使用。在本节中,我们将使用Geb来使我们的测试甚至 Groovy-er。

为什么选择 Geb 和 MockMvc?

Geb 由 WebDriver 支持,因此它提供了许多从 WebDriver 获得的same benefits。但是,Geb 通过为我们处理一些样板代码使事情变得更加轻松。

MockMvc 和 Geb 设置

我们可以轻松地使用使用MockMvc的 Selenium WebDriver来初始化 Geb Browser

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

Note

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

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

MockMvc 和 Geb 的使用

现在,我们可以像往常一样使用 Geb 了,而无需将应用程序部署到 Servlet 容器中。例如,我们可以请求视图创建以下消息:

to CreateMessagePage

然后,我们可以填写表格并将其提交以创建一条消息。

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

找不到的所有无法识别的方法调用或属性访问/引用都将转发到当前页面对象。这消除了我们直接使用 WebDriver 时需要的许多样板代码。

与直接使用 WebDriver 一样,这通过利用* Page Object Pattern *改善了HtmlUnit test的设计。如前所述,我们可以将页面对象模式与 HtmlUnit 和 WebDriver 一起使用,但使用 Geb 则更加容易。让我们看一下新的基于 Groovy 的CreateMessagePage实现。

class CreateMessagePage extends Page {
	static url = 'messages/form'
	static at = { assert title == 'Messages : Create'; true }
	static content =  {
		submit { $('input[type=submit]') }
		form { $('form') }
		errors(required:false) { $('label.error, .alert-error')?.text() }
	}
}

您会注意到的第一件事是我们的CreateMessagePage扩展了Page。我们不会详细介绍Page,但是总而言之,它包含我们所有页面的通用功能。接下来您会注意到,我们定义了一个 URL,可以在其中找到此页面。这使我们可以导航到以下页面。

to CreateMessagePage

我们还有一个at闭包,它确定我们是否在指定的页面上。如果我们在正确的页面上,它应该返回true。这就是为什么我们可以 assert 我们位于正确的页面上的原因,如下所示。

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

Note

我们在闭包中使用一个 assert,以便我们可以确定错误页面出在哪里。

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

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

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

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

15.6.3Client 端 REST 测试

Client 端测试可用于测试内部使用RestTemplate的代码。这个想法是声明预期的请求并提供“存根”响应,以便您可以专注于隔离测试代码,即无需运行服务器。这是一个例子:

RestTemplate restTemplate = new RestTemplate();

MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build();
mockServer.expect(requestTo("/greeting")).andRespond(withSuccess());

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

mockServer.verify();

在上面的示例中,Client 端 REST 测试的中心类MockRestServiceServer使用自定义ClientHttpRequestFactory配置RestTemplate,该自定义ClientHttpRequestFactory声明了与期望值相对应的实际请求并返回“存根”响应。在这种情况下,我们希望一个请求“/greeting”,并希望返回一个包含“ text/plain”内容的 200 响应。我们可以根据需要定义为其他预期请求和存根响应。当定义了预期的请求和存根响应时,RestTemplate可以照常在 Client 端代码中使用。在测试结束时,可以使用mockServer.verify()来验证是否满足所有期望。

默认情况下,请求应按声明的期望 Sequences 进行。您可以在构建服务器时设置ignoreExpectOrder选项,在这种情况下,将检查所有期望(以便)以找到给定请求的匹配项。这意味着允许请求以任何 Sequences 出现。这是一个例子:

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

即使默认情况下无 Sequences 请求,每个请求也只能执行一次。 expect方法提供了一个重载的变体,该变体接受一个ExpectedCount参数,该参数指定一个计数范围,例如oncemanyTimesmaxminbetween等。这是一个例子:

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时(默认设置),因此请求应按声明 Sequences 进行,则该 Sequences 仅适用于任何预期请求中的第一个。例如,如果期望“/foo” 2 次,然后是“/bar” 3 次,那么在请求“/bar”之前应该有对“/foo”的请求,但随后的“/foo”除外和“/bar”请求可以随时发出。

作为上述所有方法的替代,Client 端测试支持还提供了ClientHttpRequestFactory实现,可以将其配置为RestTemplate以将其绑定到MockMvc实例。这样就可以使用实际的服务器端逻辑来处理请求,而无需运行服务器。这是一个例子:

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

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

mockServer.verify();

Static Imports

就像服务器端测试一样,用于 Client 端测试的 FluentAPI 需要一些静态导入。通过搜索*“ MockRest 可以轻松找到它们。 Eclipse 用户应在 Eclipse 首选项中的 Java→编辑器→Content Assist→收藏夹下添加"MockRestRequestMatchers.*""MockRestResponseCreators.*"作为“收藏的静态成员”。这样可以在键入静态方法名称的第一个字符后使用内容辅助。其他 IDE(例如 IntelliJ)可能不需要任何其他配置。只需检查对静态成员的代码完成支持。

Client 端 REST 测试的更多示例

Spring MVC Test 自己的测试包括example tests个 Client 端 REST 测试。

15.7 PetClinic 示例

GitHub上可用的 PetClinic 应用程序说明了 JUnit 4 环境中* Spring TestContext Framework *的几个功能。大多数测试功能包含在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());
        // ...
    }

    // ...
}

Notes:

PetClinic 应用程序支持三种数据访问技术:JDBC,Hibernate 和 JPA。pass 语句@ContextConfiguration没有任何特定的资源位置,AbstractClinicTests类将从默认位置AbstractClinicTests-context.xml加载其应用程序上下文,该默认位置声明一个公共DataSource。子类指定必须声明PlatformTransactionManagerClinic的具体实现的其他上下文位置。

例如,PetClinic 测试的 Hibernate 实现包含以下实现。在此示例中,HibernateClinicTests不包含任何代码行:我们只需要声明@ContextConfiguration,并且测试是从AbstractClinicTests继承的。由于声明@ContextConfiguration时没有任何特定的资源位置,因此 Spring TestContext Framework *从AbstractClinicTests-context.xml中定义的所有 bean(即继承位置)和HibernateClinicTests-context.xml加载应用程序上下文,而HibernateClinicTests-context.xml可能会覆盖AbstractClinicTests-context.xml中定义的 bean。

@ContextConfiguration
public class HibernateClinicTests extends AbstractClinicTests { }

在大型应用程序中,Spring 配置通常分为多个文件。因此,通常在所有特定于应用程序的集成测试的通用 Base Class 中指定配置位置。这样的 Base Class 还可以添加有用的实例变量(由 Dependency Injection 填充,自然而然),例如在使用 Hibernate 的应用程序中为SessionFactory

集成测试中应尽可能具有与部署环境中完全相同的 Spring 配置文件。一个可能的不同点是数据库连接池和事务基础结构。如果要部署到功能强大的应用程序服务器,则可能会使用其连接池(可通过 JNDI 获得)和 JTA 实现。因此,在 Producing,您将对DataSourceJtaTransactionManager使用JndiObjectFactoryBean<jee:jndi-lookup>。 JNDI 和 JTA 在容器外集成测试中将不可用,因此您应使用 Commons DBCP BasicDataSourceDataSourceTransactionManagerHibernateTransactionManager之类的组合。您可以将此变异行为分解为一个 XML 文件,在应用程序服务器和“本地”配置之间进行选择,该配置与所有其他配置分开,在测试和生产环境之间不会有所不同。另外,建议将属性文件用于连接设置。有关示例,请参见 PetClinic 应用程序。

上一章 首页 下一章