文章目录

  • Pre
  • Spring Boot 中的测试解决方案
  • 测试 Spring Boot 应用程序
    • 初始化测试环境
    • @SpringBootTest
      • @SpringBootTest - webEnvironment
    • @RunWith 注解与 SpringRunner
  • 执行测试用例
  • 使用 @DataJpaTest 注解测试数据访问组件
  • Service层和Controller的测试
    • 使用 Environment 测试配置信息
    • 使用 Mock 测试 Service 层
      • Mock 机制
        • 使用 Mock
    • 测试 Controller 层
      • 使用 TestRestTemplate
      • 使用 @WebMvcTest 注解
      • 使用 @AutoConfigureMockMvc 注解
  • 小结


Pre

本篇博文我们开始梳理下Spring 提供的测试解决方案。

对于 Web 应用程序而言, 一个应用程序中涉及数据层、服务层、Web 层,以及各种外部服务之间的交互关系时,我们除了对各层组件的单元测试之外,还需要充分引入集成测试保证服务的正确性和稳定性。


Spring Boot 中的测试解决方案

和 Spring Boot 1.x 版本一样,Spring Boot 2.x 也提供了一个用于测试的 spring-boot-starter-test 组件。

在 Spring Boot 中,集成该组件的方法是在 pom 文件中添加如下所示依赖:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.junit.platform</groupId><artifactId>junit-platform-launcher</artifactId><scope>test</scope></dependency>

其中,最后一个依赖用于导入与 JUnit 相关的功能组件。

然后,通过 Maven 查看 spring-boot-starter-test 组件的依赖关系,我们可以得到如下所示的组件依赖图:

从上图中可以看到,在代码工程的构建路径中,我们引入了一系列组件初始化测试环境。比如 JUnit、JSON Path、AssertJ、Mockito、Hamcrest 等

  • JUnit:JUnit 是一款非常流行的基于 Java 语言的单元测试框架

  • JSON Path:类似于 XPath 在 XML 文档中的定位,JSON Path 表达式通常用来检索路径或设置 JSON 文件中的数据。

  • AssertJ:AssertJ 是一款强大的流式断言工具,它需要遵守 3A 核心原则,即 Arrange(初始化测试对象或准备测试数据)——> Actor(调用被测方法)——>Assert(执行断言)。

  • Mockito:Mockito 是 Java 世界中一款流行的 Mock 测试框架,它主要使用简洁的 API 实现模拟操作。在实施集成测试时,我们将大量使用到这个框架。

  • Hamcrest:Hamcrest 提供了一套匹配器(Matcher),其中每个匹配器的设计用于执行特定的比较操作。

  • JSONassert:JSONassert 是一款专门针对 JSON 提供的断言框架。

  • Spring Test & Spring Boot Test:为 Spring 和 Spring Boot 框架提供的测试工具。

以上组件的依赖关系都是自动导入, 无须做任何变动。


测试 Spring Boot 应用程序

接下来,我们将初始化 Spring Boot 应用程序的测试环境,并介绍如何在单个服务内部完成单元测试的方法和技巧。

导入 spring-boot-starter-test 依赖后,我们就可以使用它提供的各项功能应对复杂的测试场景了。

初始化测试环境

对于 Spring Boot 应用程序而言,我们知道其 Bootstrap 类中的 main() 入口将通过 SpringApplication.run() 方法启动 Spring 容器.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
public class Application {public static void main(String[] args) {SpringApplication.run(Application.class, args);}}

针对上述 Bootstrap 类,我们可以通过编写测试用例的方式,验证 Spring 容器能否正常启动。

基于 Maven 的默认风格,我们将在 src/test/javasrc/test/resources 包下添加各种测试用例代码和配置文件。

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.junit4.SpringRunner;@SpringBootTest
@RunWith(SpringRunner.class)
public class ApplicationContextTests {@Autowiredprivate ApplicationContext applicationContext;@Testpublic void testContextLoads() throws Throwable {Assert.assertNotNull(this.applicationContext);}
}

该用例对 Spring 中的 ApplicationContext 作了简单非空验证。

