03. Controller에 대한 단위 테스트
- -
Controller와 단위 테스트
Model에서 비지니스 로직을 작성할 때 작성된 메서드들이 오류없이 잘 동작하는지 점검하기 위해 단위테스트를 실시했었다. 처음 테스트 케이스를 구축할 때는 번거로웠지만 일단 테스트 케이스들이 준비되면 테스트 자동화로 인해 개발 시간이 훨씬 줄어드는 것을 확인할 수 있었다.
그럼 Controller에서의 단위 테스트는 어떨까?
Controller에서의 단위 테스트
앞선 포스트에서 handler method를 만들고 잘 동작하는지 매번 스프링 애플리케이션을 실행하고 브라우저를 통해 서버로 요청한 후 결과 페이지가 생각대로 잘 나오는지 확인 해야했다. 이 과정에 시간이 많이 소요되는 것은 물론 제대로 검증하는 것도 매우 힘들다.
이 과정에서 우리가 주로 확인한 내용들은 어떤 것들이었을까?
- 요청 경로에 대해 적절한 handler method가 호출되는가?
- 입력 파라미터는 handler method에 잘 전달되는가?
- model에 설정한 값은 잘 참조 되는가?
- 요청 결과 페이지는 잘 연결되는가?
등이다. 즉 Model에서 테스트 하던 비지니스 로직과는 사뭇 다르다. 바로 결과를 보는 것이 아니라 HttpServletRequest와 HttpServletResponse의 내용을 검증해봐야 하는데 이들을 만들기는 쉽지 않다.
또 Model의 동작과 큰 차이점은 Model에서는 모든 코드가 우리의 손끝에서 나왔지만 Controller의 동작에는 브라우저나 WAS처럼 우리가 프로그래밍하지 않은 요소가 개입된다는 점이다. 매번 브라우저, WAS를 동작시키며 테스트 하기는 시간도 많이 걸리고 환경/제품에 따라 동작도 약간씩 다를 수 있기 때문에 동일한 결과가 나오지 않을 수 있다.
따라서 Controller의 단위 테스트를 위해서는 MockMvc이라는 객체가 사용된다. MockMvc는 테스트를 위해 브라우저나 WAS의 동작을 똑같이 처리해줄 수 있는 환경이라고 생각하면 된다. MockMvc를 이용해 브라우저에서 발생하는 요청을 가상으로 만들고 컨트롤러가 응답하는 내용을 기반으로 검증을 수행한다.
휴대폰 가게 앞에 놓여있는 가짜 헨드폰을 Mockup이라고 한다. 똑같이 생긴 가짜다.
단위 테스트의 기본 틀
다음은 Controller 테스트를 위한 간단한 단위 테스트 모습을 보여준다.
package com.eshome.mvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import com.eshome.mvc.controller.HelloController;
@SpringBootTest
public class ControllerTest {
@Autowired
HelloController hController;
private MockMvc mock;
@BeforeEach
public void setup() {
mock = MockMvcBuilders.standaloneSetup(hController).build();
}
@Test
public void welcomeViewTest() throws Exception {
mock.perform(get("/")).andExpect(status().isOk());
}
}
MockMvc는 매번 테스트가 실행될 때마다 초기화 해주기 위해서 @BeforeEach에서 초기화 처리한다. standalongSetup은 Object ... 형태로 Controller 객체를 받기 때문에 테스트 하려는 여러 Controller를 등록할 수 있다.
또는 WebApplicationContext를 이용하는 webApplicationContextSetup을 사용하면 모든 Controller를 한번에 처리할 수도 있다.
@Autowired
WebApplicationContext ctx;
MockMvc mock;
@BeforeEach
public void test() {
mock = MockMvcBuilders.webAppContextSetup(ctx).build();
}
MockMvc 객체는 크게 3가지 동작을 갖는다.
- perform: 요청 즉 가상의 request를 처리한다.
- 요청은 MockHttpServletRequestBuilder를 통해서 생성한다.
- expect: 결과 즉 가상의 response에 대해 검증한다.
- 검증의 항목은 ResultMatchers를 반환하는 handler(), status(), model(), view() 등 메서드에 따른다.
- do: 테스트 과정에서 콘솔 출력 등 직접 처리할 일을 작성한다.
- 실제 동작은 ResultHandler를 사용한다.
요청 만들기
요청은 MockMvcRequestBuilders의 static 메서드인 get, post, put, delete, fileUpload 등을 이용해서 MockHttpServletRequestBuilder 객체를 생성하는 것에서 시작한다.
MockHttpServletRequestBuilder builder = get("/add");
MockHttpServletRequestBuilder는 ServletRequest를 구성하기에 필요한 다양한 메서드를 제공한다.
메서드명 | 설명 |
param / params | 요청 파라미터를 설정한다. |
header / headers | 요청 헤더를 설정한다. |
cookie | 쿠키를 설정한다. |
requestAttr | request scope에 attribute를 설정한다. |
flashAttr | flash scope에 attribute를 설정한다. |
sessionAttr | session scope에 attribute를 설정한다. |
locale | Locale 정보를 설정한다. |
characterEncoding | 요청의 인코딩 정보를 설정한다. |
contentType | Enum인 MediaType으로 요청의 컨텐트 타입을 설정한다. |
file | fileUpload로 ServletRequestBuilder를 생성한 경우 업로드 파일을 지정한다. |
위 메서드들은 method chaining을 지원하기 때문에 쭉 연결해서 작성하는 것이 일반적이다. 만들어진 요청은 MockMvc의 perform 메서드에 파라미터로 설정해준다.
MockHttpServletRequestBuilder builder =
get("/add")
.param("a", "4.5").param("b", "3")
.cookie(new Cookie("name", "홍길동"))
.locale(Locale.KOREA);
mock.perform(builder);
검증하기
perform의 결과로 ResultActions가 반환되는데 이 객체의 andExpect 메서드에 ResultMatcher를 넘겨줘서 검증한다. ResultMatcher는 아래의 MockMvcResultMatchers가 가지는 static 메서드를 통해서 얻는다.
메서드명 | 설명 |
handler | 요청에 매핑된 컨트롤러를 검증한다. |
header | 응답 헤더의 값을 검증한다. |
cookie | 응답을 통해 전달된 쿠키를 검증한다. |
content | 응답의 본문 내용을 검증한다. |
view | Controller의 handler method가 반환한 view의 이름을 검증한다. |
model | model에 담긴 attribute 값을 검증한다. |
flash | flash scope에 담긴 attribute 값을 검증한다. |
forwardedUrl / forwardedUrlPattern | forward로 이동하는 대상의 경로를 검증한다. |
redirectedUrl / redirectedUrlPattern | redirect로 이동하는 경우 대상의 경로를 검증한다. |
status | Http 상태 코드를 이용해 검증한다. |
ResultActions 역시 method chaining 형태로 작성할 수 있다.
mock.perform(builder)
.andExpect(handler().handlerType(HelloController.class))
.andExpect(handler().methodName("add"))
.andExpect(forwardedUrl("index"))
.andExpect(header().stringValues("Content-Language", "en"))
.andExpect(model().attribute("message", "4.5와 3의 합은 7.5입니다."))
.andExpect(view().name("index"))
.andExpect(status().is(200));
실행하기
실행하기는 검증하기와 마찬가지로 ResultActions의 andDo 메서드를 이용한다. 파라미터로 ResultHandler 를 전달하는데 이것들은 MockMvcResultHandlers에 static 메서드로 정의되어있다.
메서드 명 | 설명 |
실행 결과를 표준 출력(System.out)을 이용해 출력한다. |
mock.perform(builder)
.andExpect(status().is(200))
.andDo(print());
MockHttpServletRequest:
HTTP Method = GET
Request URI = /add
Parameters = {a=[4.5], b=[3]}
Headers = []
Body = <no character encoding set>
Session Attrs = {}
Handler:
Type = com.eshome.mvc.controller.HelloController
Method = com.eshome.mvc.controller.HelloController#add(Double, Integer, Model)
Async:
Async started = false
Async result = null
Resolved Exception:
Type = null
ModelAndView:
View name = index
View = null
Attribute = message
value = 4.5와 3의 합은 7.5입니다.
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Content-Language:"en"]
Content type = null
Body =
Forwarded URL = index
Redirected URL = null
Cookies = []
다양한 단위테스트 처리해보기
위 내용을 바탕으로 몇 가지 handler method에 대한 단위 테스트를 처리해보자.
public String redir(Model model)
다음은 메서드가 redirect로 다른 리소스를 호출하는 경우의 단위 테스트 예이다.
@RequestMapping("/redir")
public String redir(Model model) {
model.addAttribute("message", service.sayHello());
return "redirect:/";
}
@Test
public void redirTest() throws Exception {
mock.perform(get("/redir"))
.andExpect(status().is3xxRedirection())
.andExpect(model().attribute("message", "what"))
.andExpect(view().name("redirect:/"))
.andExpect(redirectedUrl("/?message=what"));
}
public String onlyPost(Model model)
다음은 post로 호출되어야 하는 메서드에 대한 단위 테스트 예이다.
@RequestMapping(value = "/onlypost", method = RequestMethod.POST)
public String onlyPost(Model model) {
String attr = service.echo("post 요청에만 응답함");
model.addAttribute("message", attr);
return "index";
}
@Test
public void methodTest() throws Exception {
// get으로 호출한 경우 405 오류가 발생해야 한다. 4XX로 퉁친다
mock.perform(get("/onlypost"))
.andExpect(status().is4xxClientError());
mock.perform(post("/onlypost"))
.andExpect(status().isOk());
}
public String useDto(@ModelAttribute UserInfo user, Model model)
다음은 다양한 파라미터를 처리하는 예이다.
@GetMapping(value = "/dto")
public String useDto(@ModelAttribute UserInfo user, Model model) {
model.addAttribute("message", user);
return "index";
}
@Test
public void dtoTest() throws Exception {
UserInfo user = new UserInfo();
user.setName("홍길동");
user.setAge(30);
List<String> hobbies = new ArrayList<>();
hobbies.add("sleep");
hobbies.add("study");
user.setHobbies(hobbies);
mock.perform(get("/dto").param("name", "홍길동")
.param("age", "30")
.param("hobbies", "sleep", "study"))
.andExpect(status().isOk())
.andExpect(model().attribute("message", user));
}
public String setCookie(Model model, HttpServletResponse res)
다음은 설정된 쿠키 값을 검증하는 예이다.
@RequestMapping("/cookieMaker")
public String setCookie(Model model, HttpServletResponse res) {
Cookie cookie1 = new Cookie("userName", "홍길동");
cookie1.setMaxAge(60 * 5);
res.addCookie(cookie1);
model.addAttribute("message", "쿠키 설정 완료");
return "index";
}
@Test
public void setCookieTest() throws Exception {
mock.perform(get("/cookieMaker"))
.andExpect(cookie().value("userName", "홍길동"))
.andExpect(cookie().maxAge("userName", 60 * 5))
.andExpect(status().is2xxSuccessful());
}
'Spring MVC > 01.Spring @MVC' 카테고리의 다른 글
06. 파라미터의 formatting (0) | 2020.07.07 |
---|---|
05. Handler Interceptor (1) | 2020.07.03 |
04. 웹과 관련된 설정 (0) | 2020.07.02 |
02. Controller (0) | 2020.06.30 |
01. Spring MVC 개요 (0) | 2020.06.29 |
소중한 공감 감사합니다