Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

테스트 주도 개발(TDD) 실전 프로그래밍 세미나

작년에 켄트 벡의 TDDBE를 감명깊게 보았지만 막상 해보지는 못하다가 얼마전에 출간한 테스트 주도 개발 : 고품질 쾌속개발을 위한 TDD 실천법과 도구를 읽고 다시 자극받아 올해는 TDD를 좀 제대로 수련해보자고 요즘 생각하고 있던터에... 주말용 프로젝트에 TDD해보려다가 삽질 좀 해주고 한빛미디어에서 위 책의 저자이신 doortts님세미나를 갔다왔습니다.

세미나는 크게 TDD와 Pair Programming을 나눠서 설명해 주셨습니다.
멀지는 않았지만 퇴근이 7시인데 세미나가 7시라서 사실 반차신공을 발휘하려고 하였으나 요즘 그럴만한 상황이 아니었기에 별수 없이 정상근무를 하고 열심히 갔더니 TDD에 대한 설명은 앞에 40분 정도는 놓쳐버리고 말았습니다.(곧 발표자료도 올려주신다고 하셨고 책에 다 설명해 주신 내용이리라 위안해 봅니다.)

TDD 설명뒤에 간단하게 실습을 하였습니다. 요구사항은 다음과 같았습니다.

- 배열로 받은 수중에 0에 가장 가까운 수를 보여준다.
- 2와 2가 있으면 양수를 가까운 수로 정한다.

이건 TDD로 작성하지 않고 메서드를 바로 작성하게 시키셨습니다. 다 작성한후 제공받은 JUnit 테스트소스를 돌리게 하셨는데 6개 중 2개만 pass했습니다. ㅠㅠ 간단히 수정하긴 했지만 약간 구현하면서 약간 아리까리했던 음수처리 부분에서 문제가 역시나 있었군요.



Pair Programming
두번째 시간은 짝프로그래밍에 대한 부분이었습니다. 짝 프로그래밍에 대해서 알야할 점들을 설명해 주신뒤 50분 정도의 실습에 들어갔습니다만 결론부터 말하자면 저의 짝프로그래밍은 실패였습니다.(처음 해봤는데 안좋은 기억을 가지게 되었군요. ㅠㅠ) 모르시는 분과 했는데 처음해본 터라 To-DO정의부터 구현할 To-Do라기 보다는 Flow를 작성해버린 것이 커뮤니케이션이 어려웠던 가장 큰 이유였던것 같습니다.

제가 네비게이터를 맞았는데 모르는 분이라서 그랬는지 커뮤니케이션도 부족했고 제가 할려는건 제대로 전달되지 않았고 그분이 코딩하려는 것은 뭘하시려고 하는건지 제가 잘 이해를 못하다 보니 처음 네이게이터를 해보면서 급당황하기 시작했고 마이크로 컨트롤하지 말고 인내심을 가지라는 조언에 보다가 얘기하고 하다보니 결국 구현을 못했습니다. 원래는 TDD로 작성했어야 했는데 그냥 구현부터 들어가게 되었음에도 구현은 계속 산으로만 갔습니다. ㅠㅠ

머 아쉽지만 다음에 또 연습해볼 좋은 기회가 있으리라 생각합니다. 앞부분을 못듣기는 했지만 인원수가 좀 적고 실습이 포함된 현실적인 내용들이 있어서인지 많은 도움이 되는 세미나였습니다. 개인적으로는 지난번 JCO세미나때 보다 더 좋았던 것 같습니다.



집에 와서 허니몬님이 하신 것을 보고 저도 연습삼아 혼자서 해봤습니다. 짝프로그래밍은 아니지만 TDD 연습은 될 수 있으니까요..

- 자판기 예제입니다. 거스름돈을 계산하는 부분을 계산합니다.
- 1000원을 넣고 650원짜리 음료수를 선택하면 300원 3개, 50원 1개로 거스름돈을 반환해 줍니다.
- 지폐는 하나만 넣는다고 가정합니다.


// VendingMachine.java
package kr.ne.outsider.tdd;

public class VendingMachine {
    int currentMoney = 0;
    int[] coinUnit = {500, 100, 50, 10};

    public void insertCoin(int money) {
        this.currentMoney = money;
    }

    public int displayMoney() {
        return this.currentMoney;
    }

    public void selectBerage(int priceOfBeverage) {
        this.currentMoney -= priceOfBeverage;
    }