执行该测试用例后,从输出的控制台信息中,我们可以看到 Spring Boot 应用程序被正常启动,同时测试用例本身也会给出执行成功的提示。

上述测试用例虽然简单,但是已经包含了测试 Spring Boot 应用程序的基本代码框架。其中,最重要的是 ApplicationContextTests 类上的 @SpringBootTest 和 @RunWith 注解,对于 Spring Boot 应用程序而言,这两个注解构成了一套完成的测试方案。

接下来我们对这两个注解进行详细展开。


@SpringBootTest

因为 SpringBoot 程序的入口是 Bootstrap 类,所以 SpringBoot 专门提供了一个 @SpringBootTest 注解测试 Bootstrap 类。同时 @SpringBootTest 注解也可以引用 Bootstrap 类的配置,因为所有配置都会通过 Bootstrap 类去加载。

在上面的例子中,我们是通过直接使用 @SpringBootTest 注解提供的默认功能对作为 Bootstrap 类的 Application 类进行测试。

而更常见的做法是在 @SpringBootTest 注解中指定该 Bootstrap 类,并设置测试的 Web 环境,如下代码所示。

@SpringBootTest(classes = CustomerApplication.class, webEnvironment = SpringBootTest.WebEnvironment.MOCK)

在以上代码中,@SpringBootTest 注解中的 webEnvironment 可以有四个选项,分别是 MOCK、RANDOM_PORT、DEFINED_PORT 和 NONE。


@SpringBootTest - webEnvironment

  • MOCK:加载 WebApplicationContext 并提供一个 Mock 的 Servlet 环境,此时内置的 Servlet 容器并没有正式启动。

  • RANDOM_PORT:加载 EmbeddedWebApplicationContext 并提供一个真实的 Servlet 环境,然后使用一个随机端口启动内置容器。

  • DEFINED_PORT:这个配置也是通过加载 EmbeddedWebApplicationContext 提供一个真实的 Servlet 环境,但使用的是默认端口,如果没有配置端口就使用 8080。

  • NONE:加载 ApplicationContext 但并不提供任何真实的 Servlet 环境。

在 Spring Boot 中,@SpringBootTest 注解主要用于测试基于自动配置的 ApplicationContext,它允许我们设置测试上下文中的 Servlet 环境。

在多数场景下,一个真实的 Servlet 环境对于测试而言过于重量级,通过 MOCK 环境则可以缓解这种环境约束所带来的困扰


@RunWith 注解与 SpringRunner

在上面的示例中,我们还看到一个由 JUnit 框架提供的 @RunWith 注解,它用于设置测试运行器。例如,我们可以通过 @RunWith(SpringJUnit4ClassRunner.class) 让测试运行于 Spring 测试环境。

虽然这我们指定的是 SpringRunner.class,实际上,SpringRunner 就是 SpringJUnit4ClassRunner 的简化,它允许 JUnit 和 Spring TestContext 整合运行,而 Spring TestContext 则提供了用于测试 Spring 应用程序的各项通用的支持功能。


执行测试用例

接下来我们将通过代码示例回顾如何使用 JUnit 框架执行单元测试的过程和实践,同时提供验证异常和验证正确性的测试方法。

单元测试的应用场景是一个独立的类,如下所示的 CustomerTicket 类就是一个非常典型的独立类:

public class CustomTicket {private Long id;private Long accountId;    private String orderNumber;private String description;private Date createTime;public CustomTicket (Long accountId, String orderNumber) {super();Assert.notNull(accountId, "Account Id must not be null");Assert.notNull(orderNumber, "Order Number must not be null");Assert.isTrue(orderNumber.length() == 10, "Order Number must be exactly 10 characters");this.accountId = accountId;this.orderNumber = orderNumber;}}

我们可以看到,该类对CustomTicket 做了封装,并在其构造函数中添加了校验机制。

下面我们先来看看如何对正常场景进行测试。

例如 ArtisanTicket 中orderNumber 的长度问题,我们可以使用如下测试用例,通过在构造函数中传入字符串来验证规则的正确性:

