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;
}
}
실습까지 해보았습니다.
개발을 먼저 하는 습관이 남이 있어 다소 멈칫하는 경우가 있습니다.
테스트를 먼저 작성하면 실패를 무서워하지 않아도 되어 마음에 안정이 옵니다.
테스트 주도 개발을 습관이 되면 더 즐거운 개발이 될거 같습니다.