Whiteship's Note


[수정]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에 넣어준 데이터를 바인딩하는 과정까지 테스트를 하는 것이였기 때문에 지금보다 훨씬 넓은 범위를 테스트 했다고 느껴집니다.

top

Write a comment.


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로 요청을 처리하기 시작하기 때문입니다.


top

Write a comment.


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 테스트 실행
사용자 삽입 이미지



top

Write a comment.


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);

}


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

top

Write a comment.


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() 메소드 구현이 끝났습니다.
사용자 삽입 이미지
top

Write a comment.


테스트 코드 작성은 언제 끝나는가?



참조 : Spring MVC 288페이지

1. 경계(Edge case)를 테스트 하라.
- 메소드가 예상하고 있지 않은 값들을 인자로 넘겨주는 것을 테스트 해야한다. 0, null값, 음수나 조건들과 같은 Edge 들은 비즈니스 로직을 통해 정의 된다.

2. 일반적인 경우(Common case)를 테스트 하라.
- 가장 자주 발생 하는 정상적인 시나리오 대로 메소드를 사용해본다. 보톤 경계 테스트를 빼먹는 경우가 많아서 경계 테스트가 더 중요하지만 이 것도 빠트리면 안된다.
- 분기문과 반복문의 모든 경우를 테스트 해야한다.
    - if 문에 true일 경우와 false일 경우
    - switch문의 모든 case
    - 예외가 발생할 경우

3. 가능한 모든 경우를 테스트 했는지 확인하는 툴을 사용한다.
- code coverage 툴을 사용(Clover, EMMA)
- test coverage가 100% 나와도 테스트 코드가 완벽하다고 할 수는 없다. 위 도구들이 해당 테스트가 경계 테스트를 했는지는 확인할 수 없기 때문이다.

결론
경계 테스트와 코드 커버리지 툴을 사용하면 완벽한 테스트 코드를 가졌다는 느낌을 받을 수 있을 것이다.

top

Write a comment.


Unit Tests



참조 : Spring MVC 10장

단위 테스트란?
The basic definition of a unit test is “a discrete test condition to check correctness of an isolated software module.”
진짜 단위 테스트라고 하려면 다음과 같은 특징을 가져야 합니다.
1. 빠르게 실행되어야 합니다.
- DB 커넥션이나 외부 자원등을 읽느라 테스트가 느려지면 해당 테스트의 유용성을 급격히 떨어집니다. 즉각적인 응답을 받을 수 있어야 합니다.

2. 외부 설정이 필요없습니다.
- 테스트 코드를 빠르게 실행을 하고 외부와 종속성을 제거하기 위함입니다.

3. 다른 테스트들과 독립적이어야 합니다.
- 완전히 독립적인 테스트여야 합니다. 다른 테스트를 먼저 또는 나중에 실행해야 하는 관계는 없어야 합니다.

4. 외부 자원이 필요없습니다.
- DB 커넥션이나 웹서비스에 종속성을 가지면 안됩니다.

5. 흔적을 남기지 않습니다.
- 단위 테스트를 계속해서 반복적으로 수행하기 위해서는 흔적이 남으면 안됩니다. 외부 자원에 종속되지 않는 것이 도움이 됩니다.

6. 가능한한 작은 단위로 테스트 합니다.
- OO에서는 보통 메소드 단위로 작성 합니다.

진정한 단위 테스트라면 Spring의 ApplicationContext나 BeanFactory에서 bean을 가져와서 테스트를 하면 안 되는 거군요. 외부 자원과 설정에 의존하게 되는 거니깐 그냥 TestCase만 가져다가 테스트를 만들면 되겠습니다. 다른 테스트들에 독립적인 테스트 메소드들을 구현해야겠습니다.


top

Write a comment.