스프링 컨트롤러 테스트(MockHttpServletRequest, MockHttpServletResponse)
Service나 DAO 같은 비즈니스 로직과 달리 Controller의 경우 테스트 코드를 만들기가 되게 애매하다. 이유는 Service나 Dao와 달리 Controller의 경우 Http Request나 Response등의 요청이 필요하고 또한 결과값인 Model이나 View등의 정보를 어떻게 받아야 하는지도 애매하다.
그렇기에 스프링에서는 이와같은 것들을 테스트하는데 도움을 주기위해 Mock 오브젝트를 제공하고 있다. Mock이란 아래와 같은 정의를 갖고 있다.
Mock이란?
실제 객체를 만들어 사용하기에 시간, 비용 등의 Cost가 높거나 혹은 객체 서로간의 의존성이 강해 구현하기 힘들 경우 가짜 객체를 만들어 사용하는 방법이다.
각설하고, 스프링은 MockHttpServletRequest와 MockHttpServletResponse등의 클래스를 제공하여 Controller를 테스트하기 용이하게 해주는데 오늘은 이러한 클래스 및 토비의 스프링에 나와있는 예제 클래스를 이용하여 내가 만들고 있는 프로젝트의 Controller 클래스를 테스트하는 것이 목적이다.
[HomeController.java]
[url]/sample로 GET요청을 수행시 model에 키 : name , value : 주현태인 모델을 리턴한다.
@Controller
public class HomeController
@RequestMapping(value = "/sample")
public String pathVariableSamplePOST(@RequestParam int id, Model model) {
model.addAttribute("name","주현태");
return "samplePath";
}
}
[ConfigurableDispatcherServlet.java]
토비의 스프링에 나와있는 Servlet이다. DispatcherServlet을 상속받고 있으며 Context설정 경로 및 기타 Servlet 기능을 갖고 있다.
(설정경로를 설정할 수 있도록 만든 커스텀 서블릿이라고 생각하면 될 것 같다.)
public class ConfigurableDispatcherServlet extends DispatcherServlet{
private Class<?>[] classes;
private String[] locations;
private ModelAndView modelAndView;
public ConfigurableDispatcherServlet() {
}
public ConfigurableDispatcherServlet(String[] locations) {
this.locations = locations;
}
public ConfigurableDispatcherServlet(Class<?>[] classes) {
this.classes = classes;
}
public void setLocations(String ...locations) {
this.locations = locations;
}
public void setRelativeLocations(Class clazz, String ...relativeLocations) {
String[] locations=new String[relativeLocations.length];
String currentPath=ClassUtils.classPackageAsResourcePath(clazz)+"/";
for(int i=0;i<relativeLocations.length;i++) {
locations[i]=currentPath+relativeLocations[i];
}
this.setLocations(locations);
}
public void setClasses(Class<?>[] classes) {
this.classes = classes;
}
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
modelAndView=null;
super.service(req, res);
}
@Override
protected WebApplicationContext createWebApplicationContext(ApplicationContext parent) {
AbstractRefreshableWebApplicationContext wac=new AbstractRefreshableWebApplicationContext() {
@Override
protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
if(locations!=null) {
XmlBeanDefinitionReader xmlReader=new XmlBeanDefinitionReader(beanFactory);
xmlReader.loadBeanDefinitions(locations);
}
if(classes !=null) {
AnnotatedBeanDefinitionReader reader=new AnnotatedBeanDefinitionReader(beanFactory);
reader.register(classes);
}
}
};
wac.setServletContext(getServletContext());
wac.setServletConfig(getServletConfig());
wac.refresh();
return wac;
}
@Override
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
this.modelAndView=mv;
super.render(mv, request, response);
}
public ModelAndView getModelAndView() {
return modelAndView;
}
}
[HelloControllerTest.java]
테스트 클래스이다. @WebAppConfiguration이라는 어노테이션은 테스트 클래스에서 많이 쓰이는데 spring docs에 나와있는 정의는 다음과 같다.
@WebAppConfiguration is a class-level annotation that is used to declare that the ApplicationContext loaded for an integration test should be a WebApplicationContext.
The presence of @WebAppConfiguration on a test class indicates that a WebApplicationContext should be loaded for the test using a default for the path to the root of the web application. To override the default, specify an explicit resource path via the value() attribute.
Note that @WebAppConfiguration must be used in conjunction with @ContextConfiguration, either within a single test class or within a test class hierarchy.
This annotation may be used as a meta-annotation to create custom composed annotations.
말인즉슨 테스트클래스에 테스트용도로 사용하면서 @ContextConfiguration 어노테이션과 함께 사용하는 웹 어플리케이션용 테스트 어노테이션이고 ApplicationContext에 WebApplicationContext를 통합적으로 load시켜주도록 한다. 즈ㅡ즉, 테스트하기 위해 ApplicationContext위에 HomeController클래스가 올라가져 있어야 하니 필요한 클래스이다.
아래와 같이 /sample url에 get요청을 하여서 알맞은 값이 오는지 테스트하여보자.
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(locations = { "classpath:config/*-context.xml",
"file:src/main/webapp/WEB-INF/spring/appServlet/servlet-context.xml" })
public class HomeControllerTest {
@Test
public void test() throws ServletException, IOException {
ConfigurableDispatcherServlet servlet = new ConfigurableDispatcherServlet();
servlet.setLocations("classpath:config/*-context.xml",
"file:src/main/webapp/WEB-INF/spring/appServlet/servlet-context.xml");
servlet.init(new MockServletConfig("appServlet"));
MockHttpServletRequest req = new MockHttpServletRequest("GET", "/sample");
req.addParameter("id", "1");
MockHttpServletResponse res = new MockHttpServletResponse();
servlet.service(req, res);
ModelAndView mav = servlet.getModelAndView();
String name= (User) mav.getModel().get("name");
assertThat(name, is("주현태"));
}
}
[결과]
성공적으로 테스트가 완료되었다
그런데 토비의 스프링에서는 이러한 테스팅 코드위에 아래와 같은 상위클래스를 하나 더함으로써 테스트를 더욱 간단하게 할 수 있도록 하고 있다.
[AbstractDispatcherServletTest.java]
public class AbstractDispatcherServletTest implements AfterRunService {
protected MockHttpServletRequest request;
protected MockHttpServletResponse response;
protected MockServletConfig config = new MockServletConfig("spring");
protected MockHttpSession session;
private ConfigurableDispatcherServlet dispatcherServlet;
private Class<?>[] classes;
private String[] locations;
private String[] relativeLocations;
private String servletPath;
public AbstractDispatcherServletTest initRequest(String requestUri, String method) {
this.request=new MockHttpServletRequest(method,requestUri);
this.response=new MockHttpServletResponse();
if(this.servletPath !=null) this.setServletPath(this.servletPath);
return this;
}
public AbstractDispatcherServletTest initRequest(String requestUri, RequestMethod method) {
return this.initRequest(requestUri, method.toString());
}
public AbstractDispatcherServletTest initRequest(String requestUri) {
initRequest(requestUri,RequestMethod.GET);
return this;
}
public AbstractDispatcherServletTest addParameter(String name, String value) {
if(this.request==null)
throw new IllegalStateException("request가 초기화되지 않았습니다.");
this.request.addParameter(name, value);
return this;
}
public AbstractDispatcherServletTest buildDispatcherServlet() throws ServletException{
if(this.classes ==null && this.locations ==null && this.relativeLocations==null) {
throw new IllegalStateException("classes와 locations 중 하나는 설정해야 합니다.");
}
this.dispatcherServlet=new ConfigurableDispatcherServlet();
this.dispatcherServlet.setClasses(this.classes);
this.dispatcherServlet.setLocations(this.locations);
if(this.relativeLocations!=null)
this.dispatcherServlet.setRelativeLocations(getClass(), this.relativeLocations);
this.dispatcherServlet.init(this.config);
return this;
}
public AfterRunService runService() throws ServletException, IOException{
if(this.dispatcherServlet ==null) buildDispatcherServlet();
if(this.request==null)
throw new IllegalStateException("request가 준비되지 않았습니다.");
this.dispatcherServlet.service(this.request, this.response);
return this;
}
public MockHttpServletRequest getRequest() {
return request;
}
public void setRequest(MockHttpServletRequest request) {
this.request = request;
}
public MockHttpServletResponse getResponse() {
return response;
}
public void setResponse(MockHttpServletResponse response) {
this.response = response;
}
public MockServletConfig getConfig() {
return config;
}
public void setConfig(MockServletConfig config) {
this.config = config;
}
public MockHttpSession getSession() {
return session;
}
public void setSession(MockHttpSession session) {
this.session = session;
}
public ConfigurableDispatcherServlet getDispatcherServlet() {
return dispatcherServlet;
}
public void setDispatcherServlet(ConfigurableDispatcherServlet dispatcherServlet) {
this.dispatcherServlet = dispatcherServlet;
}
public Class<?>[] getClasses() {
return classes;
}
public AbstractDispatcherServletTest setClasses(Class<?>[] classes) {
this.classes = classes;
return this;
}
public String[] getLocations() {
return locations;
}
public AbstractDispatcherServletTest setLocations(String... locations) {
this.locations = locations;
return this;
}
public String[] getRelativeLocations() {
return relativeLocations;
}
public AbstractDispatcherServletTest setRelativeLocations(String... relativeLocations) {
this.relativeLocations = relativeLocations;
return this;
}
public String getServletPath() {
return servletPath;
}
public AbstractDispatcherServletTest setServletPath(String servletPath) {
if(this.request==null){
this.servletPath=servletPath;
}
else {
this.request.setServletPath(servletPath);
}
return this;
}
@Override
public String getContentString() {
return null;
}
@Override
public WebApplicationContext getContext() {
if(this.dispatcherServlet==null)
throw new IllegalStateException("DispatcherServlet이 준비되지 않았습니다.");
return this.dispatcherServlet.getWebApplicationContext();
}
@Override
public <T> T getBean(Class<T> beanType) {
if(this.dispatcherServlet==null)
throw new IllegalStateException("DispatcherServlet이 준비되지 않았습니다.");
return this.getContext().getBean(beanType);
}
@Override
public ModelAndView getModelAndView() {
return this.dispatcherServlet.getModelAndView();
}
@Override
public AfterRunService assertViewName(String viewName) {
assertThat(this.getModelAndView().getViewName(), is(viewName));
return this;
}
@Override
public AfterRunService assertModel(String name, Object value) {
assertThat(this.getModelAndView().getModel().get(name), is(value));
return this;
}
@After
public void closeServletContext() {
if(this.dispatcherServlet!=null) {
((ConfigurableApplicationContext)dispatcherServlet.getWebApplicationContext()).close();
}
}
}
[HomeControllerTest.java]
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(locations = { "classpath:config/*-context.xml", "classpath:web-context.xml" })
public class HomeControllerTest extends AbstractDispatcherServletTest {
@Test
public void test() throws ServletException, IOException {
ConfigurableDispatcherServlet servlet = new ConfigurableDispatcherServlet();
servlet.setLocations("classpath:config/*-context.xml", "classpath:web-context.xml");
servlet.init(new MockServletConfig("appServlet"));
MockHttpServletRequest req = new MockHttpServletRequest("GET", "/sample");
req.addParameter("id", "1");
MockHttpServletResponse res = new MockHttpServletResponse();
servlet.service(req, res);
ModelAndView mav = servlet.getModelAndView();
User user = (User) mav.getModel().get("user");
assertThat(user.getName(), is("주현태"));
}
@Test
public void test2() throws ServletException, IOException {
ModelAndView mav = setLocations("classpath:config/*-context.xml", "classpath:web-context.xml")
.initRequest("/sample", RequestMethod.GET).addParameter("id", "1").runService().getModelAndView();
User user = (User) mav.getModel().get("user");
assertThat(user.getName(), is("주현태"));
}
}
결과