[JUnit & Mockito] Response Status가 200이지만, Reponse Body가 비어있을 경우

Minjun Kang·2023년 1월 12일
0

테스트 대상

/* ProductController.java */
  @PostMapping("/product")
  public ResponseEntity<ProductResponseDto> createProduct(@RequestBody ProductDto dto) {
    ProductResponseDto productResponseDto = productService.saveProduct(dto);
    return ResponseEntity.ok(productResponseDto);
  }
/* ProductServiceImpl.java */
  @Override
  public ProductResponseDto saveProduct(ProductDto dto) {

    Product product = Product.builder().name(dto.getName()).price(dto.getPrice())
        .stock(dto.getStock()).createdAt(LocalDateTime.now()).updatedAt(LocalDateTime.now())
        .build();

    Product savedProduct = productDao.insertProduct(product);

    return ProductResponseDto.builder().number(savedProduct.getNumber())
        .name(savedProduct.getName()).price(savedProduct.getPrice()).stock(savedProduct.getStock())
        .build();
  }

테스트 코드

  @Test
  @DisplayName("[컨트롤러] Product 데이터 생성 테스트")
  void createProductTest() throws Exception {
    given(productService.saveProduct(new ProductDto("pen", 5000, 2000)))
        .willReturn(new ProductResponseDto(123456L, "pen", 5000, 2000));

    ProductDto productDto = ProductDto.builder()
        .name("pen")
        .price(5000)
        .stock(2000)
        .build();

    ObjectMapper objectMapper = new ObjectMapper();
    String content = objectMapper.writeValueAsString(productDto);

    mockMvc.perform(
            post("/product")
                .content(content)
                .contentType(MediaType.APPLICATION_JSON))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.number").exists())
        .andExpect(jsonPath("$.name").exists())
        .andExpect(jsonPath("$.price").exists())
        .andExpect(jsonPath("$.stock").exists())
        .andDo(print());

  }

문제 상황

MockHttpServletRequest:
      HTTP Method = POST
      Request URI = /product
       Parameters = {}
          Headers = [Content-Type:"application/json;charset=UTF-8", Content-Length:"40"]
             Body = {"name":"pen","price":5000,"stock":2000}
    Session Attrs = {}

Handler:
             Type = com.springboot.api.controller.ProductController
           Method = com.springboot.api.controller.ProductController#createProduct(ProductDto)

...

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = []
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

Postman을 이용한 수동 테스트시에는 Response body가 정상적으로 채워져서 반환되지만, 유닛 테스트를 수행할 때 위와 같은 문제가 발생하였다.

Mockito에서 mock객체의 특정 메소드 behavior를 명시하지 않는다면 null을 기본적으로 리턴하는데, 해당 문제와 관련이 있어보였다.

그래서 실제로 given(...) 메소드가 실제로 수행되는지 검증해보았다.

Request를 서버로 보낼 때 서버에 HttpServletRequest객체가 생성되고 서비스 루틴이 실행될 때 given(...) 메소드가 실행되므로 MockMvcperform메소드를 이용하여 request를 보낸 다음, 실제로 given(...) 메소드가 실행되었는지 Mockito.verify()를 이용하여 검증해보았다.

  @Test
  @DisplayName("Product 데이터 생성 테스트")
  void createProductTest() throws Exception {
    given(productService.saveProduct(new ProductDto("pen", 5000, 2000)))
        .willReturn(new ProductResponseDto(123456L, "pen", 5000, 2000));

    ProductDto productDto = ProductDto.builder()
        .name("pen")
        .price(5000)
        .stock(2000)
        .build();

    ObjectMapper objectMapper = new ObjectMapper();
    String content = objectMapper.writeValueAsString(productDto);

    mockMvc.perform(
        post("/product")
            .content(content)
            .contentType(MediaType.APPLICATION_JSON));

// Mockito
    verify(productService).saveProduct(
        new ProductDto("pen", 5000, 2000)
    );
  }

결과는 아래와 같았다.

Argument(s) are different! Wanted:
com.springboot.api.service.ProductService#0 bean.saveProduct(
    ProductDto(name=pen, price=5000, stock=2000)
);
-> at com.springboot.api.controller.ProductControllerTest.createProductTest(ProductControllerTest.java:78)
Actual invocations have different arguments:
com.springboot.api.service.ProductService#0 bean.saveProduct(
    ProductDto(name=pen, price=5000, stock=2000)
);
-> at com.springboot.api.controller.ProductController.createProduct(ProductController.java:36)

Comparison Failure: 
<Click to see difference>

로그상으로 given으로 주어지는 인자값과 실제 주어지는 인자값이 다르다는 것을 확인하였다.

Comparision의 결과는 같았는데, 뭐가 문제였을까?
이전에 동일한 상황에서 primitive type에 대한 문제점은 없었는데 Object타입만 이러한 문제점이 발견된 것을 보아, given(A)에서 A에 매칭되는 전략이 객체는 다를 수 있겠다는 생각이 들었다.

Mockito docs에는 객체의 arguments matching 전략을 아래와 같이 소개한다.

Sometimes it's better to implement equals() for arguments that are passed to mocks (Mockito naturally uses equals() for argument matching). This can make the test cleaner.
ArgumentMatcher

위 문장을 통해서 객체의 기본 Matching 전략은 Equals()인 것을 알 수 있다.

해결

  • ProductDtoEquals() 를 재정의한다.
    Object의 Equals()는 객체의 레퍼런스를 비교하여 equality를 판단하므로, 논리적 비교가 가능하도록 재정의해준다.

  • 필자는 단순하게 lombok의 @EqualsAndHashCode를 이용하였다.

profile
성장하는 개발자

0개의 댓글