@RunWith(SpringRunner.class)
public class CustomerTicketTests {private static final String ORDER_NUMBER = "Order00001";@Testpublic void testOrderNumberIsExactly10Chars() throws Exception {CustomerTicket customerTicket = new CustomerTicket(100L, ORDER_NUMBER);assertThat(customerTicket.getOrderNumber().toString()).isEqualTo(ORDER_NUMBER);}
}

使用 @DataJpaTest 注解测试数据访问组件

数据需要持久化,接下来我们将从数据持久化的角度出发,讨论如何对 Repository 层进行测试的方法。

首先,我们讨论一下使用关系型数据库的场景,并引入针对 JPA 数据访问技术的 @DataJpaTest 注解

@DataJpaTest 注解会自动注入各种 Repository 类,并初始化一个内存数据库和及访问该数据库的数据源。在测试场景下,一般我们可以使用 H2 作为内存数据库,并通过 MySQL 实现数据持久化,因此我们需要引入以下所示的 Maven 依赖:

<dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId>
</dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope>
</dependency>

另一方面,我们需要准备数据库 DDL 用于初始化数据库表,并提供 DML 脚本完成数据初始化。其中,schema-mysql.sql 和 data-h2.sql 脚本分别充当了 DDL 和 DML 的作用。

在 customer-service 的 schema-mysql.sql 中包含了 CUSTOMER 表的创建语句,如下代码所示:

DROP TABLE IF EXISTS `customerticket`;
create table `customerticket` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`account_id` bigint(20) not null,`order_number` varchar(50) not null,`description` varchar(100) not null,`create_time` timestamp not null DEFAULT CURRENT_TIMESTAMP,PRIMARY KEY (`id`)
);

而在 data-h2.sql 中,我们插入了一条测试需要使用的数据,具体的初始化数据过程如下代码所示:

INSERT INTO customerticket (`account_id`, `order_number`,`description`) values (1, 'Order00001', ' DemoCustomerTicket1');

接下来是提供具体的 Repository 接口,我们先通过如下所示代码回顾一下 CustomerRepository 接口的定义。

public interface CustomerTicketRepository extends JpaRepository<CustomerTicket, Long> {List<CustomerTicket> getCustomerTicketByOrderNumber(String orderNumber);
}

这里存在一个方法名衍生查询 getCustomerTicketByOrderNumber,它会根据 OrderNumber 获取 CustomerTicket。

基于上述 CustomerRepository,我们可以编写如下所示的测试用例:

@RunWith(SpringRunner.class)
@DataJpaTest
public class CustomerRepositoryTest {@Autowiredprivate TestEntityManager entityManager;@Autowiredprivate CustomerTicketRepository customerTicketRepository;@Testpublic void testFindCustomerTicketById() throws Exception {             this.entityManager.persist(new CustomerTicket(1L, "Order00001", "DemoCustomerTicket1", new Date()));CustomerTicket customerTicket = this.customerTicketRepository.getOne(1L);assertThat(customerTicket).isNotNull();assertThat(customerTicket.getId()).isEqualTo(1L);}@Testpublic void testFindCustomerTicketByOrderNumber() throws Exception {    String orderNumber = "Order00001";this.entityManager.persist(new CustomerTicket(1L, orderNumber, "DemoCustomerTicket1", new Date()));this.entityManager.persist(new CustomerTicket(2L, orderNumber, "DemoCustomerTicket2", new Date()));List<CustomerTicket> customerTickets = this.customerTicketRepository.getCustomerTicketByOrderNumber(orderNumber);assertThat(customerTickets).size().isEqualTo(2);CustomerTicket actual = customerTickets.get(0);assertThat(actual.getOrderNumber()).isEqualTo(orderNumber);}@Testpublic void testFindCustomerTicketByNonExistedOrderNumber() throws Exception {              this.entityManager.persist(new CustomerTicket(1L, "Order00001", "DemoCustomerTicket1", new Date()));this.entityManager.persist(new CustomerTicket(2L, "Order00002", "DemoCustomerTicket2", new Date()));List<CustomerTicket> customerTickets = this.customerTicketRepository.getCustomerTicketByOrderNumber("Order00003");assertThat(customerTickets).size().isEqualTo(0);}
}

