본문 바로가기
게시판 만들기(Spring Boot, JPA)

게시판 만들기 05 - 테스트 코드 작성(PostController)

by burpee 2022. 12. 9.

저번 포스팅은 PostService 테스트 코드를 작성했다. 테스트 케이스는 게시글 쓰기, 게시글 단건 조회, 게시글 조회 실패 - 잘못된 ID였다. 

이번 포스팅은 PostController 테스트 코드를 작성할 예정이고, 게시글 페이징 조회, 수정, 삭제 API를 테스트 해보려고 한다.

 

Controller 테스트 코드를 작성하기 전에

Controller에 있는 로직들이 실행되려면 클라이언트로부터 내가 만든 애플리케이션으로 GET, POST, PUT, DELETE 등의 HTTP 요청이 와야 한다. 우리가 PostController를 만들 때 요청 Body에 게시글 데이터를 담고 [ic]/posts [/ic]로 POST 요청을 보내면 게시글을 생성하고, [ic]/posts/{postId}[/ic]로 GET 요청을 보내면 postId에 해당하는 게시글을 조회할 수 있다. 그럼 포스트맨 같은 API 테스트 툴을 사용하지 않고 테스트 코드 안에서 어떻게 우리가 만든 API에 웹 요청을 할 수 있을까?

 

스프링 공식문서 Testing 부분을 들어가서 보면 [ic]MockMvc[/ic] 라는 것을 제공한다고 한다. 영어로 되어있기 때문에 완벽하게 해석은 못하지만 번역기를 돌리면서 읽어보면 Spring MVC의 애플리케이션 테스트를 지원하고 자체적으로 요청을 수행하고 응답을 확인할 수도 있다고 한다. 그리고 실행 중인 서버 없이 Controller에 대한 테스트를 제공한다고 한다.

요약하면 Spring MVC에서 서블릿이 하는 일을 MockMvc라는 것으로 대체해서 서버 실행을하지 않아도 Controller에 요청을 보내고 응답을 받을 수 있는 테스트 환경을 구축할 수 있다. 이런 느낌인 것 같다. 그럼 나는 MockMvc를 사용해서 테스트 코드를 작성해 봐야겠다.

 

PostControllerTest

@SpringBootTest
@AutoConfigureMockMvc
class PostControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private PostRepository postRepository;

    @Autowired
    private PostService postService;

    @BeforeEach
    void clean() {
        postRepository.deleteAll();
    }
}

 

PostService 테스트 코드를 작성할 때 사용했던 [ic]command + shift + T [/ic]를 사용해서 PostControllerTest를 생성했다. 같은 패턴으로 [ic]@SpringBootTest [/ic] 어노테이션을 붙여주고 [ic] postRepository [/ic], [ic] postService [/ic] 의존성 주입받고, [ic]@BeforeEach [/ic] 사용해서 각 테스트 코드가 실행되기 전에 테이블을 비워주는 메서드도 만들어 준다.

그리고 처음보는 [ic] objectMapper [/ic]라는 객체가 생겼다. 이 객체는 객체를 문자열로 바꿔주는 메서드를 가지고 있다. 클라이언트가 데이터를 Body에 담아서 HTTP 요청을 할 때 JSON 형식의 문자열로 보내게 되는데, 우리는 Dto를 JSON 문자열로 바꿔서 요청을 할 때 사용할 것이다.

그리고 [ic]mockMvc[/ic]도 의존성 주입을 받았다. 보통 MockMvc를 사용할 때 [ic]@WebMvcTest [/ic] 어노테이션을 사용한다. 해당 어노테이션을 붙여서 MockMvc를 사용하면 Test에서 Controller 부분만 사용해서 테스트가 가능하고 이후에 연결된 Service, Repository 계층들은 테스트에서 사용할 수 없다고 한다. 나는 Service, Repository 계층도 사용해서 테스트를 할 예정이기 때문에 [ic]@SpringBootTest [/ic]와 [ic]@AutoConfigureMockMvc [/ic]을 같이 사용하려고 한다. 나중에는 그 두 개의 어노테이션 사이의 차이점들을 따로 알아봐야겠다.

 

이제 테스트코드를 작성해보자.

 

PostController 테스트 코드 작성

게시글 페이징 조회

