SpringMVC - MockMvc, @WebMvcTest코드까보기
개요
테스트 코드를 짜면서 어쩌다보니 쓰고 있는 이 클래스, 어노테이션들 정리할 필요를 느꼈다.
😎 MockMvc, @WebMvcTest
@WebMvcTest
어노테이션을 알기 전 MockMvc
에 대해 알고 있어야한다. @WebMvcTest
문서를 보면 MockMvc
에 대한 언급이 나오기 때문이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@WebMvcTest(controllers = UsersController.class)
class UsersControllerTest {
void signup(UserSignupRequest userSignupRequest) throws Exception {
@Autowired
MockMvc mockMvc;
...
//when , then
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(userSignupRequest))
.with(csrf())
)
...
}
...
}
이런식으로 사용하고 있는 mockMvc
mockMvc는 컨트롤러에 가짜 HTTP요청을 보낸다.
Spring은 DispatcherServlet
이라는 실제 웹 요청을 처리하는 서블릿을 가지고 있는데 mockMvc는 DispatcherServlet
을 모방하여 요청을 보낸다.
이러한 가짜 서블릿을 통해 실제 웹 서버가 뜨지 않고도 컨트롤러에 요청을 보낼 수 있어서 테스트가 가능하다.
단점으로는 실제 웹 서버와 다르게 동작할 수 있어서 정확한 테스트가 어려울 수도 있다는 것이다.
죽 mockMvc는
- 컨트롤러와 관련된 정보를 가지고 가짜 HTTP 요청 생성을 한다.
- 가짜 HTTP 요청을 컨트롤러에 전달한다. 이 요청은
DispatcherServlet
을 모방한 방식대로 처리됨. 요청한 컨트롤러를 찾고, 요청을 전달한다. - 컨트롤러의 응답을 검증한다. 이는
.andExpect()
같은 함수를 통해 검증이 가능하다.
그러면 MockMvc는 어떻게 가짜 servlet을 만들 수 있는걸까?
그러면 다시 @WebMvcTest
에 대해 간단히 언급해야한다.
이는 @SpringBootApplication
가 자동설정을 도와주듯 해당 어노테이션도 테스트에 필요한 설정들을 자동화해준다고 현재는 이해하자.
아무튼 @WebMvcTest
코드를 보면
1
2
3
4
5
@Bean
@ConditionalOnMissingBean
public MockMvc mockMvc(MockMvcBuilder builder) {
return builder.build();
}
@ConditionalOnMissingBean는 이미 등록된 빈이 없을때 새로 만들어준다는 코드
라는 코드가 있다. 빈으로 등록하는 코드이다. 파라미터로 들어온 builder에는 웹 컨텍스트의 정보가 들어있다고 알고 있자.
아무튼 MockMvc 빈을 생성할 때 빌더패턴을 통해 빌드를 하게 되는 것 같다. 그래서 해당 코드로 또 들어가보면
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Override
@SuppressWarnings("rawtypes")
public final MockMvc build() {
WebApplicationContext wac = initWebAppContext();
ServletContext servletContext = wac.getServletContext();
MockServletConfig mockServletConfig = new MockServletConfig(servletContext);
for (MockMvcConfigurer configurer : this.configurers) {
RequestPostProcessor processor = configurer.beforeMockMvcCreated(this, wac);
if (processor != null) {
if (this.defaultRequestBuilder == null) {
this.defaultRequestBuilder = MockMvcRequestBuilders.get("/");
}
if (this.defaultRequestBuilder instanceof ConfigurableSmartRequestBuilder) {
((ConfigurableSmartRequestBuilder) this.defaultRequestBuilder).with(processor);
}
}
}
Filter[] filterArray = this.filters.toArray(new Filter[0]);
return super.createMockMvc(filterArray, mockServletConfig, wac, this.defaultRequestBuilder,
this.defaultResponseCharacterEncoding, this.globalResultMatchers, this.globalResultHandlers,
this.dispatcherServletCustomizers);
}
요약하자면 테스트용 서블릿 컨텍스트가 있는데, 이에 저장된 필터, 설정들을 실제 서블릿 컨텍스트에 넣는다.
그리고 createMockMvc()
을 리턴하기에 이 코드도 까봐야한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
protected final MockMvc createMockMvc(Filter[] filters, MockServletConfig servletConfig,
WebApplicationContext webAppContext, @Nullable RequestBuilder defaultRequestBuilder,
List<ResultMatcher> globalResultMatchers, List<ResultHandler> globalResultHandlers,
@Nullable List<DispatcherServletCustomizer> dispatcherServletCustomizers) {
TestDispatcherServlet dispatcherServlet = new TestDispatcherServlet(webAppContext);
...
try {
dispatcherServlet.init(servletConfig);
}
catch (ServletException ex) {
// should never happen..
throw new MockMvcBuildException("Failed to initialize TestDispatcherServlet", ex);
}
MockMvc mockMvc = new MockMvc(dispatcherServlet, filters);
mockMvc.setDefaultRequest(defaultRequestBuilder);
mockMvc.setGlobalResultMatchers(globalResultMatchers);
mockMvc.setGlobalResultHandlers(globalResultHandlers);
return mockMvc;
}
보아하니, 테스트용 DispatcherServlet을 만들고 MockMvc객체를 생성할 때 넣는다.
new MockMvc(dispatcherServlet, filters)
을 보면
1
2
3
4
5
6
7
8
9
MockMvc(TestDispatcherServlet servlet, Filter... filters) {
Assert.notNull(servlet, "DispatcherServlet is required");
Assert.notNull(filters, "Filters cannot be null");
Assert.noNullElements(filters, "Filters cannot contain null values");
this.servlet = servlet;
this.filters = filters;
this.servletContext = servlet.getServletContext();
}
이렇게 되어있다. 즉, MockMvc는 빈에 등록되어있고, 테스트용 DispatcherServlet
과 설정값들을 가지고 있다.
그러면 실제 MockMvc인스턴스의 함수를 호출하는 곳을 보자.
1
2
3
4
5
6
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(userSignupRequest))
.with(csrf())
)
.andExpect(MockMvcResultMatchers.status().isOk())
이렇게 구현해놨는데, .perform()
은 어떻게 수행되는걸까??
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public ResultActions perform(RequestBuilder requestBuilder) throws Exception {
...
//1. 테스트용 HttpServletRequest 객체에 값을 넣음.
MockHttpServletRequest request = requestBuilder.buildRequest(this.servletContext);
//2. 비동기 서블릿 컨텍스트 호출 => null
AsyncContext asyncContext = request.getAsyncContext();
MockHttpServletResponse mockResponse;
HttpServletResponse servletResponse;
if (asyncContext != null) {
...
}
//3. null이므로 새로운 테스트용 HttpServletResponse 객체를 만듦. 이때는 빈 값이다. status도 200이고 response의 내용물은 비어있다.
else {
mockResponse = new MockHttpServletResponse();
servletResponse = mockResponse;
}
//4. 인풋 헤더를 검증하는 부분같은데 잘 이해 못했습니다.
if (requestBuilder instanceof SmartRequestBuilder) {
request = ((SmartRequestBuilder) requestBuilder).postProcessRequest(request);
}
//5. mvcResult는 mockRequest,와 mockResponse를 가지고 있다.
MvcResult mvcResult = new DefaultMvcResult(request, mockResponse);
//6. 리퀘스트 속성에 이값을 키-값 형태로 넣는다.
request.setAttribute(MVC_RESULT_ATTRIBUTE, mvcResult);
...
//7. 테스트용 필터를 생성하는데, 이 값에는 기본 필터들과 사용자가 정의한 필터(Jwt 관련)가 들어있다. 또한 인자로 들어간 테스트용 서블릿객체도 들어가 있다.
MockFilterChain filterChain = new MockFilterChain(this.servlet, this.filters);
//8. 실질적으로 요청을 보내는 부분이다. 들어온 필터를 도는데 결국 마지막에 들어간 서블릿객체의 service()도 호출하여 실질적으로 가짜HTTP요청이 MockMvc가 만든 서블릿객체에 들어간다.
filterChain.doFilter(request, servletResponse);
...
//9. 결국 요청에 대한 응답을 받는다.
applyDefaultResultActions(mvcResult);
RequestContextHolder.setRequestAttributes(previousAttributes);
//10. andExpect()처럼 후처리.
return new ResultActions() {
@Override
public ResultActions andExpect(ResultMatcher matcher) throws Exception {
matcher.match(mvcResult);
return this;
}
@Override
public ResultActions andDo(ResultHandler handler) throws Exception {
handler.handle(mvcResult);
return this;
}
@Override
public MvcResult andReturn() {
return mvcResult;
}
};
}
참고로
파라미터로 들어온 requestBuilder
는 이런식으로 저장되어있다.
쓰다말다 쓰다말다해서 내용이 좀 난잡하다. 참고만 해야할것 같다.