这里可以看到,我们使用了 @DataJpaTest 实现 CustomerRepository 的注入。同时,我们还注意到另一个核心测试组件 TestEntityManager,它的效果相当于不使用真正的 CustomerRepository 完成数据的持久化,从而提供了一种数据与环境之间的隔离机制。

执行这些测试用例后,我们需要关注它们的控制台日志输入,其中核心日志如下所示(为了显示做了简化处理):

Hibernate: drop table customer_ticket if exists
Hibernate: drop sequence if exists hibernate_sequence
Hibernate: create sequence hibernate_sequence start with 1 increment by 1
Hibernate: create table customer_ticket (id bigint not null, account_id bigint, create_time timestamp, description varchar(255), order_number varchar(255), primary key (id))
Hibernate: create table localaccount (id bigint not null, account_code varchar(255), account_name varchar(255), primary key (id))
…
Hibernate: call next value for hibernate_sequence
Hibernate: call next value for hibernate_sequence
Hibernate: insert into customer_ticket (account_id, create_time, description, order_number, id) values (?, ?, ?, ?, ?)
Hibernate: insert into customer_ticket (account_id, create_time, description, order_number, id) values (?, ?, ?, ?, ?)
Hibernate: select customerti0_.id as id1_0_, customerti0_.account_id as account_2_0_, customerti0_.create_time as create_t3_0_, customerti0_.description as descript4_0_, customerti0_.order_number as order_nu5_0_ from customer_ticket customerti0_ where customerti0_.order_number=?
…
Hibernate: drop table customer_ticket if exists
Hibernate: drop sequence if exists hibernate_sequence

从以上日志中,我们不难看出执行各种 SQL 语句的效果。


Service层和Controller的测试

与位于底层的数据访问层不同,这两层的组件都依赖于它的下一层组件,即 Service 层依赖于数据访问层,而 Controller 层依赖于 Service 层。因此,对这两层进行测试时,我们将使用不同的方案和技术。


使用 Environment 测试配置信息

在 Spring Boot 应用程序中,Service 层通常依赖于配置文件,所以我们也需要对配置信息进行测试。

配置信息的测试方案分为两种,第一种依赖于物理配置文件,第二种则是在测试时动态注入配置信息。

第一种测试方案比较简单,在 src/test/resources 目录下添加配置文件时,Spring Boot 能读取这些配置文件中的配置项并应用于测试案例中。

在介绍具体的实现过程之前,我们有必要先来了解一下 Environment 接口,该接口定义如下:

public interface Environment extends PropertyResolver {String[] getActiveProfiles();String[] getDefaultProfiles();boolean acceptsProfiles(String... profiles);
}

在上述代码中我们可以看到,Environment 接口的主要作用是处理 Profile,而它的父接口 PropertyResolver 定义如下代码所示:

public interface PropertyResolver {boolean containsProperty(String key);String getProperty(String key);String getProperty(String key, String defaultValue);<T> T getProperty(String key, Class<T> targetType);<T> T getProperty(String key, Class<T> targetType, T defaultValue);String getRequiredProperty(String key) throws IllegalStateException;<T> T getRequiredProperty(String key, Class<T> targetType) throws IllegalStateException;String resolvePlaceholders(String text);String resolveRequiredPlaceholders(String text) throws IllegalArgumentException;}

显然,PropertyResolver 的作用是根据各种配置项的 Key 获取配置属性值。

现在,假设 src/test/resources 目录中的 application.properties 存在如下配置项:

springcss.order.point = 10

那么,我们就可以设计如下所示的测试用例了。

@RunWith(SpringRunner.class)
@SpringBootTest
public class EnvironmentTests{@Autowiredpublic Environment environment;@Testpublic void testEnvValue(){Assert.assertEquals(10, Integer.parseInt(environment.getProperty("springcss.order.point"))); }
}

这里我们注入了一个 Environment 接口,并调用了它的 getProperty 方法来获取测试环境中的配置信息。

除了在配置文件中设置属性,我们也可以使用 @SpringBootTest 注解指定用于测试的属性值,示例代码如下:

