Whiteship's Note


Why OSAF 1. 테스트 코드를 익힐 수 있습니다.

OSAF : 2008.11.10 22:04


OSAF를 공개한지 한 달이 아직 안 됐습니다. 10월 23일에 공개했었죠. 지금까지 약 200에 가까운 다운로드를 기록하고 있지만, 전혀... 아무런... 반응이 없다는 것에는 가히 놀라울 뿐입니다. 그냥 제가 쓴 글의 댓글 몇 개 정도 뿐의 관심이 저에게 한 편으로는 아쉬움으로 또 다른 한 편으로는 오기로 다가옵니다.

완전 최첨단 프레임워크인 OSAF에 왜 이렇게 관심이 없을까. 고민을 많이 했습니다. 어렵나? 메이븐 떄문인가? 그거 없어도 되는데. 문서가 부족하긴 부족하고. 그래도 어떻게 이렇게 조용할 수가 있지. 홈피 디자인이 좀 구리긴 한데.. 그거 때문인가? ㅋ. OSAF 발음이 너무 어려운가? 별에 별 생각을 많이 했습니다. 당연히 기운도 빠집니다. OSAF를 공개한 건 어쩌면 OSAF에게 못씁짓을 한 건 아닌지 말이죠.(Max님의 '어디가서 밥은 먹고 다녀야 할텐데..' 라는 댓글이 생각납니다.)

긍정적으로 생각하기로 했습니다. 언젠가는 빛을 보겠지. 열심히 계속 가꾸다 보면 언젠간 알아주겠지. 하고 말이죠. 그래서 OSAF가 여러분에게 어떤 도움을 줄 수 있을지 생각하고 알려드리기로 했습니다. 그 중 첫 번째가 바로 테스트 코드입니다.

OSAF의 테스트 커버리지는 60%가 조금 넘습니다. (앞으로 차차 올릴 예정입니다.) 60%의 테스트 커버리지는 전부 OSAF 개발팀에서 직접 작성한 테스트 코드입니다. 어딘가에서 배껴온 코드가 절대로 아닙니다. 테스트는 초기에 JUnit과 EasyMock을 사용해서 작성 했었습니다. 물론 스프링 테스트 기능도 사용하고 있죠. 배포 직전에는 EasyMock을 Mockito로 교체하여 비슷한 테스트를 보다 깔끔하고 직관적이며 적은 수의 코드로 대체할 수 있었습니다. DBUnit을 확장하여 OSAF가 제공하는 테스트 케이스를 이용하면 DAO 테스트가 매우 간편해질 것 입니다.

이렇게 좋은데... 한 번 들여다 보고 뭐라고 해주시지 않으시겠어요? 좋다. 잘했다. 고맙다. 이런거 말구요. 이 부분의 테스트는 이해가 안 된다. 테스트가 조금 이상하다. 이 부분의 테스트는 왜 안했냐. 어려워서 그런거냐? 이 부분의 테스트는 이렇게 고치는게 좋치 않겠냐? 이런.. 반응이 제가 가장 좋아하는 반응이자 OSAF에게 거름을 주는 방법입니다.

소스 코드는 굳이 다운 받지 않아도(장기적으론 받아 두시면 좋겠지만..)

http://www.opensprout.org:9060/browse/OSAF/osaf/trunk

위 링크로 가시면 웹에서 직접 볼 수 있습니다. 소스 코드나 OSAF 와 관련하여 문제나 제안하고 싶은 것이 있다면 주저하지 마시고 이슈를 등록해 주세요.

http://www.opensprout.org/jira/secure/Dashboard.jspa


신고
top


expect -> run -> verify 스타일(ex. Easymock) 바이바이

모하니?/Coding : 2008.07.08 19:50


참조 : http://monkeyisland.pl/2008/02/01/deathwish/

이지목 스타일은 녹화 -> 플레이 -> 확인(expect -> run -> verify) 순으로 mocking 또는 stubbing 하는 거였습니다. 그러나 이 스타일은 다음과 같은 단점들이 있습니다.

1. 테스트 메소드가 지져분해짐.
- 이것 저것 예측/녹화를 해줘야 하는데 그게 테스트를 위해서가 아니라 Mock을 위해서 해줘야 한다는게 좀..

2. 자연스러운 테스트 스타일로 느껴지지 않는다.
- 예측을 한 담에 실행하는게 아니라, 실행 한 다음에 예측되는 Mock의 행위를 나열해 주는게 더 자연스럽다.

3. 테스트가 깨지기 쉽다.
- 새로운 기능을 추가하면, Mock을 사용한 테스트가 왕창 깨지는 경우가 발생한다.

4. 보다 자세한 실패 메시지를 보여줄 수 있었을 텐데...

5. 보다 가독성 좋게 만들 수 있었을 텐데...

