[수정]AbstractModelAndViewTests 사용하여 Controller 테스트하기

이전 글에서 내렸던 결론은 좀 더 생각해보니 확실히 틀렸습니다.

테스트를 할 단위가 컨트롤러 전체 범위라면 이전 글의 가정이 맞겠지만 하나의 테스트는 최소한 작은 부분을 테스트 하라고 했었습니다. 컨트롤러에는 다양한 종류의 lifecycle 메소드들이 있으며 그것들을 전부 handleRequest의 결과인 ModelAndView를 사용하여 테스트 한다는 것은 다소 위험한 발상인 것 같습니다.

Controller 테스트 != ModelAndView 테스트

따라서 handleRequest 전체가 아닌 onSubmit()이 테스트의 대상이 되어야 합니다. 하지만 이전 글에서는 handleRequest를 가지고 테스트를 했었습니다.

이것을 다음과 같이 수정할 수 있습니다.

public class CheckControllerTest extends AbstractModelAndViewTests{

    private CheckController controller;
    private MemberService mockMemberService;
    private String mail;
    private MemberCommand command;

    public void setUp() {
        mockMemberService = createMock(MemberService.class);
        controller = new CheckController();
        controller.setMemberService(mockMemberService);
        command = new MemberCommand();
    }

    public void testEmptyOrWhiteMail() throws Exception {
        mail = “”;
        expect(mockMemberService.findByMail(mail)).andReturn(null);
        replay(mockMemberService);
        command.setMail(mail);
        ModelAndView mav = controller.onSubmit(null, null, command, null);
        assertEquals(“redirect:join.html”, mav.getViewName());
        assertViewName(mav, “redirect:join.html”);
        verify(mockMemberService);
    }

    public void testExistMemberMail() throws Exception {
        Member member = new Member();
        mail = “keesun@mail.com”;
        member.setMail(mail);
        member.setName(“기선”);

        expect(mockMemberService.findByMail(mail)).andReturn(member);
        replay(mockMemberService);
        command.setMail(mail);
        ModelAndView mav = controller.onSubmit(null, null, command, null);
        assertViewName(mav, “confirm”);
        assertModelAttributeValue(mav, “member”, member);
    }
}

이전 테스트와의 가장 큰 차이점은 먼저 handleRequest 전부가 아닌 obSubmit 메소드만을 테스트 했다는 것입니다. 이래야 이전 보다 더 세부적인 부분을 테스트 했기 때문에 좀 더 단위 테스트라 부를 만 한 것 같습니다.

또하나 차이점은 MockHttpServletRequest와 MockHttpServletResponse의 필요가 없어졌습니다. Requet에 바인딩할 데이터를 주는 대신에 command 객체를 사용하여 이미 바인딩 된 상태라고 가정을 했습니다. 이전 테스트의 경우 binder를 사용하여 request에 넣어준 데이터를 바인딩하는 과정까지 테스트를 하는 것이였기 때문에 지금보다 훨씬 넓은 범위를 테스트 했다고 느껴집니다.

AbstractModelAndViewTests 사용하여 Controller 테스트하기

컨트롤러의 주된 목적은 handleRequest안에 Request와 Respnse 객체를 넣어서 결국 ModelAndView 객체를 반환하는 것입니다.

사용자 삽입 이미지따라서 다음과 같은 결론을 조심스래 내놓을 수 있습니다.

Controller 테스트 ==
ModelAndView
테스트

아직은 TDD에 익숙하지도 않고 테스트 클래스를 어떻게 작성해야 할지도 모르기 때문에;; 일단은 구현을 하고 그 것을 테스트 하는 방법을 익히는 식으로 공부하고 있습니다.

먼저 Controller 하나를 구현합니다.

public class CheckController extends SimpleFormController {

    private MemberService memberService;

    public CheckController() {
        setCommandClass(MemberCommand.class);
        setCommandName(“memberCommand”);
        setFormView(“check”);
        setSuccessView(“confirm”);
    }

    public void setMemberService(MemberService memberService) {
        this.memberService = memberService;
    }

    @Override
    protected ModelAndView onSubmit(HttpServletRequest request, HttpServletResponse response, Object command, BindException exception) throws Exception {
        MemberCommand memberCommand = (MemberCommand)command;
        Member member = memberService.findByMail(memberCommand.getMail());
        if(member != null)
            return new ModelAndView(getSuccessView())
                .addObject(“member”, member);
        else
            return new ModelAndView(“redirect:join.html”);
    }
}

이 컨트롤러는 입력 받은 mail 주소를 사용하여 DB에서 검색을 하여 있으면 confirm 페이지로 없으면 join 페이지로 리다이렉션하는 컨트롤러 입니다.

이것을 테스트 하려면 다음과 같이 if 조건문에 걸릴 경우(true)와 걸리지 않는 경우(false)를 모두 테스트 해봐야 합니다. 이 때 테스트 할 것은 위에서도 말했지만 결국은 ModelAndView입니다.

