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

会员注册Controller单元测试状态码异常:预期201实际返回200

问题排查:单元测试返回200而非预期201状态码

问题根源

你的MemberApiController的join方法返回自定义BaseResponse对象时,Spring MVC默认会将这类普通对象返回视为成功响应,默认返回200 OK状态码。而Postman调用能返回201,大概率是你的项目中通过全局响应拦截器、统一返回值处理逻辑等方式,在实际运行时额外绑定了HTTP状态码,但单元测试环境没有触发该逻辑,或者控制器方法本身没有显式指定HTTP响应状态码。

解决方案

两种常见修复方式:

1. 改用ResponseEntity包装返回值

修改控制器方法,显式通过ResponseEntity指定HTTP状态码:

@PostMapping(value = "/join", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE, MediaType.APPLICATION_JSON_VALUE})
public ResponseEntity<BaseResponse<MemberResponseDto>> join(
        @RequestPart(value = "profileImage", required = false) final MultipartFile image,
        @Valid @RequestPart("requestDto") final MemberSaveRequestDto requestDto
) throws IOException {
    final MemberResponseDto result = memberService.saveMember(image, requestDto);

    return ResponseEntity.status(BaseResponseStatus.JOIN_SUCCESS.getHttpStatus())
            .body(BaseResponse.of(BaseResponseStatus.JOIN_SUCCESS, result));
}

确保BaseResponseStatus.JOIN_SUCCESS中持有HttpStatus.CREATED实例。

2. 在控制器方法上添加@ResponseStatus注解

如果不想修改返回类型,直接在方法上标注对应状态码即可:

@PostMapping(value = "/join", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE, MediaType.APPLICATION_JSON_VALUE})
@ResponseStatus(HttpStatus.CREATED)
public BaseResponse<MemberResponseDto> join(
        @RequestPart(value = "profileImage", required = false) final MultipartFile image,
        @Valid @RequestPart("requestDto") final MemberSaveRequestDto requestDto
) throws IOException {
    final MemberResponseDto result = memberService.saveMember(image, requestDto);

    return BaseResponse.of(BaseResponseStatus.JOIN_SUCCESS, result);
}

验证修改

调整后重新运行单元测试,result.andExpect(status().isCreated())会正常通过,因为Spring会正确将HTTP响应状态码设置为201。


相关代码参考

1. MemberApiController 原代码

@PostMapping(value = "/join", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE, MediaType.APPLICATION_JSON_VALUE})
public BaseResponse<MemberResponseDto> join(
        @RequestPart(value = "profileImage", required = false) final MultipartFile image,
        @Valid @RequestPart("requestDto") final MemberSaveRequestDto requestDto
) throws IOException {
    final MemberResponseDto result = memberService.saveMember(image, requestDto);

    return BaseResponse.of(
            BaseResponseStatus.JOIN_SUCCESS, // HttpStatus.CREATED
            result
    );
}

2. MemberService 代码

@Transactional
public MemberResponseDto saveMember(final MultipartFile image, final MemberSaveRequestDto requestDto) throws IOException {
    validateSingUpRequest(requestDto);
    checkPasswordEquals(requestDto.password(), requestDto.checkPassword());

    final String profileImage = saveProfileImage(image);

    final Member member = Member.builder()
            .nickname(requestDto.nickname())
            .profileImage(profileImage)
            .email(requestDto.email())
            .password(passwordEncoder.encode(requestDto.password()))
            .phoneNumber(requestDto.phoneNumber())
            .role(MemberRole.ROLE_GENERAL)
            .status(MemberStatus.ACTIVE)
            .build();

    memberRepository.save(member);

    return new MemberResponseDto(member);
}

3. MemberApiControllerTest 代码

@ParameterizedTest
@AutoSource
@Customization(MemberSaveCustomization.class)
void join_success(final MemberSaveRequestDto requestDto) throws Exception {
    // given -- 测试状态设置
    final MockMultipartFile image = new MockMultipartFile(
            "profileImage",
            "profile.jpg",
            IMAGE_JPEG_VALUE,
            "test profile content".getBytes(UTF_8)
    );

    final Member member = Member.builder()
            .nickname(requestDto.nickname())
            .profileImage(image.getName())
            .email(requestDto.email())
            .password(passwordEncoder.encode(requestDto.password()))
            .phoneNumber(requestDto.phoneNumber())
            .role(ROLE_GENERAL)
            .status(ACTIVE)
            .build();

    final var request = objectMapper.writeValueAsString(requestDto);

    final var response = new MemberResponseDto(member);

    given(memberService.saveMember(any(), any())).willReturn(response);

    // when -- 执行测试行为
    final var result = mockMvc.perform(
            multipart("/api/members/join")
                    .file(image)
                    .file(new MockMultipartFile("requestDto", "", APPLICATION_JSON_VALUE, request.getBytes(UTF_8)))
                    .accept(APPLICATION_JSON_VALUE)
                    .contentType(MULTIPART_FORM_DATA_VALUE)
    );

    // then -- 验证预期结果
    result.andExpect(status().isCreated());
}

4. 测试执行结果

MockHttpServletRequest:
      HTTP Method = POST
      Request URI = /api/members/join
       Parameters = {}
          Headers = [Content-Type:"multipart/form-data;charset=UTF-8", Accept:"application/json"]
             Body = null
    Session Attrs = {}

Handler:
             Type = custom.capstone.domain.members.api.MemberApiController
           Method = custom.capstone.domain.members.api.MemberApiController#join(MultipartFile, MemberSaveRequestDto)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"application/json", X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"]
     Content type = application/json
             Body = {"code":1000,"message":"会员注册已完成","result":{"nickname":"test","profileImage":"profileImage","email":"test@test.com","phoneNumber":"010-1234-5678","password":null}}
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

Status
Expected :201
Actual   :200

内容的提问来源于stack exchange,提问作者이승희

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 11:44:57