그래서 상태 기반 테스트를 제공하는 Mockito를 강추 한다는거...

Mockito 홈에서 다음을 인용합니다.

No expect-run-verify also means that Mockito mocks are often ready without expensive setup upfront. They aim to be transparent and let the developer to focus on testing selected behavior rather than absorb attention.

Mockito has very slim API, almost no time is needed to start mocking. There is only one kind of mock, there is only one way of creating mocks. Just remember that stubbing goes before execution, verifications of interactions go afterwards. You'll soon notice how natural is that kind of mocking when test-driving java code.

즉 stubbing -> execution -> verification 라고 할 수 있겠네요. 훔.. 그래도 왠지 expect -> run -> verify 형태와 비슷해 보이네요.

사용법은 여기에 잘 나와있습니다.
신고
top


맥 애플리케이션 토렌트와 P2P 프로그램

Good Tools : 2008.07.06 22:22


http://www.mac-torrents.com/index.php

사용자 삽입 이미지

위 사이트에서 맥 애플리케이션과 관련된 토렌트를 구할 수 있습니다.

http://www.limewire.com/download/

저기서 받은 토렌트를 위의 limewire라는 무료 P2P(윈도우의 당나귀급) 프로그램에 드래그 앤 드랍을 하면 알아서 다운로드 해줍니다.

사용자 삽입 이미지
위 두 개의 프로그램 조합으로 이제 원 없이 맥을 즐길 수 있게 되었습니다. 나이수~
신고
top


EasyMock 사용할 때 주의 할 것

모하니?/Coding : 2008.04.29 19:47


    public void foo(Bar bar) {
        ...
        
        bar.toby(whiteship);
        bar.whiteship(toby);
    }

위와 같은 메소드를 테스트 할 때 EasyMock을 사용해서 다음과 같은 테스트를 작성할 수 있습니다.

@Test
public void foo(){
   Bar mockBar = createMock(Bar.class);
   ...
   mockBar.toby(whiteship);
   mockBar.whiteship(toby);
   replay(mockBar);
   a.foo(mockBar);
   verify(mockBar);
}

테스트가 통과할 것만 같은 코드입니다. 그렇쵸? 대부분은 테스트가 통과 합니다. 그런데 통과하지 않는 경우도 있습니다.

java.lang.IllegalStateException: missing behavior definition for the preceeding method call toby(whiteship);

이런 메시지와 함께 테스트가 통과하지 않습니다.

그럴 때는 뭘 확인해 봐야 할까요? Bar 인터페이스에 있는 whiteship과 foo라는 메소드의 리턴타입이 있는지 확인해봐야 합니다. 리턴타입이 있으면 http://whiteship.tistory.com/1504
신고
top

TAG EasyMock

EasyMock - Using Stub Behavior for Methods

모하니?/Coding : 2008.03.02 00:51


EasyMock으로 생성한 Mock을 마치 Stub처럼 사용할 수 있습니다. 이 말이 무슨 말이냐면... Mock은 Stub과 달리 예측이라는 개념이 추가되어 있습니다. 따라서 예측 대로 Mock이 수행되지 않으면 AssertionError가 발생하는데, Stub은 그런 개념이 없고, 따라서 구현해둔 메소드가 호출 되든 말든 상관이 없습니다. 다시 돌아가서 Stub 처럼 사용한 다는 것은 Mock으로 레코딩 한 부분이 호출되지 않아도 테스트가 통과 되도록 작성하는 것입니다.

언제 이런 걸 사용할까요. 몇 번이나 호출될지 예상할 수 없을 때 사용할 수 있을 것 같습니다. 아니면 기본적인 규칙이 있다면, 굳이 매번 설정할 필요 없이 EasyMock 문서에 나온대로 사용할 수도 있겠습니다.

expect(mock.voteForRemoval("Document")).andReturn(42);
expect(mock.voteForRemoval(not(eq("Document")))).andStubReturn(-1);

인자가 "Document"일 때는 42를 반환하고, "Document"라는 인자가 아닐 때는 -1을 반환하도록 레코딩 되어 있습니다.

voteForRemoval이라는 메소드는 최소 한 번은 "Document"라는 인자를 받아서 42를 반환하도록 되어 있으며 그 뒤로는 몇 번 호출될지도 모르지만, "Document"라는 인자가 오지는 않을 것이며, 만약 이런 메소드가 호출되면, -1을 반환하도록 레코딩 한 것입니다.

    @Test
    public void arguments() throws Exception {
        expect(memberDao.bar(1)).andReturn(1);
        expect(memberDao.bar(not(eq(1)))).andStubReturn(2);
       
        replay(memberDao);
        int i1 = memberServiceImpl.bar(1);
//        int i2 = memberServiceImpl.bar(2);
//        int i3 = memberServiceImpl.bar(2);

        assertEquals(1, i1);
//        assertEquals(2, i2);
//        assertEquals(2, i3);
        verify(memberDao);
    }

