본문 바로가기

개발(합니다)/방법론

TDD 학습 및 실습 정리9(예제연습)

반응형

TDD 학습 및 실습 정리8에 이어 정리합니다.


자판기 만들기 예제이며 TDD 학습 및 실습의 마지막입니다.




자동 판매기 잔돈 계산 모듈

음료 자판기에 탑재 될 거스름돈 반환 모듈 개발 업무를 맡게 되었습니다.

- 최소 개수의 동전으로 잔돈을 돌려줍니다.
ex) 1000원 넣고 650원짜리 음료를 선택하면 잔돈은 100,100,100,50원으로 반환합니다.
- 지폐를 잔돈으로 반환하는 경우는 없다고 가정합니다.


TDD 방식으로 개발하면서 어느 정도 걸리는지 측정해봅니다.

초중급 개발자는 약 50분정도 소요 된다고 합니다.


책 내용이 먼저 나오고 글쓴이의 내용은 아래에 있습니다.


개발 시작하기

바로 개발에 들어가기 보다는 시나리오를 작성합니다.
1. 업무 시나리오
돈을 넣습니다.
-> 투입한 금액이 표시됩니다.
-> 투입한 금액 내에서 선택 가능한 음료가 있다면 해당 버튼에 불이 들어옵니다.
-> 음료를 선택합니다.
-> 음료가 나옵니다.
-> 투입 금액 표시 화면에는 선택한 음료 가격만큼 제외된 가격이 표시됩니다.
-> 만일 표시된 남은 금액이 다른 음료를 선택 할 수 있는 금액 이하이면 바로 잔돈으로 반환합니다.
-> 다른 음료를 선택 할 수 있는 금액이 남아 있다면 최초 동전을 넣었을 떄와 동일하게 동작합니다.
-> 반환 버튼을 눌렀을 경우 상황에 관계 없이 표시되어 있는 금액을 최소 잔돈으로 반환합니다.

2. 업무 시나리오 개선하기
사람이 하는 행동을 제외합니다.
돈을 넣습니다. (사람)
-> 투입한 금액이 표시됩니다.
-> 투입한 금액 내에서 선택 가능한 음료가 있다면 해당 버튼에 불이 들어옵니다.
-> 음료를 선택합니다. (사람)
-> 음료가 나옵니다.
-> 투입 금액 표시 화면에는 선택한 음료 가격만큼 제외된 가격이 표시됩니다.
-> 만일 표시된 남은 금액이 다른 음료를 선택 할 수 있는 금액 이하이면 바로 잔돈으로 반환합니다.
-> 다른 음료를 선택 할 수 있는 금액이 남아 있다면 최초 동전을 넣었을 떄와 동일하게 동작합니다.
-> 반환 버튼을 눌렀을 경우  (사람)
상황에 관계 없이 표시되어 있는 금액을 최소 잔돈으로 반환합니다.


3. 업무 시나리오 개선하기

하드웨어와 소프트웨어를 분리합니다.

돈을 넣습니다. (사람)
-> 투입한 금액이 표시됩니다. (하드웨어+소프트웨어)
-> 투입한 금액 내에서 선택 가능한 음료가 있다면 해당 버튼에 불이 들어옵니다. (하드웨어)
-> 음료를 선택합니다. (사람)
-> 음료가 나옵니다. (하드웨어+소프트웨어)
-> 투입 금액 표시 화면에는 선택한 음료 가격만큼 제외된 가격이 표시됩니다. (하드웨어+소프트웨어)
-> 만일 표시된 남은 금액이 다른 음료를 선택 할 수 있는 금액 이하이면 바로 잔돈으로 반환합니다. (하드웨어+소프트웨어)
-> 다른 음료를 선택 할 수 있는 금액이 남아 있다면 최초 동전을 넣었을 떄와 동일하게 동작합니다. (하드웨어+소프트웨어)
-> 반환 버튼을 눌렀을 경우  (사람)
상황에 관계 없이 표시되어 있는 금액을 최소 잔돈으로 반환합니다.

4. 업무 시나리오 개선하기

개발 범위를 한정하기 위해 시나리오를 정리합니다.


