본문 바로가기
프로그래밍/테스트

[Spock] Spock Framework 이용하기 - Mock

by 사바라다 2020. 9. 2.

[Spock] Spock Framework 이용하기 - 개론편
[Spock] Spock Framework 이용하기 - 실습편
[Spock] Spock Framework 이용하기 - Mock
[Spock] Spock Framework 이용하기 - Where

안녕하세요. 오늘은 Spock Framework의 3번째 시간입니다. 이전 포스팅, [Spock] Spock Framework 이용하기 - 개론편[Spock] Spock Framework 이용하기 - 실습편에서는 이론과 실전을 하나하나 살펴보았습니다. 오늘은 좀 더 나아가 Spock Framework에서 Mock과 Stub을 어떻게 사용하고 활용할 수 있는지에 대해서 알아보는 시간을 가지도록 하겠습니다.

Mock이란 개념에 생소하신 분들은 What is Mock? 해당 StackOverFlow 게시물에서 해답을 찾으실 수 있습니다. 간단히 말씀드리면 Test시 객체간의 의존성을 제거하기 위해 사용되는 Dummy 객체라고 생각해주시면 좋을 것 같습니다.

Mock 객체 생성하기

오늘은 2개의 객체를 이용하도록 하겠습니다. JPA로 DB에 접근할 수 있는 Repository 객체 2개입니다. 또한 메서드는 Integer 값을 받아 각 DB에서 Integer값에 해당하는 값을 찾고 Integer를 String으로 변화하여 반화하는 어리석은 메서드입니다. 아래코드를 봐주시면 되겠습니다.

@Service
public class Tistory {

private final BlogRepository blogRepository;

private final PostRepository postRepository;

    public String find(Integer value) {
        postRepository.findById(value);
        blogRepository.findById(value);
        return String.valueOf(value);
    }
}

Spock Framework에서의 Mock Object는 Mock() 또는 Mock(Class Type)으로 선언할 수 있습니다. 저는 BlogRepositoryPostRepository를 각각 목객체로 선언할 것이기 때문에 아래와 같이 Mock 객체로 선언하 수 있습니다.

def blogRepository = Mock(BlogRepository)
def postRepository = Mock(PostRepository)
BlogRepository blogRepository = Mock()
PostRepository postRepository = Mock()

Mock Object를 생성하고 해당 object의 메서드를 실행할 수 있습니다. 해당 메서드를 실행하면 리턴 타입에 따라서 기본 타입을 반환합니다. 기본타입은 boolean 타입이라면 false, Number 타입이라면 0, 객체 타입이라면 null과 같습니다.

Mocking

오늘 사용할 예제를 Mocking 해보도록 하겠습니다. 아래 테스트는 통과합니다. 아래 테스트는 해석하면 이렇습니다. "tistory.find(1) 메서드를 호출하면 postRepository.findById(1) 메서드는 1번 호출되고 blogRepository.findById(1) 메서드도 1번 호출된다"

def "Mock 기본 예제" {
    given:
        BlogRepository blogRepository = Mock()
        PostRepository postRepository = Mock()
    when:
        tistory,find(1)
    then:
        1 * postRepository.findById(1)
        1 * blogRepository.findById(1)
}

공식문서에서는 위 표현식을 아래와 같이 4개의 요소가 있다고 설명하고 있습니다. 아래 4가지 요소를 적절히 사용하면 다양한 테스트 케이스를 작성할 수 있습니다. 각 요소에 대해서 구체적으로 알아보도록 하겠습니다.

1 * postRepository.findById(1)
|   |              |        |
|   |              |        argument constraint
|   |              method constraint
|   target constraint
cardinality

Cardinality ( 관계 수 )

cardinarlity는 얼마나 자주 method가 호출되었는지 나타냅니다. 이 Cardinality는 고정 값일 수 있으며 범위 값일 수 도 있습니다.

1 * postRepository.findById(1)      // 1번 호출
0 * postRepository.findById(1)      // 0번 호출
(1..3) * postRepository.findById(1) // 1번 ~ 3번 호출
(1.._) * postRepository.findById(1) // 적어도 1번 호출
(_..3) * postRepository.findById(1) // 최대 3번 호출
_ * postRepository.findById(1)      // 1번 이상 호출

target constraint

target constraint은 method 호출을 하는 object를 나타냅니다. target constraint는 명시적으로 나타낼 수 있으며 any(_) 의 형태도 나타낼 수 있습니다. any를 이용하면 해당 메서드를 호출하는 어떠한 mock object 모두를 나타낼 수 있습니다.

1 * postRepository.findById(1) // postRepository의 findById 메서드
1 * _.findById(1)          // any Mock의 findById 메서드

method constraint

method constraint은 호출 되길 원하는 메서드를 나타냅니다. 메서드 mock또한 명확히 명시 할 수 있으며 그렇지 않으면 패턴으로 찾을 수도 있습니다.

1 * postRepository.findById(1) // findById라는 이름을 가진 메서드
1 * postRepository./f.*Id/(1)  // f로 시작하고 Id로 끝나는 이름을 가진 메서드

메서드에 getter 메서드가 있다면 getter 메서드를 사용하는 대신에 직접 접근 식으로 표현할 수 있습니다.

1 * blogRepository.metaClass // 동일 : 1 * blogRepository.getMetaClass()

argument constraint

argument constranints은 호출될 때의 파라미터를 나타냅니다. 아래와 같이 다양한 조건으로 검증할 수 있습니다.

