Spring 学习笔记:Spring 测试框架

Spring 测试框架

主要内容:spring 对集成测试和单元测试的支持。

Spring测试框架具有特性的意义:

  • 让TDD编程更加容易;
  • 可以在不启动服务器的情况下进行测试, 会比启动真实的服务器更加快;
  • 可以模拟出依赖的服务, 让测试更加专注, 比如说模拟出service层, 让测试专注于controller层;
  • 对一些状态存储在外部系统(如数据库), 更加快速的做存根(通过bean定义);
  • 需要有完整数据依赖的测试,模拟服务层或者dao层, 是使用测试数据库之外的一个选择。

相较于端到端测试, 是否只是快了一些?
初次之外, 因为是执行在一个程序里的, 可以断言web应用中方法执行情况

单元测试

单元测试的优点是测试关注点集中, 运行速度块, 但是缺点是配置复杂, 需要对一个测试的目标类的依赖的类进行mock。

模拟对象

Spring包含许多专用于模拟的软件包:

  • Environment
  • JNDI
  • Servlet API
  • Spring Web Reactive

Environment

该org.springframework.mock.env软件包包含Environment和PropertySource抽象的模拟实现 (请参阅 Bean定义配置文件 和PropertySource抽象)。 MockEnvironment和MockPropertySource可以为需要依赖容器环境的单元测试提供mock环境。

JNDI

该org.springframework.mock.jndi软件包包含JNDI SPI的实现,可用于为测试套件或独立应用程序设置简单的JNDI环境。例如,如果JDBC DataSource实例在测试代码中与在Java EE容器中绑定到相同的JNDI名称,则可以在测试场景中重用应用程序代码和配置,而无需进行修改。

Servlet API

该org.springframework.mock.web软件包包含一套全面的Servlet API模拟对象,可用于测试Web上下文,控制器和过滤器。这些模拟对象是针对Spring的Web MVC框架使用的,并且通常比使用EasyMock, MockObjects等模拟框架更加方便。

Spring MVC测试框架建立在模拟Servlet API对象的基础上,为Spring MVC提供了集成测试。

Spring Web reactive

org.springframework.mock.http.server.reactive包含WebFlux应用程序的mock实现,ServerHttpRequest和ServerHttpResponse。org.springframework.mock.web.server包含一个ServerWebExchange依赖于上面那些模拟请求和响应对象的模拟。
MockServerHttpRequest并MockServerHttpResponse从相同的抽象基类继承, 在基类里定义了一些共同的行为。例如,模拟请求一旦创建便是不可变的,但是可以使用mutate()方法ServerHttpRequest来创建修改后的实例。
为了使模拟响应能够正确实现写约定并返回写完成句柄(即Mono<Void>),默认情况下,它使用Fluxwith cache().then()来缓冲数据并使其可用于测试中的断言。应用程序可以设置自定义写入功能(例如,测试无限流)。
可以使用WebTestClient测试WebFlux应用, 这种测试不需要运行真正的服务器。这个client还可以用于对真正运行的服务器进行端到端测试。

单元测试支持类库

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

  • 通用测试工具类
  • Spring MVC测试工具类

通用测试工具类

该org.springframework.test.util软件包包含几个用於单元和集成测试的通用工具类。
ReflectionTestUtils是基于反射的工具方法的集合。有设置非public字段,调用非public设置方法或调用非public生命周期回调方法的功能。如下:
纵容private或protected直接访问的ORM框架(例如JPA和Hibernate)。
Spring的注解(如支持@Autowired,@Inject和@Resource),用在private或protected字段,方法时。
当使用@PostConstruct和@PreDestroy注解非公开方法时。

Spring MVC 测试工具类

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

要对作为POJO的Spring MVC Controller类进行单元测试,请将ModelAndViewAssert与Spring的Servlet API模拟中的MockHttpServletRequest,MockHttpSession等结合使用。
完整的集成测试, 使用Spring MVC集成测试框架。

集成测试

本节涵盖了Spring应用程序的集成测试。包括以下主题:

  • 概览
  • 集成测试的目标
  • JDBC 测试支持
  • 注解
  • Spring TestContext框架
  • Spring MVC测试框架

概览

能够执行一些集成测试而无需部署到应用程序服务器或连接到其他的组件是一种很重要的测试能力。 这可以让我们测试:

  • Spring 容器中各个bean是否正确关联起来了
  • 数据访问层, 如sql语句是否正确

