优秀的编程知识分享平台

网站首页 > 技术文章 正文

程序员有福了!万字长文带你掌握SpringBoot所提供的测试解决方案

nanyue 2024-10-17 11:16:13 技术文章 9 ℃

测试Spring Boot

在本篇最后一节,我们将讨论Spring Boot所提供的测试解决方案。

对于Web应用程序而言,测试是一个难点,也是经常被忽略的一套技术。当一个应用程序涉及数据层、服务层、Web层以及各种外部服务之间的交互关系时,除了针对各层组件的单元测试之外,还需要充分引入集成测试来保证服务的正确性和稳定性。

Spring Boot中的测试解决方案

和Spring Boot 1.x版本一样,Spring Boot 2.x同样提供了针对测试的spring-boot-starter-test组件。在Spring Boot中集成该组件的方法就是在pom文件中添加如代码清单13-49所示的依赖。

代码清单13-49 spring-boot-starter-test依赖包

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-test</artifactId>

<scope>test</scope>

<exclusions>

<exclusion>

<groupId>org.junit.vintage</groupId>

<artifactId>junit-vintage-engine</artifactId>

</exclusion>

</exclusions>

</dependency>

<dependency>

<groupId>org.junit.platform</groupId>

<artifactId>junit-platform-launcher</artifactId> <scope>test</scope>

</dependency>

这里有一点要注意,从2.2.0版本开始,Spring Boot引入了JUnit 5作为默认的单元测试库。在Spring Boot 2.2.0版本之前,spring-boot-startertest包含了对JUnit 4的依赖,而在Spring Boot 2.2.0版本之后JUnit 4被替换成了JUnit Jupiter,所以在依赖关系上我们手工去除基于JUnit4的junitvintage-engine。关于这一点,我们可以通过Maven查看spring-bootstarter-test组件的依赖关系来进一步明确,如图13-6所示的就是springboot-starter-test依赖包中的组件依赖图。

我们知道Spring Boot使得编码、配置、部署和监控变得更加简单。事实上,Spring Boot也能让测试工作更加简单。从图13-6中,可以看到一系列组件被自动引入到了代码工程的构建路径中,包括JUnit、JSONPath、AssertJ、Mockito、Hamcrest等。这里有必要对这些组件展开讨论。

JUnit:JUnit是一款非常流行的基于Java语言的单元测试框架,我们会使用该框架作为基础的测试框架。

JSONPath:类似于XPath在XML文档中的定位,JSONPath表达式通常被用来实现路径检索或设置JSON文件中的数据。AssertJ:AssertJ是强大的流式断言工具,遵守3A核心原则,即Arrange(初始化测试对象或者准备测试数据)→Actor(调用被测方法)→Assert(执行断言)。

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

Hamcrest:Hamcrest提供了一套匹配器(Matcher),每个匹配器都被设计来执行特定的比较操作。

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

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

以上组件的依赖关系是自动导入的,我们一般不需要做任何变动。而对于某些特定场景而言,我们还需要手工导入一些组件以满足测试需求,例如我们可以引入专用针对测试场景的嵌入式关系型数据库H2,或者针对MongoDB的嵌入式Flapdoodle等。

Spring Boot应用程序的测试流程

接下来,我们将初始化Spring Boot应用程序的测试环境,并介绍在单个服务内部完成单元测试的方法和技巧。在导入spring-boot-starter-test依赖之后,我们就可以使用它所提供的各项功能来应对复杂的测试场景。

1. 初始化测试环境

我们知道对于Spring Boot应用程序而言,Bootstrap类中的main()函数通过SpringApplication.run()方法启动Spring容器。代码清单13-50所示的就是一个典型的Spring Boot启动类UserApplication。

代码清单13-50 UserApplication类实现代码

@SpringBootApplication

public class UserApplication{

public static void main(String[] args) {

SpringApplication.run(UserApplication.class, args);

}

}

针对上述Bootstrap类,我们可以编写测试用例来验证Spring容器是否能够正常启动。基于Maven的默认风格,我们将在src/test/java和src/test/resources包下添加各种测试用例代码和配置文件。

我们首先在src/test/java中创建一个ApplicationContextTests.java文件,并编写如代码清单13-51所示的测试用例。