- 투입한 금액이 표시됩니다. -> 투입한 금액을 알 수 있습니다. -> 현재 잔액을 알 수 있습니다.

- 투입 금액 표시화면에는 선택한 음료 가격만큼 제외 된 가격이 표시 됩니다. -> 현재 잔액을 알 수 있습니다.

- 만일 표시 된 남은 금액이 다른 음료를 선택 할 수 있는 금액 이하이면 바로 잔돈으로 반환합니다. -> 잔액이 최소 음료 가격이하인지 여부를 확인합니다.

- 다른 음료를 선택 할 수 있는 금액이 남이 있다면 최초 동전을 넣었을 때와 동일하게 동작합니다

-표시 되어 있는 금액을 최소 잔돈으로 반환합니다.


5. 업무 시나리오 개선으로 확인되는 기능

- 음료수의 가격 확인

- 잔액 확인

- 잔돈 반환


6. 업무 시나리오를 작성해야 하는 이유

요구 사항을 확인하고 개발에 들어가게 되면 행위의 주체의 구분이 모호하게 섞이고 생략되는 부분이 아래와 같이 발생합니다.

시나리오를 작성함을써 구현 볌위를 확실히 하는 과정이 필요합니다.


동전을 투입합니다. -> 음료를 화면에 출렵합니다. -> 음료를 선택합니다. -> 남은 돈을 반환합니다.


테스트 케이스 작성

음료수 선택까지 작성한 테스트를 위한 테스트 케이스

VendingMachingTest.java
package smal_change2;

import static org.junit.Assert.assertEquals;

import org.junit.Test;

public class VendingMachingTest {
// Test를 위한 Test 케이스형태입니다.
//  음료수 선택까지 구현 되어 있는 점에서 리소스 낭비이며 목적이어쓰던 잔돈 모듈에서 벗어납니다.
    @Test // 잔액 확인
    public void testGetChangeAmount() throws Exception{
        VendingMachine machine = new VendingMachine();
        machine.putCoin(100);
        assertEquals("투입 금액 100원", 100, machine.getChangeAmount());
        
        machine.putCoin(500);
        assertEquals("추가 투입 금액 500원", 600, machine.getChangeAmount());
    }
    
    @Test // 거스름돈 50원
    public void testReturnChangeCoinSet_oneCoin_50() throws Exception{
        VendingMachine machine = new VendingMachine();
        machine.putCoin(100);
        machine.putCoin(100);
        machine.putCoin(500);
        
        machine.selectDrink(new Drink("Cola",650));
        
        CoinSet expectedConinSet = new CoinSet();
        assertEquals("700원 투입 후 650원 음료 선택", expectedConinSet,
machine.getChangeCoinSet());
    }
    
    @Test // 거스름돈 180원
    public void testReturnChangeCoinSet_coins_180() throws Exception{
        VendingMachine machine = new VendingMachine();
        machine.putCoin(100);
        machine.putCoin(100);
        machine.putCoin(500);
        machine.selectDrink(new Drink("Soda", 520));
        
        CoinSet expectedCoinSet = new CoinSet();
        expectedCoinSet.add(100);
        expectedCoinSet.add(50);
        expectedCoinSet.add(10);
        expectedCoinSet.add(10);
        expectedCoinSet.add(10);
        
        assertEquals("700원 투입 후 520원 음료 선택", expectedCoinSet,
machine.getChangeCoinSet());
    }
}


VendingMachine.java

package smal_change2;

public class VendingMachine {
    private int changeAmount;
    
    public void putCoin(int coin) {
        this.changeAmount += coin;
    }
    public int getChangeAmount() {
        return this.changeAmount;
    }
    public void selectDrink(Drink drink) {
        this.changeAmount -= drink.getPrice();
    }
    
    public CoinSet getChangeCoinSet() {
        CoinSet coin = new CoinSet();
        
        while(changeAmount >= 500) {
            changeAmount -= 500;
            coin.add(500);
        }
        while(changeAmount >= 100) {
            changeAmount -= 100;
            coin.add(100);
        }
        while(changeAmount >= 50) {
            changeAmount -= 50;
            coin.add(50);
        }
        while(changeAmount >= 10) {
            changeAmount -= 10;
            coin.add(10);
        }
        
        return coin;
    }

}