따라서 위의 코드에서 주석을 풀어도 되고, 안 풀어도 테스트는 통과합니다.
andStubReturn(), andStubThrow(), andStubAnswer() 를 사용할 수 있습니다.
신고
top


EasyMock - Flexible Expectations with Argument Matchers

모하니?/Coding : 2008.03.02 00:36


레코딩을 할 때, 테스트가 의존하는 메소드에 넘겨주는 아규먼트의 폭을 유동적으로 설정할 수 있습니다. 그때 사용하는 것들이 Argument Matcher입니다. 기본으로 제공해주는 것들을 사용할 수도 있고, 사용자가 새것을 정의해서 추가하여 사용할 수도 있습니다.

예를 들어.. memberService의 foo 메소드에 어떤 값을 넘겨주던 memberDao의 foo 메소드에는 integer 값이 넘어간다고 했을 때.. 어떻게 레코딩 해야할까요.

    @Test
    public void arguments() throws Exception {
        memberDao.foo(anyInt());
       
        replay(memberDao);
        memberServiceImpl.foo(1);
       
        verify(memberDao);
    }

anyInt() 라는 Argument Matcher를 사용해서 어떤 int값이 넘겨지도록 설정할 수 있습니다. 그러면 1이 넘겨지던 2가 넘겨지든 관계없이 테스트는 통과합니다.

EasyMock에서는 aryEq()을 소개하고 있는데.. 무슨 내용인지 잘 이해가 안되더군요;

    @Test
    public void arguments() throws Exception {
        String[] names = new String[]{"whiteship", "toby"};
        memberDao.foo(names);
       
        replay(memberDao);
        memberServiceImpl.foo(names);
       
        verify(memberDao);
    }

이렇게 해도 테스트는 통과 하던데.. 어떤 때에 aryEq()을 사용해야 한다는 것인지.. 훔.. 나중에 다시 살펴봐야겠습니다.
신고
top


EasyMock - Strict Mocks, Switching Order Checking On and Off

모하니?/Coding : 2008.03.02 00:02


createMock()을 사용해서 얻어낸 Mock 객체는 기본적으로 메소드를 호출하는 순서를 확인하지는 않습니다. 그런데 경우에 떄라서는 메소드가 호출되는 순서가 중요할 수 있는데, 그럴 땐는 두 가지 방법이 있습니다.

1. Mock객체를 생성할 때 createMock()이 아니라, createStrickMock()을 호출하여 mock 객체를 생성하면, 순서까지 확인합니다.

MemberDaoMock = createStrickMocek(MemberDao.class);

2. Mock객체에 레코딩을 시작하기 전에, checkOrder(mock 객체, true); 를 사용해서 메소드의 호출 순서도 확인하도록 설정할 수 있습니다.

//테스트 코드

    @Test
    public void add() throws Exception {
        checkOrder(memberDao, true);
        Member member = new Member();
        memberDao.add(member);
        memberDao.get(member);
       
       
        replay(memberDao);
        memberServiceImpl.add(member);
       
        verify(memberDao);
    }

// MemberServiceImpl 코드

    public void add(Member member) {
        memberDao.get(member);
        // ToDo check email
        memberDao.add(member);
    }

get()으로 먼저 가져와서 확인한 다음에 추가해야 하기 때문에 위의 테스트 코드를 에러가 발생합니다.
신고
top


EasyMock - Relaxing Call Counts

모하니?/Coding : 2008.03.01 23:52


테스트 대상이 호출하는 메소드의 호출 횟수를 예측할 때 횟수의 범위를 지정해서 어느정도 테스트에 여유를 줄 수 있습니다.

예를 들어, MemberServiceImpl의 add를 호출할 때, 이미 add하려는 Member 데이터의 email이 DB에 존재하는지 확인하기 위해서, DB에 findByEmail()을 한 번 이상 호출 한다고 가정하겠습니다. 이 메소드는 정확히 몇 번 호출이 될지는 모릅니다.(사실 테스트를 만드는 입장에서는 알고 있겠죠. 그냥 그렇다고 하겠습니다.)

그럴때 사용할 수 있는 것이 atLeastOnce() 입니다. 즉 최소한 한 번은 호출되어야 한다는 거죠. 한 번 호출되든 말든.. 상관없을 때는 anyTimes() 라는 메소드를 사용할 수 있고, 이런 메소드를 붙여주지 않으면 무조건 딱 한 번 호출되는 것으로 간주됩니다.

    @Test
    public void add() throws Exception {
        Member member = new Member();
        memberDao.get(member);
        expectLastCall().anyTimes();
       
        replay(memberDao);
        memberServiceImpl.add(member);
       
        verify(memberDao);
    }

time()로 횟수를 정확하게 명시하거나

신고
top