@RunWith(SpringRunner.class)
@SpringBootTest(properties = {" springcss.order.point = 10"})
public class EnvironmentTests{@Autowiredpublic Environment environment;@Testpublic void testEnvValue(){Assert.assertEquals(10, Integer.parseInt(environment.getProperty("springcss.order.point"))); }
}

使用 Mock 测试 Service 层

Service 层依赖于数据访问层。因此,对 Service 层进行测试时,我们还需要引入新的技术体系,也就是应用非常广泛的 Mock 机制。

接下来,我们先看一下 Mock 机制的基本概念。

Mock 机制

Mock 的意思是模拟,它可以用来对系统、组件或类进行隔离。

在测试过程中,我们通常关注测试对象本身的功能和行为,而对测试对象涉及的一些依赖,仅仅关注它们与测试对象之间的交互(比如是否调用、何时调用、调用的参数、调用的次数和顺序,以及返回的结果或发生的异常等),并不关注这些被依赖对象如何执行这次调用的具体细节。

因此,Mock 机制就是使用 Mock 对象替代真实的依赖对象,并模拟真实场景来开展测试工作。

使用 Mock 对象完成依赖关系测试的示意图如下所示:

可以看出,在形式上,Mock 是在测试代码中直接 Mock 类和定义 Mock 方法的行为,通常测试代码和 Mock 代码放一起。因此,测试代码的逻辑从测试用例的代码上能很容易地体现出来。

下面我们一起看一下如何使用 Mock 测试 Service 层。

使用 Mock

@SpringBootTest 注解中的 SpringBootTest.WebEnvironment.MOCK 选项,该选项用于加载 WebApplicationContext 并提供一个 Mock 的 Servlet 环境,内置的 Servlet 容器并没有真实启动。接下来,我们针对 Service 层演示一下这种测试方式。

首先,我们来看一种简单场景,在 customer-service 中存在如下 CustomerTicketService 类:

@Service
public class CustomerTicketService {@Autowiredprivate CustomerTicketRepository customerTicketRepository;public CustomerTicket getCustomerTicketById(Long id) {return customerTicketRepository.getOne(id);}}

这里我们可以看到,以上方法只是简单地通过 CustomerTicketRepository 完成了数据查询操作。

显然,对以上 CustomerTicketService 进行集成测试时,还需要我们提供一个 CustomerTicketRepository 依赖。

下面,我们通过以下代码演示一下如何使用 Mock 机制完成对 CustomerTicketRepository 的隔离

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
public class CustomerServiceTests {@MockBeanprivate CustomerTicketRepository customerTicketRepository;@Testpublic void testGetCustomerTicketById() throws Exception {Long id = 1L;Mockito.when(customerTicketRepository.getOne(id)).thenReturn(new CustomerTicket(1L, 1L, "Order00001", "DemoCustomerTicket1", new Date()));CustomerTicket actual = customerTicketService.getCustomerTicketById(id);assertThat(actual).isNotNull();assertThat(actual.getOrderNumber()).isEqualTo("Order00001");}
}

首先,我们通过 @MockBean 注解注入了 CustomerTicketRepository;然后,基于第三方 Mock 框架 Mockito 提供的 when/thenReturn 机制完成了对 CustomerTicketRepository 中 getCustomerTicketById() 方法的 Mock。

当然,如果你希望在测试用例中直接注入真实的CustomerTicketRepository,这时就可以使用@SpringBootTest 注解中的 SpringBootTest.WebEnvironment.RANDOM_PORT 选项,示例代码如下:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class CustomerServiceTests {@Autowiredprivate CustomerTicketRepository customerTicketRepository;@Testpublic void testGetCustomerTicketById() throws Exception {Long id = 1L;CustomerTicket actual = customerTicketService.getCustomerTicketById(id);assertThat(actual).isNotNull();assertThat(actual.getOrderNumber()).isEqualTo("Order00001");}
}

运行上述代码后就会以一个随机的端口启动整个 Spring Boot 工程,并从数据库中真实获取目标数据进行验证。

以上集成测试的示例中只包含了对 Repository 层的依赖,而有时候一个 Service 中可能同时包含 Repository 和其他 Service 类或组件,下面回到如下所示的 CustomerTicketService 类:

@Service
public class CustomerTicketService {@Autowiredprivate OrderClient orderClient;private OrderMapper getRemoteOrderByOrderNumber(String orderNumber) {return orderClient.getOrderByOrderNumber(orderNumber);}}

这里我们可以看到,在该代码中,除了依赖 CustomerTicketRepository 之外,还同时依赖了 OrderClient。

请注意:以上代码中的 OrderClient 是在 customer-service 中通过 RestTemplate 访问 order-service 的远程实现类,其代码如下所示:

@Component
public class OrderClient {@AutowiredRestTemplate restTemplate;public OrderMapper getOrderByOrderNumber(String orderNumber) {ResponseEntity<OrderMapper> restExchange = restTemplate.exchange("http://localhost:8083/orders/{orderNumber}", HttpMethod.GET, null,OrderMapper.class, orderNumber);OrderMapper result = restExchange.getBody();return result;}
}

CustomerTicketService 类实际上并不关注 OrderClient 中如何实现远程访问的具体过程。因为对于集成测试而言,它只关注方法调用返回的结果,所以我们将同样采用 Mock 机制完成对 OrderClient 的隔离。

对 CustomerTicketService 这部分功能的测试用例代码如下所示,可以看到,我们采用的是同样的测试方式。

@Test
public void testGenerateCustomerTicket() throws Exception {Long accountId = 100L;String orderNumber = "Order00001";Mockito.when(this.orderClient.getOrderByOrderNumber("Order00001")).thenReturn(new OrderMapper(1L, orderNumber, "deliveryAddress"));Mockito.when(this.localAccountRepository.getOne(accountId)).thenReturn(new LocalAccount(100L, "accountCode", "accountName"));CustomerTicket actual = customerTicketService.generateCustomerTicket(accountId, orderNumber);assertThat(actual.getOrderNumber()).isEqualTo(orderNumber);
}

这里提供的测试用例演示了 Service 层中进行集成测试的各种手段,它们已经能够满足一般场景的需要。


测试 Controller 层

对 Controller 层进行测试之前,我们先来提供一个典型的 Controller 类,它来自 customer-service,如下代码所示:

@RestController
@RequestMapping(value="customers")
public class CustomerController {@Autowiredprivate CustomerTicketService customerTicketService; @PostMapping(value = "/{accountId}/{orderNumber}")public CustomerTicket generateCustomerTicket( @PathVariable("accountId") Long accountId,@PathVariable("orderNumber") String orderNumber) {CustomerTicket customerTicket = customerTicketService.generateCustomerTicket(accountId, orderNumber);return customerTicket;}
}

关于上述 Controller 类的测试方法,相对来说比较丰富,比如有 TestRestTemplate、@WebMvcTest 注解和 MockMvc 这三种,下面我们逐一进行讲解。

使用 TestRestTemplate

Spring Boot 提供的 TestRestTemplate 与 RestTemplate 非常类似,只不过它专门用在测试环境中。

如果我们想在测试环境中使用 @SpringBootTest,则可以直接使用 TestRestTemplate 来测试远程访问过程,示例代码如下:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class CustomerController2Tests {@Autowiredprivate TestRestTemplate testRestTemplate;@MockBeanprivate CustomerTicketService customerTicketService;@Testpublic void testGenerateCustomerTicket() throws Exception {Long accountId = 100L;String orderNumber = "Order00001";given(this.customerTicketService.generateCustomerTicket(accountId, orderNumber)).willReturn(new CustomerTicket(1L, accountId, orderNumber, "DemoCustomerTicket1", new Date()));CustomerTicket actual = testRestTemplate.postForObject("/customers/" + accountId+ "/" + orderNumber, null, CustomerTicket.class);assertThat(actual.getOrderNumber()).isEqualTo(orderNumber);}
}

上述测试代码中,首先,我们注意到 @SpringBootTest 注解通过使用 SpringBootTest.WebEnvironment.RANDOM_PORT 指定了随机端口的 Web 运行环境。然后,我们基于 TestRestTemplate 发起了 HTTP 请求并验证了结果。

特别说明:这里使用 TestRestTemplate 发起请求的方式与 RestTemplate 完全一致