代码清单13-51 ApplicationContextTests测试类实现代码

import static org.junit.jupiter.api.Assertions.assertNotNull;

import org.junit.jupiter.api.Test;

import org.junit.jupiter.api.extension.ExtendWith;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.boot.test.context.SpringBootTest;

import org.springframework.context.ApplicationContext;

import org.springframework.test.context.junit.jupiter.SpringExtension;

@SpringBootTest

@ExtendWith(SpringExtension.class)

public class ApplicationContextTests {

@Autowired

private ApplicationContext applicationContext;

@Test

public void testContextLoads() {

assertNotNull(this.applicationContext);

}

}

该代码中的testContextLoaded()即为一个有效的测试用例,可以看到该用例简单地对Spring中的ApplicationContext做了非空验证。

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

上述测试用例虽然简单,但已经包含了测试Spring Boot应用程序的基本代码框架。这里面最重要的就是添加在ApplicationContextTests类上的@SpringBootTest和@ExtendWith注解,我们接下来对这两个注解详细展开讨论。对于Spring Boot应用程序而言,这两个注解构成了一套完成的测试方案。

2. @SpringBootTest注解

因为Spring Boot应用程序的入口是Bootstrap类,Spring Boot专门提供了一个@SpringBootTest注解来测试这个Bootstrap类。我们知道所有配置信息都会通过Bootstrap类进行加载,而该注解可以引用Bootstrap类的配置信息。

在上面的例子中,我们直接通过@SpringBootTest注解所提供的默认功能对作为Bootstrap类的UserApplication类进行测试。更常见的做法是在@SpringBootTest注解中指定该Bootstrap类,并设置测试的Web环境,如代码清单13-52所示。

代码清单13-52 @SpringBootTest注解使用方式

@SpringBootTest(classes = UserApplication.class, webEnvironment =

SpringBootTest.WebEnvironment.MOCK)

@SpringBootTest注解中的webEnvironment配置项可以有四个选项,分别是MOCK、RANDOM_PORT、DEFINED_PORT和NONE。MOCK:加载WebApplicationContext并提供一个Mock的Servlet环境,内置的Servlet容器并没有真实地启动。

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

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

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

在Spring Boot中,@SpringBootTest注解主要用于测试基于自动配置的Application-Context,它允许设置测试上下文中的Servlet环境。在多数场景下,一个真实的Servlet环境对于测试而言过于“重量级”,通过MOCK环境可以减少这种环境约束所带来的成本和挑战。我们在后续会结合WebEnvironment.MOCK选项来对服务层中的具体功能进行集成测试。

3. @ExtendWith注解

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

SpringExtension允许JUnit5和Spring TestContext整合运行,而SpringTestContext则提供了用于测试Spring Boot应用程序的各项通用的支持功能,常见的包括使用各种@MockBean注解。同样,我们在后续的测试用例中将大量使用SpringExtension。

如果你使用过2.2.0版本之前的Spring Boot,那么你应该知道@RunWith注解,我们可以通过@RunWith(SpringJUnit4ClassRunner.class)或者@RunWith(SpringRunner.class)让测试运行于Spring环境。在采用JUnit5时,这种方法已经不被推荐,取而代之的就是使用@ExtendWith注解。

4. 执行测试用例

基于JUnit,开发并执行测试用例的过程非常简单。单元测试的应用场景是针对独立的单个类。如代码清单13-53所示的User类就是一个非常典型的独立类,该类在其构造函数中添加了校验机制。

代码清单13-53 User类实现代码

