본문 바로가기

개발(합니다)/방법론

TDD 학습 및 실습 정리4(한계극복하기)

반응형

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



Mock 객체란


- 하나의 예시로 자동차 설계 시 실 재료를 사용하면 비용이 많이듭니다.
나무를 대신해서 설계 뜬 자동차 모형을 Mock이라고 합니다.
- 제품의 외양을 흉내 낸 모조품

Mock 객체를 사용 해야 하는 시기

1. 테스트 작성을 위한 환경 구축
- 환경 구축을 위한 작업 시간이 많이 필요 한 경우에 Mock객체를 사용합니다.
- 특정 모듈을 아직 갖고 있지 않아서 테스트를 하지 못할 경우입니다.
- 타 부서와 협의나 정책이 필요한 경우에도 필요합니다.
2. 테스트가 특정 경우나 순간에 의존적 일 경우
- 예를 들어 네트워크 연결의 접속 시간 제한을 구현하는 경우
접속 시도 실패 후 5초까지 1초마다 재접속 시도 하고 이후엔 에러 메세지를 만들 때
- 파일 쓰기 시 데이터가 부정확하게 입력 되는 경우

3. 테스트 시간이 오래 걸릴 경우
- 특정 모듈을 호출 했을 때 시간이 오래 걸리는 경우


Mock의 기본

테스트 더블

오리지널 객체를 사용해서 테스트 진행이 어려울 경우 이를 대신하는 객체


테스트 더블의 하위 객체

- 더미 객체(Dummy Object) : 단순한 껍데기로 객체로써 역할은 하지 못합니다./ 메소드 호출 안함을 전제
- 테스트 스텁(Test Stub) : 더미 객체가 실제로 동작하는 것처럼 보이게 만든 객체입니다. / 특정 값으로 하드코딩
- 페이크 객체(Fake Object) : 여러 개의 객체를 대표하거나 복잡한 구현이 들어가 있는 객체입니다. / 객체 인척
- 테스트 스파이(Test Spy) : 특정 메소드의 정상 호출 여부 확인을 목적으로 합니다. / 더미부터 페이크까지 포함하고 감시 대상이 되는 건 무엇이든 기록합니다.
- Mock 객체(Mock Object) : 행위 기반으로 테스트 케이스를 작성합니다. / ?






Mock 실습하기

비밀번호 암호화/복호화
1. Mycipher 클래스를 다른 팀원이 만들어 준다는 전제
2. 아직 MockMD5Cipher 클래스는 우리에게 없다는 전제
3. Mock 클래스를 만들어 테스트 주도 개발을 함

MockTest.java

package mock;

import static org.junit.Assert.assertEquals;

import org.junit.Test;

public class MockTest {
    @Test
    public void savePassword_패스워드저장() throws Exception{
        UserRegister register = new UserRegister();
        
        MyCipher cipher = new MockMD5Cipher();
        
        String userId = "test";
        String passwd = "potato";
        
        register.savePassword(userId, cipher.encrypt(passwd));
        String decrytedPassword = cipher.decrypt(register.getPaswd());
        assertEquals(passwd, decrytedPassword);
    }
}


MyCiphr

package mock;

public interface MyCipher {
    public String encrypt(String source);
    public String decrypt(String source);
}


MockMD5Cipher.java

package mock;

public class MockMD5Cipher implements MyCipher{

    @Override
    public String encrypt(String source) {
        return "8ee2027983915ec78acc45027d874316";
    }

    @Override
    public String decrypt(String source) {
        return "potato";
    }

}








테스트 더블

스턴트맨이라는 단어에서 차용
1. 유저의 쿠폰을 추가하는 상황을 전제
2. coupon 객체가 없는 상황을 전제
3. 각 단계별로 확인하는 예제 
    더미 객체, 테스트 스텁, 페이크 파크, 테스트 스파이

더미 객체

없는 객체를 임시로 만들어 테스트가 가능하도록 합니다.
단 해당 클래스는 호출 되지 않음을 전제합니다.