EasyMock - Changing Behavior for the Same Method Call

모하니?/Coding : 2008.03.01 23:41


같은 메소드가 여러번 호출 될 때, 매번 예측되는 결과가 다르다면..

memberDao.add(member); // Member 객체가 무사히 저장된다.
memberDao.add(member); // 예외가 발생한다.

이런 경우에 그냥 fluent interface로 계속해서 andReturn() 이나 andThrow()를 호출하여 recording 할 수 있습니다.

memberDao.add(member)
    .andReturn(resultMember);
    .adndThrow(new DuplicatedEmailException());

테스트 클래스 내부에서 의존하는 객체를 여러번 호출할 때, 각각이 다른 행위를 해야 한다면 유용하겠습니다.
신고
top


EasyMock - Creating Return Values or Exceptions

모하니?/Coding : 2008.03.01 12:36


리턴값이나 예외를 실제 메소드가 호출되는 시점에 만들고 싶을 수 있는데 그럴 때는 andAnswer()을 사용합니다.
expectLastCall().andAnswer(new IAnswer<Object>() {
public Object answer() throws Throwable {
((User) getCurrentArguments()[0]).setUserId(1);
return null;
}
});
이 전 글들에서 사용했던 andReturn()이나, andThrow()의 인자로는 미리 만들어 둔 객체들을 넘겨줬다면, andAnswer()에서는 IAnswer 인터페이스를 구현하여 특정 조건에 따라 각기 다른 결과를 반환하는 구현체를 만들어 둘 수 있습니다.

특정 조건이라고 한다면.. 어떤 데이터를 가지고 판단을 하겠다는 건데, 저 안에서 접근할 수 있는 데이터는  Obejct 타입의 배열을 반환하는 getCurrentAurguments()를 사용하여, 인지로 넘겨준 값에 접근할 수 있습니다.

반환하는 값이 없으면 위 처러 null을 리턴하면 되고, 반환하는 값이 있으면 그 값을 반환하고, 예외를 던지고 싶으면 던져주면 됩니다.
신고
top


EasyMock - Working with Exceptions

모하니?/Coding : 2008.03.01 12:17


예외를 던지는 메소드가 있다면, andReturn()을 사용해서 정말 예외를 던지는 지 확인할 수 있습니다.

        memberDao.remove(member);
        expectLastCall().andThrow(new RuntimeException());
       
        replay(memberDao);
        memberServiceImpl.remove(member);
       
        verify(memberDao);

간단하지요.  인자로 왠지..class 타입을 넘겨줄 것 같았는데, andThrow(RuntimeException.class) 이렇게 말이죠. 근데;; 역시 객체를 넘겨야 되네요. 이런게 EasyMock 스타일인가 봅니다.
신고
top


EasyMock - Specifying Return Values

모하니?/Coding : 2008.03.01 12:00


테스트의 대상이 사용하는 Mock의 메소드가 반환할 값이 필요한 경우에 다음과 같이 할 수 있습니다.

1. 예상되는 메소드 호출을 expect() 메소드로 감싸기.
2. andReturn(Object returnValue) 사용해서 예상되는 리턴값을 expect() 메소드 뒤에 .으로 이어서 호출하기
public void testVoteAgainstRemoval() {
mock.documentAdded("Document"); // expect document addition
// expect to be asked to vote for document removal, and vote against it
expect(mock.voteForRemoval("Document")).andReturn((byte) -42);
replay(mock);
classUnderTest.addDocument("Document", new byte[0]);
assertFalse(classUnderTest.removeDocument("Document"));
verify(mock);
}

저 위의 빨간 줄 한 줄을 expectLastCall()을 사용해서 두 줄로 나눌 수도 있습니다.

mock.voteForRemoval("Document");
expectLastCall().andReturn((byte) 42);

메소드가 심하게 길어지지 않는 이상 굳이 두 줄로 코드를 나눌 필요는 없겠습니다.


신고
top


EasyMock - Expecting an Explicit Number of Calls

모하니?/Coding : 2008.03.01 11:53


같은 메소드 호출을 여러번 하는 경우 그런 행위를 녹화하기 위해서, 메소드를 여러번 호출할 수도 있지만.

    public void testAddAndChangeDocument() {
mock.documentAdded("Document");
mock.documentChanged("Document");
mock.documentChanged("Document");
mock.documentChanged("Document");
replay(mock);
classUnderTest.addDocument("Document", new byte[0]);
classUnderTest.addDocument("Document", new byte[0]);
classUnderTest.addDocument("Document", new byte[0]);
classUnderTest.addDocument("Document", new byte[0]);
verify(mock);
}

