MockMvc를 배워서 남주자 - gae-jang-mo/app GitHub Wiki

서론

저희는 기존에 Controller를 테스트할 때 인수테스트 목적으로 WebTestClient 를 사용했습니다. 그래서 이번 프로젝트에도 WebTestClient를 사용하려고 했지만, 스프링 시큐리티를 추가하면서 문제가 생겼습니다. WebTestClient 를 사용하면@WithMockUser 사용이 쉽지 않아서 MockMvc 에 대해서 알아보았습니다. 알아본 결과 단순한 시큐리티 테스트 목적이 아닌, 인수테스트는 WebTestClient 를, Controller 테스트는 MockMvc 사용이 적합하다고 판단하여 용도에 맞춰 나눠서 사용하기로 했습니다. MockMvc 사용 방법을 간단하게 공유하기 위해서 이 글을 작성했습니다.

주의: 잘못된 부분이 있을 수 있습니다. 잘못된 부분 발견 혹은 더 나은 방법이 있으면 피드백 부탁드립니다.

읽기 전에 참고할만한 자료

읽은 후에 참고할만한 자료

1. TestRestTemplate, WebTestClient, MockMvc 차이

나중에 번역할게요..ㅎㅎ;

TestRestTemplate vs WebTestClient

Spring Framework는 REST endpoints 호출하기 위한 두 가지 선택 사항을 제공합니다.

  • RestTemplate: The original Spring REST client with a synchronous, template method API.
  • WebClient: a non-blocking, reactive alternative that supports both synchronous and asynchronous as well as streaming scenarios.

As of 5.0, the non-blocking, reactive WebClient offers a modern alternative to the RestTemplate with efficient support for both synchronous and asynchronous as well as streaming scenarios. The RestTemplate will be deprecated in a future version and will not have major new features added going forward.

MockMvc vs TestRestTemplate, WebTestClient

가장 큰 차이점이라면 Servlet Container를 사용하느냐 안 하느냐의 차이입니다. MockMvc는 Servlet Container를 생성하지 않습니다. 반면, @SpringBootTest와 TestRestTemplate은 Servlet Container를 사용합니다. 그래서 마치 실제 서버가 동작하는 것처럼(물론 몇몇 빈은 Mock 객체로 대체될 수는 있습니다) 테스트를 수행할 수 있습니다. 또한, 테스트하는 관점도 서로 다릅니다. MockMvc는 서버 입장에서 구현한 API를 통해 비즈니스 로직이 문제없이 수행되는지 테스트를 할 수 있다면, TestRestTemplate은 클라이언트 입장에서 RestTemplate을 사용하듯이 테스트를 수행할 수 있습니다.

출처: https://meetup.toast.com/posts/124

Similarities

  • Both provide a fluent-style syntax for testing web services.
  • Both can or do operate in a simulated environment that bypasses the use of HTTP.

Major Differences

  • WebTestClient can also be used to test real web services using HTTP.
    • Specify @SpringBootTest instead of @WebFluxText.
  • WebTestClient only works if you are using Netty for your local server.
    • This feels like an artificial limitation for the test environment.
    • It is likely due to the non-blocking nature of the underlying [WebClient](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/reactive/function/client/WebClient.html).
  • WebTestClient can test Streaming Responses

출처: https://stackoverflow.com/questions/49330878/what-is-the-difference-between-mockmvc-and-webtestclient


2. MockMvc 사용 방법

2.1 기존의 WebTestClient를 사용한 코드

@Test
void 회원가입() {
    // given
    String email = "[email protected]";
    String name = "inputName";
    UserRequest userRequest = UserRequest.builder()
            .email(email)
            .name(name)
            .password("P@ssw0rd")
            .build();

    //when
    UserResponse userResponse = webTestClient.post()
            .uri("/api/users")
            .contentType(MediaType.APPLICATION_JSON_UTF8)
            .accept(MediaType.APPLICATION_JSON_UTF8)
            .body(Mono.just(userRequest), UserRequest.class)
            .exchange()
            .expectStatus().isCreated()
            .expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
            .expectBody(UserResponse.class)
            .returnResult()
            .getResponseBody();

    //then
    assertThat(userResponse.getId()).isNotNull();
    assertThat(userResponse.getEmail()).isEqualTo(email);
    assertThat(userResponse.getName()).isEqualTo(name);
}

