Antilog의 개발로 쓰다
Published 2025. 3. 3. 16:08
Spock Framework 적용기 Spring Boot
반응형

서론...

최근 테스트 코드를 작성하는 과정에서 가독성 개선을 시도한 적이 있다.
기존 UnitTest 는 Android 환경에서 Junit4 + Truth + MockK 와 같은 수단을 사용했고 Spring 환경에서는 Junit4 or Junit5 + AssertJ + Mockkito 와 같은 수단을 사용했다.
더불어 이해와 가독성을 위해 BDD 기반의 given-when-then 패턴 또한 사용하며 한글 이름의 메소드를 작성했다.

하지만 이 과정에서 Junit 뿐 아니라 Mocking, Stubbing 등을 위한 다른 라이브러리의 사용법 또한 알아야 기본적으로 Test 코드를 이해할 수 있었다.

따라서 보다 직관적으로 테스트 코드를 작성할 방법을 고민하던중 Naver D2, 우아한 기술 블로그, 지마켓 기술 블로그 등에서 Spock Framework 에 대해 알게되었다.
https://d2.naver.com/helloworld/568425
https://techblog.woowahan.com/2560/
https://dev.gmarket.com/37

이번 글에서는 Spock Framework 에 대해 알아보고 실제로 적용한 과정에 대해 작성해보고자 한다.

기존의 테스트 코드와 고민점

보다 간단한 예제를 위해 우테코 프리코스에서 사용한 과제를 이용했다.


    private final LottoTicketDispenser ticketDispenser = new LottoTicketDispenser(new AutoLottoNumberGenerator());

    @Test
    void 로또_게임에_금액을_입력하면_구매된_로또_목록을_얻을_수_있다() {
        // given
        AutoCreatedLottoGame autoCreatedLottoGame = new AutoCreatedLottoGame(ticketDispenser);
        String cost = "2000";
        // when
        PurchasedLottoTickets purchasedLottoTickets = autoCreatedLottoGame.purchaseLottoTickets(cost);
        // then
        assertThat(purchasedLottoTickets.size()).isEqualTo(2);
    }
     @Test
    void 로또_게임에_유효하지_않은_금액을_입력하면_예외가_발생한다() {
        // given
        AutoCreatedLottoGame autoCreatedLottoGame = new AutoCreatedLottoGame(ticketDispenser);
        String cost = "2000A";
        // when
        // then
        assertThatThrownBy(() -> autoCreatedLottoGame.purchaseLottoTickets(cost))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("유효하지 않은 금액입니다.");
    }

간단하게 로또가 자동으로 생성되는 게임에 적절한 금액을 넣으면 로또가 잘 구매 되는지, 적절한 금액이 아니라면 예외가 발생하는지 테스트한다.
이 과정에서는 Mockito와 같은 수단을 사용하지 않았고 가독성에 대한 문제도 너무 간단해서 크게 없었지만, 테스트 코드를 작성하며 다음과 같은 고민이 있었다.

고민 1) given 에서 제공하는 값을 세분화 할 수는 없을까?

실제 테스트를 작성하면, 실제로 작동을 위해 주어지는 값과 작동을 위해 초기화 하는 부분이 있었다.
Junit 에서 @Setup 을 이용할 수 있지만, @Setup 은 TestCase 마다 반복적으로 실행되기 때문에 Test case 마다 작동을 위한 초기화가 다른 경우 결국 @Test 내부에 작성해야 했다.

물론, 주석을 잘 작성한다면, 코드를 잘 읽는다면 문제가 없지만 개인적으로 한 눈에 봐도 동작을 이해할 수 있게 만들고 싶었다.

고민 2) 예외 테스트 케이스에서 when의 내용과 then의 내용을 분리할 수 없을까?

기존 Junit + AssertJ 활용 과정에서는 assertThatThrownBy 를 이용하여
람다로 when에 해당하는 "~가 실행되었을 때" 가 then 과 함께 작성되어야 했다.

물론, 두 내용이 함께 작성된다고 큰 죄를 지은 상황도 아니지만, 개인적으로 코드 자체가
"다음 () 안 람다는 예외가 발생되는데 그 예외는 어떤 예외이고 이런 메시지가 들어있어" 가 아닌
"다음 내용을 실행 했을 경우, 이런 예외가 발생하고 이런 메시지가 나와야해" 가 되길 원했다.

