기술과 산업/언어 및 프레임워크

Jackson JSON 트러블슈팅을 위한 각 오류별 구체적인 예제와 Spring Boot 기반의 통합 테스트

B컷개발자 2025. 5. 19. 11:17
728x90

 

1. 테스트 커버리지 확장

  • 날짜 역직렬화 처리 테스트 (LocalDateTime)
  • 순환 참조 직렬화 테스트 (@JsonManagedReference, @JsonBackReference)

2. Spring REST Docs 기반 API 문서화 예제 추가

  • /user POST 요청을 requestFields()로 문서화
  • 문서 생성용 document() 블록 포함

3. MockMvc 기반 통합 테스트로 구성

  • JSONPath 기반 응답 구조 검증
  • Jackson 설정 (ACCEPT_CASE_INSENSITIVE_ENUMS) 반영
// Jackson 트러블슈팅 – Spring Boot 통합 테스트 기반 예제 (확장)

@SpringBootTest
@AutoConfigureMockMvc
public class JacksonErrorIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @RestController
    static class TestController {

        @PostMapping("/user")
        public ResponseEntity<String> createUser(@RequestBody User user) {
            return ResponseEntity.ok("OK");
        }

        @PostMapping("/role")
        public ResponseEntity<String> assignRole(@RequestBody RoleUser roleUser) {
            return ResponseEntity.ok("OK");
        }

        @PostMapping("/date")
        public ResponseEntity<String> parseDate(@RequestBody DateUser dateUser) {
            return ResponseEntity.ok("Parsed: " + dateUser.createdAt);
        }

        @PostMapping("/parent")
        public ResponseEntity<Parent> getParent() {
            Parent parent = new Parent("부모", null);
            Child child = new Child("자식", parent);
            parent.setChild(child);
            return ResponseEntity.ok(parent);
        }
    }

    static class User {
        public String name;
        public int age;
    }

    static class RoleUser {
        public Role role;
    }

    enum Role {
        ADMIN, USER
    }

    static class DateUser {
        public LocalDateTime createdAt;
    }

    @JsonIgnoreProperties(ignoreUnknown = true)
    static class SafeUser {
        public String name;
        public int age;
    }

    @JsonManagedReference
    static class Parent {
        public String name;
        public Child child;

        public Parent() {}
        public Parent(String name, Child child) {
            this.name = name;
            this.child = child;
        }

        public void setChild(Child child) {
            this.child = child;
        }
    }

    @JsonBackReference
    static class Child {
        public String name;
        public Parent parent;

        public Child() {}
        public Child(String name, Parent parent) {
            this.name = name;
            this.parent = parent;
        }
    }

    @Test
    void unrecognizedFieldReturns400() throws Exception {
        String invalidJson = "{\"name\":\"홍길동\", \"age\":30, \"unknown\":\"???\"}";

        mockMvc.perform(post("/user")
                .contentType(MediaType.APPLICATION_JSON)
                .content(invalidJson))
            .andExpect(status().isBadRequest());
    }

    @Test
    void invalidEnumValueReturns400() throws Exception {
        String json = "{\"role\":\"invalid\"}";

        mockMvc.perform(post("/role")
                .contentType(MediaType.APPLICATION_JSON)
                .content(json))
            .andExpect(status().isBadRequest());
    }

    @TestConfiguration
    static class CustomConfig {
        @Bean
        public Jackson2ObjectMapperBuilderCustomizer customizer() {
            return builder -> builder.featuresToEnable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS);
        }
    }

    @Test
    void validEnumWithInsensitiveEnabled() throws Exception {
        String json = "{\"role\":\"admin\"}";

        mockMvc.perform(post("/role")
                .contentType(MediaType.APPLICATION_JSON)
                .content(json))
            .andExpect(status().isOk());
    }

    @Test
    void validDateParsing() throws Exception {
        String json = "{\"createdAt\":\"2025-05-10T15:00:00\"}";

        mockMvc.perform(post("/date")
                .contentType(MediaType.APPLICATION_JSON)
                .content(json))
            .andExpect(status().isOk())
            .andExpect(content().string(org.hamcrest.Matchers.containsString("Parsed: 2025")));
    }

    @Test
    void cyclicReferenceSerializesSafely() throws Exception {
        mockMvc.perform(post("/parent")
                .accept(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.name").value("부모"))
            .andExpect(jsonPath("$.child.name").value("자식"))
            .andExpect(jsonPath("$.child.parent").doesNotExist());
    }

    // Spring REST Docs 예시 (문서화)
    @Test
    void documentUserApi(@Autowired MockMvc mockMvc, @Autowired ObjectMapper objectMapper) throws Exception {
        String userJson = objectMapper.writeValueAsString(new User());

        mockMvc.perform(post("/user")
                .contentType(MediaType.APPLICATION_JSON)
                .content(userJson))
            .andExpect(status().isOk())
            .andDo(document("user-create",
                    requestFields(
                        fieldWithPath("name").description("이름"),
                        fieldWithPath("age").description("나이")
                    )
            ));
    }
}

 

 

 

1. 테스트 커버리지 확장

  • WebTestClient를 활용한 /user POST 테스트 추가
  • RestAssured 기반 HTTP 테스트 추가 (Swagger 연동 예비용)
  • 기존 MockMvc 테스트와 병행 가능하도록 구성

2. Spring REST Docs 문서화

  • @AutoConfigureRestDocs 설정 추가
  • 문서 출력 경로: build/generated-snippets
  • documentUserApi() 테스트에서 requestFields 문서 생성 유지

3. Swagger (OpenAPI) 연동 예시 주석 추가

  • Swagger 설정용 SwaggerConfig 예제 주석 제공
  • @EnableOpenApi + OpenAPI bean 정의 구조 제공

 

// Jackson 트러블슈팅 – Spring Boot 통합 테스트 기반 예제 (확장)

@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs(outputDir = "build/generated-snippets")
@Import({JacksonErrorIntegrationTest.CustomConfig.class})
public class JacksonErrorIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @RestController
    static class TestController {

        @PostMapping("/user")
        public ResponseEntity<String> createUser(@RequestBody User user) {
            return ResponseEntity.ok("OK");
        }

        @PostMapping("/role")
        public ResponseEntity<String> assignRole(@RequestBody RoleUser roleUser) {
            return ResponseEntity.ok("OK");
        }

        @PostMapping("/date")
        public ResponseEntity<String> parseDate(@RequestBody DateUser dateUser) {
            return ResponseEntity.ok("Parsed: " + dateUser.createdAt);
        }

        @PostMapping("/parent")
        public ResponseEntity<Parent> getParent() {
            Parent parent = new Parent("부모", null);
            Child child = new Child("자식", parent);
            parent.setChild(child);
            return ResponseEntity.ok(parent);
        }
    }

    // 기존 DTO 및 Enum 정의 생략 (동일 유지)

    // WebTestClient 기반 테스트 예제 추가
    @Autowired
    private WebApplicationContext context;

    private WebTestClient webTestClient;

    @BeforeEach
    void setUp() {
        this.webTestClient = WebTestClient.bindToApplicationContext(context).configureClient().baseUrl("/").build();
    }

    @Test
    void testUserPostWithWebTestClient() {
        webTestClient.post()
            .uri("/user")
            .contentType(MediaType.APPLICATION_JSON)
            .bodyValue("{\"name\":\"홍길동\", \"age\":30}")
            .exchange()
            .expectStatus().isOk();
    }

    // RestAssured 기반 예제 (Swagger 연동 예비)
    @Test
    void testUserPostWithRestAssured() {
        RestAssured.given()
            .contentType("application/json")
            .body("{\"name\":\"홍길동\", \"age\":30}")
        .when()
            .post("/user")
        .then()
            .statusCode(200);
    }

    // Swagger 연동 예시 주석 (실제 사용 시 SwaggerConfig 클래스 필요)
    // @EnableOpenApi
    // @Configuration
    // public class SwaggerConfig {
    //     @Bean
    //     public OpenAPI apiInfo() {
    //         return new OpenAPI()
    //                 .info(new Info().title("Jackson API").version("v1"));
    //     }
    // }

    // Spring REST Docs 문서 생성 테스트 예시 유지
    @Test
    void documentUserApi() throws Exception {
        String userJson = objectMapper.writeValueAsString(new User());

        mockMvc.perform(post("/user")
                .contentType(MediaType.APPLICATION_JSON)
                .content(userJson))
            .andExpect(status().isOk())
            .andDo(document("user-create",
                    requestFields(
                        fieldWithPath("name").description("이름"),
                        fieldWithPath("age").description("나이")
                    )
            ));
    }
}

 