public class User {

private String id;

private String name;

private Integer age;

private Date createdAt;

private String nationality;

private List<String> friendsIds;

private List<String> articlesIds;

public User(String id, String name, Integer age, Date createdAt,

String nationality) {

super();

Assert.notNull(id, "User Id must not be null");

Assert.isTrue(name.length() > 5, "User name should be more

than 5 characters");

this.id = id;

this.name = name;

this.age = age;

this.createdAt = createdAt;

this.nationality = nationality;

}

...

}

我们先来看如何对位于User类构造函数中的校验逻辑进行测试。以User中name字段的长度问题为例,我们可以使用如代码清单13-54所示的测试用例,验证传入字符串满足的正常业务场景的验证规则。

代码清单13-54 UserTests测试类代码

@ExtendWith(SpringExtension.class)

public class UserTests {

private static final String USER_NAME = "tianyalan";

@Test

public void testUsernameIsMoreThan5Chars() throws Exception {

User user = new User("001", USER_NAME, 38, new Date(),

"China");

assertThat(user.getName()).isEqualTo(USER_NAME);

}

}

当执行这个单元测试时,可以看到测试用例正常通过。这个单元测试用例演示了最基本的测试方式,后续的各种测试机制都会在此基础上进行扩展和演化。

测试Spring Boot数据访问层

数据需要持久化,接下来我们将从数据持久化的角度出发讨论如何对Repository层进行测试。我们先来讨论使用关系型数据库的场景,并引入针对JPA数据访问技术的@DataJpaTest测试注解。

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

代码清单13-55 H2内存数据库依赖包

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

接下来是提供具体的Repository接口,例如代码清单13-56所示的UserRepository。

代码清单13-56 UserRepository接口定义代码

public interface UserRepository extends

PagingAndSortingRepository<User, String> {

User findUserById(String id);

}

这里存在一个用于方法名衍生查询的findUserById()。基于上述UserRepository,我们可以编写如代码清单13-57所示的测试用例。

代码清单13-57 UserRepositoryTest测试类代码

@ExtendWith(SpringExtension.class)

@DataJpaTest

public class UserRepositoryTest {

@Autowired

private TestEntityManager entityManager;

@Autowired private UserRepository userRepository;

@Test

public void testFindCustomerTicketById() throws Exception {

User user = new User("001", "tianyalan", 38, new Date(),

"China");

this.entityManager.persist(user);

User actual = this.userRepository.findUserById("001");

assertThat(actual).isNotNull();

assertThat(actual.getId()).isEqualTo("001");

}

}

可以看到这里使用了@DataJpaTest注解以完成UserRepository的注入。

同时,我们还注意到另一个核心测试组件TestEntityManager。

TestEntityManager的效果相当于不使用真正的UserRepository来完成数据的持久化,从而提供了一种数据与环境之间的隔离机制。

测试Spring Boot业务逻辑层

上面我们介绍了针对数据访问层的测试方法。本节关注三层架构中的中间层,即Service层。数据访问层位于底层,而Service层依赖于数据访问层。因此,对该层的测试将使用不同的方案和技术。

1. 使用Environment测试配置信息

在前面,我们介绍了自定义配置信息的实现方式。在SpringBoot应用程序中,Service层通常会依赖于配置文件,所以我们也要对配置信息进行测试。测试配置信息的方案有两种,一种是依赖于物理配置文件,另一种则是在测试时动态注入配置信息。

第一种测试方案比较简单,当我们在src/test/resources目录下添加配置文件时,Spring Boot就会读取这些配置文件中的配置项并应用于测试案例中。在介绍具体的实现过程之前,我们有必要先来了解一下Spring Boot中的Environment接口,该接口定义如代码清单13-58所示。

代码清单13-58 Environment接口定义代码

public interface Environment extends PropertyResolver {

String[] getActiveProfiles();

String[] getDefaultProfiles();

boolean acceptsProfiles(String... profiles);

}

可以看到Environment接口的主要作用是处理Profile,而它的父接口PropertyResolver定义如代码清单13-59所示。

代码清单13-59 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配置文件中存在如代码清单13-60所示的配置项。

代码清单13-60 自定义配置项代码

springboot.user.point = 10

那么,我们就可以设计如代码清单13-61所示的测试用例。

代码清单13-61 普通EnvironmentTests测试类代码

@ExtendWith(SpringExtension.class)

@SpringBootTest

public class EnvironmentTests{

@Autowired

public Environment environment;

@Test

public void testEnvValue(){

Assert.assertEquals(10,

Integer.parseInt(environment.getProperty("springboot.user.point ")));

}

}

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

再来看第二种测试方案,除了在配置文件中设置属性,我们也可以使用@SpringBootTest注解来指定用于测试的属性值,示例代码如代码清单13-62所示。

代码清单13-62 基于@SpringBootTest注解属性值的EnvironmentTests测试

类代码

@ExtendWith(SpringExtension.class)

@SpringBootTest(properties = {"springboot.user.point=10"})

public class EnvironmentTests{

@Autowired

public Environment environment;

@Test

public void testEnvValue(){

Assert.assertEquals(10,

Integer.parseInt(environment.getProperty("springboot.user.point")));

}

}

2. 使用Mock测试Service层

因为Service层依赖于数据访问层,所以针对Service层的测试需要引入新的方法,即应用非常广泛的Mock机制。我们先来看一下Mock机制的基本概念。

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

在测试过程中,我们通常关注的是测试对象本身的功能和行为,对于测试对象涉及的一些依赖,我们仅仅关注它们与测试对象之间的交互,比如是否调用、何时调用、调用的参数、调用的次数和顺序以及返回的结果或发生的异常等。至于这些被依赖对象执行这次调用的具体细节,通常我们并不关注。因此常见的技巧就是使用Mock对象来替代真实的依赖对象,模拟真实场景来开展测试工作。使用Mock对象完成依赖关系测试的示意图如图13-7所示。

在形式上,Mock是在测试代码中直接模拟类和定义模拟方法的行为,测试代码和Mock代码通常是放在一起的,因此测试逻辑也很容易从测试用例的代码上体现出来。一起来看一下如何使用Mock来测试Service层。

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

首先来看一种简单场景,假设存在一个UserService类,该类内部使用了前面定义的UserRepository完成数据查询操作,如代码清单13-63所示。

代码清单13-63 UserService类实现代码

@Service

public class UserService {

private final UserRepository userRepository;

@Autowired

public UserService(UserRepository userRepository) {

this.userRepository = userRepository;

}

public User findUserById(String id) {

return userRepository.findUserById(id);

} ...

}

显然,对于以上UserService进行集成测试需要提供的依赖就是UserRepository,如代码清单13-64所示的代码演示了如何使用Mock机制完成对UserRepository的隔离。

代码清单13-64 UserServiceTests测试类代码

@ExtendWith(SpringExtension.class)

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)

