org.springframework.restdocs.snippet.SnippetException: Cannot document response fields as the response body is empty

경쓱타드·2024년 11월 5일

에러

목록 보기
11/19

문제 상황

org.springframework.restdocs.snippet.SnippetException: Cannot document response fields as the response body is empty

Restdocs를 이용해서 controller 테스트 중이었는데, 200 status이지만 body에 값이 제대로 전달되지 않아서 response body을 매칭할 수 없었다. 이 경우에는 response body 부분에 대한 코드를 없애면 테스트를 통과하지만 그렇게 하면 Swagger 문서를 제대로 작성할 수 없기 때문에 문제를 해결해보기로 한다.

해결방법

일단 실제 controller에서 값이 어디까지 mock이 되는지 확인하기 위해서 System.out.println을 사용했다.

// Test
    @Test
    @DisplayName("레슨 업데이트")
    void putLesson() throws Exception {
        // given
        LessonRequestDto.Put putDto = new LessonRequestDto.Put();
        putDto.setId(1L);
        putDto.setClassId(1L);
        putDto.setStartTime(LocalDateTime.now());
        putDto.setMinutes(90);
        putDto.setIsDone(false);
        putDto.setType(Type.MAKEUP);
        putDto.setProgress("수정된 수업 진도");
        putDto.getHomeworks().addAll(List.of("Homework 1", "Homework 2"));

        given(lessonService.updateLesson(any(Lesson.class))).willReturn(lesson);
        given(lessonMapper.lessonToLessonResponseDto(any(Lesson.class))).willReturn(response);

        // when, then
        mockMvc.perform(put("/lesson")
                        .header(AUTHORIZATION, "Bearer service.access.token")
                        .content(objectMapper.writeValueAsString(putDto))
                        .contentType(MediaType.APPLICATION_JSON))
                .andDo(print())
                .andExpect(status().isOk())
                .andDo(document("레슨 업데이트",
                        resource(ResourceSnippetParameters.builder()
                                .description("레슨 업데이트")
                                .tag("Lesson API")
                                .requestHeaders(headerWithName(AUTHORIZATION).description("액세스 토큰"))
                                .requestFields(
                                        fieldWithPath("id").description("레슨 ID"),
                                        fieldWithPath("classId").description("연관된 클래스 ID"),
                                        fieldWithPath("startTime").description("수업 시작 시간"),
                                        fieldWithPath("minutes").description("수업 시간 (분)"),
                                        fieldWithPath("isDone").description("수업 완료 여부"),
                                        fieldWithPath("type").description("수업 유형(ORIGINAL : 정규수업, MAKEUP : 보충수업)"),
                                        fieldWithPath("progress").description("수업 진도"),
                                        fieldWithPath("homeworks").description("숙제 목록")
                                )
                                .responseFields(
                                        fieldWithPath("id").description("레슨 ID"),
                                        fieldWithPath("classId").description("연관된 클래스 ID"),
                                        fieldWithPath("startTime").description("수업 시작 시간"),
                                        fieldWithPath("minutes").description("수업 시간 (분)"),
                                        fieldWithPath("isDone").description("수업 완료 여부"),
                                        fieldWithPath("type").description("수업 유형(ORIGINAL : 정규수업, MAKEUP : 보충수업)"),
                                        fieldWithPath("progress").description("수업 진도"),
                                        fieldWithPath("homeworks").description("숙제 목록")
                                ).build()
                        )
                ));
    }
// Controller
    // UPDATE
    @PutMapping("/lesson")
    public ResponseEntity putLesson(@Auth LoginUser loginUser,
                                    @Valid @RequestBody LessonRequestDto.Put put) {
        System.out.println("!! put : " + put);
        Lesson lesson = lessonService.updateLesson(lessonMapper.lessonPutDtoToLesson(put));
        System.out.println("!! lesson : "+ lesson);
        LessonResponseDto.Response response = lessonMapper.lessonToLessonResponseDto(lesson);
        System.out.println("!! response : "+ response);
        return new ResponseEntity<>(response, HttpStatus.OK);
    }

put 까지는 제대로 전달되지만 그 이후의 값은 제대로 전달되지 않는다. 이와 비슷한 코드는 정상적으로 테스트를 통과한다. 코드는 다음과 같다.

