Spring控制器测试中Mock WebSecurity依赖的实现方案
你碰到的这个问题很典型——@WebMvcTest作为Spring的切片测试注解,只会加载控制器、WebMvc相关配置以及Spring Security的基础配置,不会自动扫描和加载@Component、@Service、@Repository这类业务层Bean。但你的WebSecurityConfig依赖了MyUserDetailsService,而这个服务又依赖了UserObjectRepository,切片上下文里没有这些Bean,自然就抛出找不到Bean的异常了。
下面给你两种最常用的解决方案,都能在不启动完整上下文的情况下让测试正常运行:
方案一:用@MockBean快速Mock依赖服务
这是最简单直接的方式,我们只需要在测试类中给MyUserDetailsService添加@MockBean注解,Spring会自动创建这个服务的Mock实例并注册到切片上下文里,满足WebSecurityConfig的依赖需求。
因为我们用了@WithMockUser、@WithAnonymousUser这类Spring Security测试注解,测试过程中根本不会实际调用MyUserDetailsService的逻辑,所以这个Mock实例完全够用,不需要关心它的具体实现。
修改后的测试类代码如下:
@WebMvcTest(DemoController.class) class DemoControllerTests { @Autowired private MockMvc mockMvc; // Mock掉WebSecurityConfig依赖的MyUserDetailsService @MockBean private MyUserDetailsService myUserDetailsService; @Test @WithAnonymousUser void shouldNotHaveAccessWhenAnonymous() throws Exception { this.mockMvc.perform(get("/")) .andExpect(status().isUnauthorized()); } @Test @WithMockUser(username = "pascal", roles = "USER") void shouldHaveAccessWithUserRole() throws Exception { this.mockMvc.perform(get("/hello")) .andExpect(status().isOk()) .andExpect(content().string("Hello")); } // 顺便补充测试@PreAuthorize的admin接口权限控制 @Test @WithMockUser(username = "admin", roles = "ADMIN") void shouldAccessAdminEndpointWithAdminRole() throws Exception { this.mockMvc.perform(get("/admin")) .andExpect(status().isOk()) .andExpect(content().string("Admin")); } @Test @WithMockUser(username = "pascal", roles = "USER") void shouldDenyAccessToAdminEndpointForUser() throws Exception { this.mockMvc.perform(get("/admin")) .andExpect(status().isForbidden()); } }
这里不需要额外Mock UserObjectRepository,因为MyUserDetailsService已经是Mock实例了,它的方法默认不会触发对Repository的调用。
方案二:自定义测试专用的Spring Security配置
如果你想让测试环境更贴近真实的安全规则,但又不想加载整个业务层,可以自定义一个测试用的Security配置类,用@Import引入到测试中。
比如我们可以用InMemoryUserDetailsManager来模拟用户数据,代替真实的MyUserDetailsService:
@WebMvcTest(DemoController.class) @Import(TestSecurityConfig.class) class DemoControllerTests { @Autowired private MockMvc mockMvc; @Test @WithAnonymousUser void shouldNotHaveAccessWhenAnonymous() throws Exception { this.mockMvc.perform(get("/")) .andExpect(status().isUnauthorized()); } @Test @WithMockUser(username = "pascal", roles = "USER") void shouldHaveAccessWithUserRole() throws Exception { this.mockMvc.perform(get("/hello")) .andExpect(status().isOk()) .andExpect(content().string("Hello")); } // 测试admin接口权限 @Test @WithMockUser(username = "admin", roles = "ADMIN") void shouldAccessAdminEndpointWithAdminRole() throws Exception { this.mockMvc.perform(get("/admin")) .andExpect(status().isOk()) .andExpect(content().string("Admin")); } // 自定义测试用的Security配置 @Configuration @EnableWebSecurity @EnableMethodSecurity // 开启方法级安全,对应控制器的@PreAuthorize static class TestSecurityConfig { @Bean public UserDetailsService userDetailsService() { // 创建测试用的用户和管理员 UserDetails user = User.withUsername("pascal") .password("{noop}test123") // {noop}表示不加密密码 .roles("USER") .build(); UserDetails admin = User.withUsername("admin") .password("{noop}admin123") .roles("ADMIN") .build(); return new InMemoryUserDetailsManager(user, admin); } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(auth -> auth .anyRequest().authenticated() // 所有请求都需要认证 ) .formLogin(Customizer.withDefaults()); // 启用默认表单登录(测试用) return http.build(); } } }
这种方式适合你需要验证更复杂的安全规则,同时又不想依赖真实业务Bean的场景。
总结
- 如果只是想快速验证控制器的权限逻辑,**方案一(@MockBean)**是最优选择,简洁高效;
- 如果需要更贴近真实的安全配置测试,可以用方案二(自定义测试配置);
- 始终记住@WebMvcTest的核心是切片测试,要尽量隔离控制器和下层业务依赖,专注于验证Web层的逻辑。
内容的提问来源于stack exchange,提问作者pas2al

