Whiteship's Note


[봄싹] XP 적용 시나리오 3. 개발하기

모하니?/Coding : 2009.10.29 00:57


본격적으로 개발을 해야하는데, 봄싹에서는 오프라인에서 페어로 작업을 해보기도 했지만, 그렇게 자주 충분히 페어 프로그래밍을 했다고 볼 수는 없습니다. 앞으로도 좀 더 꾸준히 시도를 해봐야 그 효용이나 장단점을 파악할 수 있을 것 같습니다. 개인적으로는 뭔가 대화를 나누면서 코딩할 상대가 있어서 안심이 되긴 합니다. DB update 쿼리가 어떻게 되더라? 이거 무슨 리팩토링이지? 이 메서드 이름 맘에 들어? 여기 중복인데 어떻게 제거하면 좋을지 잘 모르겠네.. 같은 식으로 대화를 나눌 수 상대와 함께라면 좋치 않겠어요?


먼저 개발을 진행하기에 앞서 구현하려는 기능을 한 번도 만들어 본적이 없다면, 어느 정도 자신있게 개발을 진행할만큼의 학습이 필요합니다. 그 과정을 파일럿이고 표현했는데, XP 책에서도 파일럿이라고 헀었는지 잘 모르겠습니다. (뭐라고 했는지 찾아보려고 다시 살펴 봤는데 못 찾아서 그냥 썼습니다.)

그다음 과정은 좀 특이하게 바로 개발을 진행하지 않고, 인수 테스트를 만듭니다. 고객이 해당 작업이 완료 됐다는 것을 확인할 수 있는 모종의 장치를 마련하는 것이죠. 고객이 코드를 볼 수 있다면 아주 행복할텐데, 봄싹은 다행히(?) 고객이 전부 개발자 입니다. 굳이 엑셀로 이쁜 포맷을 만들고, 테스트에서 엑셀 로딩해서, 결과를 엑셀에 다시 찍어주고, 고객은 엑셀에서 수식 비교로 해당 테스트가 잘 됐나 안 됐나 확인하는 귀찮은 일은 할 필요가 없습니다. 그렇치만, 인수 테스트 코드가 고객이 원하는 시나리오를 제대로 표현해주지 못하거나, 고객이 개발자인데도 테스트 코드를 읽기가 난해하다면 테스트를 수정해야겠죠.

그다음은 페어 프로그래밍과 TDD로 해당 작업을 구현하는 일입니다. 페어 프로그래밍은 사실 오프라인에서 만났을 때의 얘기지 주중 저녁이나 회사에서 틈틈히 코딩을 하는 봄싹 개발자에데는 다소 난해한 일입니다. 그래도 메신저등을 통해서 의견은 주고 받을 수 있으니 그것도 페어 프로그래밍으로 치도록 하죠.

그렇게헤서 작업이 끝나면, 담당자 두 명은 자신들이 예상했던 난이도와 시간에 비해 실제로는 난이도가 어땠으며 실제로 소요된 시간은 어느정도인지 기록합니다. 고객은 해당 작업 결과를 본 뒤 간략한 피드백을 줍니다. "담부턴 더 빨리 만들어 주세요" 라던지.. "참 잘했어요" 라던지 ㅋ

신고
top


[GenericDao] 하이버네이트 GenericDao

모하니?/Coding : 2009.09.04 15:23


먼저, GenericDao를 만들어 쓰면 좋은 이유를 생각해보겠습니다.
- 모든 DAO에서 중복되거나 반복되는 코드를 상당량 줄일 수 있습니다.
- 테스트도 그만큼 줄일 수 있습니다.
- 개발이 좀 더 빨라집니다.
- 비슷한 기능을 하는 메서드 이름을 통일할 수 있습니다.

Entity 당 DAO를 쓰면 좋은 점과 타입-안정성을 제공하는 DAO 패턴을 사용하면 좋은 점 등은 이 글에 정리되어 있으니 궁금하신 분들은 참고하세요

TDD로 다 만든 다음, 맨 마지막에 이클립스 리팩터링 기능 중에 extract interface로 뽑아 낸 인터페이스는 다음과 같습니다.

public interface GneericDao<E> {

    void add(E entity);

    List<E> getAll();

    E getById(Serializable id);

    void delete(E entity);

    void update(E entity);

    void flush();

    E merge(E entity);

}


이것을 구현한 실제 DAO 구현체는 이렇게 생겼습니다.

public class HibernateGenericDao<E> implements GneericDao<E> {

    protected Class<E> entityClass;

    @SuppressWarnings("unchecked")
    public HibernateGenericDao() {
        ParameterizedType genericSuperclass = (ParameterizedType) getClass().getGenericSuperclass();
        Type type = genericSuperclass.getActualTypeArguments()[0];
       if (type instanceof ParameterizedType) {
         this.entityClass = (Class) ((ParameterizedType) type).getRawType();
       } else {
         this.entityClass = (Class) type;
       }
    }

    @Autowired
    protected SessionFactory sessionFactory;

    public void add(E entity) {
        getCurrentSession().save(entity);
    }

    private Session getCurrentSession() {
        return sessionFactory.getCurrentSession();
    }

    @SuppressWarnings("unchecked")
    public List<E> getAll() {
        return getCurrentSession().createCriteria(entityClass)
                .list();
    }
   
    @SuppressWarnings("unchecked")
    public E getById(Serializable id){
        return (E) getCurrentSession().get(entityClass, id);
    }
   
    public void delete(E entity){
        getCurrentSession().delete(entity);
    }
   
    public void update(E entity){
        getCurrentSession().update(entity);
    }
   
    public void flush(){
        getCurrentSession().flush();
    }
   
    @SuppressWarnings("unchecked")
    public E merge(E entity){
        return (E) getCurrentSession().merge(entity);
    }

}

특징이라고 할 수 있는 걸 꼽자면..
- 하이버네이트 SessionFactory를 사용하는 GenericDAO 라는 것.
- 별도로 엔티티 타입을 인자로 넘겨줄 필요가 없다는 것.
- 타입-안전성을 보장하기 때문에 별도의 캐스팅 등이 필요없고, 컴파일 시점에 체크 가능하다는 것.

이 클래스는 다음과 같은 테스트 클래스를 이용해서 TDD로 만들었습니다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/testContext.xml")
@Transactional
public class HibernateGenericDaoTest extends DBUnitSupport{

    @Autowired TestDao dao;
   
    @Test
    public void add() throws Exception {
        TestDomain entity = new TestDomain();
        dao.add(entity);
        assertThat(dao.getAll().size(), is(1));
    }
   
    @Test
    public void getAll() throws Exception {
        insertXmlData("testData.xml");
        assertThat(dao.getAll().size(), is(2));
    }
   
    @Test
    public void getById() throws Exception {
        insertXmlData("testData.xml");
        assertThat(dao.getById(1).getName(), is("keesun"));
    }
   
    @Test
    public void delete() throws Exception {
        insertXmlData("testData.xml");
        TestDomain entity = dao.getById(1);
        dao.delete(entity);
        assertThat(dao.getAll().size(), is(1));
    }
   
    @Test
    public void update() throws Exception {
        insertXmlData("testData.xml");
        // entity is (similar)detached object
        TestDomain entity = new TestDomain();
        entity.setId(1);
        entity.setName("whiteship");
       
        dao.update(entity);
        // now, entity has been persistent object
        entity.setName("helols");
        dao.flush();
        assertThat(dao.getById(1).getName(), is("helols"));
    }
   
    @Test
    public void merge() throws Exception {
        insertXmlData("testData.xml");
        // entity is detached object
        TestDomain entity = new TestDomain();
        entity.setId(1);
        entity.setName("whiteship");
       
        TestDomain newEntity = dao.merge(entity);
        // newEntity is persistent object, but entity is still detached object
        newEntity.setName("helols");
        entity.setName("nije");
        dao.flush();
        assertThat(dao.getById(1).getName(), is("helols"));
    }
   
}

이 테스트의 특징은 다음과 같습니다.
- 하이버네이트의 update()와 merge()의 특징과 그 차이점을 이해할 수 있도록 작성했습니다.
- 스프링 테스트 컨텍스트를 사용했습니다.
- DBUnit과 그것을 확장한 클래스를 이용했습니다.


생각해볼 것
- GenericDao에 있는 update(), merge(), flush()는 Generic하지 않다는 생각이 듭니다.
- (위에는 보여드리지 않았지만)테스트에 사용된 TestDomain 클래스와 TestDao를 GenericDaoTest 내부에 포함 시키는 것이 좋치 않을까?
- 어떤 기능을 더 추가할 수 있을까?


신고
top


JUnit에서 setUp() 또는 @Before를 사용하는 이유?

모하니?/Coding : 2009.03.27 11:50


참조: http://stackoverflow.com/questions/512184/best-practice-initialize-junit-class-fields-in-setup-or-at-declaration

JUnit은 각각의 테스트를 독립적으로 테스트하기 위해 테스트 마다 개별적인 객체를 생성합니다. 따라서..

public class SomeTest extends TestCase
{
   private final List list = new ArrayList();

    public void testPopulateList()
   {
       // Add stuff to the list
       // Assert the list contains what I expect
   }
}

이런식으로 테스트를 작성해도