사실 해석하기 나름이지만, 개인적으로는 사람의 생각에 맞춰서 절차적으로 구분되길 원했다.

Spock Framework 를 사용해보자!

어떻게 적용하지?

Spock 을 사용하기 위해서는 다음과 같은 의존성을 사용한다.

testImplementation 'org.spockframework:spock-core:2.4-M1-groovy-4.0'
testImplementation 'org.spockframework:spock-spring:2.4-M1-groovy-4.0'

필자는 gradle 환경을 사용했지만 maven 을 사용한다면 아래 링크를 참고하여 pom.xml 파일을 수정해야한다.
https://mvnrepository.com/artifact/org.spockframework/spock-core
https://mvnrepository.com/artifact/org.spockframework/spock-spring

버전 역시 본인의 환경에 맞춰서 선택할 수 있다. 필자는 아직 버전에 따른 문제나 큰 이슈를 찾지 못해 아래 레포지터리에서 2022-11-30에 릴리즈된 최신 버전을 사용했다.
https://github.com/spockframework/spock

추가 적용 : groovy plugin

Spock 의존성을 추가하다보면 groovy 라는 단어를 사용하고 있다.
Spock 은 Groovy 를 바탕으로 테스트 코드를 작성한다.

의존성만 추가하더라도 intellij 에서 테스트를 실행할 수 있었다.
아래 내용에서도 확인할 수 있지만, JUnit 을 기반으로 확장해서 만들었기 때문에 JUnit 을 실행할 수 있는 환경의 intellij 에서는 별다른 설정이 없어도 실행할 수 있다.

하지만, 개인적으로 CI 환경에서도 Spock 으로 작성한 테스트 코드를 돌리고 싶었으나 실행 되지 않아 github 를 살펴보니 다음과 같은 문구를 찾을 수 있었다.

NOTE: Spock 2.x is based on the JUnit 5 Platform and require Java 8+/groovy-2.5+ (Groovy 3.0 or 4.0 is recommended, especially in projects using Java 12+).

지금과 같이 2.x 버전을 사용하면 Junit5를 기반으로 하며 java 8 이상 groovy 2.5 이상이 필요하다고 한다.
개인적으로 진행하는 Spring 프로젝트나 상황에서 Junit5 를 이미 사용하고 있고 java 버전도 8 이상이므로 groovy 만 따로 추가하면 되는 상황이었다.
현재 프로젝트가 Java17 을 사용하므로 적혀진 내용과 같이 12이상에서는 groovy 3.0 이나 4.0 을 권장한다고 하니 다음과 같이 추가했다.

plugins {
    ...
    id 'groovy'
}

dependencies {
    ...
    testImplementation 'org.apache.groovy:groovy:4.0.18'
    testImplementation 'org.spockframework:spock-core:2.4-M1-groovy-4.0'
    testImplementation 'org.spockframework:spock-spring:2.4-M1-groovy-4.0'
}

들어가기 전 알아야할 내용

기존 Junit 환경에서는 BDD 기반 given-when-then 이 주석으로 작성해야 했지만, Spock은 given, when, then 과 같은 블럭을 사용한다.

출처: https://spockframework.org/spock/docs/2.3/spock_primer.html#_blocks

테스트 케이스에 해당하는 메서드는 Spock 에서 Feature 메소드라고 한다.
해당 feature method 에는 위 사진에 해당하는 블럭이 최소 1개 이상 존재해야 한다.

given

기존 given-when-then 패턴의 given 에 해당하는 블럭이다.
테스트에 필요한 객체나 환경을 준비하는 코드를 둔다. 블럭 이름을 setup: 으로 설정해도 같은 동작을 한다.
당연하게도 테스트에 필요한 객체나 환경을 준비하기 때문에 다른 블럭보다 먼저 사용해야 한다.
부가적으로 given 블록을 명시하지 않더라도 Feature method 는 처음부터 다른 블럭이 나오기 전까지를 암묵적으로 given, setup 으로 인식한다고 한다.

when

기존 given-when-then 패턴의 when 에 해당하는 블럭이다.
실제 테스트 하고자 하는 동작을 작성하는 블럭이다.

then

