본문 바로가기
테스트코드 & 정적분석

Spock 소개 및 튜토리얼

by 향로 (기억보단 기록을) 2017. 9. 30.
반응형

안녕하세요? 이번 시간엔 spock 에 대해 소개하는 시간을 가지려고 합니다. 
모든 코드는 Github에 있기 때문에 함께 보시면 더 이해하기 쉬우실 것 같습니다.
(공부한 내용을 정리하는 Github와 세미나+책 후기를 정리하는 Github, 이 모든 내용을 담고 있는 블로그가 있습니다. )

소개

BDD는 한 TDD 실천자가 테스트의 의도를 더 명확하게 표현하기 위한 용어를 찾는 과정에서 탄생하였다.
사실 테스트라는 단어는 원하는 동작을 정의한다는 정신을 잘 반영하지 못하며 의미가 너무 함축적이다.
개발자 커뮤니티에서는 테스트와 테스트 메소드보다는 명세와 행위라는 용어를 거론하기 시작했다.
더 적합한 용어를 찾는 노력의 부산물로, BDD 커뮤니티는 JUnit 등 기존 테스트 프레임워크의 대안도 다수 만들어낼 수 있었다.
(Effective Unit Testing p.240)

Spock을 소개하기전에 꼭 읽어보셨으면해서 책의 한 구절을 소개드렸습니다.
Spock은 BDD(Behaviour-Driven Development) 프레임워크입니다.
TDD프레임워크인 JUnit과 비슷한 점이 많으나, 기대하는 동작과 테스트의 의도를 더 명확하게 드러내주고 산만한 코드는 뒤로 숨겨주는 등의 큰 장점이 있습니다.
아직까지 JUnit을 많이 사용하시겠지만, Spock을 소개드리고 싶습니다.
(저도 최근까지 JUnit만 쓰다가 팀내 선임님의 적극적인 사용으로 조금씩 시작하게 되었습니다^^;)

제가 열심히 사용하고 이에 대해 소개드리기 보다는, 이제 시작하기 위해서 공부한 내용을 정리한 것이 이번 포스팅의 내용입니다.
그래서 보시면서 부족한 내용이 많이 보일수 있습니다.
가감없는 지적과 댓글 부탁드리겠습니다!

1. Java

소수점 자리에 맞춰 반올림/올림/버림 하는 Enum을 생성하여 테스트를 진행해보겠습니다.

FeeCalculateType

Enum 타입이 계산을 올바르게 하는지 검증하기 위한 테스트 코드를 작성합니다.

1-1. 기존 JUnit 테스트 코드

JUnit으로 테스트 코드를 작성하다보면 몇가지 문제점을 만나게 됩니다.

assertThat

1. 이 중 어느 assertThat을 써야할까요?

is

2. is는 어떤걸 써야할까요?

junit테스트코드

3. 파라미터만 변경되고 동일한 과정이 반복되는데 과정 자체를 재사용할순 없을까요?

4. case1, case2, case3, case4를 별도의 테스트처럼 보일순 없을까요?

이번엔 음수가 입력되었을때 정상적으로 Exception이 발생하는지 테스트 케이스를 만들어 보겠습니다.

expected

음수가 발생했을때 정상적으로 NegativeNumberException이 발생하는지 테스트하는 코드입니다.

5. JUnit을 처음 보시는 분들은 이 코드의 검증 코드가 어디있는지 어떻게 알수 있을까요?

6. 테스트 코드의 길이가 길어진다면, 마지막까지 코드를 읽은 후 예상 결과를 확인 하기 위해 다시 메소드의 첫줄로 이동해야만 합니다.

JUnit을 사용하면 위와 같은 불편함을 빈번하게 직면합니다.
이런 불편함을 Spock을 통해 해소해보겠습니다.

1-2. Spock 설치

Spock을 사용하기 위해선 의존성 도구(Maven, Gradle)에 2개의 의존성 추가가 필요합니다.

testCompile('org.spockframework:spock-core:1.1-groovy-2.4') // Spock 의존성 추가 testCompile('org.spockframework:spock-spring:1.1-groovy-2.4') // Spock 의존성 추가

