TDD를 공부하며, 진행하던 프로젝트에서 연습을 진행하고 있었다. 컨트롤러에서 테스트 진행을 위해 @WebMvcTest 어노테이션을 이용하여 진행하는데 문제가 발생했다.
MockMvc를 이용한 테스트를 진행하는데, 응답은 200으로 잘 오면서 MockHttpServletResponse의 Body가 비어있는 이상한 상황을 겪게 되었다.
아래 코드처럼, 응답으로 나오는 JSON에 대한 검증을 진행하고자 했다. 하지만 Response Body가 비어있어서 'No value at JSON path' 에러와 함께 테스트가 계속 실패했다. Body가 비어있기 때문에 비교할 JSON이 없으므로 해당 에러가 발생한다. 물론 여기서 Status에 대한 검증만을 진행한다면 성공하겠지만, 반쪽짜리 테스트가 되어버린다.
mockMvc.perform(
post("/center/add")
.contentType(MediaType.APPLICATION_JSON)
.content(requestJson)
).andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("센터 A"))
.andDo(print());
MockMvc를 이용한 테스트를 처음 작성해보는 나로서는 어디서 문제가 발생했을지 가늠할 수 조차 없어서 온갖 코드를 뒤지기 시작했다.
테스트 하고자 하는 코드는 CenterController의 add 메서드로, 센터 정보를 추가하는 요청을 받는 역할을 한다.
@RequestMapping("/center")
@RequiredArgsConstructor
@RestController
public class CenterController {
private final CenterService centerService;
@PostMapping("/add")
public ResponseEntity<CenterParam> add(
@RequestBody CenterRequest centerRequest
) {
return ResponseEntity.ok(centerService.add(centerRequest));
}
}
@Builder
@AllArgsConstructor
@Getter
@Setter
public class CenterRequest {
private String name;
private String address;
}
@Builder
@AllArgsConstructor
@Getter
@Setter
public class CenterParam {
private long id;
private String name;
private String address;
}
위에서 살펴본 코드는 별로 이상한 점이 보이질 않는다. CenterService에서 최소한의 코드만 작성하고 Spring을 실행시킨 뒤, Postman을 통해서 테스트를 진행해도 문제 없이 Response Body가 튀어나온다.
그런데 테스트 코드만 실패하는 것을 보면 문제는 테스트 코드에 있을 것이다.
@WebMvcTest(CenterController.class)
public class CenterConrtollerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
CenterService centerService;
@Test
@DisplayName("요청 객체와 응답 객체를 모두 Builder로 생성")
void mockMvcTestWithBuilder() throws Exception {
given(centerService.add(
CenterRequest.builder()
.name("센터 A")
.address("서울특별시 관악구")
.build())
).willReturn(
CenterParam.builder()
.id(1)
.name("센터 A")
.address("서울특별시 관악구")
.build());
CenterRequest centerRequest = CenterRequest.builder()
.name("센터 A")
.address("서울특별시 관악구")
.build();
Gson gson = new Gson();
String requestJson = gson.toJson(centerRequest);
mockMvc.perform(
post("/center/add")
.contentType(MediaType.APPLICATION_JSON)
.content(requestJson)
).andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("센터 A"))
.andDo(print());
}
}
테스트 코드를 봐서는 문제를 잘 모르겠다. given()과 when()도 잘 설정한 것 같고, perform()도 잘 설정한 것 같다. 그런데 Response가 제대로 나오질 않는다.
다시 생각해보면 CenterController는 Postman으로 테스트 했을 때 문제 없이 동작함을 확인했다. 그럼 남은 건 테스트 코드에서 @MockBean 어노테이션을 이용하여 만든 CenterService 뿐이다.
현재 테스트 코드를 보면 given()에 builder()를 이용하여 CenterRequest를 생성하고 있다. 그러나 CenterController로 전달된 JSON이 만들어낸 CenterRequest 객체가 given()에서 선언한 것과 같지 않다면 어떻게 될까? MockBean으로 선언된 CenterService는 정해진 객체가 들어오지 않았으므로, 정해진 응답이 아닌 응답을 주지 않을까?
이를 확인하기 위해서, 위에서 본 테스트 코드의 마지막에 아래 코드를 추가하고, jsonPath를 통해 검증하는 두 줄은 잠시 주석 처리를 한다.
verify(centerService).add(
CenterRequest.builder()
.name("센터 A")
.address("서울특별시 관악구")
.build());
verify() 메서드는 앞서 given-willReturn으로 선언된 행위가 실제로 수행되었는지 확인한다. 실행한 결과는 아래와 같다.
간단하게 정리하면 아래와 같은 내용이다.
인자가 다르다! 너가 원한 거 :
CenterService#0 bean.add(CenterRequest@24aedcc5)
-> mockMvcTestWithBuilder에서
실제 인자:
CenterService#0 bean.add(CenterRequest@74ebd159);
-> CenterController.add에서
테스트에서 의도한 CenterRequest와 실제로 생성된 CenterRequest의 해시 코드가 다르며, 이로 인해 MockBean으로 생성된 CenterService에서 적당한 반환을 하지 못하는 것으로 보인다. 내가 참고했던 다른 책에서는 위의 방법으로 잘 됐다. 뭐가 다른 지 아직도 모르겠다.
사실 이 문제를 처음 해결했을 땐 해결 방법을 먼저 찾았다. 물론 맞는 방법인 지에 대한 의문이 여전히 남아있다.
given(centerService.add(any()))
.willReturn(CenterParam.builder()
.id(1)
.name("센터 A")
.address("서울특별시 관악구")
.build());
given()에서 centerService.add에 전달하는 인자를 any()로 선언하는 것이다. 그러면 perform()에무엇이 들어가더라도 CenterService는 willReturn()에서 선언해놓은대로 반환할 것이다. 물론 테스트는 통과할 수 있도록 자료형은 CenterRequest로 맞추어야 한다.
그러나 이 방법은 문제가 있다. name과 address가 일치하지 않더라도, 즉 객체의 필드값이 요청과 응답에서 상이하더라도 이를 확인해주지 않는다. 요청된 정보에 id 필드만 추가되어 응답이 이루어져야 하는데, 위의 any()를 이용하는 방식은 이 부분을 신경쓰지 않는다.
그래서 perform()을 통해 검증을 진행할 때, andExpect(jsonPath("$.name").value)과 같은 방식으로 응답에 대한 추가 검증을 진행해주어야 한다.
jsonPath를 통한 추가 검증으로 추가 확인을 진행할 수 있지만, 여전히 찜찜한 구석이 남아있다. any()를 이용하면 의미상, '무엇이 들어오든 응답은 정해져있습니다!'가 된다. 물론 WebMvcTest의 한계일 수는 있지만, 이유 모를 찝찝함이 남아있는 건 어쩔 수 없을 듯 하다.