기존 given-when-then 패턴의 then 에 해당하는 블럭이다.
실제 테스트하고 나온 예외나 결과를 확인할 수 있다.
별도의 assert 를 사용하지 않아도 해당 블럭 안에서 비교하는 내용은 모두 assert 문과 동일하다.

하지만 Spock 에서는 여기에서 추가적으로 mocking stubbing 에 해당하는 동작등 Mockito 나 MockK 가 해주는 동작들을 지원한다. 따라서 별도의 Mockito 나 MockK 가 필요하지 않다.


위 3가지 블럭이 기본적으로 사용되는 블럭이라 생각한다. 개인적으로 블럭의 사용만으로도 상당히 편리함을 얻을 수 있었다.

부가적으로 다음과 같은 블럭을 사용할 수 있다.

expect

상황에 따라 when 과 then 이 나누어질 필요가 없는 경우 두 블럭을 합쳐 사용하는 것과 같은 효과를 준다.

cleanup

테스트 케이스에 해당하는 메서드에서 테스트 이후 정리해야할 리소스가 있다면 해당 블럭에서 정리한다.

where

Junit 의 ParameterizedTest 와 같이 순차적으로 실행할 값을 지정할 수 있다.

Java Test To Spock Test

참고로 위에서 Spock 은 Groovy 를 사용한다 했지만, 개인적으로 사용해본 결과 Groovy 를 잘 알지 못해도 테스트 코드 작성이 가능하다.

class LottoGameSpockTest extends Specification{

    private final LottoTicketDispenser ticketDispenser = new LottoTicketDispenser(new AutoLottoNumberGenerator())

    def "로또 게임에 금액을 입력하면 구매된 로또 목록을 얻을 수 있다"() {
        setup:
        def autoCreatedLottoGame = new AutoCreatedLottoGame(ticketDispenser)

        and:
        String cost = "2000"

        when:
        def purchasedLottoTickets = autoCreatedLottoGame.purchaseLottoTickets(cost)

        then:
        purchasedLottoTickets.size() == 2
    }
}

블로그에 작성하는 과정에서 코드 하이라이팅이 잘 먹지 않지만 intellij 기준으로 자동완성과 하이라이팅 모두 만족스럽게 작동한다
기존에 작성한 코드와 비교한다면 첫번째 고민을 해결 할 수 있었다.

첫 번째 고민에서 결국 given 에서 테스트를 위한 객체나 환경을 준비하는 부분과 실제 값을 제공하는 부분의 분리를 and 블럭으로 해결할 수 있었다.
and 블럭으로 논리적으로 다른 부분을 나눌 수 있었다.
따라서 별도의 주석은 없지만, ~내용을 setup 하고 ~가 주어진다 라는 의미로 한눈에 알아보기 쉽게 나눌 수 있었다.

다음으로는 예외 상황 테스트를 위해 다음 예제 역시 Spock을 적용했다.

    def "로또 게임에 유효하지 않은 금액을 입력하면 예외가 발생한다"() {
        setup:
        def autoCreatedLottoGame = new AutoCreatedLottoGame(ticketDispenser)

        and:
        String cost = "2000A"

        when:
        autoCreatedLottoGame.purchaseLottoTickets(cost)

        then:
        def exception = thrown(IllegalArgumentException.class)
        exception.message != "유효하지 않은 금액입니다."
    }

위 과정에서 두 번째 고민역시 해결할 수 있었다.

기존 exception 발생을 테스트 하기 위해 when 과 then 이 합쳐진 형태로 작성되었지만, Spock 에서 이를 분리 할 수 있었다.
따라서 절차적으로 "어떤 과정을 실행하면, 어떤 결과가 나오는지" 분리할 수 있었다.

이외에도 Spock 을 사용하면 다음과 같은 특이한 점을 발견할 수 있다.
1. Specification 을 상속한다.
Spock 을 사용하여 Groovy 로 테스트 코드를 작성하면 테스트 코드를 작성하는 클래스는 반드시 해당 abstract class 를 상속한다.
내부를 조금 살펴본 결과 Spock 에서 사용하는 다양한 기능이 포함되어있다.

예시에서 작성한 thrown과 같은 함수 및 noExceptionThrown 뿐 아니라 Mocking, Stubbing 과 같은 수단을 지원하는 기능이 포함되어있다.