Drink.java

package smal_change2;

public class Drink {
    private String name;
    private int price;
    
    public Drink() {
    }
    public Drink(String name, int price) {
        this.name = name;
        this.price = price;
        
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }
    
}


CoinSet.java

package smal_change2;

import java.util.ArrayList;
import java.util.List;

public class CoinSet {
    private List<Integer> coinSets = new ArrayList<Integer>();

    public void add(int coin) {
        this.coinSets.add(coin);
        
    }
    
    @Override
    public boolean equals(Object obj) {
        if(!(obj instanceof CoinSet)) {
            return false;
        }
        return this.toString().equals(obj.toString());
    }
    
    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        for(Integer coin : this.coinSets) {
            builder.append(coin);
        }
        return builder.toString();
    }
    
}


TDD에 집중한 테스트 케이스

VendingMachingTest2.java

package smal_change2;

import static org.junit.Assert.assertEquals;

import org.junit.Test;

public class VendingMachingTest2 {
//      TDD형태의 잔돈 모듈입니다.
    @Test // 잔액 확인
    public void testGetChangeAmount() throws Exception {
        VendingMachine machine = new VendingMachine();
        machine.putCoin(100);
        assertEquals("투입 금액 100원", 100, machine.getChangeAmount());

        machine.putCoin(500);
        assertEquals("추가 투입 금액 500원", 600, machine.getChangeAmount());
    }

    @Test // 거스름돈 50원
    public void testReturnChangeCoinSet_oneCoin_50() throws Exception {
        ChangeModule module = new ChangeModule();
        CoinSet expectedConinSet = new CoinSet();
        expectedConinSet.add(50);
        System.out.println(module.getChangeCoinSet(50));
        assertEquals("700원 투입 후 650원 음료 선택", expectedConinSet,
module.getChangeCoinSet(50));
    }

    @Test // 거스름돈 180원
    public void testReturnChangeCoinSet_coins_180() throws Exception {
        ChangeModule module = new ChangeModule();
        CoinSet expectedCoinSet = new CoinSet();
        expectedCoinSet.add(100);
        expectedCoinSet.add(50);
        expectedCoinSet.add(10);
        expectedCoinSet.add(10);
        expectedCoinSet.add(10);

        assertEquals("700원 투입 후 520원 음료 선택", expectedCoinSet,
module.getChangeCoinSet(180));
    }

}


ChangeModule.java

package smal_change2;

public class ChangeModule {
    enum COIN{
        KRW500(500), KRW100(100), KRW50(50), KRW10(10);
        private int value;
        COIN(int value){
            this.value = value;
        }
    }

    public CoinSet getChangeCoinSet(int changeAmount) {
        CoinSet coinSet = new CoinSet();
        int remainChangeAmount = changeAmount;
        for(COIN coin : COIN.values()) {
            remainChangeAmount = addCoinsToCoinSet(remainChangeAmount, coinSet, coin.value);
        }
        return coinSet;
    }

    private int addCoinsToCoinSet(int remainChangeAmount, CoinSet coinSet, int coin) {
        while (remainChangeAmount >= coin) {
            remainChangeAmount -= coin;
            coinSet.add(coin);
        }
        return remainChangeAmount;
    }

}




글쓴이가 작성한 테스트 케이스

개발 시나리오 작성

1. 돈을 넣는다. -> 사람
2. 얼마의 돈이 들어왔는지 확인한다. -> 소프트웨어
3. 구매 할 수 있는 물건을 표시 한다 -> 소프트웨어
4. 물건을 선택한다. -> 사람
5. 물건의 개수를 차감하고 가격을 확인한다 -> 소프트웨어
6. 물건이 나온다. -> 기계
7. 거스름 돈을 계산한다. -> 소프트웨어
8. 거스름 돈 버튼을 누룬다. -> 사람
9. 거스름 돈을 내보낸다 -> 기계

소프트웨어가 할 일
얼마의 돈이 들어왔는지 확인한다. -> 구매 할 수 있는 물건을 표시한다. -> 물건의 개수를 차감한다. -> 물건의 가격을 확인한다. -> 거스름 돈을 계산한다.