public class UserServiceTests {

@MockBean

private UserRepository userRepository;

@Autowired

private UserService userService;

@Test

public void testFindUserById() throws Exception {

String userId = "001";

User user = new User(userId, "tianyalan", 38, new Date(),

"China");

Mockito.when(userRepository.findUserById(userId)).thenReturn(user);

User actual = userService.findUserById(userId);

assertThat(actual).isNotNull();

assertThat(actual.getId()).isEqualTo(userId);

}

}

我们首先通过@MockBean注解注入UserRepository,然后基于第三方Mock框架Mockito提供的when/thenReturn机制完成对UserRepository中findUserById ()方法的Mock。这里提供的测试用例演示了在Service层中进行集成测试的基本手段。有时候一个Service可能同时包含Repository和其他Service类或第三方组件,我们也可以采用类似的测试策略和实现方法。

测试Spring Boot Web服务层

接下来,本节关注三层架构中的最后一层,即Controller层。

针对Controller层测试,我们先来提供一个典型的Controller类,这个Controller类使用了13.4.4节中构建的UserService,如代码清单13-65所示。

代码清单13-65 UserController类实现代码

@RestController

@RequestMapping("/users")

public class UserController {

private UserService userService;

@Autowired

public UserController(UserService userService) {

this.userService = userService;

}

@GetMapping(value = "/{id}")

public User getUserById(@PathVariable String id){

return userService.findUserById(id);

}

}

针对这样的Controller类,Spring Boot中提供的测试方法相对更为丰富,包括TestRest-Template、@WebMvcTest和@AutoConfigureMockMvc注解这三种。我们一一展开讨论。

1. 使用TestRestTemplate

Spring Boot所提供的TestRestTemplate和RestTemplate非常类似,只不过它专门用于测试环境。如果我们在测试环境中使用@SpringBootTest,就可以直接使用TestRestTemplate来测试远程访问过程,示例代码如代码清单13-66所示。

代码清单13-66 UserControllerTestsWithTestRestTemplate测试类代码

@ExtendWith(SpringExtension.class)

@SpringBootTest(webEnvironment =

SpringBootTest.WebEnvironment.RANDOM_PORT)

