인프런 커뮤니티 질문&답변

나민혁님의 프로필 이미지

작성한 질문수

Practical Testing: 실용적인 테스트 가이드

REST Docs 추가 작업중 직렬화,역직렬화 문제

작성

·

83

0

강사님이 기존에 작성해주신 코드들을 가지고 REST Docs를 채워보려고 하고있습니다 !

OrderController의 createOrder 메서드에 대해서 테스트 해서 docs를 채워보려고 하는 와중에 LocalDateTime에서 직렬화와 역직렬화 문제가 발생하여서 질문드립니다 !

 

public class OrderControllerDocsTest extends RestDocsSupport {

    private final OrderService orderService = mock(OrderService.class);

    @Override
    protected Object initController() {
        return new OrderController(orderService);
    }

    @BeforeEach
    void setUp() {
        objectMapper.registerModule(new JavaTimeModule());
        objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
    }

    @DisplayName("신규 주문을 생성하는 API")
    @Test
    void createOrder() throws Exception {
        OrderCreateRequest request = OrderCreateRequest.builder()
            .productNumbers(List.of("001", "002"))
            .build();

        List<ProductResponse> productResponses = List.of(
            ProductResponse.builder()
                .id(1L)
                .productNumber("001")
                .type(HANDMADE)
                .sellingStatus(SELLING)
                .name("아메리카노")
                .price(4000)
                .build(),
            ProductResponse.builder()
                .id(2L)
                .productNumber("002")
                .type(HANDMADE)
                .sellingStatus(HOLD)
                .name("카페라떼")
                .price(4500)
                .build()
        );


        given(orderService.createOrder(any(OrderCreateServiceRequest.class), any(LocalDateTime.class)))
            .willReturn(OrderResponse.builder()
                .id(1L)
                .totalPrice(8500)
                .registeredDateTime(LocalDateTime.now())
                .products(productResponses)
                .build());

        mockMvc.perform(
                post("/api/v1/orders/new")
                    .content(objectMapper.writeValueAsString(request))
                    .contentType(MediaType.APPLICATION_JSON)
            )
            .andDo(print())
            .andExpect(status().isOk())
            .andDo(document("order-create",
                preprocessRequest(prettyPrint()),
                preprocessResponse(prettyPrint()),
                requestFields(
                    fieldWithPath("productNumbers").type(JsonFieldType.ARRAY)
                        .description("상품 번호 리스트")
                ),
                responseFields(
                    fieldWithPath("code").type(JsonFieldType.NUMBER)
                        .description("코드"),
                    fieldWithPath("status").type(JsonFieldType.STRING)
                        .description("상태"),
                    fieldWithPath("message").type(JsonFieldType.STRING)
                        .description("메시지"),
                    fieldWithPath("data").type(JsonFieldType.OBJECT)
                        .description("응답 데이터"),
                    fieldWithPath("data.id").type(JsonFieldType.NUMBER)
                        .description("주문 ID"),
                    fieldWithPath("data.totalPrice").type(JsonFieldType.NUMBER)
                        .description("주문 총합 가격"),
                    fieldWithPath("data.registeredDateTime").type(JsonFieldType.STRING)
                        .description("상품 주문 시간"),
                    fieldWithPath("data.products").type(JsonFieldType.ARRAY)
                        .description("상품 판매 목록"),
                    fieldWithPath("data.products[].id").type(JsonFieldType.NUMBER)
                        .description("상품 ID"),
                    fieldWithPath("data.products[].productNumber").type(JsonFieldType.STRING)
                        .description("상품 번호"),
                    fieldWithPath("data.products[].type").type(JsonFieldType.STRING)
                        .description("상품 타입"),
                    fieldWithPath("data.products[].sellingStatus").type(JsonFieldType.STRING)
                        .description("상품 판매 상태"),
                    fieldWithPath("data.products[].name").type(JsonFieldType.STRING)
                        .description("상품 이름"),
                    fieldWithPath("data.products[].price").type(JsonFieldType.NUMBER)
                        .description("상품 가격")
                )
            ));
    }
}

이렇게 코드를 작성하니 LocalDateTime을 Array로 만들더라구요 일단 그래서 Array로 해서 반환하니까 물론 테스트는 문제없이 넘어가지만 실제 프로덕션 코드의 Response 코드를 확인해보니 LocalDateTime으로 String으로 넘어가고 있었습니다. 테스트와 실제 프로덕션 코드의 간극이 생기는데 어떻게 해결하면 좋을까요 ?

 

그리고 기존에 알려주셨던 ProductionControllerDocsTest의 코드를 많이 참고해서 테스트를 작성하였는습니다. 여기서 든 의문점이 이렇게만 작성하면 request와 response만 정의할 뿐 실제로 테스트는 동작하지 않는 것 아닌가 ? 라는 의문이 들었습니다. 실제로 OrderResponse.builder()를 맘대로 바꿔도 테스트가 깨지지않는 상황입니다. 그래서 단순히 Controller단 테스트는 수업때 설명하신 것 처럼 파라미터에 대한 검증만을 하는 것이기 때문에 REST Docs 또한 Request 와 Response의 필드가 어떻게 구성되어있는지 정도만 보여주는 용도로 사용되는건지도 궁금합니다 !