이런 코드를 expectLastCall().times(3); 를 사용해서 코드를 줄일 수 있다.

    public void testAddAndChangeDocument() {
mock.documentAdded("Document");
mock.documentChanged("Document");
expectLastCall().times(3);
        replay(mock);
classUnderTest.addDocument("Document", new byte[0]);
classUnderTest.addDocument("Document", new byte[0]);
classUnderTest.addDocument("Document", new byte[0]);
classUnderTest.addDocument("Document", new byte[0]);
verify(mock);
}

신고
top


EasyMock으로 클래스의 Mock 객체 만들기

모하니?/Coding : 2008.02.06 16:59


방명록에서 '제도관퐈터'님의 질문으로 인해 오랜만에 EasyMock을 아주 살짝 살펴봤습니다. 질문은 JFrame의 Mock객체가 안 만들어진다는 것이었습니다. 그래서 해봤습니다.

import javax.swing.JFrame;
import static org.easymock.EasyMock.*;
import static org.junit.Assert.*;

import org.junit.Test;

public class What {

    @Test
    public void createJFrameMock() throws Exception {
        JFrame mockFrame = createMock(JFrame.class);
        assertNotNull(mockFrame);
    }

}

사용자 삽입 이미지

안 되는 군요. 에러 메시지를 읽어보니까 원인도 보여줍니다. 인터페이스가 아니니까 못 만든다는 것입니다. 이걸 보셨으면 저한테 굳이 질문을 하셨을까 싶지만... 그래도 뭐 심심하니까 검색해봤습니다.

easymock createmock class 라고 입력하면, Toby'epril 블로그의 내용 중에 하나가 검색 됩니다. Hibernate Dao Test 코드 만드는 것과 관련된 Toby님의 글입니다.

거기에 보면 이지목 확장 클래스를 사용해서 HibernateTemplate의 mock객체를 만들었다는 이야기가 나오고 소스코드도 있습니다.

그래서 EasyMock 다운로드 사이트에 가봤더니 정말고 확장 클래스라는것이 있었습니다.
사용자 삽입 이미지

테스트 코드를 바꿨습니다.

import static org.junit.Assert.assertNotNull;
import static org.easymock.classextension.EasyMock.*;
import javax.swing.JFrame;

import org.junit.Test;

public class What {

    @Test
    public void createJFrameMock() throws Exception {
        JFrame mockFrame = createMock(JFrame.class);
        assertNotNull(mockFrame);
    }

}

테스트 코드는 실패합니다. 왜냐면 Cglib을 필요로 합니다. 인터페이스가 아니라 클래스의 프록시를 만들려면 필요한 라이브러리죠. Cglib은 spring 2.5 lib 폴더에 있는 것을 사용했습니다.


저걸 추가해주고 다시 테스트를 하면 테스트는 통과합니다.

사용자 삽입 이미지

이제 클래스의 Mock 객체를 만들어서 사용하실 수 있습니다.
신고
top


Spring MVC 리팩토링 2

모하니?/Coding : 2007.06.22 13:04


이전 글에서 Controller 단위 테스트를 변경하여 Controller 수정에 성공했습니다.
이번에는 MemberService를 단위 테스트 해서 MemberService의 구현을 수정하겠습니다.

이번에는 MemberService의 테스트가 만들어져 있지 않은 것을 확인했습니다. 만들어야겠습니다.

public class MemberServiceImplTest {

    private MemberService memberService;
    private MemberDao memberDao;

    @Before
    public void setUp() {
        memberService = new MemberServiceImpl();
        memberDao = createMock(MemberDao.class);
        memberService.setMemberDao(memberDao);
    }

    @Test
    public void testNotJoinedMail() {
        String mail = "";
        expect(memberDao.findByMail(mail)).andReturn(null);
        replay(memberDao);
        Member member = memberService.isJoined(mail);
        assertEquals(null, member);
        verify(memberDao);
    }
}

일단 Bad Case를 테스트 하는 코드를 작성했습니다.

사용자 삽입 이미지
이전 글에서 컨트롤러를 테스트 할 때 인터페이스 만들고 구현체에서 에러 나길래 퀵픽스로 구현해둔 곳에서 에러가 발생했습니다.

이 방법은 KSUG 2회 세미나에서 영회형이 발표하실 때 언급하셨고 J2EE Development without EJB에서 Rod Johnson이 추천하는 방법이기도 합니다.

어쨋든 테스트가 돌아가도록 ServiceImple 구현을 변경합니다.

이런 여기서도 또하나 결정을 해야겠군요. Service 계층에서 사용할 DAO의 인터페이스를 결정해야 합니다. 이전에는 findByMail을 사용하고 있었는데 썩 나쁘지 않은 것 같습니다. 따라서 그대로 사용을 하고 대신 리턴 타입을 List<Member> 로 할지 Member로 할지 고민이 됩니다. 같은 mail을 가진 데이터가 들어갈 수 없다는 제약 사항이 있다면 리턴 타입을 Member로 해야하고... 혹시라도 그런 제약 사항이 없다면 List<Member>로 받아서 뭔가 복잡한 처리가 필요하면 View까지 그 영향이 갈 것 같네요;;;

