一、问题背景

Spring Boot 框架下一般会分层测试 Controller、Service 和 Repository 层,单测和集成测试能够在上线前发现 SQL 语句,环境配置等常见问题。

Controller层涉及 Restful 请求,Header 头,请求体,filter,interceptor,认证和授权。自测起来比 Service 层和 Repository 层麻烦一些。那么 Controller 层一般怎么测试呢?

二、Controller 层测试

2.1 万能的@SpringBootTest

Service 层和 Repository 层测试简单的方法是通过@SpringBootTest。Controller 层当然也可以通过@SpringBootTest,但是因为涉及发起 Restful web 端点请求,还需要借助@AutoConfigureMockMvc注入MockMvc。类似下面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@SpringBootTest
@AutoConfigureMockMvc
class UserControllerTest {
@Autowired
private MockMvc mockMvc;

@Test
void getUserById() throws Exception {
String requestBody = "{\"userName\": xxx}";
String strTime = System.currentTimeMillis() + "";
MvcResult mvcResult = mockMvc.perform(post("/api/user/id")
.contentType("application/json") // 设置内容类型为JSON
.header("X-Request-Timestamp", strTime)
.header("X-Request-Signature", SignUtil.generateSignature("POST",
"/api/user/id",
strTime, "xxx"))
.content(requestBody)) // 设置请求体
.andExpect(status().isOk()) // 验证状态码为200
.andReturn();
//.andExpect(content().string("Received: " + requestBody)); // 验证响应体
System.out.println("Response Content: " + mvcResult.getResponse().getContentAsString());
}
...
}

2.2 @WebMvcTest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@WebMvcTest(controllers = RealUserController.class)
@ContextConfiguration(classes = {RealUserController.class}) // 仅加载 Controller 的配置
@AutoConfigureMockMvc(addFilters = false)
public class RealUserControllerTest {
@Autowired
private MockMvc mockMvc;

// controller 依赖的其他 service bean
@MockBean
TrUserService userService;


@Test
void getUserRealByPhone() throws Exception {
String requestBody = "{\"userPhone\": \"xxx\"}";
String strTime = System.currentTimeMillis() + "";
MvcResult mvcResult = mockMvc.perform(post("/api/user/real/phone")
.contentType("application/json") // 设置内容类型为JSON
.header("X-Request-Time", strTime)
.header("X-Request-Signature", SignUtil.generateSignature("POST",
"/api/user/real/phone",
strTime, "DFC54F0E-54AA-4D53-98BC-E6BAEBCB63C2"))
.content(requestBody)) // 设置请求体
.andExpect(status().isOk()) // 验证状态码为200
.andReturn();
//.andExpect(content().string("Received: " + requestBody)); // 验证响应体
mvcResult.getResponse().setCharacterEncoding("UTF-8");
System.out.println("Response Content: " + mvcResult.getResponse().getContentAsString());
}

@WebMvcTest是 Controller 层推荐的测试方式,原因是轻量,速度快,只加载 Web 层组件比如 Controller,Filter,MVC 配置等。实践中使用这种方法遇到的问题比较多,举一个例子,生产环境一般启用 Spring Security 提供认证和授权,测试过程中如果也启用,安全相关配置和组件类让测试依赖管理变得复杂,测试用例不能执行。

一个比较坑的地方是因为@WebMvcTest只在 Spring Context 中加载Web 层组件,忽略 Service,Repository 层服务,那么 Controller 如果依赖了服务层和 JPA 的功能,就会出现找不到类的异常。

Mock Service层

一般 Service 层可以通过 @MockBean 方式来解决,隔离服务层逻辑。

禁止JPA层服务

Controller 层不要引用 JPA 层服务,不符合分层设计的理念。实践中可能在 Controller 层用到了JPA @Entity 类,一般应该转化为 Dto 类;Spring Boot Application 启用了@EnableJpaAuditing等,会触发JPA metamodel must not be empty!问题。 尝试过滤 JPA 相关自动配置没有成功。比较有效的方式是通过@ContextConfiguration(classes = {RealUserController.class}) 精确指定加载类,这样测试框架才不会去找JPA 和 Spring Security 相关的自动配置类了。

1
2
3
4
5
6
// 这种方式禁止 JPA 自动配置没有生效
@WebMvcTest(controllers = RealUserController.class, excludeFilters = {
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = {JpaRepositoriesAutoConfiguration.class,
HibernateJpaAutoConfiguration.class})

})

禁止 Spring Security 组件

解决 Spring Security 相关依赖问题包括SecurityAutoConfiguration自动配置和SecurityFilterChain的加载。

禁止SecurityFilterChain

SecurityFilterChain有两种方式可以禁止加载。如上面例子中@AutoConfigureMockMvc(addFilters = false);还有一种方式是在Controller 层测试用例中定义并 import TestConfiguration。个人喜欢用第一种方式,代码简洁一些。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@WebMvcTest(controllers = RealUserController.class)
@Import(RealUserControllerTest.NoSecurityConfig.class)
@ContextConfiguration(classes = {RealUserController.class}) // 仅加载 Controller 的配置
public class RealUserControllerTest {
...


@TestConfiguration
static class NoSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 禁用所有安全配置
http.authorizeRequests().anyRequest().permitAll()
.and().csrf().disable();
return http.build();
}
}

}

禁止SecurityAutoConfiguration自动配置

实践中在测试类中加下面配置并不能生效。
@EnableAutoConfiguration(exclude = {SecurityAutoConfiguration.class})

比较有效的方式还是类似 JPA,在测试类上使用@ContextConfiguration(classes = {RealUserController.class})显式指定配置加载的类,可以解决这里的问题。

三、结论

Controller 层测试建议使用下面的注解。

1
2
3
4
5
6
7
8
9
10
11
12
@WebMvcTest(controllers = RealUserController.class)
@ContextConfiguration(classes = {RealUserController.class})
@AutoConfigureMockMvc(addFilters = false)
public class RealUserControllerTest {
@Autowired
private MockMvc mockMvc;

@MockBean
UserService userService;

...
}

如果依赖太多,@MockBean需要很多或者禁止自动配置方案失效,也可以使用下面通用方法。代价是慢。

1
2
3
4
5
6
7
8
@SpringBootTest
@AutoConfigureMockMvc
public class RealUserControllerTest {
@Autowired
private MockMvc mockMvc;

...
}

四、注意事项

  1. 单元测试执行时间尽量少,在 CI/CD 流水线中不能占有太多时间。建议只测当前模块的功能,依赖尽量走 Mock 比如采用 Mockito。当然依赖服务处理是个难点,这是为什么实际场景中我们直接使用@SpringBootTest拉起整个 Spring容器的原因,代价是慢

  2. pom文件中不要忘了引入 test starter

1
2
3
4
5
6
<!--Spring boot 测试-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
  1. @ActiveProfiles(“dev”)单测类中指定 spring.profiles.active,应用测试配置,避免对其他环境数据库造成污染,或者配置不确定问题

  2. 本文笔者使用的 Spring Boot版本为2.6.4,不同版本可能会有差异