MockMvc를 배워서 남주자 - gae-jang-mo/app GitHub Wiki
저희는 기존에 Controller
를 테스트할 때 인수테스트 목적으로 WebTestClient
를 사용했습니다. 그래서 이번 프로젝트에도 WebTestClient
를 사용하려고 했지만, 스프링 시큐리티를 추가하면서 문제가 생겼습니다. WebTestClient
를 사용하면@WithMockUser
사용이 쉽지 않아서 MockMvc
에 대해서 알아보았습니다. 알아본 결과 단순한 시큐리티 테스트 목적이 아닌, 인수테스트는 WebTestClient
를, Controller 테스트는 MockMvc
사용이 적합하다고 판단하여 용도에 맞춰 나눠서 사용하기로 했습니다. MockMvc
사용 방법을 간단하게 공유하기 위해서 이 글을 작성했습니다.
주의: 잘못된 부분이 있을 수 있습니다. 잘못된 부분 발견 혹은 더 나은 방법이 있으면 피드백 부탁드립니다.
- Spring test mvc 발표자료 - MockMvc의 What, Why, How에 대해서 설명이 좋습니다.
- Spring Mvc Test VS End-to-End Tests
- ATDD with Spring Boot - Controller TDD 방법 설명 최고입니다.
나중에 번역할게요..ㅎㅎ;
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.
가장 큰 차이점이라면 Servlet Container를 사용하느냐 안 하느냐의 차이입니다. MockMvc는 Servlet Container를 생성하지 않습니다. 반면, @SpringBootTest와 TestRestTemplate은 Servlet Container를 사용합니다. 그래서 마치 실제 서버가 동작하는 것처럼(물론 몇몇 빈은 Mock 객체로 대체될 수는 있습니다) 테스트를 수행할 수 있습니다. 또한, 테스트하는 관점도 서로 다릅니다. MockMvc는 서버 입장에서 구현한 API를 통해 비즈니스 로직이 문제없이 수행되는지 테스트를 할 수 있다면, TestRestTemplate은 클라이언트 입장에서 RestTemplate을 사용하듯이 테스트를 수행할 수 있습니다.
출처: https://meetup.toast.com/posts/124
- 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.
- WebTestClient can also be used to test real web services using HTTP.
- Specify
@SpringBootTest
instead of@WebFluxText
.
- Specify
- 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
@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);
}
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));
}
}
.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);
}
@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
를 사용하는 것도 방법입니다.
- https://www.slideshare.net/sbcoba/spring-test-mvc - Spring test mvc 발표자료
- https://jdm.kr/blog/165
- https://meetup.toast.com/posts/124