오전에 업무로 부랴부랴 시간을 보내고 오후부터 저번 스터디에서 발표했던 타칭 ORM 으로 불리우는 GenericDao에 주석을 정리하면서 시간을 보내던 중 HelloIS님으로부터 메시지가 왔다.

간단히 줄이자면 @Controller 에 대한 테스트를 방법을 찾아보고 계신다는 것이다.

얼마전에 whiteship님의 블로그에 포팅된 스프링 2.5 @MVC 컨트롤러 테스트에 관한 글도 비슷한 맥락에서 방법을 찾아보고 계신것 같았다. 덧글 중 토비님께서는 프레임워크의 테스트를 믿고 나가는게 맞지 않느냐를 말씀도 있으셨다.

테스트가 필요할까? 필요하지 않을까? 내 생각도 아직까지 정리가 되지 않고 있다. 애초에 프레임워크를 쓰는 이유 중 하나인 높은 생산성도 있을텐데 믿지 못하고 일일히 테스트를 한다면 어느 세월에 다 할것인가 싶기도 하지만 은근히 궁금하다. ^^;;

그리고 개발자도 사람인 이상 오타는 분명이 있을테니 @RequestMapping 어노테이션에 엉뚱한 URL 을 적어두고 딴데서 삽질하는 일이 있을수도 있지 않은가! -0-

그럼 어떻게 테스트를 할 수 있을까?

집에오면서 생각한게 가장 단순하게 생각하면 스프링 @MVC 는 DispatcherServlet 에서부터 시작된다는 것이다.

그럼 테스트 코드에서 DispatcherServlet 을 생성해서 사용하면 되지 않을까?

그래서 우선 다음과 같은 @Controller 을 만들었다.

@Controller
@RequestMapping("/hello/hello.htm")
public class HelloController {
   
    @RequestMapping(method=RequestMethod.GET)
    public String helloGet(HttpServletRequest req, HttpServletResponse res, ModelMap modelMap){
        modelMap.addAttribute("hello", "hi~ my name is GET");
        return "hello/get";
    }
   
    @RequestMapping(method=RequestMethod.POST)
    public String helloPost(HttpServletRequest req, HttpServletResponse res, ModelMap modelMap){
        modelMap.addAttribute("hello", "hi~ my name is POST");
        return "hello/post";
    }
}

그리고 설정 파일을 다음과 같이 만들었다.

<context:annotation-config />
<context:component-scan base-package="controllertest.controller" use-default-filters="false">
    <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" />
</context:component-scan>

<bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping"
          p:alwaysUseFullPath="true" />
         
<bean id="viewResolver"
          class="org.springframework.web.servlet.view.InternalResourceViewResolver"
          p:prefix="/WEB-INF/view/jsp/"
          p:suffix=".jsp" />

마지막으로 테스트 코드를 다음과 같이 작성했다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:/**/ApplicationContext.xml"})
public class HelloControllerTest {
   
    @Autowired
    ApplicationContext applicationContext;
   
    DispatcherServlet dispatcher;
   
    @SuppressWarnings("serial")
    public HelloControllerTest(){
        this.dispatcher = new DispatcherServlet() {
            protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) {
                GenericWebApplicationContext wac = new GenericWebApplicationContext();
                wac.setParent(applicationContext);
                wac.refresh();
                return wac;
            }
        };
        try {
            this.dispatcher.init(new MockServletConfig());
        } catch (ServletException e) {
            e.printStackTrace();
        }

    }
   
    @Test
    public void helloGet() throws Exception {     
       
        MockHttpServletRequest req = new MockHttpServletRequest("GET", "/hello/hello.htm");
        MockHttpServletResponse res = new MockHttpServletResponse();
       
        dispatcher.service(req, res);
       
        // HTTP 상태 검사
        Assert.assertEquals(200, res.getStatus());
       
        // Attribute 검사
        Object obj = req.getAttribute("hello");
        Assert.assertNotNull(obj);
       
        System.out.println(String.format("AttributeName : hello, Value : %s", obj));
       
        // 포워드 주소...
        System.out.println(res.getForwardedUrl());
    }
   
    @Test
    public void helloPost() throws Exception {

        MockHttpServletRequest req = new MockHttpServletRequest("POST", "/hello/hello.htm");
        MockHttpServletResponse res = new MockHttpServletResponse();
       
        dispatcher.service(req, res);
       
        // HTTP 상태 검사
        Assert.assertEquals(200, res.getStatus());
        // Attribute 검사
        Object obj = req.getAttribute("hello");
        Assert.assertNotNull(obj);
        System.out.println(String.format("AttributeName : hello, Value : %s", obj));
        // 포워드 주소...
        System.out.println(res.getForwardedUrl());
    }
   
    @Test
    public void hello() throws Exception {
       
        MockHttpServletRequest req = new MockHttpServletRequest("POST", "/hello/hello.ht");
        MockHttpServletResponse res = new MockHttpServletResponse();
       
        dispatcher.service(req, res);
       
        // HTTP 상태 검사
        Assert.assertEquals(404, res.getStatus());
    }   
}

