ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Jackson JSON 트러블슈팅을 위한 각 오류별 구체적인 예제와 Spring Boot 기반의 통합 테스트
    기술과 산업/언어 및 프레임워크 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
Designed by Tistory.