You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 14:12:03