해결된 질문
작성
·
470
·
수정됨
0
안녕하세요. 우빈님!
먼저 우빈님의 테스트 강의가 너무나 재미있어, 테스트에 대한 막연한 지식을 구체화하고 테스트 작성 열망을 크게 키울 수 있어서 감사하다는 말씀을 드리고 싶습니다!
다름이 아니라, Controller 테스트를 위해 @WebMvcTest를 통해 Service와 Repository를 Mocking하여 단위 테스트의 형식으로 작성한다는 것을 배웠습니다. 이렇게 배운 것을 사이드 프로젝트에 적용해보며, 의문점이 생겼는데 능력 부족으로 인해 의문이 해결되지 않아 질문을 드리려 합니다.
강의에서 작성한 컨트롤러 테스트 중 일부인 OrderControllerTest의 테스트 메서드는 다음과 같습니다.
@DisplayName("신규 주문을 등록한다.")
@Test
void createOrder() throws Exception {
// given
OrderCreateRequest request = OrderCreateRequest.builder()
.productNumbers(List.of("001"))
.build();
// when & then
mockMvc.perform(
post("/api/v1/orders/new")
.content(objectMapper.writeValueAsString(request))
.contentType(MediaType.APPLICATION_JSON)
)
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value("200"))
.andExpect(jsonPath("$.status").value("OK"))
.andExpect(jsonPath("$.message").value("OK"));
}
또한, 해당 메서드의 실행 로그를 보면 MockHttpServletResponse
이 다음과 같다는 것을 볼 수 있었습니다.
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Content-Type:"application/json"]
Content type = application/json
Body = {"code":200,"status":"OK","message":"OK","data":null}
Forwarded URL = null
Redirected URL = null
Cookies = []
이후, 저의 사이드 프로젝트의 컨트롤러 테스트를 위와 동일한 방식으로 작성하였지만, 아래와 같이 MockHttpServletResponse의 Body가 빈 채로 응답이 되어 테스트가 실패하게 되는 문제가 발생했습니다.
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [X-Content-Type-Options:"nosniff", X-XSS-Protection:"0", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"]
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []
이때, given()을 통해 Service에 대한 행위를 Stubbing해주니 정상적으로 테스트가 성공하는 것을 확인했습니다.
문제 파악을 위해 조사한 결과 추측되는 차이점은 다음과 같습니다.
강사님의 프로젝트에서 Controller의 응답 타입은 커스텀 응답 객체인 ApiResponse를 사용합니다.
저의 사이드 프로젝트에서 Controller의 응답 타입은 HttpEntity를 상속하는 ResponseEntity를 사용합니다.
Q1. 강사님의 코드를 보면, 아래와 같이 createOrder
메서드에 대한 Stubbing 없이도 정상적으로MockHttpServletResponse의 Body가 응답되어 테스트가 성공합니다.
// given
...
given(orderService.createOrder(any()))
.willReturn(OrderResponse.builder()
... // 생략
.build()
);
// 없어도 테스트는 성공한다.
저는 given() 절에 @MockBean을 통해 Mock 객체로 설정한 OrderService가 어떤 행위를 해야할지 Stubbing 해주어야 하는 것으로 이해하고 있었는데, 어떻게 Stubbing 없이 Body가 정상적으로 채워져 테스트가 성공한 것인지 궁금합니다.
Q2. Q1과 연관하여 강사님의 코드에서는 ApiResponse라는 커스텀한 응답 객체를 컨트롤러 메서드의 응답으로 사용하는데, 제 사이드 프로젝트에서의 응답 타입은 ResponseEntity를 사용하고 있습니다. 이 차이 때문에 발생하는 문제인지 궁금합니다.
Q3. 이번 의문점을 통해 컨트롤러 테스트에서 메서드의 행위에 대한 기댓값을 Stubbing하여 검증하는 것이 일종의 답정너(?)와 같은 테스트를 작성하는 것은 아닐까? 라는 생각과 함께, 컨트롤러 테스트 방식에 많은 고민을 해야 하겠다는 다짐을 하게 되었습니다. 이에 대한 우빈님의 생각은 어떠하신지 궁금합니다.
질문이 수준이 다소 떨어지지만, 이 의문점을 해결하고 싶은 마음에 장황하게 나열할 수 밖에 없었음을 양해 부탁드립니다.
답변 기다리겠습니다. 감사합니다!
답변 1
1
안녕하세요, lango님! :)
세 질문 모두 같은 내용으로 한번에 답변을 드릴 수 있을 것 같은데요.
발생하신 문제 상황의 원인은 2번 질문에서 말씀 주신 커스텀한 ApiResponse와 ResponseEntity 간의 차이 때문으로 보여집니다.
디버거를 통해 보셨다면 잘 아시겠지만, @MockBean 어노테이션을 통해 OrderService 객체를 stubbing하면, 기본적으로 해당 mock 객체의 메서드 실행 반환값은 null이 됩니다.
강의 중 작성한 코드 기준으로,
ResponseEntity를 사용한다면 body 자체에 OrderService가 반환한 null이 그대로 들어가게 되어 말씀하신 상황이 발생하는 것이고,
ApiResponse를 사용한다면 body에 OrderService가 반환한 null을 담고 있는 ApiResponse가 들어가게 되어 테스트가 통과하게 됩니다.
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Content-Type:"application/json"]
Content type = application/json
Body = {"code":200,"status":"OK","message":"OK","data":null}
Forwarded URL = null
Redirected URL = null
Cookies = []
ApiResponse로 반환했을 때의 Body를 보시면 ApiResponse 자체가 들어가 있는 것을 볼 수 있습니다.
도움이 되셨기를 바랍니다.
감사합니다. :)
감사합니다!
제가 코드의 이해를 제대로 하지 못했단 점을 반성합니다. 커스텀으로 만든 응답 객체인 ApiResponse의 data 필드에 대한 고려를 하지 않고, 단지 Body가 비어있는 것에만 혈안이 되어 제대로 된 디버깅을 하지 못했던 것 같습니다!
덕분에 디버깅에 대한 부족함을 다시 한번 느꼈고, 코드를 보다 객관적으로 바라보며 디버깅할 수 있도록 노력하리라 다짐했습니다.