@Test
@DisplayName("게시글 페이징 조회")
void getPostListTest() throws Exception {
    // given
    List<Post> postList = IntStream.rangeClosed(1, 20).mapToObj(i -> Post.builder()
                    .title("test title" + i)
                    .content("test content" + i)
                    .createdDate(LocalDateTime.now())
                    .build())
            .collect(Collectors.toList());

    postRepository.saveAll(postList);

    // expected
    mockMvc.perform(get("/posts?page=1&pageSize=10")
                    .contentType(APPLICATION_JSON))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.totalCount", is(20)))
            .andExpect(jsonPath("$.pageSize", is(10)))
            .andExpect(jsonPath("$.postList[0].title").value("test title20"))
            .andExpect(jsonPath("$.postList[0].content").value("test content20"))
            .andDo(print());
}

 

테스트 흐름을 보면 20개의 Post 엔티티를 만들고 전부 저장을 한다. 그리고 mockMvc를 사용해서 [ic]/posts? page=1&pageSize=10 [/ic]으로 GET 요청을 보낸다. 마지막으로 응답에 대한 상태와 반환 데이터들의 값을 검증한다.

 

요청을 보낼 때 [ic] perform()[/ic] 메서드를 사용한다. perform 메서드는 RequestBuilder 객체를 인자로 받는데 이 인자는 MockMvcRequestBuilders에 있는 [ic] get()[/ic], [ic] post()[/ic], [ic] put()[/ic], [ic] delete()[/ic]와 같은 정적 메서드들을 통해 생성한다. 

그리고 그 요청에 대한 결과는 [ic] andExpect()[/ic] 메서드를 이용해 확인한다. 결과 검증은 MockMvcResultMatchers의 [ic] status()[/ic], [ic] jsonPath()[/ic]를 사용했다.

마지막으로 생성된 요청과 응답 메시지를 모두 확인할 때는 [ic] andDo()[/ic] 메서드에 MockMvcResultHandler의 [ic] print()[/ic] 메서드를 사용한다.

 

Request

게시글 페이징 조회 테스트 Request

 

Response

게시글 페이징 조회 테스트 Response

 

게시글 페이징 조회 테스트 결과 - 성공

 

게시글 수정

@Test
@DisplayName("게시글 수정")
void updatePostTest() throws Exception {
    // given
    Post post = Post.builder()
            .title("test title")
            .content("test content")
            .build();

    Post savedPost = postRepository.save(post);

    RequestUpdatePostDto requestUpdatePostDto = RequestUpdatePostDto.builder()
            .title("test title edit")
            .content("test content edit")
            .build();

    String json = objectMapper.writeValueAsString(requestUpdatePostDto);

    // expected
    mockMvc.perform(put("/posts/{postId}", savedPost.getPostId())
                    .contentType(APPLICATION_JSON)
                    .content(json))
            .andExpect(status().isOk())
            .andDo(print());
}

 

테스트 흐름을 보면 Post 엔티티를 만들고 저장한다. 그리고 게시글을 수정할 때 사용하는 RequestUpdatePostDto를 만들고 ObjectMapper를 사용해서 JSON으로 만들어준다. 마지막으로 [ic]/posts/{postId}[/ic]에 PUT 요청을 보낸다. 이때 아까 만들어 둔 JSON을 함께 보내준다.

 

게시글 삭제

@Test
@DisplayName("게시글 삭제")
void deletePostTest() throws Exception {
    // given
    Post savedPost = postRepository.save(Post.builder()
            .title("test title")
            .content("test content")
            .build());

    Long postId = savedPost.getPostId();

    // expected
    mockMvc.perform(delete("/posts/{postId}", postId)
                    .contentType(APPLICATION_JSON))
            .andExpect(status().isNoContent())
            .andDo(print());
}

 

테스트 흐름을 보면 Post 엔티티를 만들고 저장한다. 그리고 [ic]/posts/{postId}[/ic]로 DELETE 요청을 보낸다. 그리고 결과를 검증한다.

 

여기까지 해서 지금까지 만든 게시글 CRUD를 만들고 테스트 코드를 작성하면서 Controller, Service 계층을 테스트해봤다. 나름대로 배운 것을 적용하면서 했지만 아직 개선하거나 추가해야 할 것이 있을 것이다. 예를 들면 예외처리 같은 것들을 한 번에 처리할 수 있게 만든다던지... 뭐 어쨌든 완벽한 결과는 아니어도 지속적으로 수정하고 추가하면서 더 좋게 만들어 봐야겠다.

 


소스코드

https://github.com/cheoljin408/simple-board-api

댓글