无法mock继承WebSecurityConfigurerAdapter的WebConfig类,求JUnit测试方案
解决方案
问题根源
原测试无法正常验证逻辑有两个核心原因:
WebConfig中依赖的UserDetailsService、JwtRequestFilter未被Mock,@InjectMocks无法完成完整的依赖注入,会触发空指针异常HttpSecurity是链式调用API,Mockito默认Mock对象的方法返回值为null,调用http.csrf().disable()时第一步返回null就会中断调用链,无法走完完整的配置逻辑
修正后的单元测试实现
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.test.util.ReflectionTestUtils; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) public class WebConfigTest { private WebConfig webConfig; @Mock private UserDetailsService userDetailsService; @Mock private JwtRequestFilter jwtRequestFilter; @Mock private AuthenticationManagerBuilder auth; @Mock private HttpSecurity http; // 提前Mock链式调用需要的返回对象 @Mock private CsrfConfigurer<HttpSecurity> csrfConfigurer; @Mock private ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry interceptUrlRegistry; @BeforeEach void setUp() { webConfig = new WebConfig(); // 手动注入私有依赖,比@InjectMocks可控性更高 ReflectionTestUtils.setField(webConfig, "userDetailsService", userDetailsService); ReflectionTestUtils.setField(webConfig, "jwtRequestFilter", jwtRequestFilter); // 配置HttpSecurity链式调用的返回值,保证调用链不中断 when(http.csrf()).thenReturn(csrfConfigurer); when(csrfConfigurer.disable()).thenReturn(http); when(http.addFilterBefore(eq(jwtRequestFilter), eq(UsernamePasswordAuthenticationFilter.class))).thenReturn(http); when(http.authorizeRequests()).thenReturn(interceptUrlRegistry); when(interceptUrlRegistry.antMatchers( "/v2/api-docs", "/swagger-resources/**", "/swagger-ui.html", "/webjars/**", "/swagger.json" )).thenReturn(interceptUrlRegistry); when(interceptUrlRegistry.permitAll()).thenReturn(interceptUrlRegistry); } @Test void testConfigureAuthentication() throws Exception { webConfig.configure(auth); // 验证认证管理器正确绑定了自定义UserDetailsService verify(auth, times(1)).userDetailsService(eq(userDetailsService)); } @Test void testConfigureHttpSecurity() throws Exception { webConfig.configure(http); // 验证CSRF禁用逻辑生效 verify(http, times(1)).csrf(); verify(csrfConfigurer, times(1)).disable(); // 验证JWT过滤器正确添加到过滤链 verify(http, times(1)).addFilterBefore(eq(jwtRequestFilter), eq(UsernamePasswordAuthenticationFilter.class)); // 验证Swagger相关接口白名单配置正确 verify(http, times(1)).authorizeRequests(); verify(interceptUrlRegistry, times(1)).antMatchers( "/v2/api-docs", "/swagger-resources/**", "/swagger-ui.html", "/webjars/**", "/swagger.json" ); verify(interceptUrlRegistry, times(1)).permitAll(); } }
注意:
ReflectionTestUtils是spring-test模块提供的工具类,正常Spring项目的测试依赖都会包含该类。
推荐集成测试方案
如果不需要做纯单元测试,用Spring Security官方测试模块做集成测试更简单,不需要手动Mock复杂的链式API,直接通过请求验证规则即可:
import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.web.servlet.MockMvc; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest @AutoConfigureMockMvc public class WebConfigIntegrationTest { @Autowired private MockMvc mockMvc; @Test void testSwaggerEndpointIsPublic() throws Exception { mockMvc.perform(get("/v2/api-docs")) .andExpect(status().isOk()); } @Test void testBusinessEndpointNeedAuth() throws Exception { mockMvc.perform(get("/api/user/info")) .andExpect(status().isUnauthorized()); } }
这种方案更贴近真实运行场景,验证结果可信度更高。
内容的提问来源于stack exchange,提问作者Saurabh Jhunjhunwala
相关产品推荐
相关产品推荐

