기술과 산업/언어 및 프레임워크
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