잔돈 계산 모듈에서 할 일
얼마의 돈이 들어왔는지 확인한다. -> 손님이 선택한 물건의 가격을 확인한다 -> 거스름 돈을 계산한다.


개발 소스 

InputMoney.java
package small_change;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import org.junit.Test;

public class InputMoneyTest {

    @Test
    public void inputMoneyTest_손님이_구매하기위해_넣은금액을_확인한다() throws Exception {
        VendingMaching vm = new VendingMaching();
        int money = vm.getMoney();
        
        assertNotNull(money);
    }
    
    @Test
    public void inputMoneyTest_손님이_넣은_금액이_0이하일수없다() throws Exception{
        VendingMaching vm = new VendingMaching();
//      vm.setInputMoney(-1);
        vm.setInputMoney(1000);
        
        assertTrue("투입 금액은 0이하 일수 없다", vm.getMoney() > 0);
    }
    
    
    @Test
    public void changeMoneyTest_손님이_선택한_물품의_가격을확인한다() throws Exception{
        VendingMaching vm = new VendingMaching();
        vm.setSelectedProduct(650);
        
        assertEquals("손님이 선택한 물품 가격을 확인한다", 650, vm.getSelectedProduct());
    }
    
    @Test
    public void changeMoneyTest_손님이_선택한_물품가격을_투입금액에서_차감한다() throws Exception{
        VendingMaching vm = new VendingMaching();
        vm.setInputMoney(1000);
        vm.setSelectedProduct(650);
        vm.inputMoneyAndProductDeduct();
        
        assertEquals("손님이 선택한 물품 가격을 투입 금액에서 차감한다",350, vm.getMoney());
    }
    @Test
    public void changeMoneyTest_손님에게_거스름돈을_반환한다() throws Exception{
        VendingMaching vm = new VendingMaching();
        vm.setInputMoney(1000);
        vm.setSelectedProduct(650);
        vm.inputMoneyAndProductDeduct();
        
        ChangeMoney cm = new ChangeMoney();
        cm.setChangeMoney(vm.getMoney());
        
        int[] money = cm.getChangeMoney();
        assertEquals("손님에게 반환 할 500원의 개수",0, money[0]);
        assertEquals("손님에게 반환 할 100원의 개수",3, money[1]);
        assertEquals("손님에게 반환 할 50원의 개수",1, money[2]);
        assertEquals("손님에게 반환 할 10원의 개수",0, money[3]);
    }
}



ChangeMoney.java

package small_change;

public class ChangeMoney {

    private int[] changeMoneys;

    public ChangeMoney() {
//      0번째 : 500원  |   1번째 : 100원  |   2번째 : 50원   |   3번째 : 10원   
        changeMoneys = new int[4];
    }

    public void setChangeMoney(int money) {
        int temp = 0;
        changeMoneys[0] = money / 500;
        temp = money % 500;
        changeMoneys[1] = temp / 100;
        temp = temp % 100;
        changeMoneys[2] = temp / 50;
        temp = temp % 50;
        changeMoneys[3] = temp / 10;
    }
    public int[] getChangeMoney() {
        return this.changeMoneys;
    }
}


VendingMachin.java

package small_change;

public class VendingMachin {
    private int money;
    private int selectedProduct;

    public VendingMaching() {
    }
    
    public int getMoney() {
        return this.money;
    }

    public void setInputMoney(int money) {
        if(money < 0) {
            throw new IllegalArgumentException("투입 금액은 0보다 커야 합니다. : " + money);
        }
        this.money += money;
    }

    public void setSelectedProduct(int selectedProduct) {
        this.selectedProduct = selectedProduct;
    }

    public int getSelectedProduct() {
        return this.selectedProduct;
    }

    public int inputMoneyAndProductDeduct() {
        this.money -= this.selectedProduct;
        return this.money;
    }

}








실습까지 해보았습니다.

개발을 먼저 하는 습관이 남이 있어 다소 멈칫하는 경우가 있습니다.

테스트를 먼저 작성하면 실패를 무서워하지 않아도 되어 마음에 안정이 옵니다.


테스트 주도 개발을 습관이 되면 더 즐거운 개발이 될거 같습니다.


반응형