추가로 groovy가 필요하기 때문에 build.gradle에 groovy 플러그인 추가가 필요합니다.

groovy_test
apply plugin: 'groovy' // groovy 지원

위와 같이 설정하면 Spock을 사용할 수 있는 환경설정은 끝입니다.

이제 기본적인 사용법을 소개드리겠습니다.

1-3. Spock 기본 사용법

아래는 간단한 예제 코드로 작성한 Spock 테스트 코드입니다.

기본사용법

groovy 언어로 작성하기 때문에 테스트 메소드 이름을 문자열로 작성할 수 있게 되었습니다.
Java에서도 한글 메소드명이 가능하긴 했지만 띄어쓰기, 가장 앞에 특수문자 사용하기 등의 제약조건이 있는 반면 groovy는 이 모든 제약 조건에서 빠져나올수 있습니다.
이제는 정말 명확하게 테스트 케이스의 의도를 표현할 수 있게 되었습니다.

위에서 사용한 코드들 중 Spock의 예약어들에 대해 간략하게 소개드리면

  • Specification: extends 하면 Spock Test 클래스가 됩니다.
  • def : groovy의 동적 타입 선언(메소드, 변수에 모두 사용할 수 있음. JS의 var 같은 존재)
    • def 대신에 Java처럼 실제 클래스 타입을 명시할 수도 있습니다.
  • given, when, then: Spock의 feature 메소드

given:, when:,then:등과 같은 메소드를 Spock에선 feature 메소드라고 합니다.
이 3개외에 추가로 3개가 더 있어 총 6단계의 라이프사이클을 가지고 있습니다.

feature메소드_라이프사이클

(출처: spock 공식 문서)

  1. given (혹은 setup) : 테스트 하기 위한 기본 설정작업 (테스트 환경 구축)
  2. when : 테스트할 대상 코드를 실행 (Stimulus)
  3. then : 테스트할 대상 코드의 결과를 검증 (이 메소드 범위에선 한줄한줄이 자동 assert가 됩니다.)
  4. expect : 테스트할 대상 코드를 실행 및 검증 (when + then)
  5. where : feature 메소드를 파라미터로 삼아 실행시킵니다.

각각의 메소드들의 변수들은 다른 메소드들에서도 공유되어 사용할 수 있습니다.
(즉, given:에서 선언한 변수는 then:에서도 사용가능 합니다.)

다른 메소드들은 많이들 들어보셨을텐데, where에 대해서는 생소하실것 같습니다.
where 블록에 대해선 공식 문서 - Where Blocks 에 있는 예시가 정말 좋아 해당 예시를 통해 소개하겠습니다.

where

위 코드를 실행해보시면 아주 재밌는 결과를 보실수 있는데요.
Math.max(a, b) == c 테스트 코드의 a,b,c에 각각 5, 1, 53, 9, 9가 입력되어 expect: 메소드가 실행됩니다.
즉, wherefeature 메소드를 파라미터화하여 실행합니다.

자 그럼 이제 Spock을 통해 기존 JUnit 테스트 코드를 개편해보겠습니다.

 

 

1-4. Spock으로 전환한 기존 테스트 코드

첫번째 테스트 케이스를 Spock으로 전환해보겠습니다.

junit2spock

너무 명확하고 간단하게 코드로 표현되지 않았나요?
어떤 라이브러리를 써야할지 명시할 필요도 없고,
파라미터만 변경되는 테스트 메소드를 코드의 추가없이 재사용할 수 있게 되었습니다.
특히 여기서 주목하실것은 where과 @Unroll입니다.

메소드 이름에 #변수명으로 되어있으면 where에 지정된 변수명들이 매칭되어 테스트 결과에 각각의 값이 반영되어 출력됩니다.
설명으로 하면 이상하니 얼른 결과를 확인해보겠습니다.
실제로 실행해보시면!

unroll결과

짜잔!
JUnit으로는 수많은 부가 코드가 필요했던 테스트가 아주 심플하게 생성되었습니다.

2번째 테스트 케이스(Exception 검증)을 Spock으로 전화해보겠습니다.

groovy_exception_test