UserTest.java
package mock.testDouble;

import static org.junit.Assert.assertEquals;

import org.junit.Test;

public class UserTest {

    @Test
    public void addCoupon_쿠폰추가하기() throws Exception{
        
        User user = new User("test");
        assertEquals("쿠폰 수령전", 0, user.getTotalCouponCount());
        
        
        ICoupon coupon = new DoummyCoupon();
        
        user.addCoupon(coupon);
        assertEquals("쿠폰 추가 후", 1, user.getTotalCouponCount());
        
    }
}

ICoupn.java

package mock.testDouble;

public interface ICoupon {
    public String getName();
    public boolean isValid();
    public int getDiscountPercent();
    public boolean isAppliable(Item item);
    public void doExpire();
}


User.java

package mock.testDouble;

public class User {
    private String name;
    private ICoupon[] coupon;
    private int couponCount;
    public User() {
        couponCount = 0;
    }
    public User(String name) {
        super();
        this.name = name;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getTotalCouponCount() {
        return this.couponCount;
    }
    public void addCoupon(ICoupon coupon) {
        this.couponCount++;
    }
}


DummyCoupon.java

package mock.testDouble;

public class DoummyCoupon implements ICoupon {

    @Override
    public String getName() {
        throw new UnsupportedOperationException("호출 되지 않는 메소드");
    }

    @Override
    public boolean isValid() {
        return false;
    }

    @Override
    public int getDiscountPercent() {
        return 0;
    }

    @Override
    public boolean isAppliable(Item item) {
        return false;
    }

    @Override
    public void doExpire() {

    }

}




테스트 스텁

객체를 생성하여 하드코딩하여 특정 구문에서만 구동 가능한 클래스를 생성합니다.

UserTest.java

    @Test
    public void getLastOccupiedCoupon_이벤트쿠폰발행() throws Exception {
        User user = new User("test");
        ICoupon eventCoupone = new StubCoupon();
        user.addCoupon(eventCoupone);
        ICoupon lastCoupon = user.getLastOccupiedCoupon();
        
        assertEquals("구폰 할인율", 7, lastCoupon.getDiscountPercent());
        assertEquals("쿠폰 이름", "VIP 고객 한가위 감사쿠폰", lastCoupon.getName());
    }
    
    @Test
    public void getOrderPrice_discounttableItem_할인되는물건찾기() throws Exception{
        PriceCalculator calculator = new PriceCalculator();
        
        Item item = new Item("LightSavor", "부엌칼", 100000);
        ICoupon coupon = new StubCoupon();
        
        assertEquals("쿠폰으로 인해 할인 된 가격", 93000, calculator.getOrderPrice(item,coupon));
        
    }


StubCoupon.java

package mock.testDouble;

public class StubCoupon implements ICoupon {

    @Override
    public String getName() {
        return "VIP 고객 한가위 감사쿠폰";
    }

    @Override
    public boolean isValid() {
        return true;
    }

    @Override
    public int getDiscountPercent() {
        return 7;
    }

    @Override
    public boolean isAppliable(Item item) {
        return true;
    }

    @Override
    public void doExpire() {

    }

}


User.java

package mock.testDouble;

import java.util.ArrayList;

public class User {
    private String name;
    private ArrayList<ICoupon> coupon;
    public User() {
        this.coupon = new ArrayList<ICoupon>();
    }
    public User(String name) {
        super();
        this.coupon = new ArrayList<ICoupon>();
        this.name = name;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getTotalCouponCount() {
        return this.coupon.size();
    }
    public void addCoupon(ICoupon coupon) {
        this.coupon.add(coupon);
    }
    public ICoupon getLastOccupiedCoupon() {
        return this.coupon.get(this.coupon.size()-1);
    }
}


item.java

package mock.testDouble;

public class Item {
    private String name;
    private String category;
    private int price;
    
    public Item() {
    }