이번에도 저는 혼자 개발하고 있기 때문에 다음과 같은 장점이 있습니다.

"니가 곧 개발자며 사용자이자 설계자다. 니가 다 알아서 해라! 그리고 심심하면 DBA도 해라."

OK 저는 같은 mail을 가진 Member 데이터가 데이터베이스에 들어가지 않기로 정했습니다. 이 제약 사항은 다음에 DAO를 테스트할 때 확인 해야 겠습니다.

    public Member isJoined(String mail) {
        return memberDao.findByMail(mail);
    }

Service 구현은 여전히 변함없이 간단합니다. 이 순간 저는 지금 상당히 많이 돌아가고 있다는 것을 느낍니다. 단지 메소드 이름 하나 변경하는 일이였는데 너무 돌아가는 거 아닌가 싶지만 목적은 구현보다는 테스트에 익숙해지는 것이기 때문에 계속해서 진행하겠습니다.

이번에는 새로운 테스트를 작성하여 해당 mail을 가지고 있는 Member를 검색했을 경우를 테스트 합니다.

    @Test
    public void testJoinedMail() {
        String mail = "keesun@mail.com";
        Member member = new Member();
        member.setMail(mail);
        expect(memberDao.findByMail(mail)).andReturn(member);
        replay(memberDao);
        assertEquals(member, memberService.isJoined(mail));
        verify(memberDao);
    }

테스트는 통과합니다.

사용자 삽입 이미지

신고

'모하니? > Coding' 카테고리의 다른 글

SimpleFormAndMultiActionController 개발 구상  (0) 2007.07.17
ServletRequestUtils 사용하기  (6) 2007.07.10
DB 바꾸기(Hibernate 사용할 때)  (0) 2007.06.27
Spring MVC 리팩토링 4  (0) 2007.06.22
Spring MVC 리팩토링 3  (0) 2007.06.22
Spring MVC 리팩토링 2  (0) 2007.06.22
Spring MVC 리팩토링 1  (0) 2007.06.22
Seminar Helper v0.8  (0) 2007.06.20
어떤 계층 순서대로 구현 하시나요?  (0) 2007.06.18
Clover 설치하기  (0) 2007.06.18
16강 토너먼트 알고리즘  (0) 2007.06.12
top


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


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


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


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


페이징 기능 구현하기(TDD, Easymock, iBATIS, MySQL)



1. 먼저 MemberRepositoryTest에 다음과 같이 Easymock을 이용한 Test코드를 추가합니다.
   @Test
   public void pagsing(){
       final Member member = new Member();
       List<Member> members = null;

       mockMemberDao.add(member);
       expectLastCall().times(15);
       expect(mockMemberDao.list(1)).andReturn(members);

       replay(mockMemberDao);
       for (int i = 0; i < 15; i++) {
           memberRepository.add(member);
       }
       assertEquals(members, memberRepository.list(1));

       verify(mockMemberDao);
   }
=> 여기서 의문이 드는 것은 "빨간색으로 표시된 부분이 올바른 표현인가?" 모르겠습니다. Return 타입을 예상하는 부분으로 기억이 나는데 다시 공부할 필요가 있겠습니다.
=> 또하나 의문이 드는 것은 return type인 "List의 크기를 예측할 순 없을까"하는 것입니다. 왠지 방법이 있을 것 같은데 이것도 좀더 공부할 필요가 있겠습니다.

이 테스트를 통과 시키기 위해서는 MemberDao 인터페이스에 새로운 메소드가 추가되어야 하며 MemberRepository 인터페이스에도 새로운 메소드가 추가되어야 합니다. 그리고 그 메소드를 구현한 MemberRepositoryImpl 에서 새로 추가한 메소드를 구현해 두어야 합니다. MemberDao의 list()를 호출하도록 수정하면 위 테스트는 간단하게 통과 합니다.

2. 이번에는 Dao Test를 하기 위해서 SqlmapMemberDaoTest 클래스에 다음의 테스트 코드를 추가합니다.
   public void testPaging(){
       final int numberOfMembers = 15;
       insertMembers(numberOfMembers);

       List<Member> members = memberDao.list(1);
       assertNotNull(members);
       assertEquals(12, members.size());

       members = memberDao.list(2);
       assertEquals(3, members.size());
   }
=> 이 테스트를 통과시키면 구현이 끝나게 됩니다.

3. 테스트를 통과 시키기 위해서 SqlmapMemberDao에 list를 아래와 같이 구현합니다.
public List<Member> list(int page) {
       Map<String, Integer> params = new HashMap<String, Integer>();
       params.put("defaultSize", 12);
       params.put("offset", 12 * (page-1));
       return getSqlMapClientTemplate().queryForList("Member.list", params);
   }
