Spring Boot/Test Code

Junit5] MockMvc equals() and hashcode()

나른한 찰리 2023. 7. 23. 02:32
반응형

오류 내용을 간단히 요약하면, Controller와 Service 코드는 정상적으로 작성되었으며,

WebMvcTest에서 내에서 Mockito.when().thenReturn() 을 통해 정상적으로 Mocking을 했음에도 불구하고 테스트 코드의 결과가 내가 예상한 대로 나오지 않는 상황이다. 

 

오류 상황

RoomController

@PostMapping(value = "/room", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<ApiResponse<?>> createRoom(@Valid @RequestBody RoomDto roomDto) {

    int result = roomService.createRoom(roomDto);
    String roomId = roomDto.getRoomId();

    if (result == HttpStatus.CREATED.value()) {
        return ResponseEntity.status(HttpStatus.CREATED)
                .body(success(ROOM_CREATED, roomId));
    } else if (result == 0) {
    // 테스트를 위해 임의로 값이 0이면 오류 출력하도록 설정
        return ResponseEntity.status(result)
                .body(ApiResponse.error(ErrorCode.SERVER_ERROR));
    }
    return null;
}

 

RoomService

 public int createRoom(RoomDto roomDto) {
    // Dto -> Entity
    Room room = roomDto.toRoom();

    // save db
    try {
        room.setRoomId(roomDto.getRoomId()); // Assign a value to the ID field manually
        roomRepository.save(room);
    } catch (DataIntegrityViolationException de) {
        // room Entity의 room_id 가 중복되는 경우
        // UUID를 사용하기 때문에 중복될 가능성이 매우 낮지만, 혹시 모르니 예외처리
        log.error("DataIntegrityViolationException err : ", de);
        return HttpStatus.INTERNAL_SERVER_ERROR.value();
    } catch (Exception e) {
        log.error("Exception err : ", e);
        return HttpStatus.INTERNAL_SERVER_ERROR.value();
    }

    return HttpStatus.CREATED.value();
}

 

 

RoomControllerTest

@Test
@DisplayName("채팅방 생성이 정상적으로 진행되었을 때, 201 응답과 그에 해당하는 메시지가 DTO 객체로 리턴된다.")
void createRoom() throws Exception {
    RoomDto roomDto = new RoomDto("testroom1", "testmentee1", "testmentor1");
    String content = gson.toJson(roomDto);

    Mockito.when(roomService.createRoom(roomDto)).thenReturn(201);

    mvc.perform(MockMvcRequestBuilders.post("/api/chat/room")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(content))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.code", is(201)))
            .andExpect(jsonPath("$.message", is("채팅방이 정상적으로 생성되었습니다")))
            .andExpect(jsonPath("$.data", is("testroom1")))
            .andDo(print());

    verify(roomService, times(1)).createRoom(roomDto);
}

 

RoomControllerTest 의 createRoom 메서드 테스트 결과

  •  java.lang.AssertionError: Status expected:<201> but was:<0> 오류 발생
  • Mockito.when().thenReturn() 으로 값을 주었음에도 불구하고, RoomController에서 int result의 값이 201이 아닌 0이 받아지는 오류였다.

 

 

해결 방법

MockMvc test POST request returns error : Actual invocations have different arguments: [duplicate]

 

MockMvc test POST request returns error : Actual invocations have different arguments:

I am new to backend testing and I am testing my spring boot app with MockMvc. However, when I want to test my Post request, I have the error mentioned in the title when I use verify(userService,

stackoverflow.com

 

내 오류와 관련 있는 해결 방법은 아니지만, 위의 참고 링크를 통해 해결했다.

RoomControllerTest에서 사용하는 RoomDto 클래스에, hashCode와 equals 메서드를 Override 하니, 테스트가 정상적으로 통과하였다.

 

 

 

230907 내용 추가

https://stackoverflow.com/questions/62560356/spring-boot-mockmvctest-mockhttpservletresponse-always-returns-empty-body

 

Spring boot @MockMvcTest MockHttpServletResponse always returns empty body

I'm struggling with a simple spring boot rest controller test which always return empty body response. Here is my test code looks like: @WebMvcTest(AdminRestController.class) @AutoConfigureMockMvc(

stackoverflow.com

 

위의 코드에서, andExpect() 부분을 모두 제거하고, verify() 코드를 실행해보면 오류 원인을 자세하게 확인할 수 있다.

@Test
@DisplayName("채팅방 생성이 정상적으로 진행되었을 때, 201 응답과 그에 해당하는 메시지가 DTO 객체로 리턴된다.")
void createRoom() throws Exception {
    RoomDto roomDto = new RoomDto("testroom1", "testmentee1", "testmentor1");
    String content = gson.toJson(roomDto);

    Mockito.when(roomService.createRoom(roomDto)).thenReturn(201);

    mvc.perform(MockMvcRequestBuilders.post("/api/chat/room")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(content))
            .andDo(print());

    verify(roomService, times(1)).createRoom(roomDto);
}

 

실행 결과 아래와 같은 오류 메시지를 확인할 수 있다.

더보기

Argument(s) are different! Wanted:
seoultech.capstone.menjil.domain.follow.application.FollowService#0 bean.followRequest(
    seoultech.capstone.menjil.domain.follow.dto.request.FollowRequest@1dbc607d
);
-> at seoultech.capstone.menjil.domain.follow.api.FollowControllerTest.followRequest_follow_deleted(FollowControllerTest.java:103)
Actual invocations have different arguments:
seoultech.capstone.menjil.domain.follow.application.FollowService#0 bean.followRequest(
    seoultech.capstone.menjil.domain.follow.dto.request.FollowRequest@1dd64243
);
-> at seoultech.capstone.menjil.domain.follow.api.FollowController.followRequest(FollowController.java:33)

 