public class UserControllerTestsWithTestRestTemplate {

@Autowired

private TestRestTemplate testRestTemplate;

@MockBean

private UserService userService;

@Test

public void testGetUserById() throws Exception {

String userId = "001";

User user = new User(userId, "tianyalan", 38, new Date(),

"China");

given(this.userService.findUserById(userId)).willReturn(user);

User actual = testRestTemplate.getForObject("/users/" +

userId, User.class);

assertThat(actual.getId()).isEqualTo(userId);

}

}

上述测试代码中,首先注意到@SpringBootTest注解通过使用SpringBootTest.WebEnvironment.RANDOM_PORT指定了随机端口的Web运行环境。然后基于TestRestTemplate发起了HTTP请求并验证了结果。使用TestRestTemplate发起请求的方式和RestTemplate完全一致,你可以回顾前面介绍RestTemplate的相关内容。

2. 使用@WebMvcTest注解

接下来的这种测试方法中,我们将引入一个新的注解@WebMvcTest,该注解将初始化测试Controller所需的Spring MVC基础设施。基于@WebMvcTest的UserController类的测试用例如代码清单13-67所示。

代码清单13-67 UserControllerTestsWithMockMvc测试类代码

@ExtendWith(SpringExtension.class)

@WebMvcTest(UserController.class)

public class UserControllerTestsWithMockMvc {

@Autowired

private MockMvc mvc;

@MockBean

private UserService userService;

@Test

public void testGetUserById() throws Exception {

String userId = "001";

User user = new User(userId, "tianyalan", 38, new Date(),

"China");

given(this.userService.findUserById(userId)).willReturn(user);

this.mvc.perform(get("/users/" +

userId).accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk())

;

}

}

以上代码的关键是MockMvc工具类。MockMvc类提供的基础方法如下所示。perform():执行一个RequestBuilder请求,会自动触发Spring MVC工作流程并映射到相应的Controller进行处理。

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

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

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

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

andReturn():返回代表请求结果的MvcResult,然后执行自定义验证或做异步处理。

执行该测试用例,我们从输出的控制台日志中发现整个流程相当于启动了User-Controller并执行远程访问,而UserController中所用到的UserService则做了Mock。显然测试UserController的目的在于验证HTTP请求返回数据的格式和内容,我们先定义了UserController将会返回的JSON结果,然后通过perform()、accept()和andExpect()组合方法最终模拟HTTP请求的整个过程并验证结果的正确性。

3. 使用@AutoConfigureMockMvc注解

到这里,请注意@SpringBootTest注解不能和@WebMvcTest注解同时使用。如果我们需要在使用@SpringBootTest注解的场景下使用MockMvc对象,可以引入@AutoConfigure-MockMvc注解。使用@AutoConfigureMockMvc注解的测试代码如代码清单13-68所示。

代码清单13-68 UserControllerTestsWithAutoConfigureMockMvc测试类代码

@ExtendWith(SpringExtension.class)

@SpringBootTest

@AutoConfigureMockMvc

public class UserControllerTestsWithAutoConfigureMockMvc {

@Autowired

private MockMvc mvc;

@MockBean

private UserService userService;

@Test

public void testGetUserById() throws Exception {

String userId = "001";

User user = new User(userId, "tianyalan", 38, new Date(),

"China");

given(this.userService.findUserById(userId)).willReturn(user);

this.mvc.perform(get("/users/" +

userId).accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk())

;

}

}

可以看到,与使用@WebMvcTest注解相比,使用@AutoConfigureMockMvc注解的唯一区别就是它需要与@SpringBootTest注解配套使用。通过与@SpringBootTest注解相结合,@AutoConfigureMockMvc注解在通过@SpringBootTest注解加载的Spring上下文环境中会自动配置MockMvc类。

前面的测试用例都是能够正常运行的。那如果我们编写的测试用例代码本身有问题会怎么样呢?假设在UserControllerTestsWithAutoConfigureMockMvc中我们不是使用get()方法发起HTTP请求,而是使用如代码清单13-69所示的post()方法。

代码清单13-69 基于post()方法发起请求代码

this.mvc.perform(post("/users/" +

userId).accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk());