Spring框架为spring-test模块中的集成测试提供了一流的支持。
此类测试的运行速度比单元测试慢,但比依赖于部署到应用程序服务器的等效Selenium测试或远程测试快。

单元和集成测试支持以注释驱动的Spring TestContext Framework的形式提供。 TestContext框架与实际使用的测试框架无关,该框架允许在各种底层测试框架(包括JUnit,TestNG和其他环境)上运行。

Spring集成测试框架的目标

有以下的主要目标:

  • 在不同测试用例之间管理Spring IoC容器缓存。
  • 提供依赖注入。
  • 提供适合集成测试的事务管理。
  • 提供特定于Spring的基类,可以提供如ApplicationContext和jdbcTemplate等实例, 可以简化测试用例的编写

JDBC测试支持

org.springframework.test.jdbc的JdbcTestUtils, 包含了以下一些支持测试的静态方法:

  • countRowsInTable(…)
  • countRowsInTableWhere(…);
  • deleteFromTables(…);
  • deleteFromTableWhere(…);
  • dropTables(…);

测试相关的注解

Spring Testing注解

以下注解在所有底层具体的测试框架都被支持:

  • @BootstrapWith
  • @ContextConfiguration
  • @WebAppConfiguration
  • @ContextHierarchy
  • @ActiveProfiles
  • @TestPropertySource
  • @DirtiesContext
  • @TestExecutionListeners
  • @Commit
  • @Rollback
  • @BeforeTransaction
  • @AfterTransaction
  • @Sql
  • @SqlConfig
  • @SqlGroup

@BootstrapWith

@BootstrapWith是一个类级别的注解,可用于配置如何引导Spring TestContext Framework。具体来说,可以使用@BootstrapWith指定自定义TestContextBootstrapper.

@ContextConfiguration

类级别的注解@ContextConfiguration声明应用容器配置文件或java配置类的位置, 用于确定如何加载和配置用于集成测试的ApplicationContext。

资源位置通常是位于类路径中的XML配置文件或Groovy脚本,而带注解的类通常是@Configuration类。但是,资源位置也可以引用文件系统中的文件和脚本,带注解的类可以是@Component类.

一个指向XML文件的@ContextConfiguration注解示例:

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

指向java配置类的示例:

@ContextConfiguration(classes = TestConfig.class) 
public class ConfigClassApplicationContextTests {
    // class body...
}

除了声明资源位置或带注释的类以外,还可以使用@ContextConfiguration声明ApplicationContextInitializer类:

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

可以选择使用@ContextConfiguration来声明ContextLoader策略(一般不需要, xml和java的加载就已经足够):

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

@WebAppConfiguration

@WebAppConfiguration是一个类级别的注解,可用于声明为集成测试加载的ApplicationContext应该是WebApplicationContext。 @WebAppConfiguration仅存在于测试类上,可以确保为测试加载WebApplicationContext,并使用默认值“ file:src / main / webapp”作为Web应用程序根目录(即资源)的路径。资源基础路径用于在后台创建MockServletContext,该MockServletContext用作测试的WebApplicationContext的ServletContext。
示例:

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

指定web环境的资源路径:

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

支持classpath和file前缀, 默认为file路径。
注意,@ WebAppConfiguration必须在单个测试类中或在测试类层次结构中与@ContextConfiguration结合使用

支持classpath和file前缀, 默认为file路径。
注意,@ WebAppConfiguration必须在单个测试类中或在测试类层次结构中与@ContextConfiguration结合使用

@ContextHierarchy

@ContextHierarchy是一个类级别的批注,用于定义用于集成测试的ApplicationContext实例的层次结构。示例:

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

@ActiveProfiles

@ActiveProfiles是一个类级别的注释,用于声明集成测试中启用的profile。
启用dev profile的示例:

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

启用多个profile:

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

启用多个profile:

@TestPropertySource

@TestPropertySource是一个类级别的批注,您可以使用它来配置属性文件和内联属性, 这些属性将在测试时被加载到ApplicationContext中。

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

从类路径加载属性的示例:

@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的状态),应将其关闭。当应用程序上下文标记为脏时,会将其从测试框架的缓存中删除并关闭。因此,将为需要上下文具有相同配置元数据的任何后续测试重建基础Spring容器。
可以将@DirtiesContext用作类或类层次结构中的类级别和方法级别的注释。

进入这个测试类之前, 准备一个新的容器环境:

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

}

@TestExecutionListeners

可以通过注册监听器, 在测试的各个阶段执行回调。
示例:

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