使用 @WebMvcTest 注解

接下来测试方法中,我们将引入一个新的注解 @WebMvcTest,该注解将初始化测试 Controller 所必需的 Spring MVC 基础设施,CustomerController 类的测试用例如下所示:

@RunWith(SpringRunner.class)
@WebMvcTest(CustomerController.class)
public class CustomerControllerTestsWithMockMvc {@Autowiredprivate MockMvc mvc;@MockBeanprivate CustomerTicketService customerTicketService;@Testpublic void testGenerateCustomerTicket() throws Exception {Long accountId = 100L;String orderNumber = "Order00001";given(this.customerTicketService.generateCustomerTicket(accountId, orderNumber)).willReturn(new CustomerTicket(1L, 100L, "Order00001", "DemoCustomerTicket1", new Date()));this.mvc.perform(post("/customers/" + accountId+ "/" + orderNumber).accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk());}
}

MockMvc 类提供的基础方法分为以下 6 种,下面一一对应来看下。

  • Perform:执行一个 RequestBuilder 请求,会自动执行 SpringMVC 流程并映射到相应的 Controller 进行处理。

  • get/post/put/delete:声明发送一个 HTTP 请求的方式,根据 URI 模板和 URI 变量值得到一个 HTTP 请求,支持 GET、POST、PUT、DELETE 等 HTTP 方法。

  • param:添加请求参数,发送 JSON 数据时将不能使用这种方式,而应该采用 @ResponseBody 注解。

  • andExpect:添加 ResultMatcher 验证规则,通过对返回的数据进行判断来验证 Controller 执行结果是否正确。

  • andDo:添加 ResultHandler 结果处理器,比如调试时打印结果到控制台。

  • andReturn:最后返回相应的 MvcResult,然后执行自定义验证或做异步处理。

执行该测试用例后,从输出的控制台日志中我们不难发现,整个流程相当于启动了 CustomerController 并执行远程访问,而 CustomerController 中使用的 CustomerTicketService 则做了 Mock。

显然,测试 CustomerController 的目的在于验证其返回数据的格式和内容。在上述代码中,我们先定义了 CustomerController 将会返回的 JSON 结果,然后通过 perform、accept 和 andExpect 方法模拟了 HTTP 请求的整个过程,最终验证了结果的正确性。

请注意 @SpringBootTest 注解不能和 @WebMvcTest 注解同时使用。


使用 @AutoConfigureMockMvc 注解

在使用 @SpringBootTest 注解的场景下,如果我们想使用 MockMvc 对象,那么可以引入 @AutoConfigureMockMvc 注解。

通过将 @SpringBootTest 注解与 @AutoConfigureMockMvc 注解相结合,@AutoConfigureMockMvc 注解将通过 @SpringBootTest 加载的 Spring 上下文环境中自动配置 MockMvc 这个类。

使用 @AutoConfigureMockMvc 注解的测试代码如下所示:

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class CustomerControllerTestsWithAutoConfigureMockMvc {@Autowiredprivate MockMvc mvc;@MockBeanprivate CustomerTicketService customerTicketService;@Testpublic void testGenerateCustomerTicket() throws Exception {Long accountId = 100L;String orderNumber = "Order00001";given(this.customerTicketService.generateCustomerTicket(accountId, orderNumber)).willReturn(new CustomerTicket(1L, 100L, "Order00001", "DemoCustomerTicket1", new Date()));this.mvc.perform(post("/customers/" + accountId+ "/" + orderNumber).accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk());}
}

在上述代码中,我们使用了 MockMvc 工具类完成了对 HTTP 请求的模拟,并基于返回状态验证了 Controller 层组件的正确性。


小结