// test
    @Test
    @DisplayName("수업 생성")
    void postLesson() throws Exception {
        // given
        LessonRequestDto.Post postDto = new LessonRequestDto.Post();
        postDto.setClassId(1L);
        postDto.setStartTime(LocalDateTime.now());
        postDto.setMinutes(60);
        postDto.setIsDone(false);
        postDto.setType(Type.ORIGINAL);
        postDto.setProgress("수업 진도");
        postDto.getHomeworks().addAll(List.of("Homework1", "Homework2"));

        given(lessonService.createLesson(any())).willReturn(lesson);
        given(lessonMapper.lessonToLessonResponseDto(lesson)).willReturn(response);

        // when, then
        mockMvc.perform(post("/lesson")
                        .header(AUTHORIZATION, "Bearer service.access.token")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(postDto)))
                .andDo(print())
                .andExpect(status().isCreated())
                .andDo(document("수업 생성",
                        resource(ResourceSnippetParameters.builder()
                                .description("수업 생성")
                                .tag("Lesson API")
                                .requestHeaders(headerWithName(AUTHORIZATION).description("액세스 토큰"))
                                .requestFields(
                                        fieldWithPath("classId").description("수업을 연결할 클래스 ID"),
                                        fieldWithPath("startTime").description("수업 시작 시간"),
                                        fieldWithPath("minutes").description("수업 시간 (분)"),
                                        fieldWithPath("isDone").description("수업 완료 여부"),
                                        fieldWithPath("type").description("수업 유형(ORIGINAL : 정규수업, MAKEUP : 보충수업)"),
                                        fieldWithPath("progress").description("수업 진도"),
                                        fieldWithPath("homeworks").description("숙제 목록")
                                )
                                .responseFields(
                                        fieldWithPath("id").description("수업 ID"),
                                        fieldWithPath("classId").description("수업이 연결된 클래스 ID"),
                                        fieldWithPath("startTime").description("수업 시작 시간"),
                                        fieldWithPath("minutes").description("수업 시간 (분)"),
                                        fieldWithPath("isDone").description("수업 완료 여부"),
                                        fieldWithPath("type").description("수업 유형(ORIGINAL : 정규수업, MAKEUP : 보충수업)"),
                                        fieldWithPath("progress").description("수업 진도"),
                                        fieldWithPath("homeworks").description("숙제 목록")
                                ).build()
                        )
                ));
    }
// controller
    // CREATE
    @PostMapping("/lesson")
    public ResponseEntity postLesson(@Auth LoginUser loginUser,
                                     @Valid @RequestBody LessonRequestDto.Post post) {
        Lesson lesson = lessonService.createLesson(lessonMapper.lessonPostDtoToLesson(post));
        LessonResponseDto.Response response = lessonMapper.lessonToLessonResponseDto(lesson);

        return new ResponseEntity<>(response, HttpStatus.CREATED);
    }

이 코드는 또 정상 작동을 한다. 처음부터 이 코드를 비교해야했지만 다른 정상작동하는 코드를 통해서 문제를 해결하려다가 문제 해결에 시간이 오래걸렸다. 문제는 any()였다.

// 문제없이 정상 동작했던 코드
given(lessonService.createLesson(any())).willReturn(lesson);
// null값이 전달되었던 코드
given(lessonService.updateLesson(any(Lesson.class))).willReturn(lesson);

여기서 문제를 확인할 수 있었다. postLesson 메서드에서 lessonMapper.lessonPostDtoToLesson(post)에 대해서 mock을 하지 않아도 any()이기 때문에 정상적으로 mock설정이 적용되었다. 하지만 putLesson 메서드에서는 any(Lesson.class)를 해서 Lesson 객체가 있어야 mock 설정이 동작한다. 하지만 lessonMapper.lessonPutDtoToLesson(put)에 대한 mock 설정이 없어서 lessonService.updateLesson 인자값에 null이 들어가고, 그로 인해 mock 설정이 정상적으로 적용되지 않아서 원하지 않은 에러가 발생하였다. any()를 넣는 것보다는 lessonMapper.lessonPutDtoToLesson(put)에 대한 mock 설정을 통해서 이 문제를 해결했다.

  • 에러 흐름도
// << putLesson >>
// 여기서 lessonMapper.lessonPutDtoToLesson(put) 값에 대한 mock 설정이 없어서 null값이 전달됨
// given(lessonService.updateLesson(any(Lesson.class))).willReturn(lesson)에서 null값은 any(Lesson.class)이 아니기 때문에 mock 설정이 동작하지 않음
// 결국 lesson은 null이 됨
Lesson lesson = lessonService.updateLesson(lessonMapper.lessonPutDtoToLesson(put));
// lesson값은 null로 전달됨
// given(lessonMapper.lessonToLessonResponseDto(any(Lesson.class))).willReturn(response) 여기서 any(Lesson.class)가 null값에는 동작하지 않아서 response 또한 null이 됨
LessonResponseDto.Response response = lessonMapper.lessonToLessonResponseDto(lesson);

// << createLesson >> 
// lessonMapper.lessonPostDtoToLesson(post)에 대해서 null값이 전달됨
// given(lessonService.createLesson(any())).willReturn(lesson)에서 any()이므로 lesson에 원하는 인자값을 전달할 수 있음
Lesson lesson = lessonService.createLesson(lessonMapper.lessonPostDtoToLesson(post));
// given(lessonMapper.lessonToLessonResponseDto(lesson)).willReturn(response)
// 여기에서도 객체 lesson값이 전달되므로 정상적으로 response값을 mock할 수 있음
LessonResponseDto.Response response = lessonMapper.lessonToLessonResponseDto(lesson);
  • 최종 해결 방법
// 추가한 콛,
given(lessonMapper.lessonPutDtoToLesson(any(LessonRequestDto.Put.class))).willReturn(lesson);
given(lessonService.updateLesson(any(Lesson.class))).willReturn(lesson);
given(lessonMapper.lessonToLessonResponseDto(any(Lesson.class))).willReturn(response);
profile
백엔드 개발자를 시작으로 도메인 이해도까지 풍부한 개발자가 목표입니다!

0개의 댓글