public class SomeTest extends TestCase
{
   private List list;

    @Override
   protected void setUp() throws Exception
   {
       super.setUp();
       this.list = new ArrayList();
   }

    public void testPopulateList()
   {
       // Add stuff to the list
       // Assert the list contains what I expect
   }
}

이렇게 작성한 것과 동일하게 각각의 테스트를 실행하기 전에 초기화 하는 코드를 실행합니다.

그럼 대체 setUp이나 @Before는 왜 사용할까요? 그냥 생성자를 사용하거나 저렇게 필드에 직접 객체를 생성하게 해도 비슷한데 말이죠.

윗 글을 찾아보기 전에는 '그냥 tearDown이랑 균형을 맞출려고 만들었나?', '생성자를 쓰면 안 좋은 뭔가가 있나?' 등등 막연하게 생각하고 있었습니다. 그러다가 도무지 궁금해서 구글신에게 문의했더니 윗 글을 찾을 수 있었습니다.

댓글들을 조사해 본 결과.

1. Exception 발생: setUp()에서 예외를 던지면 JUnit이 유용한 스택 트레이스 정보를 돌려주지만, 생성자에서 예외가 발생하면 그냥 테스트 객체를 못 만드는 것이기 때문에 유용한 정보를 얻을 수 없음.

2. 베스트 프랙티스: 테스트 하려는 클래스의 인스턴스는 테스트 또는 setUp()에서 생성하고, 테스트 대상이 아닌 속성들은 필드에서 직접 new로 생성해도 상관없다.

이 두 가지가 유력해 보이네요.
신고
top


봄싹 3기 TDD 스터디 장소 시간 확정 됐습니다.

모하니?/Planning : 2008.12.30 11:59


봄싹 3기 첫 번째 모임 장소, 시간, 참가자 확정

봄싹 블로그도 꼭 구독해주세요. 윗 글에 댓글을 다신 다음에 참석하시면 됩니다. 정말 간단하죠. 비용은 5천원입니다. 장소는 신촌. 시간은 오후 4~6시 두 시간. 뭘하냐면.. 처음이니만큼.. 참가자 분들 소개와 봄싹 소개 및 이번 TDD 스터디 개요와 진행 방법을 정하고 공유하겠습니다.

최대 25인까지 참석할 수 있으니까요. 많이들 신청하고 참석해주세요. (17인보다 적게 오시면 오시는 분들 비용이 늘어날 수 있습니다. 주변에 친한 동료를 한 분씩 데리고 오세요.ㅋㅋ)

5천원이면;; 웬만한 커피숖에서 젤 싼 커피값 아닌가요. 오셔서 다른 개발자 분들과 담소도 나누고 스터디도 하고 음료수도 마시고~. 좋자나요.
신고
top


TDDBE - xUnit 3 (Screen Casting)





TDDBE 20장을 실습했습니다.

ToDO
- WasRun에서 플래그 대신 로그 메시지로 확인한다.
- 메소드 호출 뒤에는 tearDown()을 호출한다.
신고
top


TDDBE - xUnit 2 (Screen Casting)





TDDBE 19장을 실습했습니다.

ToDo
-실행 전에 setUp() 메소드를 호출한다.

이 작업을 TDD로 구현했습니다. 물론 책을 따라서..ㅋㅋ
신고
top


TDDBE - xUnit 23장

모하니?/Coding : 2008.09.01 20:37


드디어 마지막이군요.

ToDo
테스트 여러 개 실행하기

여러 개 테스트를 한 번에 묶어서 실행합니다. 테스트 코드를 실행하던 메인 메소드를 보고 그 중복을 제거하려는 시도에서 비롯된 거 같습니다.

new TestCaseTest("testTemplateMethod").run().summary();
new TestCaseTest("testResult").run().summary();
new TestCaseTest("testFailedResultFormatting").run().summary();
new TestCaseTest("testFailedResult").run().summary();


바로 이 코드를

        TestSuite suite = new TestSuite();
        suite.add(new TestCaseTest("testTemplateMethod"));
        suite.add(new TestCaseTest("testResult"));
        suite.add(new TestCaseTest("testFailedResultFormatting"));
        suite.add(new TestCaseTest("testFailedResult"));
        suite.add(new TestCaseTest("testSuite"));
        TestResult result = new TestResult();
        suite.run(result);
        System.out.println(result.summary());

이렇게 바꿀 겁니다.

그래서 일단 testSuite이라는 테스트 클래스를 작성합니다.
    public void testSuite(){
        TestSuite suite = new TestSuite();
        suite.add(new WasRun("testMethod"));
        suite.add(new WasRun("testBrokenMethod"));
        TestResult result = new TestResult();
        suite.run(result);
        assert result.summary().equals("2 run, 1 failed");
    }

이 클래스는 Composite 패턴을 구현할 생각이라고 합니다. 따라서 TestSuite에 있는 run()과 TestCase()에 있는 run() 메소드는 똑 같은 모양을 해야합니다. 그러다 보니, run() -> run(TestResult)로 설계상의 변화를 줍니다. 그 결과 이미 기존에 존재하는 코드를 수정해야 하는 일이 생겼습니다. 하지만, 테스트 코드가 있었기 때문에 매우 안전하게 변화를 줄 수 있었습니다. 시간도 그만큼 단축이 되는거겠죠.

간단에 TDD의 달콤함을 다시 맛 볼 수 있었던 좋은 시간이었습니다. 이제는 JUnit 1.0 코드를 볼 차례로군요.. 그건 내일;;

신고

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

OSAF 검색 폼 태그 파일 완성  (0) 2008.09.25
Principle of least astonishment  (0) 2008.09.24
How to Design a Good API & Why it Matters  (0) 2008.09.24
delete 요청 처리 컨트롤러 코드 고민  (0) 2008.09.22
@Resource 활용 팁  (6) 2008.09.08
TDDBE - xUnit 23장  (0) 2008.09.01
TDDBE - xUnit 22장  (0) 2008.09.01
TDDBE - xUnit 21장  (0) 2008.09.01
TDDBE - xUnit 20장  (0) 2008.09.01
TDDBE - xUnit 19장  (2) 2008.09.01
TDDBE - xUnit 18장.  (0) 2008.09.01
top

TAG TDD, xUnit

TDDBE - xUnit 22장

모하니?/Coding : 2008.09.01 20:30


ToDo
실패한 테스트 보고하기

TestResult에서 실패한 테스트 갯수를 세는 메소드 부터 테스트합니다.

    public void testFailedResultFormatting(){
        TestResult result = new TestResult();
        result.testStarted();
        result.testFailed();
        assert result.summary().equals("1 run, 1 failed");
    }

TestResult의 메소드가 갯수를 잘 세고 있는지 확인합니다. 구현은 간단합니다.

public class TestResult {

    int runCount;
    int failedCount;
   
    public void testStarted(){
        this.runCount += 1;
    }

    public void testFailed() {
        this.failedCount += 1;
    }

    public String summary() {
        return runCount + " run, " + failedCount + " failed";
    }

}

그리고 이제 실패하는 테스트 결과를 확인하는 테스트를 작성합니다.

    public void testFailedResult(){
        test = new WasRun("testBrokenMethod");
        TestResult result = test.run();
        assert result.summary().equals("1 run, 1 failed");
    }

총. 1개를 실행했는데, 1개가 실패한 겁니다. 일단, WasRun 클래스에 예외를 발생시키는(실패하는) 테스트를 추가합니다.

    public void testBrokenMethod() throws RuntimeException  {
        throw new RuntimeException("예외 발생하는  테스트");
    }