2. 타입 추론을 제공하며 세미콜론; 을 사용할 필요가 없다.
자바와 같이 타입 변수명 순서로 작성할 수 있지만, 정적으로 타입을 지정하지 않아도 된다.
물론 타입은 선택적으로 적을 수 있다.
개인적으로는 kotlin, js/ts 사용 경험이 있어 이는 상당히 편리했다.

더불어 세미콜론을 사용할 필요가 없어 코드 작성 과정에서 보다 편리함을 느꼈다.

그 외 Spock 이 제공하는 기능(JUnit 과 비교)

그 외에도 기존 JUnit 을 기반으로 확장하였기 때문에 유사한 기능역시 제공된다.

https://spockframework.org/spock/docs/1.0/spock_primer.html

기본적으로 상속한 Specification 이외에도 실제 어노테이션으로 사용하는 각 부분을 함수로 이용하여 구현할 수 있다.
또한 각 부분 역시 JUnit 과 비교된 부분과 동일하게 작동한다.

Spock 을 사용하며 느낀 장점

직관적이고 편리하다
직접 타입을 모두 명시하지 않아도 타입 추론이 제공되어 기존 Java JUnit 환경의 테스트 코드 보다 테스트 코드 작성이 편리하다고 느꼈다.

또한 Feature method 에서 블럭을 사용하여 보다 명시적이고 간결한 느낌을 받을 수 있었다.


실패 케이스의 경우 보다 상세하게 정보를 얻을 수 있다
기존 JUnit 에서는 단순히 A 값을 예상했지만 B가 나왔다 정도 알 수 있다.
사실 충분한 정보라고 생각했지만 Spock 의 경우 아래와 같이 보다 자세한 정보를 얻을 수 있었다.

exception.message != "유효하지 않은 금액입니다."
|         |       |
|         |       false
|         유효하지 않은 금액입니다.
java.lang.IllegalArgumentException: 유효하지 않은 금액입니다.

각 부분 마다 실제 어떤 값이 담겨있고 실제 어떤 값이 반환되는지 알 수 있다.

추가적으로 JUnit 에서는 여러 케이스를 한 테스트 메서드에서 돌리면 처음 실패한 케이스만 알 수 있었다.
물론 해당 부분도 JUnit 에서 해결할 수 있지만, Spock 에서 보다 명확하고 상세하게 틀린 케이스를 확인할 수 있었다.


별도의 라이브러리를 더 사용하지 않아도 된다
기존에는 Mock 라이브러리로 EasyMock, Mockito, MockK 등등 다양한 라이브러리를 선택하거나 별도로 AssertJ, Truth 와 같은 라이브러리를 추가했다.
하지만 Spock 은 별도의 세팅이나 사용 방법을 학습하지 않고도 편리하게 테스트 코드를 구성할 수 있었다.

마무리 하며...(Spock의 단점)

실제로 적용하고 테스트 코드를 작성하며 직관적이고 동적 테스트 작성이 쉬워서 좋았다.
또한 groovy 를 사용하는 것 치고 별도로 많은 것을 알지 않아도, 기존 Java 테스트 코드 작성과 비슷하게 작성할 수 있다는 것도 큰 몫을 했다.

하지만 실제 적용 과정에서 단점도 찾을 수 있었다.
실제 아래와 같은 케이스는 아니었지만, 실험하다 보니 아래와 같은 케이스도 얻을 수 있었다.

String cost = 2000A

기존 Java 에서는 컴파일 에러가 발생하는 부분이지만, Spock 에서는 해당 부분에서 컴파일 에러가 발생하지 않았다.

String cost = 2000

추가적으로 위와 같은 경우에도 넣어준 값은 String 이 아니지만, 실제로는 String 으로 처리되고있었다.

즉 컴파일 타임에 오류가 발생할 경우 이를 찾기 어렵다는 단점이 있었다.

결국 상황에 따라 기존 Java 기반 테스트냐, Spock이냐 선택과 집중이 필요하겠다 생각이 들었다.

반응형

'Spring Boot' 카테고리의 다른 글

[JPA] JPA 효율 개선 과정에서 알아본 N+1 문제  (0) 2025.03.03
profile

Antilog의 개발로 쓰다

@Parker_J_S

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

profile on loading

Loading...