这时候执行该测试用例,就会得到如代码清单13-70所示的日志信息。

代码清单13-70 基于post()方法发起请求日志信息

MockHttpServletRequest:

HTTP Method = POST

Request URI = /users/001

Parameters = {}

Headers = [Accept:"application/json"]

Body = null

Session Attrs = {}

Handler:

Type = null

Async:

Async started = false

Async result = null

Resolved Exception:

Type =

org.springframework.web.HttpRequestMethodNotSupportedException

ModelAndView:

View name = null

View = null

Model = null

FlashMap:

Attributes = null

MockHttpServletResponse:

Status = 405 Error message = Request method 'POST' not supported

Headers = [Allow:"GET"]

Content type = null

Body =

Forwarded URL = null

Redirected URL = null

Cookies = []

显然,从测试用例的执行结果中,我们就能明确得到HTTP请求方法使用不当的提示。

Spring Boot测试案例分析

结合前面关于Spring Boot应用程序各层组件的测试方法,本节将通过一个完整的案例来展示这些测试方法的实施过程及相关工具。

为了高效管理测试用例,我们有必要梳理代码的组织结构。如图13-8所示的就是案例工程中代码的基本目录结构。

上述案例实际上来源于本书第4章中介绍Spring WebMVC时所采用的案例,我们保持src/main/java和src/main/resources目录下的代码不变,而专门添加了src/test/java目录中的各个测试用例类。

在各个测试用例类中,关于针对User对象的UserTests、针对UserService的UserService-Tests,以及针对UserController的UserControllerTestsWithAutoConfigureMockMvc、UserControllerTestsWithMockMvc和UserControllerTestsWithTestRestTemplate的实现过程,我们在前面几节内容中都已经做了介绍。这里唯一没有介绍的是EmbeddedUserRepositoryTest和LiveUserRepositoryTest类。

在案例中,因为我们使用MongoDB作为数据存储的媒介,所以针对数据访问层组件的测试用例设计和实现也是围绕这个数据库展开。与传统的关系型数据库一样,针对MongoDB的测试也有两种主流的方法,一种是基于内置的嵌入式(Embedded)数据库,另一种是基于真实的数据库。

测试内嵌式MongoDB需要引入flapdoodle依赖,Flapdoodle是一个内存级别的Mongo-DB数据库,与传统关系型数据库中所使用的H2内嵌式数据库类似。Flapdoodle允许我们在不使用真实MongoDB数据库的情况下编写测试用例并执行测试。在Maven中引入flapdoodle依赖的方法如代码清单13-71所示。

代码清单13-71 flapdoodle依赖包

<dependency>

<groupId>de.flapdoodle.embed</groupId>

<artifactId>de.flapdoodle.embed.mongo</artifactId>

<scope>test</scope>

</dependency>

想要测试MongoDB,需要引入一个新的测试注解,即@DataMongoTest。

@DataMongo-Test注解会使用配置自动创建与MongoDB的连接,并基于此连接初始化ReactiveMongo-Template工具类。@DataMongoTest注解默认使用的就是基于Flapdoodle的内嵌式MongoDB实例。

现在我们编写测试类EmbeddedUserRepositoryTest,测试用例代码如代码清单13-72所示。@DataMongoTest注解为我们自动嵌入了Flapdoodle数据库。

代码清单13-72 EmbeddedUserRepositoryTest测试类代码

@ExtendWith(SpringExtension.class)

@DataMongoTest

@TestInstance(TestInstance.Lifecycle.PER_CLASS)

public class EmbeddedUserRepositoryTest {

@Autowired

private UserRepository userRepository;

@Autowired

MongoOperations operations;

@BeforeAll

public void setUp() {

operations.dropCollection(User.class);

User user = new User("001", "tianyalan", 38, new Date(),

"China");

operations.insert(user);

}

@Test

public void testFindUserById() throws Exception {

String userId = "001";

User user = userRepository.findUserById("001");

assertThat(user).isNotNull();

assertThat(user.getId()).isEqualTo(userId);

}

}

可以看到上述代码实际上由两个部分组成。首先使用一个MongoOperations对象进行数据的初始化操作。这里就用到了JUnit5所提供的@BeforeAll注解,在执行所有测试用例之前要先执行该注解所对应的代码。

