SpringBoot를 사용한 게시판 API 만들기_05

Travel·2023년 8월 14일
0

PostApiController와 Swagger3.0

PostApiController

@Api(tags = "게시판 CRUD API")
@RestController
@RequiredArgsConstructor
@RequestMapping("/posts")
public class PostApiController {

    private final PostService postService;

    /**
     * [API] 게시글 등록
     *
     * @return ResponseEntity<Object>: 생성된 게시글 번호 및 응답 코드 반환
     */
    @Operation(summary = "게시글 등록", description = "게시글을 등록 합니다.\n + X-USERID는 3~10자 사이여야 합니다.")
    @ApiResponse(code = 201, message ="INSERT SUCCESS")
    @PostMapping
    public CustomApiResponse<Long> addPost(@Valid @RequestBody CreatePostDto createPostDto, @RequestHeader(name = "X-USERID") @Size(min=3,max = 10) String userId) {

        Long result = postService.addPost(createPostDto, userId);


        return new CustomApiResponse<>(result,SuccessCode.INSERT_SUCCESS);

    }

    /**
     * [API] 게시글 단건 조회
     *
     * @return ResponseEntity<Object>: 조회한 게시글 결과 및 응답 코드 반환
     */
    @Operation(summary = "게시글 단건 조회", description = "입력한 게시글 번호에 대한 게시글 정보를 조회 합니다.")
    @ApiResponse(code = 200, message ="INSERT SUCCESS", response = CustomApiResponse.class)
    @GetMapping("/{postId}")
    public CustomApiResponse<ResponsePostDto> getPost(@PathVariable Long postId) {

        ResponsePostDto result = postService.getPost(postId);

        return new CustomApiResponse<>(result,SuccessCode.SELECT_SUCCESS);
    }

    /**
     * [API] 게시글 수정
     *
     * @return RResponseEntity<Object>: 수정된 게시글 번호 및 응답 코드 반환
     */

    @Operation(summary = "게시글 수정", description = "해당 게시글에 대한 정보를 수정합니다.\n + X-USERID는 3~10자 사이여야 합니다.")
    @ApiResponse(code = 200, message ="INSERT SUCCESS")
    @PatchMapping
    public CustomApiResponse<Long> modifyPost(@Valid @RequestBody UpdatePostDto updatePostDto, @RequestHeader(name = "X-USERID") @Size(min=3,max = 10) String userId) {

        if (updatePostDto.getId() == null) {
            throw new IllegalArgumentException("게시글 번호는 필수 입니다.");
        }

        Long result = postService.modifyPost(updatePostDto, userId);

        return new CustomApiResponse<>(result,SuccessCode.UPDATE_SUCCESS);
    }

    /**
     * [API] 게시글 삭제
     *
     * @return ResponseEntity<ApiResponse<PostDto>>: 삭제된 게시글 번호 및 응답 코드 반환
     */
    @Operation(summary = "게시글 삭제", description = "해당 게시글을 삭제합니다.\n + X-USERID는 3~10자 사이여야 합니다.")
    @ApiResponse(code = 204, message ="INSERT SUCCESS")
    @DeleteMapping("{postId}")
    public CustomApiResponse<Long> deletePost(@PathVariable Long postId, @RequestHeader(name = "X-USERID") @Size(min=3,max = 10) String userId) {

        postService.deletePost(postId, userId);

        return new CustomApiResponse<>(postId,SuccessCode.DELETE_SUCCESS);
    }

    /**
     * [API] 게시글 목록 조회
     *
     * @return ResponseEntity<Object>: 삭제된 게시글 번호 및 응답 코드 반환
     */
    @Operation(summary = "게시글 목록 조회", description = "해당되는 카테고리의 게시글 목록을 조회합니다. - 빈 값일시 모든 게시글을 기준으로 조회")
    @ApiResponse(code = 200, message ="INSERT SUCCESS")
    @GetMapping
    public CustomApiResponse<PageResultDto<ResponsePostListDto>> getPostList(
            @RequestParam(name = "page", defaultValue = "1") int page,
            @RequestParam(name = "size", defaultValue = "5") int size,
            @RequestParam(name = "keyword", required = false) String keyword
    ) {

        PageRequestDto pageRequestDto = new PageRequestDto(page,size,keyword);

        PageResultDto<ResponsePostListDto> result = postService.getPostList(pageRequestDto);


        return new CustomApiResponse<>(result,SuccessCode.SELECT_SUCCESS);
    }
}