1. 테스트 커버리지 확장

  • @Order, @Tag, @DisplayName을 사용하여 테스트 분류 및 리포팅 정리
  • 날짜 처리 (LocalDateTime) 파싱 성공 케이스 추가
  • 순환 참조 직렬화 테스트 (@JsonManagedReference, @JsonBackReference) 추가

2. API 문서 연동 (Spring REST Docs)

  • @AutoConfigureRestDocs 설정 유지
  • documentUserApi() 테스트에서 requestFields() 기반 문서 생성
  • 문서 출력 위치: build/generated-snippets

3. Swagger (springdoc-openapi) 설정 예시 주석 제공

  • @OpenAPIDefinition, GroupedOpenApi 설정 예시 추가
  • 실제 적용 시 swagger-ui 경로: /swagger-ui.html 또는 /v3/api-docs
// Jackson 트러블슈팅 – Spring Boot 통합 테스트 기반 예제 (확장 + 문서화 + 리포트)

@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs(outputDir = "build/generated-snippets")
@Import({JacksonErrorIntegrationTest.CustomConfig.class})
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class JacksonErrorIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @RestController
    static class TestController {

        @PostMapping("/user")
        public ResponseEntity<String> createUser(@RequestBody User user) {
            return ResponseEntity.ok("OK");
        }

        @PostMapping("/role")
        public ResponseEntity<String> assignRole(@RequestBody RoleUser roleUser) {
            return ResponseEntity.ok("OK");
        }

        @PostMapping("/date")
        public ResponseEntity<String> parseDate(@RequestBody DateUser dateUser) {
            return ResponseEntity.ok("Parsed: " + dateUser.createdAt);
        }

        @PostMapping("/parent")
        public ResponseEntity<Parent> getParent() {
            Parent parent = new Parent("부모", null);
            Child child = new Child("자식", parent);
            parent.setChild(child);
            return ResponseEntity.ok(parent);
        }
    }

    // WebTestClient 기반 테스트 예제 추가
    @Autowired
    private WebApplicationContext context;

    private WebTestClient webTestClient;

    @BeforeEach
    void setUp() {
        this.webTestClient = WebTestClient.bindToApplicationContext(context).configureClient().baseUrl("/").build();
    }

    @Test
    @Order(1)
    @Tag("web")
    void testUserPostWithWebTestClient() {
        webTestClient.post()
            .uri("/user")
            .contentType(MediaType.APPLICATION_JSON)
            .bodyValue("{\"name\":\"홍길동\", \"age\":30}")
            .exchange()
            .expectStatus().isOk();
    }

    @Test
    @Order(2)
    @Tag("restassured")
    void testUserPostWithRestAssured() {
        RestAssured.given()
            .contentType("application/json")
            .body("{\"name\":\"홍길동\", \"age\":30}")
        .when()
            .post("/user")
        .then()
            .statusCode(200);
    }

    // Swagger 연동 예시 (springdoc-openapi)
    // @OpenAPIDefinition(info = @Info(title = "Jackson API", version = "v1"))
    // @Configuration
    // public class SwaggerConfig {
    //     @Bean
    //     public GroupedOpenApi userApi() {
    //         return GroupedOpenApi.builder()
    //                 .group("user")
    //                 .pathsToMatch("/user/**")
    //                 .build();
    //     }
    // }

    // REST Docs 문서 생성 테스트 예시 유지
    @Test
    @Order(3)
    @Tag("docs")
    void documentUserApi() throws Exception {
        String userJson = objectMapper.writeValueAsString(new User());

        mockMvc.perform(post("/user")
                .contentType(MediaType.APPLICATION_JSON)
                .content(userJson))
            .andExpect(status().isOk())
            .andDo(document("user-create",
                    requestFields(
                        fieldWithPath("name").description("이름"),
                        fieldWithPath("age").description("나이")
                    )
            ));
    }

    // Allure 리포트용 태깅 예시 포함 가능
    @Test
    @Order(4)
    @Tag("date")
    @DisplayName("날짜 파싱 성공 케이스")
    void validDateParsing() throws Exception {
        String json = "{\"createdAt\":\"2025-05-10T15:00:00\"}";

        mockMvc.perform(post("/date")
                .contentType(MediaType.APPLICATION_JSON)
                .content(json))
            .andExpect(status().isOk())
            .andExpect(content().string(org.hamcrest.Matchers.containsString("Parsed: 2025")));
    }

    @Test
    @Order(5)
    @Tag("cycle")
    @DisplayName("순환 참조 직렬화 테스트")
    void cyclicReferenceSerializesSafely() throws Exception {
        mockMvc.perform(post("/parent")
                .accept(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.name").value("부모"))
            .andExpect(jsonPath("$.child.name").value("자식"))
            .andExpect(jsonPath("$.child.parent").doesNotExist());
    }
}

 

 

1. 테스트 커버리지 구조화

  • @Order, @Tag, @DisplayName, @Description을 활용한 테스트 메타 정보 부여
  • 테스트 목적이 명확히 드러나도록 함수명/어노테이션 구성 개선

2. Spring REST Docs 문서화 연동

  • @AutoConfigureRestDocs(outputDir = "build/generated-snippets") 설정 유지
  • documentUserApi() 테스트에 REST Docs 문서 스니펫 생성 포함

3. Swagger / springdoc-openapi 연동 예시 포함

  • SwaggerConfig 예시 주석 포함 (@OpenAPIDefinition, GroupedOpenApi)
  • 실제 적용 시 /v3/api-docs, /swagger-ui.html로 API 문서 확인 가능

4. Allure 리포트 통합 준비

  • @Tag("allure"), @Description 사용
  • 테스트 리포트 추적/분석용 메타 정보 정의

 

이 코드는 다음을 만족합니다:

  • 기능별 분리된 테스트
  • 문서 자동 생성
  • Swagger 문서 연동 구조
  • Allure 등 외부 리포팅 도구와의 통합 가능성

 

// Jackson 트러블슈팅 – Spring Boot 통합 테스트 기반 예제 (완성: 테스트 커버리지 + REST Docs + Swagger + Allure 리포트)

@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs(outputDir = "build/generated-snippets")
@Import({JacksonErrorIntegrationTest.CustomConfig.class})
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class JacksonErrorIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @RestController
    static class TestController {

        @PostMapping("/user")
        public ResponseEntity<String> createUser(@RequestBody User user) {
            return ResponseEntity.ok("OK");
        }

        @PostMapping("/role")
        public ResponseEntity<String> assignRole(@RequestBody RoleUser roleUser) {
            return ResponseEntity.ok("OK");
        }

        @PostMapping("/date")
        public ResponseEntity<String> parseDate(@RequestBody DateUser dateUser) {
            return ResponseEntity.ok("Parsed: " + dateUser.createdAt);
        }

        @PostMapping("/parent")
        public ResponseEntity<Parent> getParent() {
            Parent parent = new Parent("부모", null);
            Child child = new Child("자식", parent);
            parent.setChild(child);
            return ResponseEntity.ok(parent);
        }
    }

    @Autowired
    private WebApplicationContext context;

    private WebTestClient webTestClient;

    @BeforeEach
    void setUp() {
        this.webTestClient = WebTestClient.bindToApplicationContext(context).configureClient().baseUrl("/").build();
    }

    @Test
    @Order(1)
    @Tag("web")
    void WebTestClient_를_이용한_유저_등록_성공() {
        webTestClient.post()
            .uri("/user")
            .contentType(MediaType.APPLICATION_JSON)
            .bodyValue("{\"name\":\"홍길동\", \"age\":30}")
            .exchange()
            .expectStatus().isOk();
    }

    @Test
    @Order(2)
    @Tag("restassured")
    void RestAssured_를_이용한_JSON_전송_테스트() {
        RestAssured.given()
            .contentType("application/json")
            .body("{\"name\":\"홍길동\", \"age\":30}")
        .when()
            .post("/user")
        .then()
            .statusCode(200);
    }

    // Swagger springdoc 설정 클래스 (별도 파일로도 분리 가능)
    // @OpenAPIDefinition(info = @Info(title = "Jackson API", version = "v1"))
    // @Configuration
    // public class SwaggerConfig {
    //     @Bean
    //     public GroupedOpenApi jacksonApi() {
    //         return GroupedOpenApi.builder()
    //                 .group("jackson")
    //                 .pathsToMatch("/**")
    //                 .build();
    //     }
    // }

    @Test
    @Order(3)
    @Tag("docs")
    @DisplayName("Spring REST Docs를 통한 API 문서 생성")
    void document_user_api() throws Exception {
        String userJson = objectMapper.writeValueAsString(new User());

        mockMvc.perform(post("/user")
                .contentType(MediaType.APPLICATION_JSON)
                .content(userJson))
            .andExpect(status().isOk())
            .andDo(document("user-create",
                    requestFields(
                        fieldWithPath("name").description("이름"),
                        fieldWithPath("age").description("나이")
                    )
            ));
    }

    @Test
    @Order(4)
    @Tag("allure")
    @DisplayName("날짜 파싱 성공 케이스 (LocalDateTime)")
    @Description("정상적인 날짜 포맷이 들어왔을 때 LocalDateTime으로 역직렬화되는지 확인합니다.")
    void valid_date_parsing_with_allure() throws Exception {
        String json = "{\"createdAt\":\"2025-05-10T15:00:00\"}";

        mockMvc.perform(post("/date")
                .contentType(MediaType.APPLICATION_JSON)
                .content(json))
            .andExpect(status().isOk())
            .andExpect(content().string(org.hamcrest.Matchers.containsString("Parsed: 2025")));
    }

    @Test
    @Order(5)
    @Tag("cycle")
    @DisplayName("순환 참조 직렬화 테스트 (Jackson @JsonManagedReference)")
    @Description("Jackson이 순환 참조 구조를 안전하게 직렬화하는지 확인합니다. Child → Parent 역참조는 생략되어야 합니다.")
    void cyclic_reference_serialization_test() throws Exception {
        mockMvc.perform(post("/parent")
                .accept(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.name").value("부모"))
            .andExpect(jsonPath("$.child.name").value("자식"))
            .andExpect(jsonPath("$.child.parent").doesNotExist());
    }
}
728x90