请注意,因为@BeforeAll只能作用于静态类方法,如果想要在实例类方法上使用这个注解,就需要在当前类上添加@TestInstance(TestInstance.Lifecycle.PER_CLASS)注解。然后,我们通过@Test注解实现了具体的测试用例。

接下来,我们讨论如何测试真实MongoDB。在测试真实MongoDB时,我们不需要引入flapdoodle依赖,但同样需要使用@ DataMongoTest注解。然后,我们编写LiveUser-RepositoryTest类来对UserRepository进行测试,LiveUserRepositoryTest使用了真实的MongoDB数据库环境,如代码清单13-73所示。

代码清单13-73 LiveUserRepositoryTest测试类代码

@ExtendWith(SpringExtension.class)

@DataMongoTest(excludeAutoConfiguration =

EmbeddedMongoAutoConfiguration.class)

@TestInstance(TestInstance.Lifecycle.PER_CLASS)

public class LiveUserRepositoryTest {

@Autowired

private UserRepository userRepository;

@Autowired

MongoOperations operations;

@BeforeAll

public void setUp() {

operations.dropCollection(User.class);

User user = new User("001", "tianyalan", 38, new Date(),

"China");

operations.insert(user);

}

@Test

public void testFindUserById() throws Exception { String userId = "001";

User user = userRepository.findUserById("001");

assertThat(user).isNotNull();

assertThat(user.getId()).isEqualTo(userId);

}

}

相较EmbeddedUserRepositoryTest类,LiveUserRepositoryTest类只有一个地方不同,如代码清单13-74所示。

代码清单13-74 excludeAutoConfiguration配置项

@DataMongoTest(excludeAutoConfiguration =

EmbeddedMongoAutoConfiguration.class)

事实上,@DataMongoTest注解能使Spring Boot中默认使用真实MongoDB数据库的配置内容失效,而自动采用内嵌式的Flapdoodle数据库。显然,为了测试真实环境的MongoDB,我们需要把内嵌式的Flapdoodle数据库转换为真实的MongoDB数据库。代码清单13-74所示的代码展示了这一场景下的具体做法,即使用excludeAutoConfiguration属性显式排除EmbeddedMongoAutoConfiguration配置。

通过前面内容的学习,希望你已经感受到,在测试Spring Boot应用程序时,各种测试注解发挥了核心作用。表13-1罗列了我们使用到的主要测试注解及其描述。

表13-1 Spring Boot常见测试注解列表

生态扩展面试题分析

面试题1:SPI机制有什么作用?在Spring Boot中如何实现SPI机制?

答案:SPI机制本质上属于一种插件机制,在开源框架中应用非常广泛,例如本篇中提到的ShardingSphere以及Dubbo、SkyWalking等知名框架都使用该机制来确保系统的扩展性。JDK已经为开发人员提供了SPI机制的实现工具类ServiceLoader以及对应的开发约定,而Spring Boot中的自动配置功能也基于SPI机制来实现组件的动态加载。在实现过程中,Spring Boot中的SPI和JDK中的并没有太多区别,只是在部分开发约定上做了一些调整。

面试题2:Spring Boot如何控制自动配置类的加载过程?

答案:Spring Boot内置了很多自动配置的组件,而加载这些组件显然是有条件的,Spring Boot通过提供一组@ConditionalOn条件注解来控制自动配置类的加载过程。开发人员可以基于属性、类和类的实例、各种自定义表达式以及特定环境等要素来构建加载条件。这些@ConditionalOn条件注解的功能非常强大,几乎所有Spring Boot Starter都会使用到它们。

面试题3:如何实现自定义的Spring Boot Starter组件?

答案:基于Spring Boot内置的Starter开发机制,实现自定义Starter有着固定的开发步骤。首先,我们需要引入spring-boot-starter依赖包,并编写配置文件。然后,我们基于配置项内容编写自动配置类。最后,我们可以通过被动生效和主动生效这两种方式让这个自定义Starter生效。其中,被动生效使用了前面分析的SPI机制进行加载,而主动生效则需要自己定义一个@Enable注解并通过@Import注解引入到应用程序中。在本章中,我们基于ShardingSphere介绍了自定义Starter的实现过程,你可以参考这个案例,并梳理问题的答案。