@Commit

@Commit指示应在测试方法完成后提交用于事务性测试方法的事务。可以将@Commit用作@Rollback(false)的直接替代品,以更明确地传达代码的意图。与@Rollback类似,@ Commit也可以声明为类级别或方法级别的注释。
示例:

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

@Rollback

@Rollback指示在测试方法完成后是否应回退用于事务性测试方法的事务。如果为true,则回滚该事务。否则,将提交事务(另请参见@Commit)。即使未明确声明@ Rollback,Spring TestContext Framework中用于集成测试的回滚默认为true, 可以在类级别和方法级别定义。

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

@BeforeTransaction

在执行事务之前执行。 示例:

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

@AfterTransaction

在执行事务之后执行。 示例:

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

@Sql

@Sql用于注解测试类或测试方法,以配置在集成测试期间针对给定数据库运行的SQL脚本, 可以在之前或者之后运行, 配置好这个注解相应的属性即可。示例:

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

@SqlConfig

@SqlConfig定义元数据,该元数据用于确定如何解析和运行使用@Sql注释配置的SQL脚本, 用于指定公共前缀, 分割符等配置。示例:

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

@SqlGroup

组合多个@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
}

Spring的标准注解支持

支持以下注解:

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

使用JUnit时支持的注解

@IfProfileValue

@IfProfileValue表示已为特定的测试环境启用带注解的测试。如果配置的ProfileValueSource返回提供的名称的匹配值,则启用测试, 否则,禁用测试。

可以在类级别,方法级别或两者上应用@IfProfileValue。对于该类或其子类中的任何方法,@ IfProfileValue的类级别用法优先于方法级别用法。

示例:

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

可以使用值列表(带有OR语义)配置@IfProfileValue:

@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是一个类级别的注解,它指定@IfProfileValue注解使用哪种ProfileValueSource类型来检索profile。如果未为测试声明@ProfileValueSourceConfiguration,则默认使用SystemProfileValueSource。以下示例显示了如何使用@ProfileValueSourceConfiguration:

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

@Timed

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

该时间段包括运行测试方法本身,测试的任何重复(请参阅@Repeat)以及测试的任何setUp和tearDown的时间。示例:

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

Spring的@Timed注释与JUnit 4的@Test(timeout = …)支持具有不同的语义。如果测试花费的时间太长,@ Test(timeout = …)会在测试完成之前失败。另一方面,Spring的@Timed则是等待测试完成然后再失败。

@Repeat

@Repeat表示带注解的测试方法必须重复运行, 注解的value指定了重复的次数。
重复执行的范围包括测试方法本身的执行以及测试的任何setUp或tearDown, 示例:

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

Spring TestContext框架

pring TestContext Framework(位于org.springframework.test.context包中)提供了通用的,注解驱动的单元和集成测试支持,这些支持与所使用的测试框架无关。 TestContext框架还非常重视约定优于配置,可以通过基于注解的配置覆盖合理的默认值。

关键抽象

该框架的核心由TestContextManager类和TestContext,TestExecutionListener和SmartContextLoader接口组成。
为每个测试类创建一个TestContextManager(例如,用于在JUnit Jupiter中的单个测试类中执行所有测试方法)。TestContextManager管理一个TestContext,它保存当前测试的上下文。随着测试的进行,TestContextManager还更新了TestContext的状态,并且回调通知TestExecutionListener的实现,这些TestExecutionListener提供依赖项注入,管理事务等功能。 SmartContextLoader负责为给定的测试类加载ApplicationContext。

TestContext

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

TestContextManager

TestContextManager是Spring TestContext Framework的主要入口点,负责管理单个TestContext, 并在测试执行点向每个注册的TestExecutionListener发出事件信号:

  • 在特定测试框架的任何“before class”或“before all”方法之前。
  • 测试实例后处理。
  • 在特定测试框架的任何“before”或“before each”方法之前。
  • 在执行测试方法之前但在setup方法之后。
  • 在执行测试方法之后但在tearDown方法之后。
  • 在特定测试框架的任何“after”或“after each”方法之后。
  • 在特定测试框架的任何“after class”或“after all”方法之后。

ContextLoader

ContextLoader是一个策略接口,用于为Spring TestContext Framework管理的集成测试加载ApplicationContext。应该实现SmartContextLoader而不是此接口,以提供对组件类,活动bean定义配置文件,测试属性源,上下文层次结构和WebApplicationContext支持的支持。