PostApiControllerTest

@WebMvcTest을 사용했으며 MockMvc를 사용하여 API 엔드포인트의 용청과 응답을 테스트를 진행했다.

Error "jpa metamodel must not be empty"
@WebMvcTest시에는 controller, SpringMV 레벨의 컴포넌트만 구성되는데 JPA-Auditing 관련 빈이 등록이 안된 상태로 Application 에 있던 @EnableJpaAuditing이 수행되면서 발생하는 에러
@MockBean(JpaMetamodelMappingContext.class)을 Mock 빈을 주입하여 해결


@WebMvcTest(PostApiController.class)
@MockBean(JpaMetamodelMappingContext.class)
class PostApiControllerTest {

    @Autowired
    MockMvc mockMvc;

    @MockBean
    PostService postService;

    @Autowired
    ObjectMapper objectMapper;

    @Nested
    @DisplayName("게시글 등록")
    class addPostControllerTest {

        @DisplayName("성공")
        @Test
        void success() throws Exception {
            //given
            CreatePostDto postDto = CreatePostDto.builder()
                    .name("SpringBoot")
                    .title("게시글 생성")
                    .content("게시글내용")
                    .build();

            String xUserId = "user1";

            Post post = Post.builder()
                            .name("SpringBoot")
                            .title("게시글 생성")
                            .content("게시글내용")
                            .author(xUserId)
                            .build();

            when(postService.addPost(any(CreatePostDto.class), anyString())).thenReturn(post.getId());

            //when
            //then
            mockMvc.perform(post("/posts")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(postDto))
                            .header("X-USERID",xUserId))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.resultMsg").value("INSERT SUCCESS"));
        }