=> 아직도 테스트를 통과하지 못합니다. iBATIS에 list라는 id를 가진 SQL을 작성해야 합니다.

4. Member.xml에 다음과 같이 추가하면 테스트는 통과하고 기능 추가가 끝나게 됩니다.
<select id="list" resultMap="memberMap">
       SELECT *
       FROM Member
       ORDER BY id
       LIMIT #defaultSize# OFFSET #offset#
   </select>
=> LIMIT에 대한 MySQL document를 참조했습니다.
기본구조)  [LIMIT {[offset] row_count | row_count OFFSET offset}]
예) LIMIT 5 => 상위 5 줄
    LIMIT 2, 5 => 상위에서 2줄 버리고 5 줄
    LIMIT 5 OFFSET 2 => 상위에서 2줄 버리고 5줄

5. 코드에 수정할 부분을 찾아서 리팩토링 합니다. 한 페이지에 12개 씩 보여주기로 정했습니다.(제 맘대로ㅋㅋ) 그래서 위에 표시해 둔 12라는 숫자는 의미가 있는 숫자입니다. MemberRepository에 DEFAULT_LIST_SIZE 상수로 바꿉니다.

신고

'Spring > 주소록 만들기' 카테고리의 다른 글

주소록 화면 완성  (3) 2006.12.19
중간점검  (0) 2006.12.17
HTML 공부 중 2탄  (2) 2006.12.14
Strict HTML 4.01 지침서  (6) 2006.12.14
HTML 공부 중  (2) 2006.12.13
페이징 기능 구현하기(TDD, Easymock, iBATIS, MySQL)  (2) 2006.12.11
Easymock 연습하기 3탄  (0) 2006.12.06
Easymock 연습하기 2탄  (0) 2006.12.05
Easymock 연습하기  (0) 2006.12.05
단위 테스트 모르겠슴 ㅠ.ㅠ  (0) 2006.12.05
Eclipse에서 Rename Method 리팩토링  (0) 2006.12.04
top


Easymock 연습하기 3탄



Easymock 연습하기 2탄에서는 mock 객체를 이용해서 void 형태의 메소드를 테스트 했습니다. 즉 올바른 인자가 들어가는지 확인을 한것입니다.

이번에는 나오는 값도 테스트를 해보겠습니다. 나오는 값을 테스트하기 위해서는 expect라는 메소드와 andReturn메소드를 사용합니다.

소스보기

more..


expect를 이용해서 리턴값이 있는 메소드를 감싸고 그 뒤에 andReturn을 호출하면서 반활 될 타입의 객체를 지정해 줍니다. => "memberDao에 get메소드를 호출하면 member가 나온다."

그리고 replay를 실행하여 실제로 member를 추가한 뒤에 memberRepository를 이용해서 get을 호출하면 memberRepository 안에 있는 memberDao의 Mock 객체가 member를 반환해주게 됩니다. 따라서 시나리오와 액션이 일치하게 되며 verify를 이용해서 확인할 수 있습니다.


위 메소드의 내용을 한번 그려봤습니다. Dao 쪽은 Mock으로 처리하고 Service만 test를 할 수 있게 되었습니다.
신고

'Spring > 주소록 만들기' 카테고리의 다른 글

중간점검  (0) 2006.12.17
HTML 공부 중 2탄  (2) 2006.12.14
Strict HTML 4.01 지침서  (6) 2006.12.14
HTML 공부 중  (2) 2006.12.13
페이징 기능 구현하기(TDD, Easymock, iBATIS, MySQL)  (2) 2006.12.11
Easymock 연습하기 3탄  (0) 2006.12.06
Easymock 연습하기 2탄  (0) 2006.12.05
Easymock 연습하기  (0) 2006.12.05
단위 테스트 모르겠슴 ㅠ.ㅠ  (0) 2006.12.05
Eclipse에서 Rename Method 리팩토링  (0) 2006.12.04
세련된 SQL map (iBATIS)  (0) 2006.12.04
top


Easymock 연습하기 2탄



import static org.easymock.EasyMock.*;
import org.junit.*;
import net.webapp2.member.dao.MemberDao;
import net.webapp2.member.domain.Member;

public class MemberRepositoryTest {

   MemberDao mockMemberDao;
   MemberRepository memberRepository;

   @Before
   public void setup(){
       mockMemberDao = createMock(MemberDao.class);
       memberRepository = new MemberRepositoryImpl();

       memberRepository.setMemberDao(mockMemberDao);

   }

   @Test
   public void add(){
       final Member member = new Member();
       mockMemberDao.add(member);

       replay(mockMemberDao);
       memberRepository.add(member);

       verify(mockMemberDao);
   }
}

