[Test] @RequestBody와 Mock

wlsh44·2022년 10월 25일

테스트

목록 보기
3/3

@RestController 에서 Post 메서드에 대해 단위 테스트할 때, 보통 @RequestBody 를 인자로 받는 servicemocking 할 때가 많습니다.

하지만 최근에 원하는 대로 테스트 코드 작성이 되지 않아서 고생을 하고 공부한 부분을 기록합니다. 😂

문제 상황

위와 비슷한 환경을 만들기 위해 다음과 같은 코드를 작성해보겠습니다.
TestController 에서 @RequestBodyTestDto 가 넘어오고, 그 데이터를 TestService 가 넘겨받아 같은 TestDto 를 리턴하는 코드입니다.


//controller
@RestController
@RequiredArgsConstructor
public class TestController {

    private final TestService testService;

    @PostMapping("/")
    public TestDto hello(@RequestBody TestDto dto) {
        TestDto testDto = testService.doService(dto);
        return testDto;
    }
}

//service
@Service
public class TestService {

    public TestDto doService(TestDto dto) {
        return dto;
    }
}

//dto
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class TestDto {
    private String testData;
}

테스트 코드는 다음과 같이 작성했습니다.

@WebMvcTest(TestController.class)
class TestControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Autowired
    ObjectMapper objectMapper;

    @MockBean
    TestService testService;

    @Test
    void RequestBody와_같은_ResponseBody를_리턴한다() throws Exception {
        //given
        TestDto dto = TestDto.builder()
                .testData("test")
                .build();
        String content = objectMapper.writeValueAsString(dto);
        given(testService.doService(dto)).willReturn(dto);

        //when then
        mockMvc.perform(post("/")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(content))
                .andDo(print())
                .andExpect(content().json(content));
    }
}

큰 특징이 없이 testService.doService(dto) 의 형태로 stubbing 을 해둔 상태입니다.

문제 없이 테스트가 성공할 줄 알았지만 다음과 같은 결과가 나왔습니다.

원인 분석

사실 이 부분은 controller 부분에서 코드를 어떻게 작성하냐에 따라 다른 오류로 바뀔 수 있습니다.

우선 저 오류가 발생한 부분은 오류 메세지를 보면 알겠지만 .andExpect(content().json(content)); 에서 contentjson 형식으로 바꿔주는 과정에서 발생합니다.

그렇다는 말은 원하는 content 가 리턴되지 않는다는 뜻이고, 이를 리턴하는 부분을 확인해봐야 하겠죠.

given(testService.doService(dto)).willReturn(dto); 를 통해 특정 dto 를 리턴하게 해놨음에도 null 을 리턴했습니다.

그래서 stubbing 할 때 dto 와 실제 controller 에 넘어온 dto 가 다른지 확인을 했더니 역시나 다른 결과가 나왔습니다.

다른 객체가 넘어온 이유

사실 생각을 해보면 다른 dto 객체가 넘어오는 것은 당연한 일입니다.

json 형식으로 requestBody에 데이터를 담아 보낼 때, servlet에서는 바이트 코드 형식으로 데이터를 받습니다.
그리고 spring boot에서 자동으로 bean에 등록해주는 jackson 라이브러리의 objectMapper를 통해 POJO 객체로 변환을 시켜줍니다.

objectMapper 기본 생성자를 통해 새로운 객체를 만들고 setterreflection 을 통해 값을 넣어주기 때문에 테스트 코드에서 stubbing 할 때 사용한 객체와는 다른 객체일 수밖에 없습니다.

여태까지 이런 적이 없어서 도대체 이전이랑 어떤 차이가 있길래 이런 예외가 발생하는지 궁금해서 기존에 했던 프로젝트와의 차이를 보다가 dto 에서 답을 찾았습니다.

@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class TestDto {
    private String testData;
}

항상 dto 를 만들 때 습관처럼 @Data 어노테이션을 붙였는데, 이번 코드에서는 setter를 제거하기 위해 추가하지 않았습니다.

그러다보니 @Data 에 포함되어있는 @EqualsAndHashCode 가 추가되지 않았고, stub 된 메서드는 다른 인자가 넘어온 것으로 판단했기 때문에 작동하지 않았던 겁니다.

해결

사실 해결 방법은 정말 간단하고 저도 굳이 이걸 블로그에 작성을 해야 되나 고민을 했습니다.
하지만 두 가지 방법 모두 약간의 찝찝함이 있기 때문에 언젠가 다른 방법을 알게 되었을 때를 위해 기록하는 편이 좋다고 생각했습니다.

방법 1

단순히 dto@EqualsAndHashCode 을 추가하는 것으로 해결할 수 있습니다.

이렇게 간단한 해결책이 있음에도 굳이 두 가지 방법으로 나누는 이유는, 테스트를 위해 실제 코드에 필요 없는 기능을 추가해야 하는지에 대한 고민 때문이었습니다.

방법 2

실제 코드를 건드리지 않고 해결하는 방법에는 mockito에서 제공하는 any() 메서드를 이용하는 것이 있습니다.

given(testService.doService(any(TestDto.class))).willReturn(dto);

하지만 개인적인 생각으로는 any() 메서드를 쓰는 테스트 코드가 올바른 테스트 코드인가에 대한 생각이 들었습니다.


두 방법 모두 장단점이 있다고 생각합니다.
다만 아직 제 부족한 실력으로는 어떤 방법이 더 낫다라고 하기 힘드네요...😂
언젠가 조금 더 경험이 쌓이고 좀 더 나은 방법을 알게 되면 추가하겠습니다.

여담

equals() 메서드를 오버라이드 함으로써 해결할 수 있는 것은 알았지만 정말 해결 가능한지 눈으로 확인해보고 싶어졌습니다.

doService() 메서드부터 차근차근 디버깅을 하다보니 다음과 같은 코드를 볼 수 있었습니다. (조금 더 현명했더라면 equals 부터 뒤로 돌아갔을텐데...)

argumentsMatch 에서 조금 더 들어가다 보니 원하던 코드를 찾을 수 있었네요. 🙂

equals 를 오버라이드하게 되면 마지막 else 문에서 true를 반환하는 모습을 볼 수 있었습니다.

profile
정리정리

0개의 댓글