        @DisplayName("실패 - X-USERID 값 null")
        @Test
        void xUserIdIsNull() throws Exception {
            //given
            CreatePostDto postDto = CreatePostDto.builder()
                    .name("SpringBoot")
                    .title("게시글 생성")
                    .content("게시글내용")
                    .build();

            //when
            //then
            mockMvc.perform(post("/posts")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(postDto)))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.message").value("X-USERID 값은 필수 입니다."));
        }

        @DisplayName("실패 - X-USERID 유효성")
        @Test
        void xUserIdInvalid() throws Exception{
            //given
            CreatePostDto postDto = CreatePostDto.builder()
                    .name("SpringBoot")
                    .title("게시글 생성")
                    .content("게시글내용")
                    .build();

            String xUserId = "uuser1asduwqrqrq";

            //when
            //then
            mockMvc.perform(post("/posts")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(postDto))
                            .header("X-USERID", xUserId))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.message").value("X-UERID는 3자에서 10자 사이여야 합니다."));
        }

        @DisplayName("실패 - 게시글 카테고리 null")
        @Test
        void postNameIsNull() throws Exception{
            //given
            CreatePostDto postDto = CreatePostDto.builder()
                    .title("게시글 제목")
                    .content("게시글내용")
                    .build();

            String xUserId = "user1";

            //when
            //then
            mockMvc.perform(post("/posts")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(postDto))
                            .header("X-USERID", xUserId))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.message").value("게시글의 카테고리는 필수 값 입니다."));
        }

        @DisplayName("실패 - 게시글 제목 길이")
        @Test
        void postTitleIsEmpty() throws Exception{
            //given
            String title = "게시글제목";
            title = title.repeat(21);
            CreatePostDto postDto = CreatePostDto.builder()
                    .name("SpringBoot")
                    .title(title)
                    .content("게시글내용")
                    .build();

            String xUserId = "user1";

            //when
            //then
            mockMvc.perform(post("/posts")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(postDto))
                            .header("X-USERID", xUserId))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.message").value("게시글 제목은 1~100자 사이로 작성해 주세요."));
        }

        @DisplayName("실패 - 게시글 제목 길이")
        @Test
        void postTitleIsNull() throws Exception{
            //given
            CreatePostDto postDto = CreatePostDto.builder()
                    .name("SpringBoot")
                    .content("게시글내용")
                    .build();

            String xUserId = "user1";

            //when
            //then
            mockMvc.perform(post("/posts")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(postDto))
                            .header("X-USERID", xUserId))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.message").value("게시글의 제목은 필수 값 입니다."));
        }

    }

    @Nested
    @DisplayName("게시글 수정")
    class modifyPostControllerTest {

        @DisplayName("성공")
        @Test
        void success() throws Exception {
            //given
            String modifiedText = "텍스트 수정";
            UpdatePostDto updatePostDto = UpdatePostDto.builder()
                    .id(1L)
                    .name("Spring")
                    .title("타이틀 변경")
                    .content(modifiedText)
                    .build();

            String xUserId = "user1";

            when(postService.modifyPost(any(),anyString()))
                    .thenReturn(1L);


            //when
            //then
            mockMvc.perform(patch("/posts")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(updatePostDto))
                            .header("X-USERID",xUserId))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.resultMsg").value("UPDATE SUCCESS"));
        }

        @DisplayName("실패 - 게시글번호 null")
        @Test
        void postIdIsNull() throws Exception {
            //given

            UpdatePostDto updatePostDto = UpdatePostDto.builder()
                    .name("SpringBoot")
                    .title("게시글 생성")
                    .content("게시글 내용")
                    .build();
            //when
            //then
            mockMvc.perform(patch("/posts")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(updatePostDto))
                            .header("X-USERID", "user2"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.message").value("게시글 번호는 필수 값 입니다."));
        }

        @DisplayName("실패 - 작성자 수정자 다름")
        @Test
        void mismatchXUerId() throws Exception{
            //given
            Post post = Post.builder()
                    .name("SpringBoot")
                    .title("게시글 생성")
                    .content("게시글 내용")
                    .author("user1")
                    .build();

            UpdatePostDto updatePostDto = UpdatePostDto.builder()
                    .id(1L)
                    .name("SpringBoot")
                    .title("게시글 생성")
                    .content("게시글 내용")
                    .build();

            when(postService.modifyPost(any(), anyString()))
                    .thenThrow(new BusinessExceptionHandler("작성자와 수정자가 다릅니다.", ErrorCode.FORBIDDEN_ERROR));

            //when
            //then
            mockMvc.perform(patch("/posts")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(updatePostDto))
                            .header("X-USERID", "user2"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.message").value("작성자와 수정자가 다릅니다."));
        }

    }

    @DisplayName("게시글 삭제")
    @Test
    void deletePost() throws Exception {
        //given
        Long postId = 1L;
        String xUserId = "user1";

        //when
        //then
        mockMvc.perform(delete("/posts/"+postId)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(postId))
                .header("X-USERID", xUserId))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.resultMsg").value("DELETE SUCCESS"));
    }

    @DisplayName("게시글 목록 조회")
    @Test
    void getPostList() throws Exception {
        //given
        String param = "page=1&size=5&keyword='카테고리'";

        //when
        //then
        mockMvc.perform(get("/posts?"+param)
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.resultMsg").value("SELECT SUCCESS"));
    }


}

Swagger

@Api

  • 클래스 레벨에 사용되며, 해당 컨트롤러 클래스에 대한 API 문서화 정보를 제공합니다.
    tags 속성으로 API 문서를 그룹화할 태그를 지정할 수 있습니다.

@Operation

  • 메서드 레벨에 사용되며, 해당 메서드에 대한 기능 설명

  • summary:작업에 대한 간단한 설명을 제공

  • description: 작업에 대한 보다 자세한 설명을 제공

@ApiResponse

  • 메서드 레벨에 사용되며, 메서드의 응답에 대한 정보를 제공합니다.
  • code: HTTP 응답 코드를 지정
  • message: 응답 코드에 대한 설
  • response 응답의 타입을 지정

@NotNull, @Min, @Max, @Size
Swagger 문서에서 API테스트 진행시 해당 어노테이션 기능을 지원해준다고한다.
ex)헤더의 X-USERID의 경우에 3~10자를 만족하지 않을경우 Excute가 실행되지 않음, 아래 이미처럼 붉게 표시되는걸 확인할 수 있다.

Swagger 문서

2개의 댓글

comment-user-thumbnail
2023년 8월 14일

큰 도움이 되었습니다, 감사합니다.

1개의 답글