Spring Boot控制器测试崩溃:JwtService Bean缺失问题求助
问题:Spring Boot控制器测试因缺失JwtService Bean崩溃
在Spring Boot应用中添加认证功能后,运行控制器测试时所有测试均失败,报错如下:
Parameter 0 of constructor in com.league_buddies.backend.security.authentication.AuthenticationFilter required a bean of type 'com.league_buddies.backend.security.jwt.JwtService' that could not be found.
应用运行时无异常,但测试阶段触发该错误。相关代码如下:
配置类ApplicationConfiguration
@Configuration @RequiredArgsConstructor public class ApplicationConfiguration { private final UserService userService; @Bean public UserDetailsService userDetailsService() { return username -> userService.findByEmailAddress(username); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public AuthenticationProvider authenticationProvider() { DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(); daoAuthenticationProvider.setUserDetailsService(userDetailsService()); daoAuthenticationProvider.setPasswordEncoder(passwordEncoder()); return daoAuthenticationProvider; } @Bean public AuthenticationManager authenticationManager() { ProviderManager providerManager = new ProviderManager(authenticationProvider()); return providerManager; } }
AuthenticationFilter类
@Component @RequiredArgsConstructor public class AuthenticationFilter extends OncePerRequestFilter { private final JwtService jwtService; private final UserDetailsService userDetailsService; @Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { final String authorization = request.getHeader("Authorization"); final String token; if (authorization == null || !authorization.startsWith("Bearer ")) { filterChain.doFilter(request, response); return; } // JWT tokens start with "Bearer ". We have to remove that part to get the actual token. token = authorization.substring("Bearer ".length()); String username = jwtService.extractSubject(token); if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); if(jwtService.isTokenValid(token, userDetails)) { // This is the recommended way for setting the context by Spring Security. SecurityContext context = SecurityContextHolder.createEmptyContext(); context.setAuthentication(new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities() )); SecurityContextHolder.setContext(context); } } filterChain.doFilter(request, response); } }
JwtService类
@Service public class JwtService { // TODO move this to application config level. private final String privateKey = "SDGJKHJK35346KHJ4/sdfjkhsdka98dsgd357qjahf2897452381!"; public String generateToken(String username) { return Jwts .builder() .setSubject(username) .setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis() + 86400000)) .signWith(getSignInKey(), SignatureAlgorithm.HS256) .compact(); } private Claims extractClaims(String token) { return Jwts.parserBuilder() .setSigningKey(getSignInKey()) .build() .parseClaimsJws(token) .getBody(); } public String extractSubject(String token) { return extractClaims(token).getSubject(); } private Date extractExpiration(String token) { return extractClaims(token).getExpiration(); } private Key getSignInKey() { byte[] keyBytes = Decoders.BASE64.decode(privateKey); return Keys.hmacShaKeyFor(keyBytes); } private boolean isTokenExpired(String token) { return extractExpiration(token).before(new Date()); } public boolean isTokenValid(String token, UserDetails userDetails) { return extractSubject(token).equals(userDetails.getUsername()) && !isTokenExpired(token); } }
控制器测试类UserControllerTest
@ExtendWith(MockitoExtension.class) @WebMvcTest(UserControllerTest.class) class UserControllerTest { // TODO Make a constant for the API url to avoid writing it over and over. @Mock private UserRepository userRepository; private UserService userService; private UserController userController; @Autowired CustomExceptionHandler customExceptionHandler; @Autowired private MockMvc mockMvc; @Autowired private ObjectMapper objectMapper; private long id; private Optional<User> optionalUser; private String displayName; private String email; private String password; @BeforeEach void setUp() { // Stub user service with mock user repository userService = new UserService(userRepository); // Start controller with user service that has the mock user repository userController = new UserController(userService); // Builds a MockMvcBuilder and passes it the CustomExceptionHandler // so that it can use it, otherwise it will use the default one Spring Boot // comes with. mockMvc = MockMvcBuilders.standaloneSetup(userController) .setControllerAdvice(customExceptionHandler) .build(); id = 1L; displayName = "Isolated"; email = "email@gmail.com"; password = "pw12345"; User user = new User(); user.setId(id); user.setDisplayName(displayName); user.setEmailAddress(email); user.setPassword(password); optionalUser = Optional.of(user); } @Test public void throwsWhenUserDoesNotExistInDatabaseWithGivenId() throws Exception { // Arrange when(userRepository.findById(anyLong())).thenReturn(Optional.empty()); // Act MockHttpServletResponse response = mockMvc.perform( get(String.format("/api/v1/user/%d", id)).accept(MediaType.APPLICATION_JSON)) .andReturn().getResponse(); ApiException apiException = objectMapper.readValue(response.getContentAsString(), ApiException.class); // Assert assertEquals(HttpStatus.NOT_FOUND.value(), response.getStatus()); assertEquals(String.format("User with id: %d was not found.", id), apiException.getMessage()); } @Test public void throwsWhenGivenNegativeValueToGetById() throws Exception { // Act id = -1L; MockHttpServletResponse response = mockMvc.perform( get(String.format("/api/v1/user/%s", id)).accept(MediaType.APPLICATION_JSON)) .andReturn().getResponse(); ApiException apiException = objectMapper.readValue(response.getContentAsString(), ApiException.class); // Assert assertEquals(HttpStatus.BAD_REQUEST.value(), response.getStatus()); assertEquals("Id cannot be negative.", apiException.getMessage()); } }
尝试过通过配置文件禁用Spring Security、使用@WithMockUser注解均无效。
解决方案
问题根源在于@WebMvcTest注解的特性:它只会加载控制器相关的Bean,而你的AuthenticationFilter作为Spring Security的过滤器被自动注册到测试上下文,但它依赖的JwtService并没有被加载。同时你手动构建了MockMvc(standaloneSetup),但Spring Security的过滤器依然会被上下文尝试初始化,导致依赖缺失。
方案1:在测试中排除Spring Security自动配置
在测试类上添加@AutoConfigureMockMvc(addFilters = false),同时修正@WebMvcTest的参数(应该指定实际控制器类而非测试类):
@ExtendWith(MockitoExtension.class) @WebMvcTest(UserController.class) @AutoConfigureMockMvc(addFilters = false) class UserControllerTest { // ... 原有测试代码 }
方案2:手动Mock所需的Security依赖Bean
如果需要保留Security逻辑进行测试,可以在测试类中用@MockBean注解MockJwtService和UserDetailsService,让Spring测试上下文自动注入这些Bean到过滤器中:
@ExtendWith(MockitoExtension.class) @WebMvcTest(UserController.class) class UserControllerTest { @Mock private UserRepository userRepository; @MockBean private JwtService jwtService; @MockBean private UserDetailsService userDetailsService; private UserService userService; private UserController userController; @Autowired private MockMvc mockMvc; @Autowired private ObjectMapper objectMapper; @Autowired private CustomExceptionHandler customExceptionHandler; @BeforeEach void setUp() { userService = new UserService(userRepository); userController = new UserController(userService); // 移除手动构建MockMvc的代码,直接使用@Autowired的MockMvc即可 id = 1L; displayName = "Isolated"; email = "email@gmail.com"; password = "pw12345"; User user = new User(); user.setId(id); user.setDisplayName(displayName); user.setEmailAddress(email); user.setPassword(password); optionalUser = Optional.of(user); } // ... 测试方法 }
方案3:使用@SpringBootTest替代@WebMvcTest(适合集成测试)
如果需要完整的应用上下文,可以改用@SpringBootTest配合@AutoConfigureMockMvc,但测试启动速度会变慢:
@SpringBootTest @AutoConfigureMockMvc class UserControllerTest { // ... 测试代码 }
内容的提问来源于stack exchange,提问作者Noel Covarrubias
相关产品推荐
相关产品推荐