답변 2

1

박우빈님의 프로필 이미지
박우빈
지식공유자

안녕하세요, 나민혁 님!

 

이렇게 코드를 작성하니 LocalDateTime을 Array로 만들더라구요 일단 그래서 Array로 해서 반환하니까 물론 테스트는 문제없이 넘어가지만 실제 프로덕션 코드의 Response 코드를 확인해보니 LocalDateTime으로 String으로 넘어가고 있었습니다. 테스트와 실제 프로덕션 코드의 간극이 생기는데 어떻게 해결하면 좋을까요 ?

LocalDateTime이 array로 응답되는 부분은, 다음과 같은 키워드로 한번 찾아보시면 좋을 것 같습니다. 🙂

 

그리고 기존에 알려주셨던 ProductionControllerDocsTest의 코드를 많이 참고해서 테스트를 작성하였는습니다. 여기서 든 의문점이 이렇게만 작성하면 request와 response만 정의할 뿐 실제로 테스트는 동작하지 않는 것 아닌가 ? 라는 의문이 들었습니다. 실제로 OrderResponse.builder()를 맘대로 바꿔도 테스트가 깨지지않는 상황입니다. 그래서 단순히 Controller단 테스트는 수업때 설명하신 것 처럼 파라미터에 대한 검증만을 하는 것이기 때문에 REST Docs 또한 Request 와 Response의 필드가 어떻게 구성되어있는지 정도만 보여주는 용도로 사용되는건지도 궁금합니다 !

동작하지 않는다기 보다는, DocsTest 자체의 목적이 내부 구현보다는, 요청과 응답 스펙을 검증하는 데에 초점이 맞춰져 있다고 보시면 됩니다.
실제로 테스트에 있는 응답 스펙을 예상 결과와 다르게 잘못 입력하면 테스트가 깨지는 것을 볼 수 있는데요, 그렇기에 동작을 하지 않는 것은 아닙니다 ㅎㅎ

도움이 되셨기를 바랍니다.
감사합니다. 🙂

0

안녕하세요, 인프런 AI 인턴입니다.

LocalDateTime의 직렬화와 역직렬화 문제는 ObjectMapper 설정을 확인해야 할 필요가 있습니다. 이미 setUp 메서드에서 JavaTimeModule을 등록하고, WRITE_DATES_AS_TIMESTAMPS 옵션을 false로 설정하셨기 때문에, LocalDateTime은 ISO-8601 형식의 문자열로 직렬화되어야 합니다. 이 설정은 일반적으로 LocalDateTime을 문자열로 올바르게 변환하기에 충분합니다. 하지만 테스트에서 LocalDateTime이 배열로 변환되는 문제가 발생한다면, 다음의 가능성을 확인해볼 필요가 있습니다:

  1. 테스트 환경과 실제 환경의 ObjectMapper 설정 차이 - 테스트 환경에서 사용하는 ObjectMapper와 실제 애플리케이션에서 사용하는 ObjectMapper가 다르거나, 설정이 제대로 적용되지 않은 경우가 있을 수 있습니다. 이 경우 테스트 환경의 설정이 실제 환경과 일치하는지 확인해보세요.

  2. 다른 설정이나 라이브러리에 의한 영향 - 때로는 다른 라이브러리나 설정에 의해 ObjectMapper의 동작이 예상과 다르게 변경될 수 있습니다. 예를 들어, 다른 설정 파일이나 자동 구성(Auto Configuration)에 의해 ObjectMapper 설정이 덮어씌워질 수 있습니다.

Controller 테스트와 실제 동작 간의 간극 문제를 해결하기 위해, MockMvc를 통해 수행하는 테스트에서 objectMapper를 명시적으로 사용하여 요청 본문을 직렬화하는 방식이 올바른 접근입니다. 테스트가 실제 응답과 일치하는지 보장하려면, responseFields를 통해 응답 필드를 검증하는 것 외에도, MockMvc에서 반환된 응답값이 예상한 LocalDateTime 형식과 일치하는지 확인하는 추가적인 검증 로직을 포함시킬 수 있습니다.

테스트가 실제 코드의 동작을 모두 포괄하지 않는 경우가 있으며, 이는 테스트의 범위나 목적에 따라 달라질 수 있습니다. Controller 단위 테스트의 경우, 주로 HTTP 요청과 응답의 형태, 또는 스프링 MVC의 설정이 올바르게 동작하는지를 검증하는 데 초점을 맞춥니다. 따라서, 테스트가 깨지지 않는 경우라도 실제 비즈니스 로직의 동작을 포괄적으로 검증하기 위해서는 서비스 계층이나 통합 테스트 등 추가적인 테스트가 필요할 수 있습니다.

마지막으로, RestController에서 처리하는 것처럼 실제 요청과 응답 처리에 대해 테스트하고자 한다면, @RestClientTest 같은 다른 테스트 어노테이션이나, 실제 HTTP 요청을 시뮬레이션 할 수 있는 도구(예: TestRestTemplate)를 사용해 볼 수도 있습니다. 이를 통해 더 현실적인 시나리오에서 API 테스트를 진행할 수 있습니다.