오류 메시지를 확인하면, Mockito.when().thenReturn() 에서 사용한 객체와(RoomDto), verify에서 사용하는 RoomDto의 객체가 일치하지 않아서 발생하는 원인인 것을 파악할 수 있다. 

 

아래 사진은 위의 stackoverflow의 답변인데, 이를 참고하면.

나의 경우 Gson 라이브러리를 사용하여 RoomDto를 String 객체로 변환한 뒤, Mockmvc 를 사용하여 post 요청을 보냈는데, 변환한 다음 equals() 및 hashCode() 가 오버라이드 되어있지 않아서, 변환한 content 객체를 RoomDto와 다른 객체로 Mockito가 판단해서 오류가 발생하였다고 생각한다. 

 

 

 

 

혹은 다음과 같이 사용이 가능하다.

Mockito.any(...) 사용해서 어떠한 객체가 전달되던지에 관계없이 모의 동작을 수행하도록 한다. 

@Test
@DisplayName("로그인 시 google, kakao 외에 다른 플랫폼이 온 경우 ErrorCode.PROVIDER_NOT_ALLOWED 리턴")
void signIn_provider_type_mismatch() throws Exception {
    // given
    String providerMisMatch = "naver";
    SignInRequest request = new SignInRequest("k337kk@kakao.com", providerMisMatch);
    String content = gson.toJson(request);

    // when
    Mockito.when(authService.signIn(Mockito.any(SignInServiceRequest.class)))
            .thenThrow(new CustomException(ErrorCode.PROVIDER_NOT_ALLOWED));

    // then
    mockMvc.perform(MockMvcRequestBuilders.post("/api/auth/signin")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(content))
            .andExpect(status().isBadRequest())
            .andExpect(jsonPath("$.code",
                    is(ErrorCode.PROVIDER_NOT_ALLOWED.getHttpStatus().value())))
            .andExpect(jsonPath("$.message",
                    is(ErrorCode.PROVIDER_NOT_ALLOWED.getMessage())))
            .andDo(print());

    verify(authService, times(1)).signIn(Mockito.any(SignInServiceRequest.class));
}

 

 

아래와 같이 사용하면, 객체의 동일성을 보장하지 못할 수 있다.

request.toServiceRequest()를 호출하면 새로운 SignInServiceRequest 인스턴스가 생성된다. 테스트에서 이 메소드를 호출할 때마다 새로운 객체가 생성되므로, 실제 서비스 메소드 호출 시 사용되는 인스턴스와 Mockito 설정에 사용된 인스턴스가 서로 다를 수 있다.

 

마찬가지로 해결책은, SignInServiceRequest에 equals(), hashcode() 메서드를 재정의하면 통과한다. 하지만 위처럼 Mockito.any()를 사용하는 것이 더 적절해보인다고 생각함.

    @Test
    @DisplayName("로그인 시 google, kakao 외에 다른 플랫폼이 온 경우 ErrorCode.PROVIDER_NOT_ALLOWED 리턴")
    void signIn_provider_type_mismatch() throws Exception {
        // given
        String providerMisMatch = "naver";
        SignInRequest request = new SignInRequest("k337kk@kakao.com", providerMisMatch);
        String content = gson.toJson(request);

        // when
        Mockito.when(authService.signIn(request.toServiceRequest()))
                .thenThrow(new CustomException(ErrorCode.PROVIDER_NOT_ALLOWED));


        // then
        mockMvc.perform(MockMvcRequestBuilders.post("/api/auth/signin")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(content))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.code",
                        is(ErrorCode.PROVIDER_NOT_ALLOWED.getHttpStatus().value())))
                .andExpect(jsonPath("$.message",
                        is(ErrorCode.PROVIDER_NOT_ALLOWED.getMessage())))
                .andDo(print());

        // 이 경우 오류 발생!
        verify(authService, times(1)).signIn(request.toServiceRequest());
    }
반응형