[Spock] 포팅 경험기1

페이스북에 “테스트코드와 친해지기“라는 그룹을 만들고 처음으로 한 프로젝트가

최범균님의 DDD START책에 java + junit으로 되어 있는 테스트코드를 groovy + spock으로

포팅하는 것이었습니다.

해당 프로젝트는 ddd-repeat-start 에서 확인하실 수 있습니다.

테스트코드와 친하지 않더라도 java 개발자라면 junit으로 간단한 테스트코드는 만들어 본

경험이 있을텐데요.

굳이 기존에 익숙한 junit을 쓰지 않고 spock으로 포팅하게 된 이유를 51개의 테스트케이스를

포팅하면서 정리해 보았습니다.

* groovy언어만의 장점은 해당 내용에서 제외하였습니다.


1. 의존 라이브러리 추가

spock을 사용하기 위해 기존 최범균님 프로젝트 pom.xml 파일에서 아래 라이브러리를

추가했습니다.

기존 테스트코드가 spring boot로 되어 있었기 때문에 spock-spring 모듈을 사용했습니다.

<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-spring</artifactId>
<version>1.0-groovy-2.4</version>
</dependency>

<dependency>
<groupId>cglib</groupId>
<artifactId>cglib-nodep</artifactId>
<version>3.2.4</version>
</dependency>

cglib는 class mocking을 위해 추가했습니다. (SecurityCancelPolicyTest.groovy에 사용)

2. given-when-then 템플릿을 프레임워크 차원에서 제공

spock의 장점은 BDD(Behavior Driven Development)에서 나온 템플릿인

given(테스트할 조건을 주고)-when(테스트를 실행한 뒤)-then(결과를 검증한다) 템플릿을

프레임워크에서 제공하고 있다는 점입니다.

예전에 junit을 사용할 때는 주석을 사용해 템플릿으로 만들어 쓰고 있었지만, 이제

프레임워크에서 제공해주다 보니 자연스럽게 이 패턴대로 테스트코드를 작성할 수 있습니다.


//junit
@Test
public void addTest() {
    // given
    int a = 1;
    int b = 2;
 
    // when
    int result = a + b;

    // then
    assertThat(result, is(3));
}


//spock
def "addTest"() {
    given:
    def a = 1
    def b = 2

    when:
    def result = a + b

    then:
    result == 3
}

둘 간의 차이점이 확 느껴지시나요? spock을 사용했을 때가 훨씬 명시적이고 직관적임을

알 수 있습니다.

3. 결과검증 간편

junit을 사용할 때는 기본적으로 Junit이 제공해주는 Assert 클래스의 기능과 부가적으로

hamcrest, fest assert, assertj등의 라이브러리를 사용해서 결과를 검증해야만 했습니다.

//junit 
assertThat(result, is(1));
assertThat(result, is(true));
assertThat(result, is("test"));
assertThat(result, is(1));
assertThat(result, instanceOf(TestClass.class));
assertThat(result, nullValue());
assertThat(result, notNullValue());

@Test(expected = IllegalArgumentException.class)

try {
    a.function();
}catch(IllegalArgumentException e) {
    fail();
}
   
//spock
then:
result == 1
result == true
result == "test"
result instanceof TestClass
result == null
result != null
thrown IllegalArgumentException
notThrown IllegalArgumentException

하지만 위의 예제에서도 나오듯이 spock에서는 상대적으로 결과검증이 간편하고 직관적입니다.

4. Mock 사용 편리

junit으로 유닛테스트를 만들 때, Mock사용을 위해 mockito 라이브러리를 많이 사용합니다.

하지만 spock을 사용하면 자체적으로 Mock기능을 사용할 수 있습니다.

//junit
@Mock
private Order order;

when(order.getOrderer()).thenReturn(new MemberID("user1"), "");
 
verify(order, times(2)).purchase();

//spock
//SecurityCancelPolicyTest.groovy
given:
def order = Mock(Order)
order.getOrderer() >> new Orderer(new MemberId("user1"), "")

then:
2 * order.purchase();

stub이나 spy등을 사용할 때도, mockito에 비해 상대적으로 명시적으로 사용할 수 있습니다.

그리고 무엇보다 결과검증을 할 때, 메소드 호출의 경우 “호출횟수 * 객체.메서드명()“같이

사용할 수 있어 편리합니다.

다만, PlaceOrderServiceTest.groovy에서는 ArgumentCaptor를 사용하기 위해

mockito를 사용했습니다.

then:
ArgumentCaptor<Order> orderCaptor = ArgumentCaptor.forClass(Order.class)
verify(mockOrderRepository).save(orderCaptor.capture())

Order createdOrder = orderCaptor.getValue()
createdOrder != null

5. 동일 실행구문에서 여러 조건으로 테스트 용이

테스트코드를 만들다 보면 실행구문은 동일한데 조건을 다르게 줘야 하는 경우가 있습니다.

보통 이럴 때는 조건에 따라 변수에 값 할당이나 객체를 다르게 만들어줘야 하는데요.

spock에서는 이런 상황에서 expect(혹은 when~then)~where로 보다 편리하게

테스트코드를 만들 수 있습니다.

def "EventStore에서 이벤트를 가져온다."() {
    given:
    List<EventEntry> entries = eventStore.get(1, 2)
    
    expect:
    SampleEvent event = objectMapper.readValue(entries.get(index).getPayload(), SampleEvent.class)
    event.getName() == name
    event.getValue() == value
    
    where:
    index | name    | value
    0     | "name2" | 12
    1     | "name3" | 13
}

6. 기타

그 밖에 spock을 사용하면서 아래와 같은 장점을 느꼈습니다.

  • 메서드명에 “한글“을 넣는 것이 junit에 비해 자연스럽다
  • private 필드나 메서드 접근이 가능하다
  • 테스트 실패시 나오는 결과를 보고 원인을 쉽게 알 수 있다.

다음 포스팅에서는 spock으로 포팅하면서 겪은 어려움이나 불편함에 대해 정리할 예정입니다.

* 위 내용 중 잘못된 내용이나 수정해야 할 부분이 있으면 언제든지 의견 부탁드립니다.

Advertisements