SpringBoot - 应用程序测试方案相关推荐

  1. 网络bcc程序测试方案

    网络bcc程序测试方案 1. 具体做法 1.1 准确性测试 1.2 性能测试 2. 数据模拟工具 3. 传统工具举例 3.1 iftop实时流量监控工具(此工具可用来测试网络流量指标程序) 3.2 n ...

  2. springboot配置连接rds_java – 在AWS-EC2上运行的SpringBoot应用程序无法连接到MySQL AWS-RDS数据库...

    我在运行我在EC2实例中开发的应用程序时遇到问题.当我执行.jar(java -jar app.jar)时,SpringBoot应用程序启动但尝试连接到我的MySQL RDS数据库时失败.问题是当我在 ...

  3. (附源码)Springboot小程序交通违法举报系统 毕业设计 242045

    Springboot小程序交通违法举报系统 摘 要 随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱,交 ...

  4. 【问题】14500充电锂电池电量问题及测试方案

    微信关注 "DLGG创客DIY" 设为"星标",重磅干货,第一时间送达. 最近在研究电池供电的电源方案,考虑到电池的体积和电压,使用到了14500充电锂电池,用 ...

  5. SpringBoot简介、SpringBoot 入门程序搭建、与JDBC、Druid、Mybatis和SpringData JPA的整合

    一.SpringBoot 简介: spring boot并不是一个全新的框架,它不是spring解决方案的一个替代品,而是spring的一个封装.所以,你以前可以用spring做的事情,现在用spri ...

  6. travis-ci_使用Travis-CI的SpringBoot应用程序的CI / CD

    travis-ci 在本文中,我们将学习如何将Travis CI用于SpringBoot应用程序的持续集成和持续部署(CI / CD). 我们将学习如何运行Maven构建目标,使用JaCoCo插件,使 ...

  7. 使用Travis-CI的SpringBoot应用程序的CI / CD

    在本文中,我们将学习如何将Travis CI用于SpringBoot应用程序的持续集成和持续部署(CI / CD). 我们将学习如何运行Maven构建目标,使用JaCoCo插件,使用SonarClou ...

  8. 使用spring-session外部化Spring-boot应用程序的会话状态

    Spring-session是一个非常酷的新项目,旨在提供一种更简单的方法来管理基于Java的Web应用程序中的会话. 我最近在spring-session中探索的功能之一是它支持外部化会话状态的方式 ...

  9. Memory及其controller芯片整体测试方案(上篇)

    如果你最近想买手机,没准儿你一看价格会被吓到手机什么时候偷偷涨价啦! 其实对于手机涨价,手机制造商也是有苦难言,其中一个显著的原因是存储器芯片价格的上涨↗↗↗ >>> 存储器memo ...

最新文章

  1. 皮一皮:成人世界的潜台词...
  2. 2.逆向分析Hello World!程序-上
  3. js中const,var,let区别与用法
  4. C++中的多态(一)
  5. 设有两个16位整数变量A和B,试编写完成下述操作的程序。
  6. Spring Cloud Config服务端配置细节(二)之加密解密
  7. Spring Boot 学习系列(04)—分而治之,多module打包
  8. LeetCode 515. Find Largest Value in Each Tree Row
  9. Linux 命令(56)—— telnet 命令
  10. testlink php nginx,linux环境部署testlink步骤说明
  11. 《浪潮之巅》读书笔记——第9章 雅虎
  12. oracle 11g函数包缓存,Oracle11新特性——PLSQL函数缓存结果(二)
  13. jupyter 安装问题 No such notebook dir
  14. 网页打印怎样去掉网址
  15. 异常检测论文(一):CutPaste
  16. 浓缩就是精华——21行python实现输入法自动提示(带过程举例,附录也精彩)
  17. 基金收益率计算1:资管业务、资管产品和基金
  18. xshell连接不上虚拟机Linux
  19. 批量保存西瓜无水印视频的方法步骤
  20. 使用Python调用opencv学习(-)打开图片,显示图片

热门文章

  1. 机器学习算法-k-means聚类算法介绍
  2. pod install 之后 需要重新 设置teamId
  3. 【python】request HTTPS代理
  4. 实践检验递归查询SQL
  5. APS in Linux for Lenovo R61i
  6. jsp+ssm计算机毕业设计高校教师教学助手系统的设计与实现【附源码】
  7. 筛子点数相同概率—以四个骰子为例;三个点数相同;两个点数相同;顺子概率
  8. AR(增强现实)技术的运用方式
  9. 学的Java如何找兼职?
  10. 视频教程-【2019精品课】构建ELK海量日志分析平台-ELK