SmartContextLoader是Spring 3.1中引入的ContextLoader接口的扩展,取代了原始的最小ContextLoader SPI。具体来说,SmartContextLoader可以选择处理资源位置,组件类或上下文初始化程序。此外,SmartContextLoader可以在其加载的上下文中bean配置文件和测试属性源。

启动TestContext框架

Spring TestContext Framework内部的默认配置足以满足所有常见使用场景。但是,有时开发团队或第三方框架希望更改默认的ContextLoader,实现自定义TestContext或ContextCache,扩展ContextCustomizerFactory和TestExecutionListener实现的默认集 ,等等。为了对TestContext框架的运行方式进行低级控制,Spring提供了启动策略。

TestContextBootstrapper定义用于引导TestContext框架的SPI。TestContextBootstrapper被TestContextManager用来加载TestExecutionListener实现, 和构建TestContext。可以@BootstrapWith直接使用或作为元注释,为测试类(或测试类层次结构)配置自定义引导策略。如果启动程序没有明确地通过使用被配置@BootstrapWith,则DefaultTestContextBootstrapper或WebTestContextBootstrapper将被启用。
由于TestContextBootstrapperSPI将来可能会发生变化(以适应新的要求),因此我们强烈建议实现者不要直接实现此接口,而应继承AbstractTestContextBootstrapper或其某个具体子类。

TestExecutionListener配置

TestContext提供了默认就已经配置的TestExecutionListener:

  • ServletTestExecutionListener:配置Servlet API的mock WebApplicationContext。
  • DirtiesContextBeforeModesTestExecutionListener:处理的@DirtiesContext 模式为“befroe”的注解。
  • DependencyInjectionTestExecutionListener:为测试提供依赖注入。
  • DirtiesContextTestExecutionListener:处理的@DirtiesContext“after” 模式的注解。
  • TransactionalTestExecutionListener:提供事务能力, 默认回滚
  • SqlScriptsTestExecutionListener:运行@Sql 注解配置的SQL脚本。

合并默认的TestExecutionListener

如果TestExecutionListener通过来注册自定义@TestExecutionListeners,则不会注册默认的侦听器。在很多场景下, 这迫使开发人员手动声明除任何自定义侦听器之外的所有默认侦听器:

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

可以通过以下属性启用默认的监听器:

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

上下文管理

TestContext为其负责的每一个测试用例提供上下文管理和缓存支持。测试用例不会自动接收对已配置的访问ApplicationContext。但是,如果测试类可以通过实现ApplicationContextAware接口,来获得ApplicationContext引用。

使用@ContextConfiguration来配置上下文。
路径可以是类路径或者文件系统路径:

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

如果省略注解中的位置属性,则TestContext框架将尝试检测默认的XML资源位置。具体来说,GenericXmlContextLoader和GenericXmlWebContextLoader根据测试类的名称检测默认位置。如果您的类是命名的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...
}

使用java配置类进行配置
示例:

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

对测试用例进行依赖注入

测试请求和会话作用域的bean

事务管理

执行Sql脚本

在针对关系数据库编写集成测试时,执行SQL脚本来修改数据库模式或将测试数据插入表中通常是非常有意义的。

编程方式执行sql脚本

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

  • org.springframework.jdbc.datasource.init.ScriptUtils
  • org.springframework.jdbc.datasource.init.ResourceDatabasePopulator
  • org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests
  • org.springframework.test.context.testng.AbstractTransactionalTestNGSpringContextTests

ScriptUtils如果要对脚本如何解析和执行有完全的控制, 这个是更加合适的选择。

ResourceDatabasePopulator提供用于配置在解析和运行脚本时使用的字符编码,语句分隔符,注释定界符和错误处理标志的选项。它的内部也是ScriptUtils:

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

使用@Sql声明式执行SQL脚本

可以@Sql在测试类或测试方法上声明注释,以配置应该在集成测试方法之前或之后运行SQL脚本。方法级别的声明将覆盖类级别的声明。对的支持@Sql由SqlScriptsTestExecutionListener提供,默认情况下启用。
路径资源语义
每个路径都被解释为一个Spring Resource。纯路径(例如 “schema.sql”)被视为相对于定义测试类的程序包的类路径资源。以斜杠开头的路径被视为绝对类路径资源(例如"/org/example/schema.sql")。引用的URL的路径(例如,前缀的路径classpath:,file:,http:),通过使用指定的资源协议加载。
@Sql在基于JUnit Jupiter的集成测试:

@SpringJUnitConfig
@Sql("/test-schema.sql")
class DatabaseTests {

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

默认脚本检测

如果未指定SQL脚本,则尝试default根据@Sql声明的位置来检测脚本。如果无法检测到默认值,则抛出一个IllegalStateException异常。

  • 类级别的声明:如果带注释的测试类为com.example.MyTest,则相应的默认脚本为classpath:com/example/MyTest.sql。
  • 方法级别的声明:如果已命名testMethod()并在类中定义了带注释的测试方法com.example.MyTest,则相应的默认脚本为classpath:com/example/MyTest.testMethod.sql。

声明多个@Sql

如果需要为给定的测试类或测试方法配置多组SQL脚本,而且使用不同的语法配置,不同的错误处理规则或每组不同的执行阶段,则可以声明的多个实例@Sql。使用Java 8,可以将@Sql用作可重复zhujie。否则,需要用 @SqlGroup声明的多个 @Sql。
以下示例说明如何在@SqlJava 8中用作可重复注解:

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

兼容java8以下的语法:

@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脚本是在测试用例运行之前执行的。 但是也可以通过配置在执行测试之后运行一段脚本。示例:

@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注解配置脚本解析和错误处理。当在集成测试类上声明为类级别的注解时,@SqlConfig 充当测试类层次结构中所有SQL脚本的全局配置。通过使用@Sql注解的config属性直接声明时,@SqlConfig 用作这个@Sql 注解中声明的SQL脚本的配置。@SqlConfig中的每个属性都有一个隐式默认值,由于Java语言规范中为注释属性定义了规则,因此无法分配null注释属性。因此,为了支持对继承的全局配置的覆盖,@SqlConfig属性具有""(对于字符串)或DEFAULT(对于枚举)的显式默认值。通过提供非默认值,可以使局部@SqlConfig覆盖全局声明中的各个属性。只要局部 属性不提供默认值之外的任何显式制定,就会继承全局属性。

并行执行测试

Spring 5.0支持在一个JVM中并行执行多个测试。
以下情况不适合使用并行执行测试:

  • 使用@DirtiesContext时;
  • 使用JUnit的@FixMethodOrder或者其他框架的类似配置时;
  • 改变外部系统状态时, 如更改数据库, 消息中间件, 文件系统等;

总之, 是并行和串行语意不等同时。

框架支持类

Spring JUnit 4 Runner

可以指定Spring 的Runner在Junit4在获得TestContext等支持:
示例:

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

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

在前面的示例中,为@TestExecutionListeners配置了一个空列表以禁用默认侦听器,否则将需要通过@ContextConfiguration配置ApplicationContext。

Spring Junit 4 Rules

SpringClassRule是一个JUnit TestRule,它支持Spring TestContext Framework的类级功能,而SpringMethodRule是一个JUnit MethodRule,它支持Spring TestContext Framework的实例级和方法级功能。

与SpringRunner相比,Spring的基于规则的JUnit支持具有独立于任何org.junit.runner.Runner实现的优点,因此可以与现有的替代Runner(例如JUnit 4的Parameterized)或第三方Runner结合使用(例如MockitoJUnitRunner)。

为了支持TestContext框架的全部功能,必须将SpringClassRule与SpringMethodRule结合使用:

// Optionally specify a non-Spring Runner via @RunWith(...)
@ContextConfiguration
public class IntegrationTest {

    @ClassRule
    public static final SpringClassRule springClassRule = new SpringClassRule();
    
    @Rule
    public final SpringMethodRule springMethodRule = new SpringMethodRule();
    
    @Test
    public void testMethod() {
        // execute test logic...
    }
}

Junit 4 支持类

AbstractJUnit4SpringContextTests是抽象的基础测试类,该类可以获取ApplicationContext实例。

AbstractTransactionalJUnit4SpringContextTests是AbstractJUnit4SpringContextTests的针对事务的抽象子类,为JDBC访问添加了一些便利功能。

关于Junit Jupiter的Spring扩展

Spring TestContext Framework提供了与JUnit 5中引入的JUnit Jupiter测试框架的完全集成。通过使用@ExtendWith(SpringExtension.class)注释测试类,可以引入TestContext框架。

Spring在Spring支持JUnit 4和TestNG的功能集之外提供了以下功能:

  • 测试构造函数,测试方法和测试生命周期回调方法的依赖注入。
  • 基于SpEL表达式,环境变量,系统属性等的条件测试执行的强大支持。
  • 定制组合注解,结合了Spring和JUnit Jupiter的注释。

以下示例展示如何配置测试类以将SpringExtension与@ContextConfiguration结合使用:

// Instructs JUnit Jupiter to extend the test with Spring support.
@ExtendWith(SpringExtension.class)
// Instructs Spring to load an ApplicationContext from TestConfig.class
@ContextConfiguration(classes = TestConfig.class)
class SimpleTests {

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

由于还可以在JUnit 5中将注解用作元注解,因此Spring提供了@SpringJUnitConfig和@SpringJUnitWebConfig,以简化测试ApplicationContext和JUnit Jupiter的配置:

// Instructs Spring to register the SpringExtension with JUnit
// Jupiter and load an ApplicationContext from TestConfig.class
@SpringJUnitConfig(TestConfig.class)
class SimpleTests {

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

WebApplicationContext 示例:

// Instructs Spring to register the SpringExtension with JUnit
// Jupiter and load a WebApplicationContext from TestWebConfig.class
@SpringJUnitWebConfig(TestWebConfig.class)
class SimpleWebTests {

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

TestNG支持类

  • AbstractTestNGSpringContextTests是一个抽象的基础测试类,可以获取ApplicationContext的引用。
  • AbstractTransactionalTestNGSpringContextTests是AbstractTestNGSpringContextTests的抽象事务扩展,为JDBC访问添加了一些便利功能。

Spring MVC 测试框架

可以在不运行servlet服务器时, 提供对mvc的环境, 对mvc应用进行测试。

服务端测试

Spring MVC Test的目标是通过执行请求并通过实际的DispatcherServlet生成响应来提供一种测试控制器的有效方法。
Spring MVC Test基于spring-test模块中可用的Servlet API的“模拟”实现。这允许执行请求和生成响应,而无需在Servlet容器中运行。在大多数情况下,一切都应像在运行时一样工作。以下是基于JUnit Jupiter的示例:

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

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

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

设置选择

有两种方式来创建MockMvc实例。
首先是通过TestContext框架加载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配置的情况下手动创建控制器实例。可以在一定程度上对其进行自定义。以下示例显示了如何执行此操作:

public class MyWebTests {

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

}

webAppContextSetup加载实际的Spring MVC配置,从而进行更完整的集成测试。由于TestContext框架缓存了已加载的Spring配置,因此即使在测试套件中引入了更多测试,它也有助于保持测试的快速运行。此外,可以通过Spring配置将模拟服务注入控制器中,以专注于测试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更接近於单元测试, 它一次测试一个控制器。

与大多数“集成与单元测试”争辩一样,没有正确或错误的答案。

设置mvc特性

无论使用哪种MockMvc构建器,所有MockMvcBuilder实现都提供一些常见且非常有用的功能, 配置一切全局的except:

// static import of MockMvcBuilders.standaloneSetup
MockMvc mockMvc = standaloneSetup(new MusicController())
        .defaultRequest(get("/").accept(MediaType.APPLICATION_JSON))
        .alwaysExpect(status().isOk())
        .alwaysExpect(content().contentType("application/json;charset=UTF-8"))
        .build();

设置session:

// static import of SharedHttpSessionConfigurer.sharedHttpSession

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

// Use mockMvc to perform requests...

参见ConfiurableMockMvcBuilder的Javadoc.

执行请求

示例:

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

执行模拟的Multipart请求:

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

可以使用URI模板风格指定查询参数:

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

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

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

注意,URI模板提供的查询参数会被解码,而通过param(…)方法提供的请求参数则被当成已被解码。

设置contextPath和servletPath:

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

定义期望

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

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

期望分为两大类。
第一类断言验证响应的属性(例如,响应状态,标头和内容)。这些是要断言的重要的内容。

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

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

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

在某些情况下,可能需要直接访问结果并验证否则无法验证的内容。可以通过在所有其他期望之后附加.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()

当响应内容是JSON时,可以使用JsonPath表达式来验证结果,如以下示例所示:

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

当响应内容是XML时:

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

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

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

Spring MVC 测试 vs 端到端测试

对于Spring mvc测试,默认情况下没有上下文路径。没有jsessionid cookie;没有转发,错误或异步调度;因此,没有实际的JSP呈现。而是将“转发”和“重定向” URL保存在MockHttpServletResponse中,并且可以按预期进行声明。

可以考虑使用@WebIntegrationTest从Spring Boot获得完整的端到端集成测试支持。

测试示例

https://github.com/spring-projects/spring-framework/tree/master/spring-test/src/test/java/org/springframework/test/web/servlet/samples
https://github.com/spring-projects/spring-mvc-showcase/tree/master/src

HtmlUnit集成

Spring提供了MockMvc和HtmlUnit之间的集成。使用基于HTML的视图时,这简化了执行端到端测试的过程。 具有如下的能力:

  • 使用HtmlUnit,WebDriver和Geb等工具可以轻松测试HTML页面,而无需部署到Servlet容器。
  • 测试页面内的JavaScript。
  • 使用模拟服务进行测试以加快测试速度。
  • 在容器内端到端测试和容器外集成测试之间共享逻辑。

为什么集成 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表单与控制器不同步,我们的表单测试也会继续通过。为了解决这个问题,我们可以结合以下两个测试:

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

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

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

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

  • 如果页面上有多个表单怎么办?诚然,我们可以更新XPath表达式,但是由于我们考虑了更多因素,它们变得更加复杂:字段是正确的类型吗?是否启用了字段?等等

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

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

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

端到端测试

为了解决前面提到的问题,我们可以执行端到端集成测试:

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

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

  • 确保数据库中包含正确的消息可能很繁琐。 (考虑外键约束。)
  • 测试可能会变慢,因为每次测试都需要确保数据库处于正确的状态。
  • 由于我们的数据库需要处于特定状态,因此我们无法并行运行测试。
  • 对诸如自动生成的ID,时间戳等项目进行断言可能很困难。

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

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

集成HtmlUnit的选择

要将MockMvc与HtmlUnit集成时,有很多选择:

  • MockMvc和HtmlUnit:如果要使用原始的HtmlUnit库,请使用此选项。
  • MockMvc和WebDriver:使用此选项可简化集成和端到端测试之间的开发和重用代码。
  • MockMvc和Geb:如果要使用Groovy进行测试,简化开发并在集成和端到端测试之间重用代码,请使用此选项。

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

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

使用

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

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

一旦有了对HtmlPage的引用,就可以填写该表单并提交以创建一条消息,如以下示例所示:

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

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

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

前面的代码以多种方式改进了我们的MockMvc测试。首先,我们不再需要显式验证表单,然后创建类似于表单的请求。相反,我们请求表单,将其填写并提交。
另一个重要因素是HtmlUnit使用Mozilla Rhino引擎来评估JavaScript。这意味着还可以在页面内测试JavaScript的行为。

MockMvcWebClientBuilder高级特性

配置更加高级的东西:

@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 和 WebDriver

在Selenium WebDriver中使用其他抽象简化测试的编写。

为什么使用 MockMvc 和 WebDriver?

Selenium WebDriver提供了一个非常优雅的API,使我们可以轻松地组织代码。为了更好地说明其工作原理,我们在本节中探索一个示例。

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

这种方法会导致大量单独的测试,因为我们也想测试错误情况。例如,如果只填写表格的一部分,我们要确保得到一个错误。

如果其中一个字段被命名为“ 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的Object中,如以下示例所示:


public class CreateMessagePage {

    final HtmlPage currentPage;
    
    final HtmlTextInput summaryInput;
    
    final HtmlSubmitInput submit;
    
    public CreateMessagePage(HtmlPage currentPage) {
        this.currentPage = currentPage;
        this.summaryInput = currentPage.getHtmlElementById("summary");
        this.submit = currentPage.getHtmlElementById("submit");
    }
    
    public <T> T createMessage(String summary, String text) throws Exception {
        setSummary(summary);
    
        HtmlPage result = submit.click();
        boolean error = CreateMessagePage.at(result);
    
        return (T) (error ? new CreateMessagePage(result) : new ViewMessagePage(result));
    }
    
    public void setSummary(String summary) throws Exception {
        summaryInput.setValueAttribute(summary);
    }
    