위와 같이 작성 후 테스트를 실행하면 다음과 같은 결과를 얻을 수 있었다.

AttributeName : hello, Value : hi~ my name is GET
/WEB-INF/view/jsp/hello/get.jsp

AttributeName : hello, Value : hi~ my name is POST
/WEB-INF/view/jsp/hello/post.jsp

마지막 테스트 함수의 경우 올바른 경로를 입력하지 않았기 때문에 HTTP 상태는 404(파일없음)이 나오는 것이다.

이것이 올바른 방법으로 테스트가 되었고 결과값이 나온건이 대해서는 아직 확신이 없다.
Posted by Arawn Trackback 1 : Comment 3
Annotation 기반 Controller 에서는 HTTP 요청 파라미터를 @RequestParam 을 사용해서 메소드의 파라미터로 바로 전달 할 수 있다.

@RequestParam 은 Key=Value 형태의 HTTP 요청 파라미터를 메소드의 파라미터에 전달해준다.





getBoard 메소드 호출시 Request 파라미터에서 "board_seq" 를 찾아 int board_seq 에 넣어둔다.

만약 HTTP 요청 중에 "board_seq" 가 없다면 Exception 이 발생한다.

org.springframework.web.bind.MissingServletRequestParameterException: Required int parameter 'board_seq' is not present

파라미터의 값이 필수가 아니라면 required 속성의 값을 설정해주면 된다. 기본값은 true 이다.



전달받는 파라미터 타입 또한 아주 민감하다. int 형으로 선언된 타입에 숫자가 아닌 다른값이 들어가면 유연한 행동을 보이지 않고 바로 Exception 을 발생시킨다. 그외에도 원시 유형의 변수에 null 값이 들어가도 마찬가지로 Exception 이 발생한다. ( int 보다는 Integer 래퍼(warpper) 를 사용하는게 안전할듯... )

org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.springframework.beans.TypeMismatchException: Failed to convert value of type [java.lang.String] to required type [int]; nested exception is java.lang.NumberFormatException: For input string: ""
Posted by Arawn Trackback 0 : Comment 3
보통 Spring Web MVC 에서 log4j 의 사용을 위해 web.xml 에 다음과 같이 설정파일을 불러올 위치를 지정해준다.

<context-param>
    <param-name>log4jConfigLocation</param-name>
    <param-value>classpath:/resources/properties/log4j.properties</param-value>
</context-param>

<listener>
    <listener-class>org.springframework.web.util.Log4jConfigListener</listener-class>
</listener>

WAS 가 가동되면 web.xml 설정에 따라서 log4j 의 설정값을 읽어서 정상적으로 로깅을 시작한다.

하지만 다음과 같이 Junit 를 통한 직접적인 테스트에서는 log4j가 올바르게 작동하지 않는다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:/resources/configs/DataBaseContext.xml",
           "classpath:/resources/configs/OzNoteContext.xml"})
@TransactionConfiguration(transactionManager = "transactionManager", defaultRollback = true)
@Transactional
public class SqlMapNoteDaoImplTest {

    @Test
    public void testGetBoardList(){
        // 테스트 코드
    }

}

web.xml 을 읽어서 환경을 구성해줄 컨테이너가 없기 때문이다. 특별히 설정이 없는 경우 Spring 은 log4j 의 설정파일을 classpath root에서 파일을 찾아보고 없다면 경고 메시지만 보여주고 로깅은 하지 않는듯 싶다.

log4j:WARN No appenders could be found for logger(org.springframework.test.context.junit4.SpringJUnit4ClassRunner).
log4j:WARN Please initialize the log4j system properly.

그럴 경우에는 대비해서 Spring 은 ApplicationContext 에서 직접 log4j 의 설정파일을 위치를 받을 수 있는 클래스가 만들어져 있다.

org.springframework.util.Log4jConfigurer

위 클래스를 import 시킨 후 다음과 같이 사용 할 수 있다.

Log4jConfigurer.initLogging("classpath:resources/properties/log4j.properties");

Resource에서 쓰던 prefix들 classpath: 와 file:을 사용해서 프로퍼티 파일 위치를 알려주면 된다. 설정파일이 찾지 못한다면 FileNotFoundException 을 던지니 예외처리를 해주어야한다.




이 정보는 스프링 포럼을 통해서 알 수 있었습니다.
Posted by Arawn Trackback 0 : Comment 3