JUnit의 TestCase를 직접 사용해도 되지만 spring-mock.jar 에 Spring 2.0에서 추가된 AbstractModelAndViewTests 클래스를 사용하여 다음과 같이 테스트를 할 수 있습니다.

public class CheckControllerTest extends AbstractModelAndViewTests{

    private CheckController controller;
    private MockHttpServletRequest request;
    private MockHttpServletResponse response;
    private MemberService mockMemberService;
    private String mail;

    public void setUp() {
        request = new MockHttpServletRequest();
        response = new MockHttpServletResponse();
        mockMemberService = createMock(MemberService.class);
        controller = new CheckController();
        controller.setMemberService(mockMemberService);
    }

    public void testEmptyOrWhiteMail() throws Exception {
        mail = “”;
        expect(mockMemberService.findByMail(mail)).andReturn(null);
        replay(mockMemberService);
        request.addParameter(“mail”, mail);
        request.setMethod(“POST”);
        ModelAndView mav = controller.handleRequest(request, response);
        assertEquals(“redirect:join.html”, mav.getViewName());
        assertViewName(mav, “redirect:join.html”);
        verify(mockMemberService);
    }

    public void testExistMemberMail() throws Exception {
        Member member = new Member();
        mail = “keesun@mail.com”;
        member.setMail(mail);
        member.setName(“기선”);

        expect(mockMemberService.findByMail(mail)).andReturn(member);
        replay(mockMemberService);
        request.addParameter(“mail”, mail);
        request.setMethod(“POST”);
        ModelAndView mav = controller.handleRequest(request, response);
        assertViewName(mav, “confirm”);
        assertModelAttributeValue(mav, “member”, member);
    }
}

EasyMock을 사용하여 MemberSerive의 Mock 객체를 사용했으며 spring-mock.jar의 MockHttpServletRequest와 MockHttpServletReponse를 사용했습니다.

여기서 주목할 것은 테스트의 대상인 ModelAndView 객체를 받아 올 때 호출한 메소드가 handleRequest라는 것입니다. 이 것은 Spring 의 Controller 클래스의 Workflow 때문이죠. 결국은 모든 컨트롤러들이 handleRequest로 요청을 처리하기 시작하기 때문입니다.

Testing Controller

매우 간단한 Controller를 테스트 하겠습니다.
앞에서 만든 MultiActionController를 테스트 하는 코드를 작성하겠습니다. 앞에서 작서해준 컨트롤러는 다음과 같이 View이름만 넘겨 주도록 만든 Stub 형태 입니다.

    public ModelAndView list(HttpServletRequest request, HttpServletResponse response){
        return new ModelAndView(“issue/list”);
    }

이 녀석을 EasyMock과 spring-mock.jar안에 있는 클래스들을 사용하여 테스트 클래스를 만들어서 IssueService로 부터 받아온 List<Issue>를 ModelAndView에 담아서 반환 하도록 구현할 것입니다.

1. 먼저 테스트 클래스를 작성하고 기본적으로 필요한 변수들을 설정합니다.

public class IssueControllerTest {

    private IssueController issueController;
    private IssueService mockIssueService;
    private MockHttpServletRequest request;
    private MockHttpServletResponse response;

    @Before
    public void setUp() {
        issueController = new IssueController();
        mockIssueService = createMock(IssueService.class);
        issueController.setIssueService(mockIssueService);
        request = new MockHttpServletRequest();
        response = new MockHttpServletResponse();
    }

2. 테스트를 작성합니다.

    @Test
    public void testList() {
        List<Issue> issueList = new ArrayList<Issue>();
        expect(mockIssueService.getAll()).andReturn(issueList);
        replay(mockIssueService);
        ModelAndView mav = issueController.list(request, response);
        assertEquals(“issue/list”, mav.getViewName());
        assertEquals(issueList, mav.getModel().get(“issueList”));
        verify(mockIssueService);
    }

위 테스트는 컨트롤러의 list 메소드에서 반환되는 ModelAndView의 viewName과 “issueList”라는 key로 List<Issue> 객체를 가지고 있는지 확인합니다. 이 때 필요한 IssueService의 행위를 ‘녹화-> 재생->검사’ 하는 작업을 거칩니다.

3. JUnit 테스트를 실행합니다.
사용자 삽입 이미지list 메소드를 제대로 구현해두지 않았기 때문에 에러가 발생합니다.

4. list 메소드 구현하기

