Rlog

[TEST] When 에서 하나 이상의 메소드 실행 피하기 본문

Test

[TEST] When 에서 하나 이상의 메소드 실행 피하기

dev_roach 2021. 12. 17. 00:27
728x90

요즘 테스트 코드를 적을때는 항상 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