먼저 Test할 대상 클래스와 Mock 테스트를 할 클래스 타입의 객체를 만듭니다.(하늘색) 그리고 memberRepository에서 memberDao객체를 사용해야 하기 때문에 setter를 사용하여 위에서 만든 Mock 객체로 setting 해줍니다.(첫번째 분홍색)

그리고 시나리오를 생각해 봅니다.
"Member를 추가하는 기능이기 때문에 memberRepository의 add(Member)를 호출하면 memberDao의 add(Member)가 호출이 되서 DB에 들어가겠구나 리턴값은 없고..."

다음 테스트 메소드를 작성하기 시작하는데 먼저 시나리오에 해당하는 부분(두번째 분홍색 윗부분)에서 Mock 객체(mockMemberDao)에게 일어날 일을 생각합니다. mockMemberDao.add(member); 이것 밖에 없네요.

이제 시나리오 대로 돌아가는지 replay(Mock 객체) 시켜봅니다. replay 시키면서 당연히 "액션"을 취해 줘야 하는데 이 "액션"이 여기서는 memberRepository.add(member) 가 됩니다.

그리고 시나리오 대로 액션이 이뤄졌는지 verify(Mock 객체)를 합니다.

신고

'Spring > 주소록 만들기' 카테고리의 다른 글

HTML 공부 중 2탄  (2) 2006.12.14
Strict HTML 4.01 지침서  (6) 2006.12.14
HTML 공부 중  (2) 2006.12.13
페이징 기능 구현하기(TDD, Easymock, iBATIS, MySQL)  (2) 2006.12.11
Easymock 연습하기 3탄  (0) 2006.12.06
Easymock 연습하기 2탄  (0) 2006.12.05
Easymock 연습하기  (0) 2006.12.05
단위 테스트 모르겠슴 ㅠ.ㅠ  (0) 2006.12.05
Eclipse에서 Rename Method 리팩토링  (0) 2006.12.04
세련된 SQL map (iBATIS)  (0) 2006.12.04
메소드 추상화  (2) 2006.12.04
top

TAG EasyMock

Easymock 연습하기



http://www.easymock.org/Downloads.html
위 링크에서 Easymock을 다운로드 합니다.

압축을 풀면 easymock.jar 파일이 있습니다. 이것을 Eclipse 프로젝트에 추가합니다. 그리고 JUnit 도 추가합니다. JDK는 5.0 이상에서 작동합니다. (easymock 버젼 2 부터 그런 것 같습니다.)

MemberRepositoryImpl 클래스를 테스트 하기 위한 코드 입니다.
import static org.easymock.EasyMock.*;
import org.junit.*;
import net.webapp2.member.dao.MemberDao;

public class MemberRepositoryTest {

   MemberDao mockMemberDao;
   MemberRepository memberRepository;

   @Before
   public void setup(){
       mockMemberDao = createMock(MemberDao.class);
       memberRepository = new MemberRepositoryImpl();
   }

   @Test
   public void add(){
       //recoding

       //verify
   }
}

이 전에는 MemberDao 까지 거쳐서 테스트를 했었지만 위의 상태는 아래의 그림과 같이 MemberRepository에만 국한 될 수 있습니다.

Easymock을 사용하는 방법은 세 단계로 나뉘어 집니다. 먼저 createMock(Interface Type)을 사용하여 Mock 객체를 만들게 됩니다. 인터페이스 타입의 객체는 못만들기 때문에 MemberDao를 사용하고 있던 MemberRepositoryImpl에서 SqlmapMemberDao(MemberDao 구현체) 까지 사용해가며 테스트를 했었는데 이제는 거기까지 가지 않고 MemberDao에서 끊기게 됩니다.

그 다음 어떤 활동이 벌어질지 시나리오를 작성하고(recoding) 실제 작업이 돌아갈 때 결과가 시나리오와 일치 하는지 확인(verify)합니다.

참고 :
Easymock Document
Easymock 시작하기
단위 테스트의 '단위'
단위 테스트의 경계 : 어디까지가 단위 테스트인가?
신고

'Spring > 주소록 만들기' 카테고리의 다른 글

Strict HTML 4.01 지침서  (6) 2006.12.14
HTML 공부 중  (2) 2006.12.13
페이징 기능 구현하기(TDD, Easymock, iBATIS, MySQL)  (2) 2006.12.11
Easymock 연습하기 3탄  (0) 2006.12.06
Easymock 연습하기 2탄  (0) 2006.12.05
Easymock 연습하기  (0) 2006.12.05
단위 테스트 모르겠슴 ㅠ.ㅠ  (0) 2006.12.05
Eclipse에서 Rename Method 리팩토링  (0) 2006.12.04
세련된 SQL map (iBATIS)  (0) 2006.12.04
메소드 추상화  (2) 2006.12.04
log4j.properties 파일 위치 시키기  (2) 2006.12.04
top







티스토리 툴바