when: 절을 실행했을때 발생할 예외를 thrown으로 검증합니다.
여기선 NegativeNumberException이 발생할 것이라 예상하고 작성하였습니다.
재밌는 것은 thrown은 발생한 Exception을 검증하는 것 뿐만 아니라, 반환까지 됩니다.
테스트 코드의 마지막줄엔 반환된 Exception의 message 값을 검증합니다.

 

1-5. Mock 테스트

Spock의 강력한 기능 중 또 하나는 Mock 입니다.
아래 2가지 중 본인이 원하시는 방식을 선택하시면 됩니다.

def mockService = Mock(CustomerService) CustomerService mockService = Mock()

Spock에서 Mock 객체의 반환값은 >>로 지정합니다.
예를 들어 일반적인 값의 반환을 Mocking 한다면 아래와 같이 표현 가능합니다.

customerRepository.findName(1) >> "jojoldu"

만약 Exception을 반환해야한다면

customerService.validate() >> { throw new ResourceNotFoundException}

와 같이 표현할 수 있습니다.

실제로 한번 Mock 테스트 코드를 작성해보겠습니다.

mockTest

mockAmountService.getAmount에서 999원을 반환하도록 Mocking 한 뒤, 이를 계산한 결과가 의도한것과 맞는지 검증하는코드입니다.

이를 바로 실행하면 다음과 같은 아래가 발생합니다.

mocking_exception

현재까지 발생하는 문제(2017.09.30)라 해결하기 위해 아래 의존성을 추가합니다.

testCompile('net.bytebuddy:byte-buddy:1.6.4')

그리고 이제 실행해보시면!

mock결과

잘 실행됨을 확인할 수 있습니다.

여기까지만 보면 특별히 다를게 없는 Mock 기능으로 보입니다.
Spock의 Mock 기능은 이게 끝이 아닙니다.
Spock의 Mock은 Mock의 호출 횟수를 검증할 수 있습니다.
실제로 코드로 한번 어떤 기능인지 보겠습니다.

mock_service

이벤트에 참여한 고객이 vip면 포인트 적립을 2번하는 메소드입니다.
(실제로는 이렇게 작성하진 않습니다. Spock Mock의 기능을 소개하기 위함을 다시한번 말씀드립니다^^;)

자 여기서 금액이 2배가 되는것이 아닌, customerRepository.savePoint()가 2번 실행되는지를 테스트해보고 싶다고 가정하겠습니다.

생각만해도 코드가 얼마나 복잡해질지 아찔하네요!
Spock은 아주 쉽게 해결할 수 있습니다.
테스트 코드를 한번 볼까요?

mock횟수검증

then절에 검증하고 싶은 Mock 메소드 앞에 숫자를 *하면 그게 바로 수행횟수 검증 코드가 됩니다.
여기서는 2 * mockCustomerRepository.savePoint(customer, point)를 통해 실제로 savePoint가 2번 발생했는지 검증하였습니다.
실제로 테스트를 수행해보면

mock횟수검증결과

통과함을 알 수 있습니다.

만약 횟수가 딱 고정된 것이 아니라면 아래와 같이 표현할 수 있습니다.

(3.._) * mockCustomerRepository.savePoint(customer, point) // 최소 3번  (_..3) * mockCustomerRepository.savePoint(customer, point) // 최대 3번

추가로 파라미터 값에 무관하게 횟수 검증도 할 수 있습니다.

(_..2) * mockCustomerRepository.savePoint(_, _) // 어떤 파라미터값이 와도 최대 2번 수행된다.

 

1-6. Spock과 JUnit 비교

Spock이 조금 생소한 용어를 사용하고 있지만, JUnit과 크게 다르지 않은 사용법을 제공하고 있습니다.
아래는 Spock과 JUnit의 대치되는 예약어를 비교한 것입니다.

spock vs junit

(출처: 공식문서)

마무리

Spock을 이용한 기본적인 Java 어플리케이션을 테스트하는 방법을 알아보았습니다.
다음 시간엔 Spock을 이용한 SpringBoot 테스트 방법을 알아보겠습니다.

감사합니다!

출처

 

반응형