如何在Spring Boot Controller单元测试中模拟Security Context Holder获取用户名
问题描述
我要编写修改用户个人信息的单元测试,但不知道如何在单元测试中模拟Security Context Holder,尤其是需要提取用户名以通过User Repository查询获取用户信息。此前已通过Postman成功调用该编辑信息API,但调用前需先登录并使用Bearer JWT。
单元测试执行后返回错误:
java.lang.AssertionError: Status expected:<200> but was:<400> Expected :200 Actual :400
相关代码如下:
单元测试代码
@Test public void whenSendRequestToModifyUserInformation_returnUserWithNewInformation () throws Exception { String userName = "thanhnghi"; InformationRespondDTO informationRespondDTO = mock(InformationRespondDTO.class); Information information = mock(Information.class); ObjectMapper objectMapper = new ObjectMapper(); ModifyUserRequestDTO modifyUserRequestDTO = ModifyUserRequestDTO.builder() .dateOfBirth(new Date()) .firstName("Martin") .lastName("Charlie") .address("12 Washington District") .phoneNumber("0794562342") .email("martinCharlie@gmail.com").build(); ; when(informationService.update(modifyUserRequestDTO)).thenReturn(information); when(informationMapper.toDTO(information)).thenReturn(informationRespondDTO); mvc.perform(MockMvcRequestBuilders.put("/api/users/information") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(modifyUserRequestDTO)) ) .andExpect(status().isOk()) .andDo(print()); }
Service代码
@Override public Information update(ModifyUserRequestDTO modifyUserRequestDTO) { String userName = userLocal.getLocalUserName(); Users users = this.userService.findByUserName(userName); Information information = informationMapper.toExistedInformation(modifyUserRequestDTO, users.getInformation()); return this.informationRepository.save(information); }
UserLocal组件(获取Security上下文用户名)
@Component public class UserLocal { public String getLocalUserName(){ String userName = SecurityContextHolder.getContext().getAuthentication().getName(); if(userName == null){ throw new ResourceNotFoundException("You haven't Login !!!"); } return userName; } }
Controller代码
@RestController @RequestMapping("/api/users") @CrossOrigin(maxAge = 3600, origins = "*") public class UserController { UserService userService; InformationService informationService; InformationMapper informationMapper; @Autowired public UserController(UserService userService, InformationService informationService, InformationMapper informationMapper) { this.userService = userService; this.informationService = informationService; this.informationMapper = informationMapper; } @PutMapping ("/information" ) public InformationRespondDTO modifyInformation(@RequestBody @Valid ModifyUserRequestDTO modifyUserRequestDTO){ Information information = this.informationService.update(modifyUserRequestDTO); return informationMapper.toDTO(information); } }
ModifyUserRequestDTO代码
@Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor public class ModifyUserRequestDTO { @NotNull(message = "date of birth is required") private Date dateOfBirth; @Pattern(regexp = "[A-Za-z]+", message = "First name cannot be number or special characters") @NotNull(message = "First name cannot be null") private String firstName; @Pattern(regexp = "[A-Za-z]+", message = "Last name cannot be number or special characters") @NotNull(message = "Last name is required") @NotEmpty(message = "Last name must not be empty") private String lastName; @NotNull(message = "Address is required") @NotEmpty(message = "Address must not be empty") private String address; @Size(min = 10, max = 11, message = "Phone number must has at least 11 characters and no more") @NotNull(message = "phone number is required") @NotEmpty(message = "phone number must not be empty") private String phoneNumber; @Email(message = "Invalid Email Address") @NotNull(message = "email is required") @NotEmpty(message = "email must not be empty") private String email; }
解决方案
原因分析
返回400的核心原因有两个:
- Security上下文未初始化:单元测试中没有设置登录状态,
UserLocal.getLocalUserName()会获取不到认证信息,抛出ResourceNotFoundException,或请求被Spring Security拦截返回400。 - DTO序列化验证问题:
Date类型直接序列化可能格式不符合要求,触发@Valid校验失败返回400。
解决步骤
方法1:使用Spring Security测试注解快速模拟登录
Spring Security提供@WithMockUser注解,可直接在测试方法或类上添加,模拟已登录用户:
修改后的单元测试代码:
import org.springframework.security.test.context.support.WithMockUser; import java.text.SimpleDateFormat; @Test @WithMockUser(username = "thanhnghi") // 模拟登录用户名为thanhnghi的用户 public void whenSendRequestToModifyUserInformation_returnUserWithNewInformation () throws Exception { String userName = "thanhnghi"; InformationRespondDTO informationRespondDTO = mock(InformationRespondDTO.class); Information information = mock(Information.class); ObjectMapper objectMapper = new ObjectMapper(); // 配置Date序列化格式,避免DTO校验失败 objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd")); ModifyUserRequestDTO modifyUserRequestDTO = ModifyUserRequestDTO.builder() .dateOfBirth(new Date()) .firstName("Martin") .lastName("Charlie") .address("12 Washington District") .phoneNumber("0794562342") .email("martinCharlie@gmail.com").build(); when(informationService.update(modifyUserRequestDTO)).thenReturn(information); when(informationMapper.toDTO(information)).thenReturn(informationRespondDTO); mvc.perform(MockMvcRequestBuilders.put("/api/users/information") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(modifyUserRequestDTO)) ) .andExpect(status().isOk()) .andDo(print()); }
方法2:手动设置SecurityContextHolder
如果需要更灵活的认证信息模拟,可手动在测试方法中设置Security上下文:
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import java.util.Collections; import java.text.SimpleDateFormat; @Test public void whenSendRequestToModifyUserInformation_returnUserWithNewInformation () throws Exception { String userName = "thanhnghi"; // 手动设置Security上下文 Authentication authentication = new UsernamePasswordAuthenticationToken(userName, null, Collections.emptyList()); SecurityContextHolder.getContext().setAuthentication(authentication); InformationRespondDTO informationRespondDTO = mock(InformationRespondDTO.class); Information information = mock(Information.class); ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd")); ModifyUserRequestDTO modifyUserRequestDTO = ModifyUserRequestDTO.builder() .dateOfBirth(new Date()) .firstName("Martin") .lastName("Charlie") .address("12 Washington District") .phoneNumber("0794562342") .email("martinCharlie@gmail.com").build(); when(informationService.update(modifyUserRequestDTO)).thenReturn(information); when(informationMapper.toDTO(information)).thenReturn(informationRespondDTO); mvc.perform(MockMvcRequestBuilders.put("/api/users/information") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(modifyUserRequestDTO)) ) .andExpect(status().isOk()) .andDo(print()); // 测试结束后清理Security上下文,避免影响其他测试 SecurityContextHolder.clearContext(); }
额外注意事项
- Date序列化问题:使用
ObjectMapper时指定DateFormat,确保Date字段序列化格式符合后端校验要求,避免因格式问题触发@Valid校验失败。 - Mock依赖完整性:如果
userService.findByUserName(userName)在测试中会被调用,需补充对userService的mock,避免空指针异常:Users mockUser = new Users(); mockUser.setInformation(new Information()); when(userService.findByUserName(userName)).thenReturn(mockUser);
内容的提问来源于stack exchange,提问作者Monach
相关产品推荐
相关产品推荐

