一、问题背景 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" ) .header("X-Request-Timestamp" , strTime) .header("X-Request-Signature" , SignUtil.generateSignature("POST" , "/api/user/id" , strTime, "xxx" )) .content(requestBody)) .andExpect(status().isOk()) .andReturn(); 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}) @AutoConfigureMockMvc(addFilters = false) public class RealUserControllerTest { @Autowired private MockMvc mockMvc; @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" ) .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()) .andReturn(); 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}) 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; ... }
四、注意事项
单元测试执行时间尽量少,在 CI/CD 流水线中不能占有太多时间。建议只测当前模块的功能,依赖尽量走 Mock 比如采用 Mockito。当然依赖服务处理是个难点,这是为什么实际场景中我们直接使用@SpringBootTest拉起整个 Spring容器的原因,代价是慢
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>
@ActiveProfiles(“dev”)单测类中指定 spring.profiles.active,应用测试配置,避免对其他环境数据库造成污染,或者配置不确定问题
本文笔者使用的 Spring Boot版本为2.6.4,不同版本可能会有差异