    public String calculateCharge(int chargeCoin) {
        String resultCharge = "";
        
        int coinCount = this.calculateCoinCount(chargeCoin);;
        this.currentMoney -= (chargeCoin * coinCount);
        
        if (coinCount > 0) {
            resultCharge = Integer.toString(chargeCoin) + "*" + Integer.toString(coinCount) + " ";
        }
        return resultCharge;
    }
    
    private int calculateCoinCount(int chargeCoin) {
        int coinCount = this.currentMoney / chargeCoin;
        return coinCount;
    }

    public String returnCharge() {
        String resultCharge = "";
        
        for(int coin: coinUnit) {
            String temp = this.calculateCharge(coin);
            resultCharge += temp;
        }
        return resultCharge;
    }

}


// VendingMachineTest.java
package kr.ne.outsider.tdd;

import static org.junit.Assert.*;

import org.junit.Before;
import org.junit.Test;

public class VendingMachineTest {
    VendingMachine vending;
    
    @Before
    public void setUp() {
        vending = new VendingMachine();
    }
    
    @Test
    public void testDisplayMoney_isDefaultZero() {
        assertEquals(0, vending.displayMoney());
    }
    
    @Test
    public void testInsertCoin_Insert1000() {
        vending.insertCoin(1000);
        assertEquals(1000, vending.displayMoney());
    }
    
    @Test
    public void testSelectBeverage_Insert1000andSelect650() {
        vending.insertCoin(1000);
        vending.selectBerage(650);
        assertEquals(350, vending.displayMoney());
    }
    
    @Test
    public void testCaculateCharge_of500_When_money_is_1000(){
        vending.insertCoin(1000);
        assertEquals("500*2 ", vending.calculateCharge(500));
        assertEquals(0, vending.displayMoney());
    }
    
    @Test
    public void testCaculateCharge_of500_When_money_is_350(){
        vending.insertCoin(350);
        assertEquals("", vending.calculateCharge(500));
        assertEquals(350, vending.displayMoney());
    }
    
    @Test
    public void testCaculateCharge_of500_When_money_is_650(){
        vending.insertCoin(650);
        assertEquals("500*1 ", vending.calculateCharge(500));
        assertEquals(150, vending.displayMoney());
    }
    
    @Test
    public void testReturnCharge_Insert_1000_Select_650() {
        vending.insertCoin(1000);
        vending.selectBerage(650);
        assertEquals("100*3 50*1 ", vending.returnCharge());
    }
    
    @Test
    public void testReturnCharge_Insert_1000_Select_410() {
        vending.insertCoin(1000);
        vending.selectBerage(410);
        assertEquals("500*1 50*1 10*4 ", vending.returnCharge());
    }
}

리펙토링을 해야할게 좀 더 눈에 보이기는 하지만 일단 여기서 마무리 합니다. 하다보니 궁금중이 생기더군요....

  1. 테스트를 만들고 메서드를 만들고 테스트도 더 추가했는데 하고나서 보니 private로 되는게 맞는것 같아서 private로 수정을 하게되니 깨져버린 테스트들은 삭제를 해야하는 것인가 궁금했습니다.(private은 기본적으로는 테스트를 하지 않는다는 데 접근을 public으로 하고 method추출을 했어야 했는데 접근 자체를 잘못한건가 싶기도 하고요. fupfin님은 protected로 테스트한다고 들었는데 어느쪽이 좋을지는 연습을 더 해봐야 할것 같습니다.)

  2. fail과 pass가 주기적으로 반복되어야 하는데 이게 쉽지 않더군요. 처음 실패하는 테스트를 만들고 간단히 성공하는 테스트를 만든 후에는 (간단한 로직이라서 그런지) 그냥 성공상태에서 계속 로직을 작성하게 되더군요. 물론 잘못짰을 때는 failure가 뜨지만요. 배운대로라면 테스트와 로직구현이 반복되어야 하고(이상적으론 30초 주기로 바뀌는게 좋다더군요.) 실패와 성공이 반복되어야 하는데 아직은 감이 잘 안왔습니다. 테스트를 만든 후 성공으로 계속 작성하게 되거나 로직작성후 여러가지 경계조건에 대한 테스트들을 계속 만들게 되더군요.

수련을 더 해야할 것 같습니다. 사실 얼마전에도 주말용 프로젝트인 Javascript코딩에서 TDD를 해보려고 하였지만 상당히 많은 난관에 부딪히면서 결국 좌절하고 말았습니다. 이 부분도 한번 정리는 해보려고 합니다만 너무 무지한 부분이 있어서 좀더 고민해보고 기회가 되면 포스팅을 해야할 것 같습니다.
2010/08/28 23:56 2010/08/28 23:56