    public static boolean at(HtmlPage page) {
        return "Create Message".equals(page.getTitleText());
    }
}

此模式称为页面对象模式。虽然我们当然可以使用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();
}

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

用法

现在,我们可以像往常一样使用WebDriver,而无需将应用程序部署到Servlet容器。例如,我们可以请求视图:

CreateMessagePage page = CreateMessagePage.to(driver);

填写表格并提交以创建一条消息,如下所示:

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

通过利用Page Object Pattern,这可以改善我们的HtmlUnit测试的设计。正如我们在“为什么要使用WebDriver和MockMvc?”中提到的那样,我们可以将页面对象模式与HtmlUnit一起使用,但使用WebDriver则要容易得多。考虑以下CreateMessagePage实现:

public class CreateMessagePage
        extends AbstractPage { 


    private WebElement summary;
    private WebElement text;


​    
​    @FindBy(css = "input[type=submit]")private WebElement submit;public CreateMessagePage(WebDriver driver) {
        super(driver);
    }
    
    public <T> T createMessage(Class<T> resultPage, String summary, String details) {
        this.summary.sendKeys(summary);
        this.text.sendKeys(details);
        this.submit.click();
        return PageFactory.initElements(driver, resultPage);
    }
    
    public static CreateMessagePage to(WebDriver driver) {
        driver.get("http://localhost:9990/mail/messages/form");
        return PageFactory.initElements(driver, CreateMessagePage.class);
    }
}

断言结果:

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();
    }
}
MockMvcHtmlUnitDriverBuilder 的高级用法

配置全局属性:

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

客户端REST测试

这里的客户端测试指测试使用restTemplate调用远程服务的代码。 基本思想是mock这些调用, 让我们不需要启动相应的远程服务器:

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

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

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

默认情况下即使是无顺序请求,每个请求也只能执行一次。 Expect方法提供了一个重载的变量,该变量接受一个ExpectedCount参数,该参数指定一个计数范围(例如,一次,多次,最大,最小,之间等等)。以下示例使用时间:

RestTemplate restTemplate = new RestTemplate();

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

// ...

mockServer.verify();

可以将RestTemplate以将其绑定到MockMvc实例。这样就可以使用实际的服务器端逻辑来处理请求,而无需运行服务器。以下示例显示了如何执行此操作:

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

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


WebTestClient(用于WebFlux应用的测试)

WebTestClient是WebClient的薄封装,可用于执行请求并公开专用的流畅风格API来验证响应。 WebTestClient通过使用模拟请求和响应绑定到WebFlux应用程序,或者它可以通过HTTP连接测试任何Web服务器。

配置

绑定到Controller上

绑定到一个@Controller:

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

绑定到Router 函数

示例:

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

在内部,配置被传递到RouterFunctions.toWebHandler。通过使用模拟请求和响应对象,可以在没有HTTP服务器的情况下测试WebFlux应用程序。

绑定到ApplicationContext中

示例:

    @RunWith(SpringRunner.class)
    @ContextConfiguration(classes = WebConfig.class) 
    public class MyTests {

        @Autowired
        private ApplicationContext context; 
    
        private WebTestClient client;
    
        @Before
        public void setUp() {
            client = WebTestClient.bindToApplicationContext(context).build(); 
        }
    }

绑定到一个正在运行的服务器

示例:

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

使用Client Builder配置一些属性

示例:

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

用法

WebTestClient提供了与WebClient相同的API,直到使用exchange()执行请求为止。 exchange()之后是链接的API工作流,用于验证响应。
通常,首先声明响应状态和标头,如下所示:

   client.get().uri("/persons/1")
            .accept(MediaType.APPLICATION_JSON_UTF8)
            .exchange()
            .expectStatus().isOk()
            .expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
            // ...

接下来, 可以指定如何解码和如何处理响应的body:

  • ExpectBody(Class<T>):解码为单个对象。
  • ExpectBodyList(Class<T>):解码并将对象收集到List<T>
  • ExpectBody():解码为byte []以获取JSON内容或一个空的正文。

然后,可以对主体使用内置的断言。下面的示例显示了一种方法:

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

可以自定义自己的断言:

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

退出流, 直接获取一个结果:

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

No Content

如果响应没有内容(或者内容不关紧要),请使用Void.class,以确保释放资源:

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

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

client.post().uri("/persons")
            .body(personMono, Person.class)
            .exchange()
            .expectStatus().isCreated()
            .expectBody().isEmpty();

JSON Content

对响应体进行JSON相关的断言.
全匹配:

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

使用jsonPath:


   client.get().uri("/persons")
            .exchange()
            .expectStatus().isOk()
            .expectBody()
            .jsonPath("$[0].name").isEqualTo("Jane")
            .jsonPath("$[1].name").isEqualTo("Jason");

流响应

要测试无限流(例如,“ text / event-stream”或“ application / stream + json”),需要在响应状态和header断言之后立即退出链接的API(使用returnResult):

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

校检结果:

  Flux<Event> eventFux = result.getResponseBody();

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章