面试题4:Spring Boot和Spring Cloud有什么关联关系?

答案:一方面,Spring Boot是Spring Cloud的基础,每一个微服务实际上就是一个Spring Boot应用程序。另一方面,Spring Cloud是一款集成性较强的开源框架,整合了Netflix OSS中的很多开源组件,例如Eureka、Ribbon、Hystrix等。在整合过程中,这些组件通过Spring Boot Starter进行了二次封装,从而能够集成到Spring Cloud平台中。因此,Spring Boot也充当了第三方开源框架与Spring Cloud之间的桥梁。

面试题5:Spring Native框架为Spring Boot解决了哪些问题?

答案:Spring Boot应用程序存在两大问题,一方面基于JVM的应用程序的启动时间比较长,另一方面在运行时也比较占内存。而Spring Native应用程序构建在GraalVM之上,提供了比JIT更为高效的AOT编译技术,并实现了对原生镜像的支持。开发人员可以通过Spring Native框架内置的一系列插件以及Maven命令来构建原生镜像,并在Docker环境中运行。通过本章中的案例分析,我们发现Spring Native应用程序的启动时间可以控制在100ms之内。

面试题6:如果在测试过程中需要隔离对其他组件的依赖,你有什么办法?

答案:组件隔离是我们实施集成测试的最基本需求,而Mock机制为实现这一需求提供了良好的技术支持。业界关于如何实现Mock机制提供了一系列实用的测试框架,在Spring Boot中,我们可以基于Mockito框架来实现对隔离组件的Mock操作。同时,Spring Boot内置了@SpringBootTest注解来对整个测试环境进行Mock,在日常测试过程中,我们经常会用到该注解的这一功能特性。

面试题7:在使用Spring Boot时,如何对Web服务的正确性进行测试?

答案:Spring Boot对Web服务的测试支持是非常完善的,一共提供了TestRestTemplate、@WebMvcTest注解和MockMvc这三套解决方案。这三套解决方案非常类似,只是在使用方式上有所区别。其中TestRestTemplate的使用方式与第4章中介绍的RestTemplate模板工具类一致,而@WebMvcTest注解与@AutoConfigureMockMvc注解内部都使用了MockMvc工具类来实现对远程请求的模拟,区别在于@AutoConfigureMockMvc注解可以和@SpringBootTest注解一起使用,而@WebMvcTest注解则不能。

本章小结

自动配置是Spring Boot最核心的功能特性。在本章一开始,我们就对这一功能特性展开了讨论,并基于源码分析了自动配置的内部实现原理。我们发现,Spring Boot采用了与JDK中SPI机制类似的实现方式来动态管理各种外部组件。而Spring Boot的Starter机制构建在自动配置的实现原理之上,允许开发人员开发自定义的Starter。本章也通过一个案例详细分析了自定义Starter的实现过程。

本章第二部分关注微服务系统的构建,这也是Spring Boot最常见的应用场景之一,因为每个微服务实际上就是一个Spring Boot应用程序。在本章中,我们基于Spring家族的Spring Cloud框架实现了一个系统性的案例,并展示了Spring Boot在微服务系统中的使用方式。

本章第三部分关注云原生,这块内容对于Spring Boot而言还处于试验阶段,但Spring Boot也专门实现了一个Spring Native框架来支持云原生。

本章通过一个系统性案例展示了如何使用Spring Native来构建基于GraalVM的原生镜像。我们发现,基于Spring Native构建的应用程序比传统的SpringBoot应用程序的启动时间更短,占用的资源也更少。

测试是一套独立的技术体系,需要开发人员充分重视且付诸实践,这点对于Web应用程序测试领域而言更是如此。本章最后一部分基于Spring Boot给出了完整的测试方法及核心注解,并基于具体的案例,分别讨论了如何针对Spring Boot应用程序中的数据访问层、业务逻辑层和Web服务层组件开展系统测试。

本文给大家讲解的内容是监控和扩展:SpringBoot生态体系及扩展,测试Spring Boot

  • 下文给大家讲解的是高性能Java核心知识概述

Tags:

最近发表
标签列表