요즘 테스트 코드를 적을때는 항상 given - when - then 방식으로 많이 작성한다.
given 에서는 Test 로 작성될 SUT 에 대한 의존성 주입 및 들어갈 값들에 대한 정의를 내린다.
when 에서는 Test 할 로직을 실행하고
then 에서 결과값을 검증한다.
아래도 위와 같이 given - when - then 으로 작성된 테스트 코드이다.
class CustomerTest {
@Test
void purchaseSucceedsWhenEnoughInventory() {
//given
var store = new Store();
store.addInventory(Product.SHAMPOO, 10);
var customer = new Customer();
//when
boolean purchaseSucceedResult = customer.purchase(store, Product.SHAMPOO, 5);
store.removeInventory(purchaseSucceedResult, Product.SHAMPOO, 5);
//then
assertTrue(purchaseSucceedResult);
assertEquals(5, store.getProductCount(Product.SHAMPOO));
}
}
위의 테스트 코드를 보자. 문제가 있어 보이는가?
만약 없어보인다면.. 직접 구현해보길 바란다.
일단 구현에 앞서 말하자면 when 에서 두번이상의 API 콜이 호출된다면 캡슐화에 문제가 있는 것 일수있다.
또는 협력관계로 이루어져야 하는 과정이 독립적으로 시행되고 있을 수도 있다.
우리는 TDD 를 하는 것이라고 치고 위의 코드를 구현해보자.
일단 간단하게 코드를 분석해보면 Customer 는 물건을 구입하는데
Store(상점), Product(제품), Count(구매할 수량) 을 받는 것으로 확인된다.
그리고 해당 물건을 정상적으로 구입했다면 Flag 값으로 True 를 리턴하고, 구매에 실패했다면 False 를 리턴한다.
한번 코드를 진짜 간단하게만 작성해보자.
public class Customer {
private Store store;
public boolean purchase(Store store, Product product, int count) {
return true;
}
}
일단 정말 간단하게 통과만 되도록 작성했다.
일단 Store 를 그럼 구현해야 하니까 Store 를 어떻게 구현할지 생각해보자.
Store 는 일단 Inventory 를 가지고 있고, 인벤토리는 Product - 수량 관계를 맺고 있는 것 같다.
일단 Key-Value 구조이므로 Map Structure 를 써서 만들어보자.
/**
* 1급 컬렉션 객체
*/
public class Store {
private final Map<Product, Integer> inventory = new HashMap<>();
public int getProductCount(Product product) {
return this.inventory.get(product);
}
public void addInventory(Product product, int count) {
inventory.put(product, count);
}
public boolean removeInventory(boolean isSucceedPurchase, Product product, int count) {
if (!isSucceedPurchase) {
throw new RuntimeException("결제가 정상적으로 성공되야 차감이 진행됩니다.");
}
if (!inventory.containsKey(product)) {
throw new IllegalArgumentException("해당 제품은 존재하지 않습니다.");
}
int stock = inventory.get(product);
int remainStock = stock - count;
if (remainStock < 0) {
throw new IllegalArgumentException("현재 재고보다 많은 수량입니다.");
}
inventory.put(product, remainStock);
return true;
}
}
위와 같이 일단은 구현됬다.
Customer 의 구현을 위해서는 TestCode 를 다시 한번 더 볼 필요가 있다.
그렇다면 다시 TestCode 로 가보자.
//when
boolean purchaseSucceedResult = customer.purchase(store, Product.SHAMPOO, 5);
store.removeInventory(purchaseSucceedResult, Product.SHAMPOO, 5);
유저가 구입하고 구입에 성공하면 -> store 의 재고를 감소시킨다.
일단 하나 알 수 있는건 그렇다면 유저의 purchase 메소드는 사실 구입가능여부와 다를게 없다는 것이다.
일단 저 메소드가 동작할 수 있도록 Context 를 파악해 코드를 작성해보자.
public class Customer {
public boolean purchase(Store store, Product product, int count) {
boolean isBuyed = store.hasProductByQuantity(product, count);
//TODO 샀을때 하는 행위들
return isBuyed;
}
}
위 처럼 생각보다 예상치 못한 코드가 등장한다.
store.removeInventory(purchaseSucceedResult, Product.SHAMPOO, 5);
이건 왜 그럴까? 코드를 적다보면 알았을 텐데 purchase 메소드 안에서
위의 메소드가 캡슐화되어 이뤄져야 했음을 알수 있을 것이다.
그럼 캡슐화 해보자.
일단 캡슐화 될경우 성공 여부는 알 필요가 없다.
그러니 Store 의 메소드는 아래와 같이 변할 것이다.
public boolean removeInventory(Product product, int count) {
if (!inventory.containsKey(product)) {
throw new IllegalArgumentException("해당 제품은 존재하지 않습니다.");
}
int stock = inventory.get(product);
int remainStock = stock - count;
if (remainStock < 0) {
throw new IllegalArgumentException("현재 재고보다 많은 수량입니다.");
}
inventory.put(product, remainStock);
return true;
}
일단 유저의 결제 성공 여부를 Parameter 로 받던 것이 사라졌다. (Flag 제거)
그렇다면 이제 이 메소드를 Purchase 안으로 넣어보자.
public class Customer {
public boolean purchase(Store store, Product product, int count) {
//TODO 샀을때 하는 행위들
return store.removeInventory(product, count);
}
}
위와 같이 캡슐화 되어서 작성됬다.
또한 고객은 구매를 할때 Store 에 협력을 요청하고 있다.
이제 테스트코드를 한번보자.
class CustomerTest {
@Test
void purchaseSucceedsWhenEnoughInventory() {
//given
var store = new Store();
store.addInventory(Product.SHAMPOO, 10);
var customer = new Customer();
//when
boolean purchaseSucceedResult = customer.purchase(store, Product.SHAMPOO, 5);
//then
assertTrue(purchaseSucceedResult);
assertEquals(5, store.getProductCount(Product.SHAMPOO));
}
}
When 에서 이제는 하나의 API 콜만 호출하고 있다.
명심: when 에서 API Call 이 두번 일어난다면 코드를 의심해라!
'Test' 카테고리의 다른 글
테스트 더블의 종류 (0) | 2021.11.22 |
---|---|
테스트 더블 (0) | 2021.11.19 |
어떤 것이 좋은 테스트 코드인가? (0) | 2021.11.19 |
테스트를 어떻게 해야하는가? (0) | 2021.10.27 |