1 * postRepository.findById(1)              // 파라미터 값을 1을 가지는 메서드
1 * postRepository.findById(!1)             // 파라미터 값을 1을 가지지 않는 메서드
1 * postRepository.findById(_)              // 파라미터 값을 어떤 것이든 가질 수 있는 메서드, list 불 포함
1 * postRepository.findById(*_)             // 파라미터 값을 어떤 것이든 가질 수 있는 메서드, list 포함
1 * postRepository.findById(!null)          // 파라미터 값을 null이 아닌 값을 가지는 메서드
1 * postRepository.findById(_ as String)    // 파라미터 값을 String Type의 값을 가지는 메서드

Verification

interaction 증명에는 2가지 실패 요인이 있을 수 있습니다. 원했던 것보다 호출수가 많거나 원했던 것보다 호출수가 적거나 입니다. spock framework에서는 이러한 verification 실패에 대해서 명확하게 알려줍니다. 아래는 원했던 호출 수 보다 더 많았을 경우에 출력되는 에러입니다.

Too many invocations for:

2 * subscriber.receive(_) (3 invocations)

만약 원했던 호출 수 보다 적다면 아래와 같이 출력됩니다.


Too few invocations for:

1 * subscriber.receive("hello") (0 invocations)

Stubbing

Stubbing은 Mock 객체의 응답을 대신하는 방법입니다. 우리가 외부와 통신하는 로직이 있을 때 해당 로직이 포함되어있는 경우가 있습니다. 이럴경우 테스트는 외부 시시템의 응답에 의존적일 수 밖에 없습니다. 이럴 경우 테스트를 외부 시스템과 고립시킬 필요가 있습니다. 이럴때 Stub을 사용할 수 있습니다. Stub은 실제 객체의 의한 실제 로직이 아닌 이렇게 응답할 것이다라는 것을 정의해놓고 대신 이용하는 것입니다.

우리가 테스트하고자 하는 테스트를 다시 보겠습니다. 기존 예제를 조금 수정해 보겠습니다. Repository에 의해 DB에 의존성을 가지고 있는것을 알 수 있습니다. 따라서 이 경우 만약 Database에 접근하지 못하거나 한다면 에러가 출력될 수 있습니다.

@Service
public class Tistory {

private final BlogRepository blogRepository;

    public Blog find(Integer value) {
        return blogRepository.findById(value).orElseThrows();
    }
}

Stub은 >> 으로 만들 수 있습니다. 아래와 같이 한다면 findById를 호출했을 때 new Blog를 반환하게 됩니다. 이러한 Stub은 Mock 객체에 사용할 수 있습니다.

blogRepository.findById(_) >> new Blog("사바라다는 차곡차곡")

Returning Fixed Values

기본적으로 고정된 값을 리턴할 수 있습니다. 고정된 값은 >> 명령어로 제공할 수 있습니다. 파라미터에 따라 다른 값이 리턴되게 할 수 도 있습니다. 아래를 보시면 ID를 1로 테스트 했을 때와 2로 테스트 했을 때 반환되는 값을 다르게 설정한 것입니다. 만약 3이 ID로 들어왔다면 Mock의 기본값인 null이 반환됩니다.

blogRepository.findById(1) >> new Blog("사바라다는 차곡차곡")
blogRepository.findById(2) >> new Blog("차곡차곡은 사바라다")

Returning Sequences of Values

여러번의 동일한 메서드의 호출에 대해서 각각 다른 응답을 원할 수 있습니다. 이럴 때는 >>>를 이용할 수 있습니다. 아래는 첫번째 호출했을 경우는 new Blog("사바라다는 차곡차곡")가 리턴되며 두번째 호출 했을 경우에는 new Blog("차곡차곡은 사바라다")가 호출되는 됩니다.

blogRepository.findById(1) >>> [new Blog("사바라다는 차곡차곡"), new Blog("차곡차곡은 사바라다")]

Computing Return Values

리턴값을 테스트에서 재 가공하는 식의 Stubbing도 가능합니다. 아래의 예제를 보겠습니다. 아래는 receive 메서드에서 반환되는 값의 크기가 3보다 크면 OK를 반환하고 그렇지 않다면 fail을 반환하는 예제입니다. 아래와같이 사용할 수 있습니다.

subscriber.receive(_) >> { args -> args[0].size() > 3 ? "ok" : "fail" }

Performing Side Effects

Exception과 같은 예외 테스트도 작성할 수 있습니다.

blogRepostiory.findById(_) >> { throw new IllegalArgumentException("ouch") }

또한 Chaning도 가능합니다. 아래의 예제는 1, 2 째는 단순한 메시지만 나오다 4번째 호출에는 Exception이 출력되는 예제입니다.

blogRepository.findById(1) >>> [new Blog("사바라다는 차곡차곡"), new Blog("차곡차곡은 사바라다")] >> { throw new InternalError() }

마무리

오늘은 이렇게 Spock Framework의 Mock에 대해서 전반적으로 알아보는 시간을 가졌습니다. Mock 역시도 JUnit을 사용했을 때 보다는 가독성이 좋고 쉽게 사용할 수 있는것을 알 수 있었습니다. 또한 동일 메서드의 호출 순서에 따른 다른 결과와 같은 유연함을 추가로 제공해주기 때문에 사용하기 정말 편하고 좋은 Framework라고 생각되어집니다.

감사합니다.

참조

spockframework Interaction Based Testing

댓글