[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으로 포팅하면서 겪은 어려움이나 불편함에 대해 정리할 예정입니다.

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

[TDD]숫자야구게임 연습

테스트 주도 개발(Test Driven Development)로 숫자야구게임을 구현해보았다.

1시간 30분동안 “실패-성공-리팩토링“의 과정을 거치며 최범균님이 구현한 결과와

비교해 보았다.

================================================================

1. “실패 => 성공 => 리팩토링”에서 “실패“과정을 따로 확인하지 않은 적이 많다.                   이것은 실제로 보폭을 넓게 가지고 갔다기보다 테스트코드를 만들 때 “성공“시키기           위해 구현을 먼저 시작한 경우가 많다는 뜻이다.

2. 적절한 시기에 리팩토링이 잘 되지 않았다.

3. 실제 구현에 대한 로직이 세련되지 못하다.

4. 그리고, 전에 범균님 동영상을 보지 않고 구현했으면 더 좋았을 것 같다! ㅎㅎ

5. IntelliJ는 Eclipse에 비해 리팩토링기능은 더 뛰어나고 테스트코드 실행속도는                  훨씬 느리다.

===============================================================

범균님 동영상을 이전에 보고 구현한 결과, 동영상 속의 결과물이 떠올라

제대로 내 생각을 구현에 넣지 못하는 문제가 있었다.

혹시나, TDD로 숫자야구게임을 구현해 보고 싶다면 꼭 먼저 구현해보고 범균님

동영상을 봤으면 하는 생각이다.

TDD로 개발할 때 개발속도나 설계상의 미흡한 점은 연습을 하면 할수록 보완되리라

생각된다.

그리고 이렇게 TDD로 구현을 완료하고 남은 테스트코드가 추후 유지보수시에도 훌륭한

문서가 될 것이기 때문에 꾸준한 TDD연습이 필요함을 느꼈다.

[참고 URL]

[Mockito]Spring framework에서 Controller 테스트케이스 만들기

요즘 저에게 가장 화두가 되고 있는 단어 중에 하나가 바로 “테스트“입니다.

어떻게 하면, 테스트케이스를 통해 실제 코드를 더욱 견고하고 깔끔하게 만드는가 하는데에

따른 고민에서 시작됩니다.

바로 그 시작점인 “테스트“케이스를 만드는데 있어, 현재 가장 많이 쓰이고 있는

프레임워크인 “스프링“에서 웹 어플리케이션의 서버사이드 “프론트“를 담당하고 있는

“Contoller”부분에 대한 간단한 CRUD 테스트케이스를 만들어 보았습니다.

public class TempVo {

    private Integer id;
    private String name;

    ... 이하 setter/getter코드 생략

}

 

public interface TempService {

    TempVo getListUserInfo(int id);

    int insertUserInfo(TempVo temp);

    int updateUserInfo(TempVo temp);

    int deleteUserInfo(TempVo temp);

    List getListTemp();
}

 

import static com.glider.framework.test.ReflectionInjectorUtils.injector;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.util.List;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.web.servlet.ModelAndView;

import com.google.common.collect.Lists;

@RunWith(MockitoJUnitRunner.class)
public class CommonControllerTest {

	// 테스트대상 클래스의 의존객체에 Mock Object 주입
	@Mock
	private TempService tempService;

	// 테스트대상 클래스에 Mock Object 주입
	@InjectMocks
	private CommonController commonController = new CommonController();

	private ModelAndView model;

	@Before
	public void setUp() {
		// 테스트대상 Mock 클래스에 의존 Mock객체를 쉽게 주입할 수 있음.
		// http://wiki.kwonnam.pe.kr/java/unittest/reflectioninjector 참고.
		injector(commonController).set(tempService);
		model = new ModelAndView();
	}

	@Test
	public void getTemp() {
		int id = 1;
		TempVo temp = new TempVo();

		when(tempService.getListUserInfo(id)).thenReturn(temp);

		commonController.temp(id, model);

		verify(tempService).getListUserInfo(id);
		assertThat(model.getViewName(), is("temp/temp"));
	}

	@Test
	public void insertTemp(){
		TempVo temp = new TempVo();
		temp.setId(1);
		temp.setName("bluepoet");

		when(tempService.insertUserInfo(temp)).thenReturn(1);

		commonController.insertTemp(name, model);

		verify(tempService).insertUserInfo(temp);
		assertThat(model.getViewName(), is("temp/temp"));
	}

	@Test
	public void updateTemp() throws Throwable {
		TempVo temp = new TempVo();
		temp.setId(3);
		temp.setName("bluepoet");

		when(tempService.updateUserInfo(temp)).thenReturn(1);

		commonController.updateTemp(name, model);

		verify(tempService).updateUserInfo(temp);
		assertThat(model.getViewName(), is("temp/temp"));
	}

	@Test
	public void deleteTemp() throws Throwable {
		TempVo temp = new TempVo();
		temp.setId(1);
		temp.setName("bluepoet");

		when(tempService.deleteUserInfo(temp)).thenReturn(1);

		commonController.deleteTemp(name, model);

		verify(tempService).deleteUserInfo(temp);
		assertThat(model.getViewName(), is("temp/temp"));
	}

	@Test
	public void getListTemp() {
		List list = Lists.newArrayList();

		when(tempService.getListTemp()).thenReturn(list);

		commonController.getListTemp(model);

		verify(tempService).getListTemp();
		assertThat(model.getViewName(), is("temp/tempList"));
	}
}

물론 CRUD Controller 메서드안에 더욱 복잡한 로직이 들어갈 수도 있습니다.

그럴땐, 테스트케이스가 더 복잡해지겠지요.

다만, 큰 것을 만들때는 작은 것부터 Step by Step으로 나가는 것이 좋다고 생각합니다.

스프링 framework에서 controller를 만들때는 위 테스트케이스를 뼈대로 해서,

테스트케이스를 확장해나가시면 더욱 견고한 real code를 만드실 수 있으리라고 봅니다.

단위테스트에 대해서는 토비의 스프링3에서도 여러차례 리팩토링과 함께 잘 나와있으니

참고하시면 될것 같구요(테스트대역, 스텁,  Mock Object의 개념등..)

아래 포스팅에서는 테스트케이스를 작성하는 방법과 활용에 대해서 더욱 잘 나와있으니

꼭 일독을 권해드립니다.

* 참고 url