본문 바로가기
개발방법론

[TDD] Mock

by 혀나Lee 2016. 10. 12.

Mock 객체

Mock Object 는 검사하고자 하는 코드와 맞물려 동작하는 객체들을 대신하여 동작하기 위해 만들어진 객체이다. 검사하고자 하는 코드는 Mock Object 의 메서드를 부를 수 있고, 이 때 Mock Object는 미리 정의된 결과 값을 전달한다. MockObject는 자신에게 전달된 인자를 검사할 수 있으며, 이를 테스트 코드로 전달할 수도 있다.

테스트를 통해 개발을 이끌어나가는 방식인 테스트 주도 개발(Test Driven Developement, TDD) 에서는 자동화된 테스트가 필수적인 요소중의 하나이다. 모의 객체를 이용하면 상당 부분의 테스트를 사용자의 개입 없이 자동화 할 수 있다.

1. 사전적 의미

  • Mock : 무시하다, 거짓된, 가짜의 의미
  • 조각하기 쉬운 재료(보통 나무나 점토 등)를 이용해 추후 만들어질 제품의 외양을 흉내 낸 모조품.

2. 소프트웨어

  • 가짜 객체

3. 예제

// 테스트 클래스
public class Security {
    /**
    * 패스워드 일치 테스트 후 저장 테스트
    * @throws Exception
    */
    @Test
    public void testSavePassword() throws Exception {
        UserRegister register = new UserRegister();
        Cipher cipher = new MockMD5Cipher();
        String password = "potato";
        String userId = "sweet88";
        register.savePassword(userId, cipher.encryption(password));
        String decrytedPassword = cipher.decryption(register.getPassword(userId));
        assertEquals(password, decrytedPassword);
    }
}
public class UserRegister {
    private String userId;
    private String password;
    Map<String, String> map = new HashMap<String, String>();
    /**
    * 생성자
    */
    public UserRegister() {
    }
    /**
    * 패스워드 저장
    * @param userId
    * @param password
    */
    public void savePassword(String userId, String password) {
        try{
            this.password = String.valueOf(map.get(userId));
        }catch(Exception e) {
            e.printStackTrace();
        }
        return this.password;
    }
}
 

// interface Cipher
public interface Cipher {
    /**
    * 추상메소드 encryption
    */
    public String encryption(String source);
    /**
    * 추상메소드 decryption
    * @return
    */
    public String decryption(String source);
}
 

// interface 구현체
public class MockMD5Cipher implements Cipher{
    @Override
    public String decryption(String source) {
        return "potato";
    }
    @Override
    public String encryption(String source) {
        return "e561f9248d7563d15dd93457b01ebbb6";
    } 
}

Mock 객체는 위의 MockMD3Cipher 클래스와 같이 우리가 구현을 해야하지만 실제로 준비하기엔 어려움이 따르는 대상을 필요한 부분만 채워넣어서 만들어진 객체를 말한다.

4. 언제 Mock 객체를 만드는가

  • 테스트 작성을 위한 환경 구축이 어려울 경우
  • 환경 구축을 위한 작업 시간이 많이 필요한 경우
  • 특정 모듈을 갖고 있지 않아서 테스트 환경 구축이 어려운 경우
  • 타 부서와의 협의나 정책이 필요한 경우
  • 테스트가 특 정 경우나 순간에 의존적인 경우
  • 테스트 시간이 오래 걸리는 경우