    public ModelAndView list(HttpServletRequest request, HttpServletResponse response){
        return new ModelAndView(“issue/list”, “issueList”, issueService.getAll());
    }


5. 다시 JUnit 테스트 실행
사용자 삽입 이미지

EasyMock을 사용한 Service 계층 테스트2

참조 : http://www.easymock.org/EasyMock2_2_Documentation.html

reset() 메소드 활용하기

이 전글에서 원래 하나의 테스트 메소드에 넣어뒀던 내용을 세 개의 메소드로 쪼개두었습니다. 의도적으로 쪼갠 것은 아니였고 다만 expect() 메소드를 사용하여 MemberDao의 get() 메소드를 설정해 두었는데 Mock 객체의 특정 메소드를 여러 번 재정의 할 수가 없어서 에러가 발생했습니다.

이럴 때 reset으로 Mock 객체에 녹화 해둔 것들을 싹 지우고 다시 녹화를 할 수 있습니다. 즉 Mock 객체를 재사용할 수 있습니다.



@Test


public void testGetMember() {


       mail = null;


       expect(mockMemberDao.get(mail)).andReturn(null);


       replay(mockMemberDao);


       member = memberService.get(mail);


       assertNull(member);


       verify(mockMemberDao);


 


       reset(mockMemberDao);


 


       mail = “nonExistMail@mail.com”;


       expect(mockMemberDao.get(mail)).andReturn(null);


       replay(mockMemberDao);


       member = memberService.get(mail);


       assertNull(member);


       verify(mockMemberDao);


 


       reset(mockMemberDao);


 


       mail = “existMail@mail.com”;


       Member correctMember = new Member(mail);


       expect(mockMemberDao.get(mail)).andReturn(correctMember);


       replay(mockMemberDao);


       member = memberService.get(mail);


       assertNotNull(member);


       assertEquals(mail, member.getMail());


       verify(mockMemberDao);


}


이렇게 하면 메소드 하나에 여러 행위들을 설정하여 테스트 해볼 수 있습니다.

EasyMock을 사용한 Service 계층 테스트1

참조 : http://www.easymock.org/EasyMock2_2_Documentation.html

패키지 구조는 다음과 같습니다.
MemberService를 구현하려는데 아직 MemberDao는 구현되어 있지 않고 MemberDao라는 인터페이스만 존재합니다.사용자 삽입 이미지이 때 MemberServiceImpl 클래스를 TDD로 개발하기 위해 다음과 같이 작성했습니다.

public class MemberServiceImplTest {

 

       MemberService memberService;

       MemberDao mockMemberDao;

 

       @Before

       public void setUp() {

             memberService = new MemberServiceImpl();

             memberService.setMemberDao(mockMemberDao);

       }

 

       @Test

       public void testGetMember() {

             //Edge Case Test

             String mail = null;

             Member member = memberService.get(mail);

             assertNull(member);

 

             mail = “nonExistMail@mail.com”;

             member = memberService.get(mail);

             assertNull(member);

 

             //Common Case Test

             mail = “existMail@mail.com”;

             member = memberService.get(mail);

             assertNotNull(member);

             assertEquals(mail, member.getMail());

       }

}



위 테스트 코드를 실행하면 NullPointerExeption이 발생합니다. MemberDao 객체를 만들지도 않고 MemberService객체에 세팅하고 있기 때문이죠.

1. EasyMock을 사용하여 mock객체를 만들어서 세팅을 하겠습니다.
– EasyMock을 static import 합니다.

import static org.easymock.EasyMock.*;
– MemberDao를 MemberService에 세팅하기 전에 createMock()메소드를 사용하여 mock객체를 생성합니다.

       @Before

       public void setUp() {

             memberService = new MemberServiceImpl();

             mockMemberDao = createMock(MemberDao.class);

             memberService.setMemberDao(mockMemberDao);

       }



– 테스트를 돌려서 확인합니다.
사용자 삽입 이미지MemberDao의 get() 메소드의 행위를 사전(테스트 하기 전)에 설정해주지 않았기 때문에 에러가 났습니다.

MemberServiceImple을 다음과 같이 구현해 두었습니다.

public class MemberServiceImpl implements MemberService {

 

       private MemberDao memberDao;

 

       public void setMemberDao(MemberDao memberDao) {

             this.memberDao = memberDao;

       }

 

       public Member get(String mail) {

             return memberDao.get(mail);

       }

}


2. 녹화 -> 재생 -> 검증
MemberService를 구현할 때 사용한 MemberDao의 get() 메소드가 어떻게 동작할지 녹화를 해야합니다.
그리고 녹화 한 상태를 재생(replay) 시킨 다음 MemberService를 동작 시키고 마지막으로 녹화한 대로 잘 동작하였는지 검증(verify)하면 됩니다.

       @Test

       public void testGetMemberByNull() {

             mail = null;

             expect(mockMemberDao.get(mail)).andReturn(null);

             replay(mockMemberDao);

             member = memberService.get(mail);

             assertNull(member);

             verify(mockMemberDao);

       }

 

       @Test

       public void testGetMemberByWrongMail() {

             mail = “nonExistMail@mail.com”;

             expect(mockMemberDao.get(mail)).andReturn(null);

             replay(mockMemberDao);

             member = memberService.get(mail);

             assertNull(member);

             verify(mockMemberDao);

       }

 

       @Test

       public void testGetMemberByCorrectMail() {

             mail = “existMail@mail.com”;

             Member correctMember = new Member(mail);

             expect(mockMemberDao.get(mail)).andReturn(correctMember);

             replay(mockMemberDao);

             member = memberService.get(mail);

             assertNotNull(member);

             assertEquals(mail, member.getMail());

             verify(mockMemberDao);

       }


3. 테스트가 모두 통과하여 MemberService의 get() 메소드 구현이 끝났습니다.
사용자 삽입 이미지