2.2 MockMvc로 구현하기

import com.fasterxml.jackson.databind.ObjectMapper;
import com.wootecobook.turkey.user.service.dto.UserRequest;
import com.wootecobook.turkey.user.service.dto.UserResponse;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;

import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@SpringBootTest
@AutoConfigureMockMvc // Mock테스트시 필요한 의존성을 제공해줍니다.
class UserApiControllerTestWithMock {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Test
    void 회원가입() throws Exception {
        // given
        String email = "[email protected]";
        String name = "inputName";
        UserRequest userRequest = UserRequest.builder()
                .email(email)
                .name(name)
                .password("P@ssw0rd")
                .build();

        // when
        ResultActions resultActions = mockMvc.perform(post("/api/users")
                .content(objectMapper.writeValueAsString(userRequest))
                .accept(MediaType.APPLICATION_JSON_UTF8)
                .contentType(MediaType.APPLICATION_JSON_UTF8))
                .andDo(print());

        // then
        resultActions
                .andExpect(status().isCreated())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
                .andExpect(jsonPath("id").exists())
                .andExpect(jsonPath("email").value(email))
                .andExpect(jsonPath("name").value(name));
    }
}

2.3 리턴 받은 json을 객체로 매핑하는 방법

.andExpect(jsonPath("id").exists()) 이 아닌 직접 객체로 받아서 테스트 하고 싶은 경우 ObjectMapper를 사용하는 방법이 있습니다. UserResponse userResponse = objectMapper.readValue(contentAsByteArray, UserResponse.class);

@Test
void 회원가입_json() throws Exception {
    // given
    String email = "[email protected]";
    String name = "inputName";

    // when
    ResultActions resultActions = mockMvc.perform(post("/api/users")
            .param("name", name).param("email", email).param("password", "P@ssw0rd")
            .accept(MediaType.APPLICATION_JSON_UTF8)
            .contentType(MediaType.APPLICATION_JSON_UTF8))
            .andDo(print());

    // then
    byte[] contentAsByteArray = resultActions
            .andExpect(status().isCreated())
            .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
            .andReturn().getResponse().getContentAsByteArray();

    UserResponse userResponse = objectMapper.readValue(contentAsByteArray, UserResponse.class);

    assertThat(userResponse.getId()).isNotNull();
    assertThat(userResponse.getEmail()).isEqualTo(email);
    assertThat(userResponse.getName()).isEqualTo(name);
}

2.4 @WebMvcTest

@SproingBootTest는 수많은 스프링 빈을 등록하여 테스트에 필요한 의존성을 추가해줍니다. 따라서 이 수많은 빈들을 등록하지 않고 필요한 빈만 등록하여 테스트를 할 때 @WebMvcTest를 사용합니다. @WebMvcTest 어노테이션을 사용하면 테스트에 사용할 @Controller 클래스@ControllerAdvice, @JsonComponent, @Filter, WebMvcConfigurer, HandlerMethodArgumentResolver 등을 스캔합니다. 그리고 MockMvc를 자동으로 설정하여 빈으로 등록합니다.

@WebMvcTest(ArticleApiController.class)
public class ArticleApiControllerTest {
    @Autowired
    private MockMvc mvc;
    @MockBean
    private ArticleService articleService;

    @Test
    public void testGetArticles() throws Exception {
        List<Article> articles = asList(
                new Article(1, "kwseo", "good", "good content", now()),
                new Article(2, "kwseo", "haha", "good haha", now()));

        given(articleService.findFromDB(eq("kwseo"))).willReturn(articles);

        mvc.perform(get("/api/articles?author=kwseo"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("@[*].author", containsInAnyOrder("kwseo", "kwseo")));
    }

    private Timestamp now() {
        return Timestamp.valueOf(LocalDateTime.now());
    }
}

출처: https://meetup.toast.com/posts/124

ArticleApiController와 관련된 빈만 로드하여 가벼운 테스트를 수행합니다. 하지만 다양한 설정이 필요한 경우 (ex. @MockBean) @SpringBootTest 를 사용하는 것도 방법입니다.

References

⚠️ **GitHub.com Fallback** ⚠️