@Test
public void testSavePassword() throws Exeption {
    UserRegister register = new UserRegister();
	Cipher cipher = ????	// 옆 팀에서 만들어주기만 하면 되는데...
	
	String userId = "sweet88";
	String password = "potato";
 
	register.savePassword( userId, cipher.encrypt(password) );
	String decryptedPassword = cipher.decrypt(register.getPassword(userId)); 
assertEquals(password, decryptedPassword);

5. 테스트 더블

테스트 더블(Test Double) : '대역, 스턴트맨'을 나타내는 스턴트 더블(Stunt Double)이라는 단어에서 차용해온 단어.

오리지널 객체를 사용해서 테스트를 진행하기 어려울 경우 이를 대신해서 테스트를 진행할 수 있도록 만들어주는 객체를 지칭한다.

(Mock 객체와 의미가 겹치는데, 테스트 더블이라는 단어를 만든 제라드는 Mock 객체를 좀 더 프레임워크와 밀접한 형태로 설명하면서 테스트 더블의 하위로 분류해놓았다.)

    더미 객체 (DummyObject)

더미 객체는 말 그대로 멍청한 모조품, 단순한 껍데기에 해당한다. 더미 객체는 단지 인스턴스화된 객체가 필요할 뿐 해당 객체의 기능까지는 필요하지 않은 경우에 사용한다.

따라서 해당 더미 객체의 메소드가 호출됐을 때의 정상 동작은 보장되지 않는다.

...
 
public class DummyCoupon implements ICoupon {
	...
	@Override
	public int getDiscountPercent() {
		return 0;
	}
 
	@Override
	public String getName() {
		return null;
	}
	...
}
 
@Test
public void testAddCoupon() throws Exception {
	User user = new User("area88");
	assertEquals(0, user.getTotalCouponCount());
 
	ICoupon coupon = new DummyCoupon();
 
	user.addCoupon(coupon); 

assertEquals(1, user.getTotalCouponCount());

위의 테스트 함수에서는 ICoupon 이라는 객체를 만들기 위해(new DummyCoupon()) '더미 객체'를 사용했다.(해당 DummyCoupon의 기능까지는 필요하지 않은 경우)

DummyCoupon 객체 안의 함수가 어떤지 무엇을 갖고있는지는 관심이 없고 객체만 만들기 위해서는 더미 객체를 사용한다.

    스트 스텁 (Test Stub)

테스트 스텁은 더미 객체가 마치 실체로 동작하는 것처럼 보이게 만들어놓은 객체다. 더미 객체로 만들어진 DubmmyCoupon의 경우에도, 메소드가 호출되면 동작을 하긴한다. 하지만, 리턴 타입이 있는 메소드는 타입 기본값으로 리턴이 되고, void 메소드일 경우 아무 일도 안 일어난다. 반면에 테스트 스텁은 객체의 특정 상태를 가정해서 만들어놓은 단순 구현체다. 특정한 값을 리턴해주거나 특정 메시지를 출력하는 등의 작업을 한다. 

public class StubCoupon implements ICoupon {
	...
    @Override
	public int getDiscountPercent() {
		return 7;
	}
	
	@Override
	public String getName() {
		return 'VIP 고객 한가위 감사쿠폰';
	} 
...
  • 더미 : 단지 인스턴스화될 수 있는 객체 수준
  • 스텁 : 인스턴스화된 객체가 특정 상태나 모습을 대표

스텁은 특정 객체가 상태를 대신해주지만, 거의 하드코딩된 형태이기 때문에 로직이 들어가는 부분은 테스트할 수 없다. 

    페이크 객체 (Fake Object)

  • 스텁 : 하나의 인스턴스를 대표하는 데 주로 쓰임
  • 페이크 : 여러 개의 인스턴스를 대표할 수 있는 경우이거나, 좀 더 복잡한 구현이 들어가 있는 객체를 지칭

예를들어 실제로 DB를 통해 쿠폰 적용 가능 카테고리나 아이템을 확인한다고 하면, 페이크 객체에서는 테스트에 사용할 아이템과 카테고리에 대해서만 실제로 DB에 접속해서 비교할 때와 동일한 모습처럼 보이게 만들 수 있다. 보통 내부에 리스트(List)나 맵(Map)을 이용해서 DB 같은 외부 의존 환경을 대체한다. 다음은 DB 대신 목록을 리스트로 관리하도록 만든 코드다.

public class FakeCoupon implements ICoupon {
	List<String> categoryList = new ArrayList();	// 내부용으로 사용할 목록
 
	public FakeCoupon () {
		categoryList.add("부엌칼");
		categoryList.add("아동 장난감");
		categoryList.add("조리기구");
	}
 
	@Override
	public boolean isAppliable(Item item) {
		if (this.categoryList.contains( item.getCategory() )) {
			return true;
		}
		return false;
	} 
...

페이크 객체는 복잡한 로직이나, 객체 내부에서 필요로 하는 다른 외부 객체들의 동작을 비교적 단순화하여 구현한 객체다. 결과적으로 테스트 케이스 작성을 진행하기 위해 필요한 다른 객체(혹은 클래스)들과의 의존성을 제거하기 위해 사용된다.

UserTest를 작성할 때 실제 RealCoupon이 제공됐을 때의 동작 방식


RealCoupon 대신 만들어놓은 FakeCoupon


FakeConpon은 의존성 객체를 이용해 동작하는 방식을 흉내 내어 구현해놓는다면, 사용하는 쪽인 UserTest에서는 차이를 느끼지 못한다. 

    테스트 스파이(Test Spy)

테스트에 사용된 객체에 대해서 특정 객체가 사용됐는지, 그 객체의 예상된 메소드가 정상적으로 호출됐는지를 확인하여 호출 여부를 몰래 감시해서 기록하다가 나중에 요청이 들어오면 해당 기록 정보를 전달한다.

특정 메소드의 정상호출 여부 확인을 목적으로 구현. 보통 스파이가 다른 일을 하며 스파이 일을 겸업으로 하듯, 테스트 스파이 객체도 다른 동작을 하면서 스파이 기능까지 하는 경우가 많다.

public class SpyCoupon implements ICoupon {
	List<String> categoryList = new ArrayList();
	private int isAppliableCallCount;
 
 
	@Override
	public boolean isAppliable(Item item) {
		isAppliableCallCount++;	// 호출되면 증가
		if(this.categoryList.contains(item.getCategory())) {
			return true;
		}
		return false;
	}
 
	public int getIsAppliableCallCount() {	// 몇번 호출됐나?
		return this.isAppliableCallCount;
	} 
...

isAppliable 메소드 호출 횟수를 저장할 내부 변수를 하나 생성하고 그 변수값을 호출하는 함수를 만들었다.

getIsAppliableCallCount() 함수는 프로젝트 스펙에 없는 함수이지만 SpyCounpon 객체 또한 테스트를 위한 객체이기 때문에 상관다.

일반적으로 테스트 스파이는 아주 특수한 경우를 제외하고는 잘 쓰이지 않는다. (테스트 스파이가 필요한 경우에도 Mock 프레임워크를 사용하는 것이 더 편리하다.)

unittest.mock

파이썬 표준 라이브러리(3.3 부터), 이전 버전은 pip install mock

python object 들을 동적으로 대체하고 사용 결과를 확인 하기 위한 다양한 기능들을 제공

>>> from unittest.mock import MagicMock
>>> thing = ProductionClass()
>>> thing.method = MagicMock(return_value=3)
>>> thing.method(3, 4, 5, key='value')
3 
>>> thing.method.assert_called_with(3, 4, 5, key='value')


댓글