그리고 TestCase의 run() 메소드에서 예외가 발생할 때마다 TestResult에 처음에 만들어 둔 testFailed()를 호출합니다.

        try {
            Method method = this.getClass().getMethod(methodName, null);
            method.invoke(this, null);
        } catch (InvocationTargetException e) {
            result.testFailed();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

예외를 잡는 부분에 대해서 조금 고민을 했는데, InvocationTargetExcepion만 따로 잡고, 나머진 일단 덩어리로 처리해버렸습니다. 어차피 세세하게 분리를 해놔도 각각의 catch 블럭에서 전부 throw new Runtime(e); 으로 구현할꺼기 때문에, 저렇게 덩어리로 잡아버렸습니다.


신고

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

Principle of least astonishment  (0) 2008.09.24
How to Design a Good API & Why it Matters  (0) 2008.09.24
delete 요청 처리 컨트롤러 코드 고민  (0) 2008.09.22
@Resource 활용 팁  (6) 2008.09.08
TDDBE - xUnit 23장  (0) 2008.09.01
TDDBE - xUnit 22장  (0) 2008.09.01
TDDBE - xUnit 21장  (0) 2008.09.01
TDDBE - xUnit 20장  (0) 2008.09.01
TDDBE - xUnit 19장  (2) 2008.09.01
TDDBE - xUnit 18장.  (0) 2008.09.01
static inner class를 Spring에 bean으로 등록하기  (2) 2008.08.21
top

TAG TDD, xUnit

TDDBE - xUnit 21장

모하니?/Coding : 2008.09.01 20:19


ToDO
테스트 결과 출력하기

테스트 중에서도 잘 실행하고 종료된 상황을 테스트 합니다.

public class TestCaseTest extends TestCase {

    private WasRun test;

    public TestCaseTest(String methodName) {
        super(methodName);
    }
   
    public void testTemplateMethod(){
        test = new WasRun("testMethod");
        test.run();
        assert test.log.equals("setUp testMethod tearDown ");
    }
   
    public void testResult(){
        test = new WasRun("testMethod");
        TestResult result = test.run();
        assert result.summary().equals("1 run, 0 failed");
    }
   
    public static void main(String[] args) {
        new TestCaseTest("testTemplateMethod").run();
        new TestCaseTest("testResult").run();
    }
}

모든 테스트 케이스 마다 매번 결과를 확인할 수 있도록 설계를 합니다. 그래서 run() 메소드에서 실행 결과를 나타내는 TestResult를 반환 하도록 합니다.

 public class TestCase {
   
    String methodName;

    public TestCase(String methodName) {
        this.methodName = methodName;
    }
   
    public TestResult run() {
        TestResult result = new TestResult();
        result.testStarted();
        setUp();
        try {
            Method method = this.getClass().getMethod(methodName, null);
            method.invoke(this, null);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        tearDown();
        return result;
    }

    protected void tearDown() {}

    protected void setUp() {}
}

물론 첨부터 저렇게 구현하진 않았습니다. 처음에는 그냥 return new TestResult()를 했다가. TestResult에서 설계가 들어갑니다. 결과값을 testStarted()라는 걸 호출 해서 속에 있는 카운터를 증가시키는 구조로 말이죠.

public class TestResult {

    int run;
   
    public void testStarted(){
        this.run += 1;
    }

    public String summary() {
        return run + " run, 0 failed";
    }

}

얘도 첨엔 이렇게 안 생겼었고, 처음엔 상수를 반환했습니다. 그랬다가 일단 앞에 있는 실행 갯수만을 변수화 했고, 위의 설계 결정에 따라 구현을 변경한 겁니다.

리팩터링과, 설계, 그리고 TDD의 관계를 생각하게 만드는 챕터였습니다.
신고
top

TAG TDD, xUnit

TDDBE - xUnit 20장

모하니?/Coding : 2008.09.01 20:13


ToDo
나중에 tearDown 호출하기.

이 작업을 하기 전에 일단 이전까지 사용하던 flag 방식은 실행 순서를 못 잡기 때문에..이 작업을 잠시 미루고

ToDo
WasRun에 로그 문자열 남기기.

이 작업을 먼저 합니다. 모든 작업을 마치고 나면...
public class TestCaseTest extends TestCase {

    public TestCaseTest(String methodName) {
        super(methodName);
    }
   
    public void testTemplateMethod(){
        WasRun test = new WasRun("testMethod");
        test.run();
        assert test.log.equals("setUp testMethod tearDown ");
    }
   
    public static void main(String[] args) {
        new TestCaseTest("testTemplateMethod").run();
    }
}

테스트 코드가 줄어듭니다. 두둥.. 과정을 보여드리지 못해 아쉽네요.

public class WasRun extends TestCase {

    String log;
   
    public WasRun(String methodName) {
        super(methodName);
    }
   
    public void testMethod() {
        log += "testMethod ";
    }
   
    @Override
    protected void setUp() {
        log = "setUp ";
    }
   
    @Override
    protected void tearDown() {
        log += "tearDown ";
    }

}

WasRun에서는 flag를 없애고 로그를 남기는 방식으로 바껴서.. 역시 코드가 또 줄어들었습니다.

public class TestCase {
   
    String methodName;

    public TestCase(String methodName) {
        this.methodName = methodName;
    }
   
    public void run() {
        setUp();
        try {
            Method method = this.getClass().getMethod(methodName, null);
            method.invoke(this, null);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        tearDown();
    }

    protected void tearDown() {}

    protected void setUp() {}
}

끝..

신고
top

TAG TDD, xUnit

TDDBE - xUnit 19장

모하니?/Coding : 2008.09.01 16:42


이번에는 "먼저 setUp() 메소드를 호출한다."를 구현했습니다.

public class TestCaseTest extends TestCase {

    WasRun test;
   
    public TestCaseTest(String methodName) {
        super(methodName);
    }
   
    protected void setUp() {
        test = new WasRun("testMethod");
    }
   
    public void testRunning(){
        test.run();
        assert test.wasRun == true;
    }
   
    public void testSetUp(){
        test.run();
        assert test.wasSetUp == true;
    }
   
    public static void main(String[] args) {
        new TestCaseTest("testRunning").run();
        new TestCaseTest("testSetUp").run();
    }
}

테스트 클래스는 이미 setUp()을 반영했지만, 사실 setUp()을 반영하기 전에 구현을 마치고, 리팩터링 하면서 방금 구현한 setUp()을 사용해서 테스트 클래스까지 간단하게 리팩터링을 했습니다.

import java.lang.reflect.Method;

public class TestCase {
   
    String methodName;

    public TestCase(String methodName) {
        this.methodName = methodName;
    }
   
    public void run() {
        setUp();
        try {
            Method method = this.getClass().getMethod(methodName, null);
            method.invoke(this, null);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    protected void setUp() {}
}

Template Method 패턴을 사용해서, 하위 클래스에서 setUp()을 맘대로 구현할 수 있도록.. 했구요. run()에 워크 플로우를 정의했죠.

public class WasRun extends TestCase {

    boolean wasRun;
    boolean wasSetUp;
   
    public WasRun(String methodName) {
        super(methodName);
    }
   
    public void testMethod() {
        wasRun = true;
    }
   
    @Override
    protected void setUp() {
        wasRun = false;
        wasSetUp = true;
    }

}

WasRun에서도 역시 방금 구현한 setUp()을 사용해서 생성자를 좀 더 간단하게 했습니다.

'이번 장의 핵심 내용은 테스트 케이스들 간의 의존성을 없애기'였습니다. 지금처럼 매번 별도의 인스턴스를 만들기 때문에, 전역 변수를 사용하는 테스트가 아닌 이상 테스트 케이스들끼리 의존할 가능성은 거의 없게 됩니다.
신고

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

@Resource 활용 팁  (6) 2008.09.08
TDDBE - xUnit 23장  (0) 2008.09.01
TDDBE - xUnit 22장  (0) 2008.09.01
TDDBE - xUnit 21장  (0) 2008.09.01
TDDBE - xUnit 20장  (0) 2008.09.01
TDDBE - xUnit 19장  (2) 2008.09.01
TDDBE - xUnit 18장.  (0) 2008.09.01
static inner class를 Spring에 bean으로 등록하기  (2) 2008.08.21
국내 최초 OSGi 기반 애플리케이션 프레임워크 OSAF 1.5 - 멀지 않았다.  (2) 2008.08.12
Mockito - 아규먼트 Matcher  (0) 2008.08.12
JavaMail - 첨부파일 읽기  (0) 2008.08.07
top

TAG 19장, TDD, xUnit

TDDBE - xUnit 18장.

모하니?/Coding : 2008.09.01 16:00


파이썬으로 되어있는 예제라서, TDD(테스트 주도 개발) 책을 읽으면서 그냥 넘어갔던 부분인데.. 도무지 그냥 넘어갈 만한 부분이 아닌거 같아서 자바로 코딩해보기로 했습니다. 파이썬으로 코딩해도 되겠지만, 저는 자바로.. 그래서 18장을 우선 코딩해봤습니다.

캬~ 재밌네요.

public class TestCaseTest extends TestCase {

    public TestCaseTest(String methodName) {
        super(methodName);
    }
   
    public void testRunning(){
        WasRun test = new WasRun("testMethod");
        assert test.wasRun == false;
        test.run();
        assert test.wasRun == true;
    }
   
    public static void main(String[] args) {
        new TestCaseTest("testRunning").run();
    }
}

위에 있는 TestCase는 JUnit에 있는게 아닙니다. 직접 구현한 클래스입니다. 구현체는 다음과 같습니다.

public class TestCase {
   
    String methodName;

    public TestCase(String methodName) {
        this.methodName = methodName;
    }
   
    public void run() {
        try {
            Method method = this.getClass().getMethod(methodName, null);
            method.invoke(this, null);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

이것을 상속받은 WasRun은 다음과 같습니다.

public class WasRun extends TestCase {

    boolean wasRun;
   
    public WasRun(String methodName) {
        super(methodName);
        wasRun = false;
    }

    public void testMethod() {
        wasRun = true;
    }

}

TestCase와 WasRun은 원래 하나의 클래스였는데, 책임이 두 개가 되자(하나는 실행됐는지 여부를 확인하는 플래스, 하나는 동적으로 메소드를 실행하는 역할) 그 즉시 리팩터링을 합니다. 상위 클래스를 만들고 그 쪽으로 Pull Up Field, Pull Up Method를 합니다.

코드도.. 정말 딱 테스트가 통과할 만큼만 만들고, 테스트 성공 시킨 다음에는 상수를 변수로 바꾸는 리팩터링 부터 시작해서, 위에서 언급한 리팩터링까지.. 쫘르륵...

캬.. 역시 그냥 읽는 것 보다 한 번 해보는거랑은 차이가 큽니다.
신고
top

TAG TDD, xUnit

TDD 연습하기 - RomanNumber

모하니?/Coding : 2008.04.18 14:21


1부터 1000사이의 숫자를 주면 로마숫자로 변환하는 프로그램을 TDD로 개발하시오.

점심시간에 밥 빨리먹고 심심하신 분들 한 번 해보세요. 재밌네요. 캬캬..
토비형님은 자바코드 열댓줄 루비코드 대여섯줄이면 끝난다고 하네요... 워우...

자바코드로 짜실 분들을 위테 테스트코드를 올려드리겠습니다.

        assertEquals("I", maker.convert(1));
        assertEquals("II", maker.convert(2));
        assertEquals("III", maker.convert(3));
        assertEquals("IV", maker.convert(4));
        assertEquals("V", maker.convert(5));
        assertEquals("VI", maker.convert(6));
        assertEquals("VII", maker.convert(7));
        assertEquals("VIII", maker.convert(8));
        assertEquals("IX", maker.convert(9));
        assertEquals("X", maker.convert(10));
        assertEquals("XI", maker.convert(11));
        assertEquals("XII", maker.convert(12));
        assertEquals("XIII", maker.convert(13));
        assertEquals("XIV", maker.convert(14));
        assertEquals("XV", maker.convert(15));
        assertEquals("XIX", maker.convert(19));
        assertEquals("XX", maker.convert(20));
        assertEquals("XXX", maker.convert(30));
        assertEquals("XL", maker.convert(40));
        assertEquals("L", maker.convert(50));
        assertEquals("LX", maker.convert(60));
        assertEquals("LXX", maker.convert(70));
        assertEquals("LXXX", maker.convert(80));
        assertEquals("XC", maker.convert(90));
        assertEquals("C", maker.convert(100));
        assertEquals("CC", maker.convert(200));
        assertEquals("CD", maker.convert(400));
        assertEquals("D", maker.convert(500));
        assertEquals("DCLXVI", maker.convert(666));
        assertEquals("CM", maker.convert(900));
        assertEquals("M", maker.convert(1000));
        assertEquals("MCMXLV", maker.convert(1945));
        assertEquals("MCMXCIX", maker.convert(1999));
        assertEquals("MM", maker.convert(2000));
        assertEquals("MMM", maker.convert(3000));
        assertEquals("ↁ", maker.convert(5000));

간단하지요. 테스트 코드 짜는건 쉬운데.. (위키피디아에 있는 표를 전부 테스트 해봤습니다. 불필요한 것도 있겠지만.. 뭐 어때요. 캬캬캬) 전 구현해보니 60줄 정도 나왔습니다. 토비형님 코드의 4 배... 라인수는 Ctrl + Shift + F 기준입니다.

저녁값 내기로 가장 짧은 코드로 구현하기 뭐 이런거 해도 재밌겠네요. 전 .. 그러고 놀 수 사람이... 없어요. 흑흑.. 외로워... 흑흑흑... 이젠 어려운 자바습이랑 놀아야지.. @.@ 자바습아 이리온...


풀어보신 분들은 저처럼 소스 코드를 올려주세요. 부끄럽지만 제 파일도 올려둡니다. 재밌게 공유해 보아요.


신고
top


99% Line Coverage

모하니?/Coding : 2008.01.23 16:55


몇일전 기록한 98% Line Coverage 기록을 깨습니다. 이제 남은 건 100% Line Coverage 뿐이군요.

사용자 삽입 이미지

볼링 게임을 구현한 것인데, 완성하는데 하루가 걸렸습니다. 아 어제 이맘때부터 계속 머릿속을 괴롭히던 녀석을 처리하니 피곤이 몰려오는 것 같습니다.

볼링 게임이 매우 간단한 것 같은데, Stike, Spare 점수 계산 그리고 마지막 프레임의 특이함 때문에 코드가 금방 지져분해집니다.

만들면서 느낌점을 요약했는데, 다음과 같습니다.

느낀점.
- 어떤 테스트를 통과Pass 하게 만들려다가 그 전에 먼저 작성해야 할 메소드가 생긴다.
    - 이 때는 지금 하던 걸 멈추고 사전에 먼저 작성해야 할 것 부터 만들자.
    - @Ignore
- 테스트를 잘못 작성하면 구멍이 생긴다.
    - 테스트를 작게 잘 작성하자. 하나의 Task가 5분을 넘기지 말아야 한다.
    - 어떻게 하면 구멍이 생기지 않는 테스트를 작성할 수 있을까?
- 객체 지향 기본 원칙 지키며 코딩하는게 정말 어렵다.
    - 코딩하다보면, 어느새 객체 지향 원칙은 안중에도 없어 진다. 사실 잘 발견을 못한다.
    - 변하는 부분과 변하지 않는 부분을 분리하라.
    - 어떻게 하면 자연스럽게 패턴이 튀어나올까?


신고
top


Spring MVC 리팩토링 4

모하니?/Coding : 2007.06.22 17:39


이전에서 단위테스트를 통해 Controller과 Service, DAO 계층을 테스트 했습니다.
  1. 13:17:31 Spring MVC 리팩토링 3
  2. 13:04:41 Spring MVC 리팩토링 2
  3. 12:28:58 Spring MVC 리팩토링 1
사실 DAO 계층 테스트를 단위테스트라고 할 수 있을지 모르겠지만 테스트용 DB와 테스트 데이터를 별로도 사용해서 실제 DB에는 전혀 지장이 없었습니다. 그리고 spring-mock.jar에 있는 AbstractTransactionalDataSourceSpringContextTests를 사용하여 자동 롤백이 되도록 하여 테스트의 흔적이 테스트 DB에 남지도 않았습니다. 그러면서도 DAO를 충분히 테스트 할 수 있었습니다.

이제는 Service Layer에서 DAO 계층과 함꼐 테스트를 해보고 그 다음은 Controller 에서 그 아래에 있는 Service 계층과 DAO 계층을 아울러서 테스트 할 차례입니다.
사용자 삽입 이미지
먼저 위 그림의 파란색은 이전에 테스트를 했고 빨간색 부분의 테스트를 작성합니다. 이 때도 이전 글에서 사용한 osaf의 클래스를 사용합니다.

public class MemberServiceIntegrationTest extends AbstractTransactionalDataSourceSpringContextControllerTest{

    protected MemberService memberService;

    @Override
    protected String[] getConfigLocations() {
        return new String[] {
            "file:web/WEB-INF/spring/applicationContext-dao.xml",
            "file:test/applicationContext-jdbc-datasource.xml",
            "file:web/WEB-INF/spring/dao/daoContext-member.xml",
            "file:web/WEB-INF/spring/application/applicationContext-member.xml",
        };
    }

    @Override
    protected void onSetUpInTransaction() throws Exception {
        insertFlatXmlDataSet("test/kr/co/springframework/member/dao/memberData.xml");
    }

    public void testDI() throws Exception {
        assertNotNull(memberService);
    }

    public void testGetAll() throws Exception {
        assertEquals(1, memberService.getAll().size());
    }

    public void testNotJoinedMail() throws Exception {
        Member member = memberService.isJoined("");
        assertEquals(null, member);
        assertEquals("기선", memberService.isJoined("keesun@mail.com").getName());
    }
}

간단하게 테스트 할 수 있습니다. 단위 테스트하고의 차이는 Spring의 컨테이너를 사용하여 의존성이 삽입된 객체를 사용하고 있으며 DB역시 DBUnit을 이용하여 사용하고 있습니다. 테스트용 DB라는 것만 빼고는 전부 같습니다. 아... 테스트용 DB(HSQL)와 실제 개발 DB(MySQL)와 다르군요. 하지만 ORM(하이버네이트)를 사용하고 있기 때문에 전~혀 문제가 없습니다. 테스트용 database.properties 파일 하나만 작성해주면 되죠.

사용자 삽입 이미지
이번에는 컨트롤러를 테스트 합니다. 이것도 역시 이전에 파란색 박스는 테스트를 했고 이번에는 빨간 박스를 테스트 합니다.

    public void testEmptyOrWhiteMail() throws Exception {
        mail = "";
        MemberCommand command = new MemberCommand();
        command.setMail(mail);
        mav = checkController.onSubmit(null, null, command, null);
        assertEquals("redirect:join.html", mav.getViewName());
    }

    public void testExistMemberMail() throws Exception {
        mail = "keesun@mail.com";
        mav = checkController.onSubmit(null, null, command, null);
        Map model = mav.getModel();
        assertNotNull(model.get("member"));
        assertEquals("confirm", mav.getViewName());
    }

서비스 계층의 통합 테스트 코드와 상당부분이 같기 때문에 테스트 메소드만 붙여넣었습니다. (테스트 코드에서만 볼 수 있는)단위 테스트와의 차이는 easymock의 사용이 없어졌다는 것입니다. 이전에는 MemberService의 Mock객체를 만들어 사용했었습니다. 하지만 이제는 진짜 MemberSerive 객체를 사용하고 있죠.

이것으로 매~~~우 간단한 리팩토링(메소드 이름 하나 바꾸는 것)을 매우 길~~~~게 돌아오며 테스트를 연습해 봤습니다.

요약 및 느낀점
- Easy Mock은 생각보다 간단하다.(createMock() -> expect() -> replay() -> verify() -> reset())
- spring-mock.jar 에 유용한 Mock 객체들(Spring MVC 책에서는 Stub이라고 얘기하고 있지만)이 많이 있는데 그 중에 자주 사용할 것은 MockHttpServletRequest와 MockHttpServletResponse
- 계층 별 테스트 할 대상, 목록, 요점을 명확히 해야겠다.
- 테스팅(단위 테스트와 통합 테스트)은 확실히 애플리케이션에 자신감을 불어넣어준다.
- View를 테스트 하는 방법은 무엇일까? (Spring view test라고 구글링을 해봤지만 원하는 결과는 못봤다.)
- 컨트롤러를 테스트 할 때 데이터 바인딩과 벨리데이션 역시 테스트 해야겠다.
- 테스트 코드를 작성할 때 참고할 명세서가 있어야겠다.(비즈니스 로직을 자세히 설명한 어떤 문서...)

신고
top


Spring MVC 리팩토링 3

모하니?/Coding : 2007.06.22 13:17


이전 글에서 Controller와 Service Layer를 단위 테스트 했습니다. 이번에는 DAO Layer를 단위(?) 테스트 하겠습니다.

이번에는 OASF를 사용하여 DBUnit과 spring-mock.jar에 있는 AbstractTransactionalDataSourceSpringContextTests 를 확장한 클래스를 사용하겠습니다. 토비님께서 만드셨는데 DBUnit 사용하기도 좋고 설정파일들의 위치를 특정 컨벤션만 지켜주면 알아서 읽어들입니다.

하지만 제가 테스트 하는 애플리케이션은 OSAF 설정파일들의 컨벤션을 알지 못했기 때문에;; 그냥 getConfigLocations 를 사용해서 필요한 설정 파일들과 테스트 DB를 사용하도록 합니다. 테스트 DB는 java 6에 내장되어 있는 HSQL을 사용합니다.

먼저 테스트용 데이터를 XML 형식으로 준비합니다.
<dataset>
    <member id="1" name="기선" mail="keesun@mail.com" mobile="111-1111-1111" job="BIT" springAtHome="dd" springAtWork="dd" springModules="dd" attendType="N" />
</dataset>

다음 테스트를 작성합니다.
    public void testFindByMail() throws Exception {
        insertFlatXmlDataSet("test/kr/co/springframework/member/dao/memberData.xml");
        Member member = memberDao.findByMail("keesun@mail.com");
        assertNotNull(member);
        assertNull(memberDao.findByMail(""));
    }

테스트가 통과 하도록 findByMail을 구현합니다.
    @SuppressWarnings("unchecked")
    public Member findByMail(final String mail) {
        List<Member> memberList = getHibernateTemplate().executeFind(new HibernateCallback() {
            public Object doInHibernate(Session s) throws HibernateException, SQLException {
                Query q = s.createQuery("from Member m where m.mail = :mail")
                    .setParameter("mail", mail);
                return q.list();
            }
        });
        if(memberList.size() == 0)
            return null;
        else
            return memberList.get(0);
    }

사용자 삽입 이미지

단위 테스트가 끝났습니다. 이제 남은 건
1. Service Layer에서 통합테스트를 해보고
2. Controller Layer에서 통합테스트를 하면

테스트&구현이 끝이납니다. 그럼 톰캣 돌려서 돌려보면 끝이겠지만 지금도 어느정도 잘 돌아갈 것 같은 자신이 생긴 상태입니다. 나머지는 밥먹고..계속 합니다.
신고
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


Spring MVC 리팩토링 1

모하니?/Coding : 2007.06.22 12:28


Service Layer의 인터페이스를 살펴보니 다음과 같습니다.
사용자 삽입 이미지
마지막에 이는 저 메소드의 이름과 하는 일이 맘에 안듭니다. 구현도 제가 했고 구현한지 2틀 정도밖에 안됐습니다. 더 늦기 전에 수정해야겠습니다.

public Member findByMail(String mail)

다분히 DAO Layer에 있어야 할 것 같은 이름이며 하는 일 역시 그저 DAO 계층에 있는 메소드 하나를 호출하는 겁니다. 본래 저 메소드를 만들게 된 의도는 해당 mail로 가입한 멤버가 있는지 확인하려는 의도였습니다. 하지만 저 메소드의 이름으로는 도무지 그런 의도가 안보입니다. 따라서 다음과 같이 수정해야 겠습니다.

public boolean isJoinedMail(String mail)

이러한 인터페이스로 변경되어야 겠습니다. 근데 어디 부터 손을 대야 할까요;;;;

일단 Controller로 가봐야겠습니다. 그곳에서 Service Layer의 메소드를 호출하고 있기 때문에 그곳에서 새로운 isJoinedMail(mail)을 호출하도록 수정하여 구현을 변경하면 될 것 같습니다.

아니군요. Controller로 바로 가지 않고 해당 컨트롤러의 테스트 클래스로 갑니다. 이 곳이야 말로 진짜 리팩토링을 시작할 지점입니다.

수정 전 테스트 메소드 입니다.

    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 testEmptyOrWhiteMail() throws Exception {
        mail = "";
        expect(mockMemberService.isJoined(mail)).andReturn(false);
        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);
    }

매우 간단한 변경입니다. 위에서 언급한 것과 하나 차이가 있다면 메소드 명을 isJoined()로 변경하였습니다. 어차피 가입 확인을 mail로 만 할 것이기 때문에 굳이 적어줄 필요가 없다고 느꼈습니다. 이 메소드를 사용할 때 다음과 같이 이쁜 문장이 만들어 지겠죠. :-)

isJoined(mail);

사용자 삽입 이미지

테스트가 실패한 이유는 컨트롤러를 테스트 할 때(onSubmit()이 호출 됐을 때) 각본에는 MemberService의 isJoined()가 주인공으로 열연을 펼친뒤 false를 리턴하라고 했는데 실제로 재생을 해보니까 주연은 커녕 엑스트라로도 보이지 않았기 때문입니다.

자 이제 이 테스트가 돌아갈 수 있도록 컨트롤러의 구현을 변경합니다.

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

이런 문제가 생겼습니다. isJoined() 메소드를 사용하여 boolean 값을 받아왔는데 그걸로는 View에 넘겨줄 정보가 부족합니다. 다시 말하면 View에서는 member 객체를 필요로 하는데 여기서는 그 객체를 받아 오려면 memberService에 새로운 mail 로 해당 member를 가져오는 것을 구현해야 할 것 같습니다.

이럴 때 혼자 개발할 때의 장점이 저를 살려줍니다.

"니가 곧 개발자며 사용자이자 설계자다. 니가 다 알아서 해라!"

저는 isJoined(mail)의 인터페이스를 다시 한번 변경합니다.

public Member isJoined(mail)

만약 해당 mail 이 없다면 null을 반환합니다. 따라서 컨틀롤러의 테스트 코드르 다시 수정합니다.

    public void testEmptyOrWhiteMail() throws Exception {
        mail = "";
        expect(mockMemberService.isJoined(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);
    }

이제 다시 컨트롤러의 구현을 수정합니다.

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

끝!! 테스트는 돌아갑니다.

사용자 삽입 이미지

이제 남은 일은 Controller를 단위 테스트가 아닌 통합 테스트를 해보는 것입니다. 그러면 당연히 에러가 나겠죠. 아직 MemberService와 MemberDAO 구현을 변경하지 않았기 때문이죠. 이 때 두가지 선택 사항이 있습니다.

1. Controller를 통합 테스트 해버려서 구현한다
2. MemberService를 단위 테스트 -> MemberDAO를 단위(?) 테스트 -> MemberService 통합 테스트 -> Controller 통합테스트

두 가지 선택 사항이 있습니다. 테스팅 공부를 제대로 할려면 돌아가야겠죠. 저는 2번을 선택하겠습니다.
신고

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

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
GmailSender.java  (2) 2007.05.23
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


4. 화면과 모델 통합에서의 갈등

모하니?/Coding : 2007.02.22 15:09


미쳐 생각하지 못했던 문제들이 슬슬 Clocking을 풀고 있습니다.

1. 저장을 어떤 형태로 할 것인가? 저는 text 파일로 저장 읽어 오기를 구현해 뒀습니다. 하지만 List를 저장하기엔..ObjectStream이 좋을 것 같네요. text로 읽어와서 파싱하는 노가다를 하기 싫어요~ ㅠ.ㅠ... 그렇다면 text 파일로 리포트를 만들어 주는 기능을 나중에 만들고 일단 저장하기와 불러오기는 ObejctStream으로 변경 할까요? 네!
사용자 삽입 이미지

2. 이런 이런 스크롤이 마우스 휠을 움직여도 움직이지 않아요. 마우스 휠로 스크롤을 움직이게 하든지..하니면 ExpendBar를 열거나 닫을 때마다 화면 크기를 딱 맞게 변하게 스크롤이 생기지 않도록 합시다. 방법은? 모르는데요. 으흐흐흐... 그렇다면 알고 나면 고칩니다. 일단 그냥 구현 합시다. 넹~

3. ToDo List 에서는 하나의 작업 만 선택하도록 합시다. 왜요? 여러 작업을 동시에 START 시킬 순 없자나요. 아아.. OK. 뭐 그럽시다. 사실은 코딩하기 귀찮아서 그런거죠? 에이.. 설마..

4. 처음 프로그램을 실행 했을 때를 생각해 봅시다.
4.1. 일단 Current Work에 Work가 없기 때문에 PAUSE 와 END 버튼은 disable 상태여야 합니다.
4.2. ToDo List 역시 아무것도 없기 때문에 START와 DELETE 역시 disable 상태여야 합니다. ADD만 그냥 두면 되겠군요.
4.3. 작업 하나를 입력 합니다. 그럼 START와 DELETE 버튼이 이용 가능해 지겠군요.
4.4. 작업 하나를 선택해서 START 시킵니다. 그럼 PAUSE 와 END 버튼이 이용 가능해 지겠군요. 대신에 START 버튼은 불이 꺼집니다.
4.5. PAUSE 버튼을 클릭하면 PAUSE 버튼의 title이 RESTART 로 바뀝니다. 그리고 START에는 다시 불이 들어옵니다.
4.6. PAUSE 버튼을 클릭한 상태에서 ToDo에서 작업을 선택하고 START를 클릭하면... 음.. 그렇게 되겠군요.
4.7. END 버튼을 클릭하면 PAUSE 버튼은 불이 꺼지고 Completed Work List에 완료된 작업이 추가 됩니다.
신고

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

랜덤 리스트 축출기 v1.0  (0) 2007.03.08
Tdd Helper 사용 중  (0) 2007.03.07
랜덤 리스트 축출기  (2) 2007.03.06
TDD Helper 만들었습니다.  (4) 2007.02.23
5. version 0.9  (0) 2007.02.22
4. 화면과 모델 통합에서의 갈등  (0) 2007.02.22
3. 작업 분류 2  (0) 2007.02.22
3. 작업 분류  (0) 2007.02.22
TDD Helper 화면 Version 0.5  (4) 2007.02.21
2. 작업 소요 시간 측정  (0) 2007.02.21
1. 작업 리스트 관리(CRUD)  (0) 2007.02.20
top


3. 작업 분류 2

모하니?/Coding : 2007.02.22 14:39


3.6. 작업 분류별 리스트를 가져옵니다.

    @Test public void workTypeList(){
        Work work1 = new Work("work1");
        workList.add(work1);
        assertEquals(work1, workList.getToDoList().get(1));

        work1.start();
        assertEquals(work1, workList.getCurrentWork());
        assertEquals(0, workList.getToDoList().size());

        work1.pause();
        assertEquals(work1, workList.getPausedWorks().get(1));
        assertEquals(null, workList.getCurrentWork());

        work1.end();
        assertEquals(work1, workList.getEndWorks().get(1));
        assertEquals(0, workList.getPausedWorks().size());
    }

Work가 List에 있는 것만 확인하면 되는데 특정 위치를 가리켜서 확인하는 건 그닥 맘에 들지 않습니다. 그래서 contain()을 추가합니다.

    @Test public void workTypeList(){
        Work work1 = new Work("work1");
        workList.add(work1);
        assertTrue(workList.getToDoList().contains(work1));

        work1.start();
        assertEquals(work1, workList.getCurrentWork());
        assertEquals(0, workList.getToDoList().size());

        work1.pause();
        assertTrue(workList.getPausedWorks().contains(work1));
        assertEquals(null, workList.getCurrentWork());

        work1.end();
        assertTrue(workList.getEndWorks().contains(work1));
        assertEquals(0, workList.getPausedWorks().size());
    }

3.7. 하나의 작업이 실행 중일 땐 다른 작업을 실행 할 수 없습니다.

이걸 조금 구현 해보니깐..Work에서 WorkList를 가지고 있어야 합니다. 그렇게 되면 굉장히 복잡해지고 커플링이 심해집니다. UI 상에서 이런 시도를 막을 수 있을 것 같습니다. UI를 구현하는 코드가 복잡해지겠지만.. 일단 한번 해본 뒤에 생각해 봐야겠습니다.


신고

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

Tdd Helper 사용 중  (0) 2007.03.07
랜덤 리스트 축출기  (2) 2007.03.06
TDD Helper 만들었습니다.  (4) 2007.02.23
5. version 0.9  (0) 2007.02.22
4. 화면과 모델 통합에서의 갈등  (0) 2007.02.22
3. 작업 분류 2  (0) 2007.02.22
3. 작업 분류  (0) 2007.02.22
TDD Helper 화면 Version 0.5  (4) 2007.02.21
2. 작업 소요 시간 측정  (0) 2007.02.21
1. 작업 리스트 관리(CRUD)  (0) 2007.02.20
TDD 연습용 프로그램  (4) 2007.02.20
top


3. 작업 분류

모하니?/Coding : 2007.02.22 12:41


3.1. 새로 추가된 작업은 "해야할 작업" 목록에 추가 됩니다.

    @Test public void newWorkType(){
        Work work = new Work("work1");
        workList.add(work);
        assertEquals(WorkType.TODO, work.getType());
    }

음.. 화면을 먼저 만들걸.. 하는 생각이 계속 들면서 작업을 방해하기 시작하는데 메모를 해둬야 겠습니다.

- 일시 정지 클릭하고 ToDo List에서 다른 작업의 시작 버튼을 클릭하면 일시 정지 된 작업은 TODO 리스트로 옮겨지고 Current Work에는 방금 선택한 작업이 올라옵니다.

3.2. "작업 시작" 버튼을 클릭한 작업은 "진행 중인 작업" 목록에 추가 됩니다.

    @Test public void startWorkType(){
        Work work = new Work("work1");
        workList.add(work);
        work.start();
        assertEquals(WorkType.CURRENT, work.getType());
    }

3.3. "작업 완료" 버튼을 클릭한 작업은 "완료된 작업" 목록에 추가 됩니다.

- "PAUSE" 버튼과 "RESTART" 버튼을 눌렀을 때 상태 변화를 생각 못했었군요. 테스트에 추가해야겠습니다.

그 전에 일단 3.3. 부터 마무리 하구요.

    @Test public void endWorkType(){
        Work work = new Work("work1");
        workList.add(work);
        work.start();
        work.end();
        assertEquals(WorkType.END, work.getType());
    }

=> 어라.. 여기까지 하다보니 테스트 코드에 엄청난 중복이 생겼습니다. test 메소드 하나로 위에 있는 세개를 전부 합치겠습니다.

    @Test public void workType(){
        Work work = new Work("work1");
        workList.add(work);
        assertEquals(WorkType.TODO, work.getType());
        work.start();
        assertEquals(WorkType.CURRENT, work.getType());
        work.end();
        assertEquals(WorkType.END, work.getType());
    }

그럼 이제 위에 있는 세개는 지워도 되겠군요. 책 읽은게 도움이 됐습니다. 흐흐 쌩큐 Beck!

3.4. 선택한 분류의 작업만 보여주기.

이건 SWT에 있는 EnpendBar로 구현을 했기 때문에.. 코드로 처리하지 않아도 될 것 같습니다.

3.5. "PAUSE" 버튼을 클릭하면 pause 상태로 바꿔줍니다. 위에 있는 테스트 코드를 수정해서 시나리오를 만듭니다.

    @Test public void workType(){
        Work work = new Work("work1");
        workList.add(work);
        assertEquals(WorkType.TODO, work.getType());
        work.start();
        assertEquals(WorkType.CURRENT, work.getType());
        work.pause();
        assertEquals(WorkType.PAUSE, work.getType());
        Work work2 = new Work("work2");
        workList.add(work2);
        work2.start();
        assertEquals(WorkType.CURRENT, work2.getType());
        work.end();
        assertEquals(WorkType.END, work.getType());
    }

- 하나의 작업이 실행 중일 때 다른 작업을 실행하지 못하게 합니다.

SWT에서 실행 버튼을 disable 하게 해두는 걸로 하면.. 되겠죠? 훔
신고

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

랜덤 리스트 축출기  (2) 2007.03.06
TDD Helper 만들었습니다.  (4) 2007.02.23
5. version 0.9  (0) 2007.02.22
4. 화면과 모델 통합에서의 갈등  (0) 2007.02.22
3. 작업 분류 2  (0) 2007.02.22
3. 작업 분류  (0) 2007.02.22
TDD Helper 화면 Version 0.5  (4) 2007.02.21
2. 작업 소요 시간 측정  (0) 2007.02.21
1. 작업 리스트 관리(CRUD)  (0) 2007.02.20
TDD 연습용 프로그램  (4) 2007.02.20
CallBack 사용해 보기  (2) 2007.02.20
top


2. 작업 소요 시간 측정

모하니?/Coding : 2007.02.21 12:59


2.1. "작업 시작" 버튼을 클릭 한 시간 부터 "작업 완료" 버튼을 클릭 했을 때 까지의 시간을 잽니다.

이걸 좀 더 잘개 쪼개야 겠습니다. 시간은 초 단위가 좋을 것 같습니다.
2.1.1. "작업 시작" 버튼을 클릭하면 시간을 재기.

    @Test public void getStartTime(){
        int startTime = new Work("work1").start();
        assertFalse(startTime == 0);
    }

다음 작업을 하기 전에 startTime을 Work 클래스에 저장해 둬야 할 것 같아졌습니다. 그래야 나중에 작업이 완료 됐을 때 계산할 수 있으니깐요..

2.1.2. startTime 저장하기

    @Test public void getStartTime(){
        Work work = new Work("work1");
        work.start();
        assertNotNull(work.getStartTime());
    }

2.1.2. "작업 완료" 버튼을 클릭하면 현재 시간에서 작업 시작 시간을 빼서 소요 시간 구하기.
   
    @Test public void getWorkedTime(){
        Work work = new Work("work1");
        work.start();
        long workedTimeInMilli = work.end();
        assertEquals(work.getEndTime() - work.getStartTime(), workedTimeInMilli);
        assertEquals(workedTimeInMilli * 1000, work.getWorkedTime());
    }

2.2. "일시 정지" 버튼을 클릭하면 시간 재는 걸 잠시 중단 합니다. "작업 시작"버튼을 클릭하면 다시 이어서 시간을 잽니다.

이 작업을 구현하려니까 시간을 가지고 테스트 하는게 엄청 불편하게 느껴집니다. start와 end를 할 때 로그 메시지를 기록 하도록 하고 그 메시지를 확인 하도록 테스트를 변경합니다.

    @Test public void pausedTime(){
        Work work = new Work("work1");
        work.start();
        work.pause();
        work.reStart();
        long workedTime = work.end();
        assertEquals("작업 시작 -> 일시 중지 -> 다시 시작 -> 작업 종료", work.getLog().toString());
        assertEquals((work.pause() - work.start()) + (work.getEndTime() - work.reStart()), workedTime);
    }

2.3. "작업 완료" 버튼을 클릭하면 해당 작업한 시간이 기록됩니다. 이건 이미 위에 것들을 구현하다보니 구현이 되버렸군요.

현재 까지 작업 상황을 보면

- 시작 -> 종료 :: 2.1.
- 시작 -> 일시 정지 -> 다시 시작 -> 종료 :: 2.2.
- 시작 -> 일시 정지 -> 종료
- 시작 -> 일시 정지 -> 다시 시작 -> 일시 정지 -> 다시 시작 -> 종료

미쳐 아래 두 가지 경우 수를 생각 못했었군요.

그리고 작업한 시간을 출력해보니 전부 0으로 나오기 때문에 수치가 정확한지 가늠할 수 있는 뭔가 더 좋은 테스트 방안을 모색해 봐야겠습니다.

테스트를 위해서 log를 만들었지만 이 것이 차후 프로그램에서 필요한가를 생각해보니.. 시간 테스트를 위한 대안이였는데 전혀 적절한 대안이 되진 못한 것이 절 초라하게 만드는군요. ㅠ.ㅠ
신고

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

TDD Helper 만들었습니다.  (4) 2007.02.23
5. version 0.9  (0) 2007.02.22
4. 화면과 모델 통합에서의 갈등  (0) 2007.02.22
3. 작업 분류 2  (0) 2007.02.22
3. 작업 분류  (0) 2007.02.22
TDD Helper 화면 Version 0.5  (4) 2007.02.21
2. 작업 소요 시간 측정  (0) 2007.02.21
1. 작업 리스트 관리(CRUD)  (0) 2007.02.20
TDD 연습용 프로그램  (4) 2007.02.20
CallBack 사용해 보기  (2) 2007.02.20
spring 코딩 할 때  (0) 2006.11.06
top


1. 작업 리스트 관리(CRUD)

모하니?/Coding : 2007.02.20 18:46


1.1. 새로운 작업 추가 - 완료

    @Test public void addNewWork(){
        WorkList workList = new WorkList();
        String workName = "새로운 작업 추가하기";
        workList.add(new Work(workName));
        assertEquals(1, workList.size());
        assertEquals(workName, workList.get(1).name);
    }

1.2. 선택한 작업 삭제 하려다 보니 Work 마다 번호가 매겨져 있어야 할 것 같아졌습니다. 새로운 작업을 추가 했을 때 그 작업의 번호를 리턴 받도록 합니다. 작업 번호는 1번 부터 시작합니다. 새로 추가되는 작업의 번호는 리스트 요소중에 제일 큰 번호를 갖게 됩니다.

1.2 작업 번호 받기 - 완료

     @Test public void addNewWork(){
        WorkList workList = new WorkList();
        String workName = "새로운 작업 추가하기";
        int workNumber = workList.add(new Work(workName));
        assertEquals(1, workNumber);
        assertEquals(1, workList.size());
        assertEquals(workName, workList.get(workNumber).name);
    }

1.3. 선택한 작업 삭제 - 완료

    @Test public void deleteWork(){
        WorkList workList = new WorkList();
        int workNumber1 = workList.add(new Work("work1"));
        int workNumber2 = workList.add(new Work("work2"));
        assertEquals(2, workList.size());
        workList.delete(workNumber1);
        assertEquals(1, workList.size());
        assertEquals("work2", workList.get(workNumber2 - 1).name);
    }

리스트에서 작업이 삭제 되면 작업의 번호도 그에 따라 변경 됩니다. 예를 들어 2개의 작업에서 첫번째(1번) 작업이 삭제 되면 두번째(2번) 작업의 번호가 첫번째 작업의 번호로 바뀌게 됩니다.

두 개의 메소드에 중복이 발생했습니다. workList 객체를 @Before 가 붙은 메소드로 올리겠습니다.

    WorkList workList;

    @Before public void setUp(){
        workList = new WorkList();
    }

1.4. 파일로 저장하기 - 완료

    @Test public void saveList() throws IOException{
        workList.add(new Work("work1"));
        String fileName = "ToDoList.txt";
        workList.save(fileName);
        StringBuffer buffer = new StringBuffer();
        String line;
        BufferedReader reader = new BufferedReader(new FileReader(fileName));
        while ((line = reader.readLine()) != null) {
            buffer.append(String.format(line + "\n"));
        }
        reader.close();
        assertEquals("work1\n", buffer.toString());
    }

1.5. 파일에서 읽어오기 - 완료

    @Test public void loadList(){
        int workNumber = workList.add(new Work("work1"));
        String fileName = "ToDoList.txt";
        workList.save(fileName);
        workList = new WorkList();
        workList.load(fileName);
        assertEquals(1, workList.size());
        assertEquals("work1", workList.get(workNumber).name);
    }

1.6. 전체 리스트 보여주기 - 완료

    @Test public void printList(){
        workList.add(new Work("work1"));
        workList.add(new Work("work2"));
        workList.add(new Work("work3"));
        assertEquals("work1\nwork2\nwork3\n", workList.toString());
    }

그리고 여태까지 Work class에 name 변수가 public으로 되어 있었는데 private으로 바꿔주고 에러나는 부분은 getter와 setter로 포장해 줍니다.




신고

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

TDD Helper 만들었습니다.  (4) 2007.02.23
5. version 0.9  (0) 2007.02.22
4. 화면과 모델 통합에서의 갈등  (0) 2007.02.22
3. 작업 분류 2  (0) 2007.02.22
3. 작업 분류  (0) 2007.02.22
TDD Helper 화면 Version 0.5  (4) 2007.02.21
2. 작업 소요 시간 측정  (0) 2007.02.21
1. 작업 리스트 관리(CRUD)  (0) 2007.02.20
TDD 연습용 프로그램  (4) 2007.02.20
CallBack 사용해 보기  (2) 2007.02.20
spring 코딩 할 때  (0) 2006.11.06
top


TDD 연습용 프로그램

모하니?/Coding : 2007.02.20 17:40


"TDD 도우미"

TDD를 사용하여 개발 또는 공부를 할 때 도움이 될 프로그램을 생각해 봤습니다. 조그만 목록 관리와 작업 당 소요 시간 측정은 작업의 난이도가 어땠는지.. 혹은 작업의 단위를 너무 크게 한 것은 아닌지 생각해볼 수 있지 않을까 해서 넣어봤습니다.

1. 작업 리스트 관리(CRUD)
1.1. 새로운 작업 추가 - 완료
1.2. 선택한 작업 삭제
1.3. 작업 리스트 파일로 저장
1.4. 작업 리스트 읽어 오기
1.5. 전체 리스트 보여주기

2. 작업 소요 시간 측정
2.1. "작업 시작" 버튼을 클릭 한 시간 부터 "작업 완료" 버튼을 클릭 했을 때 까지의 시간을 잽니다.
2.2. "일시 정지" 버튼을 클릭하면 시간 재는 걸 잠시 중단 합니다. "작업 시작"버튼을 클릭하면 다시 이어서 시간을 잽니다.
2.3. "작업 완료" 버튼을 클릭하면 해당 작업한 시간이 기록됩니다.

3. 작업 자동 분류(완료된 작업, 진행 중인 작업, 해야할 작업)
3.1. 새로 추가된 작업은 "해야할 작업" 목록에 추가 됩니다.
3.2. "작업 시작" 버튼을 클릭한 작업은 "진행 중인 작업" 목록에 추가 됩니다.
3.3. "작업 완료" 버튼을 클릭한 작업은 "완료된 작업" 목록에 추가 됩니다.
3.4. 선택한 분류의 작업만 보여주기.


신고

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

TDD Helper 만들었습니다.  (4) 2007.02.23
5. version 0.9  (0) 2007.02.22
4. 화면과 모델 통합에서의 갈등  (0) 2007.02.22
3. 작업 분류 2  (0) 2007.02.22
3. 작업 분류  (0) 2007.02.22
TDD Helper 화면 Version 0.5  (4) 2007.02.21
2. 작업 소요 시간 측정  (0) 2007.02.21
1. 작업 리스트 관리(CRUD)  (0) 2007.02.20
TDD 연습용 프로그램  (4) 2007.02.20
CallBack 사용해 보기  (2) 2007.02.20
spring 코딩 할 때  (0) 2006.11.06
top


테스트 주도 개발

모하니?/Reading : 2007.02.17 14:46


사용자 삽입 이미지

읽으면서 코딩도 따라 해보고 재밌습니다. AJN에서 이 책으로 스터디를 하면서 위키에 정리 중입니다. 저는 svn에 한 챕터 마다 커밋을 하고 있습니다.

프로젝트 구조는 아래 처럼 매우 단순합니다.
사용자 삽입 이미지
svn 주소는 http://agilejava.googlecode.com/svn/trunk/ 이겁니다.

목차 별로 구현한 코드 중심으로 요약하겠습니다.

1. 다중 통화를 지원하는 Money 객체 - Dollar 만들기, times() 스텁 구현, p48 의존성과 중복
2. 타락한 객체 - times() 구현 마무리.
3. 모두를 위한 평등 - equlas() 구현하기.
4. 프라이버시 - amount를 보이지 않게 하기.
5. 솔직히 말하면 - Franc 만들기.
6. 돌아온 '모두를 위한 평등' - equals()를 Money로 올리기.
7. 사과와 오렌지 - Franc과 Dollar 비교하기.
8. 객체 만들기 - Dollar와 Franc를 new가 아닌 Money의 Factory 메소드로 만들 도록 변경.
9. 우리가 사는 시간 - Dollar와 Franc을 없애기 위해서 둘을 구분한 currency를 만들어 냄.
10. 흥미로운 시간 - times()를 Money로 올리기, p103 생략된 부분 있는듯..
11. 모든 악의 근원 - Franc과 Dollar 클래스 없애기. 필요 없어진 test 없애기. p110 잘못 된 부분 있는듯...
12. 드디어, 더하기 - Expression이라는 메타포 도입하여 plus()구현 중.
13. 진짜로 만들기 - plus()는 Expression 반환하고, Expression에 Money reduce(String to) 인터페이스 만들고, Sum과 Money에서 reduce() 구현함. p126 복잡함.
14. 바꾸기 - 정말 많이 바꿨다. rate(Bank, String), Pair 클래스, Bank에서 rate 책임지기.
15. 서로 다른 통화 바꾸기 - Expression에 plus() 추가, Sum에 있는 reduce()가 재귀호출 인게 이제야 보이는 나..
16. 드디어, 추상화 - Sum에 있는 plus() 구현 마무리, Expression에 times() 추가. 맨 마지막 테스트는 실패한 것인가??




신고

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

뉴욕의 프로그래머  (4) 2007.09.21
프로젝트의 시작은 '사용자 스토리'와 함께  (0) 2007.09.14
조엘 온 소프트웨어 시즌 2  (2) 2007.09.07
나무  (0) 2007.03.19
  (2) 2007.03.16
테스트 주도 개발  (0) 2007.02.17
The Goal  (6) 2007.02.15
적의 화장법  (2) 2007.01.17
폰더 씨의 위대한 하루  (4) 2007.01.16
설득의 심리학  (0) 2006.12.09
배고픔의 자서전  (0) 2006.11.29
top


9. Tag만들기



CustomTag를 공부 하다가 느낀 점은.. 'servlet으로 html 코드 뿌리는 것 같다.', '복잡하네..언제는 뭐 상속 받고 언제는 뭐 상속 받고...어떤거 구현해줘야 하고 뭐를 리턴 해줘야 하고,,,,'

그런데 TagFile을 사용하면 매우 단순하게 태그를 만들 수 있었습니다.

1. Tag로 만들 부분 물색.
2. Tag를 만들었다는 가정하에 Tag를 사용해서 변경.
3. Tag 파일 작성.
4. 확인.

2번 다음에 확인하고 5. 수정하고 싶은 부분 수정하고 다시 확인 요거만 추가 하면 TDD랑 많이 닮은 것 같네요.

1. Tag로 만들 부분 물색.
사용자 삽입 이미지
2. 먼저 위에 있는 빨간색 부분을 다음과 같이 Tag가 있다고 생각하고 바꿔줍니다.
사용자 삽입 이미지
3. 그리고 WEB-INF/tags/os 안에 새로운 태그를 정의해 줍니다. page라는 태그명을 사용했기 때문에 page.tag라는 파일을 작성합니다.
<%@ attribute name="title" required="true" %>
<html>
    <head>
        <title>${title}</title>
        <link rel="stylesheet" type="text/css" href="/css/style.css" />
    </head>
    <body>
        <jsp:doBody />
    </body>
</html>

JSP 페이지 지시자를 사용해서 태그의 속성을 나타내고 EL태그로 원하는 위치에 사용하면 됩니다. 몸체가 있는 태그라면 하늘색 부분 처럼 몸체가 들어갈 부분에 <jsp:doBody /> 태그를 사용해 주면 됩니다. 이밖에도.. JSTL의 <c:set /> <c:if /> 이런 태그들을 이용해서 속성값에 기본 값을 setting하고 조건에 따라 값을 바꿀 수 있습니다.

4. 확인하기.
사용자 삽입 이미지
이런 에러가 납니다. title이 필수 속성이라고 했는데 왜 안넣어줬느냐!! 라는 것인데.. 전 넣는다고 넣어는데?? 라고 생각하고 소스를 다시 보니.. titile 라고 오타를 쳤네요. ㅠ.ㅠ

사용자 삽입 이미지
OK! 수정하고 새로고침을 해보니깐 제대로 나왔습니다.
신고

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

9. Tag만들기  (0) 2007.02.12
8.4. 기능 구현  (0) 2007.01.25
8.3. Criteria 공부하기  (0) 2007.01.25
8.2. HQL 공부하기  (0) 2007.01.24
8.2.4. HQL 공부하기 - inner join  (0) 2007.01.24
8.2.3. HQL 공부하기 - order by절  (0) 2007.01.24
8.2.2. HQL 공부하기 - where절  (0) 2007.01.24
8.2.1. HQL 공부하기 - select절  (0) 2007.01.24
8.1. DbUnit 사용하기  (0) 2007.01.24
8. DAO 기능 구현하기  (4) 2007.01.23
7.3. 새로운 타입으로 맵핑하기.  (0) 2007.01.22
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


Self Testing Code design



원문 : http://www.martinfowler.com/bliki/SelfTestingCode.html

‘자체 테스트 코드’는 리팩토링에서 기능적인 소프트웨어와 결합되어 전체적으로 자동화된 테스트를 언급하기 위해 사용했던 단어다. 이 단어를 이야기 할 때는 제일 먼저 XUnit 군의 테스트 프레임워크들이 생각난다.

테스트 주도 개발(TDD)
은 자체 테스트 코드를 작성할 때 선호하는 방법이지만 이 방법밖에 없는 것은 아니다. 기능 코드를 개발하기 전에 테스트를 작성하는 것은 그렇지 않은 경우에 비해 많은 장점을 얻을 수 있다. 자체 테스트 코드에서 가장 중요한 것은 테스트 코드를 가지고 있다는 것이지 그것들을 어떻게 만들어 냈는가가 아니다.
신고
top


TestDrivenDevelopment design



TestDrivenDevelopment design

원문 : http://www.martinfowler.com/bliki/TestDrivenDevelopment.html

테스트 주고 개발(TDD)는 테스트를 통해서 개발을 이끌어 내는 설계 기술이다. 본질적으로 다음 세 개의 단순한 단계를 반복하는 것이다.

•    다음으로 추가하고 싶은 기능을 위한 테스트를 작성한다.
•    테스트가 통과 하도록 기능 코드를 작성하라.
•    전체적으로 구조화 되도록 새로운 코드와 기존의 코드를 재구성한다.

시스템의 기능을 구현할 때 한번에 하나씩 테스트 하면서 지속적으로 이 세 단계를 반복한다. XPE2가 선 테스트 프로그래밍(Test First Programming)이라고 부르는 대로 Test를 먼저 작성하는 것은 두 가지 주요 장점이 있다. 그 중 가장 명백한 것으로 테스트를 통과하는 기능 코드만을 작성할 수 있기 때문에 자체 테스트 코드를 얻을 수 있다. 두 번째로 테스트를 먼저 생각하게 되면 코드의 인터페이스를 먼저 생각하게 된다. 인터페이스 그리고 클래스를 어떻게 사용할 것인지에 초점을 맞추는 것은 구현으로부터 인터페이스를 분리해 내는데 도움이 된다.

TDD의 효율성을 올리는 가장 흔한 방법으로 세 번째 단계를 생략하는 것이라고 들었다. 깔끔하게 유지하기 위해 코드를 재구성 하는 것은 TDD 프로세스의 핵심이다. 그렇게 하지 않으면 난잡한 코드 조각 덩어리들의 모임을 보게 될 것이다. (그래도 최소한 테스트는 가지고 있을 것이기 때문에 대부분의 설계 실패에 대한 고통보다는 덜 할 것이다.)

신고
top







티스토리 툴바