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

如何在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的核心原因有两个:

  1. Security上下文未初始化:单元测试中没有设置登录状态,UserLocal.getLocalUserName()会获取不到认证信息,抛出ResourceNotFoundException,或请求被Spring Security拦截返回400。
  2. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 05:30:59