    public Item(String name, String category, int price) {
        super();
        this.name = name;
        this.category = category;
        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;
    }

    public String getCategory() {
        return category;
    }

    public void setCategory(String category) {
        this.category = category;
    }   
}


PriceCalculator.java

package mock.testDouble;

public class PriceCalculator {

    public int getOrderPrice(Item item, ICoupon coupon) {
        if (coupon.isValid() && coupon.isAppliable(item)) {
            return (int) (item.getPrice() * getDiscountRate(coupon.getDiscountPercent()));
        }
        return item.getPrice();
    }

    private double getDiscountRate(int percent) {

        return (100 - percent) / 100d;
    }

}


하드 코딩 되지 않은 다른 물건을 호출할 경우 페이크 스텁에서 오류가 발생합니다.

페이크 스텁

특정 상황이 아닌 다른 여러 상황에서 동작 가능하도록 합니다.

UserTest.java
@Test
    public void getOrderPrice_undiscounttableItem_할인되지_않은_물건() throws Exception{
        PriceCalculator calculator = new PriceCalculator();
        
        Item item = new Item("R2D2", "알람시계", 20000);
//      ICoupon coupon = new StubCoupon();
        ICoupon coupon = new FakeCoupon();
        
        assertEquals("쿠폰 적용 안되는 아이템", 20000, calculator.getOrderPrice(item, coupon));
    }

FakeCoupon.java
package mock.testDouble;

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

public class FakeCoupon implements ICoupon {
    
    List<String> categoryList = new ArrayList<>();
    
    public FakeCoupon() {
        categoryList.add("부엌칼");
        categoryList.add("아동 장난감");
        categoryList.add("조리 기구");
    }

    @Override
    public String getName() {
        return null;
    }

    @Override
    public boolean isValid() {
        return false;
    }

    @Override
    public int getDiscountPercent() {
        return 0;
    }

    @Override
    public boolean isAppliable(Item item) {
        if(this.categoryList.contains(item.getCategory())) {
            return true;
        }
        return false;
    }

    @Override
    public void doExpire() {
        
    }

}




테스트 스파이

더미 객체부터 페이크 스텁까지 포함하며 다른 동작을 하면서 스파이 기능까지 하는경우 잇습니다.

더미 객체부터 페이크 스텁까지의 기능을 포함하며 그외의 쿠폰 횟수를 확인하는 기능을 테스트합니다.





UserTest.java
@Test
    public void getOrderPrice_discounttableItem_할인되었고_몇번호출되었는지_획인() throws Exception{
        PriceCalculator calc = new PriceCalculator();
        Item item = new Item("LightSavor", "Kitchen knife", 100000);
        
        ICoupon coupon = new SpyCoupon();
        
        assertEquals("쿠폰으로 인해 할인 된 가격", 93000, calc.getOrderPrice(item, coupon));
        int methodCallCount = ((SpyCoupon)coupon).getIsAppliableCallCount();
        assertEquals("coupon.isAppliable 메소드 호출 횟수", 1, methodCallCount);
    }


SpyCoupon.java
package mock.testDouble;

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

public class SpyCoupon implements ICoupon {
    
    List<String> categoryList = new ArrayList<>();
    private int isAppliableCallCount;
    public SpyCoupon() {
        categoryList.add("부엌칼");
        categoryList.add("아동 장난감");
        categoryList.add("조리 기구");
        categoryList.add("Kitchen knife");
    }

    @Override
    public String getName() {
        return null;
    }

    @Override
    public boolean isValid() {
        return true;
    }

    @Override
    public int getDiscountPercent() {
        return 7;
    }

    @Override
    public boolean isAppliable(Item item) {
        isAppliableCallCount++;
        if(this.categoryList.contains(item.getCategory())) {
            return true;
        }
        return false;
    }

    @Override
    public void doExpire() {
        
    }
    
    public int getIsAppliableCallCount() {
        return this.isAppliableCallCount;
    }

}





반응형