Spock 도입기

stanley.·2025년 1월 7일
0

💻 Spock - 정의


  • Java 및 Groovy 애플리케이션을 테스트 하기 위한 BDD(Behaviour-Driven Development) 프레임워크에요.
  • TDD 프레임워크인 Junit과 비슷한 점이 많지만, 기대하는 동작과 테스트의 의도를 더 명확하게 드러내줘요.
  • 직관적인 문법과 강력한 기능으로 널리 사용되고 있어요.
  • Groovy 언어를 기반으로 작성되었으며, Groovy의 간결한 문법과 동적 기능을 활용해 테스트 코드 작성을 더 쉽게 만들어줘요.

💡 BDD (Behaviour-Driven Development) 란 ?

행동(Behavior) 중심으로 개발 과정을 이끌어가는 방식입니다.
BDD는 TDD(Test-Driven Development)에서 파생된 개념으로, 
 사용자 관점에서 시스템의 동작을 정의하고, 이를 개발 및 테스트 과정에 반영합니다.

BDD는 개발자, 테스터, 비즈니스 이해관계자(Stakeholder) 간의 효과적인 소통을 돕고,
이해를 공유하며, 요구사항을 구체화하는 데 중점을 둡니다. 
 
이로 인해 개발팀과 비즈니스 팀 간의 협업이 강화되고, 
사용자 요구에 부합하는 소프트웨어를 더 효과적으로 개발할 수 있습니다.
  • 개발자 커뮤니티에서는 테스트와 테스트 메소드보다는 명세와 행위라는 용어를 거론하기 시작했어요.
  • 더 적합한 용어를 찾는 노력의 부산물로, BDD 커뮤니티는 JUnit 등 기존 테스트 프레임워크의 대안도 다수 만들어낼 수 있었다고 해요.

Spock - 특징


  1. 직관적이고 간결한 DSL (Domain Specific Language)

  • Spock은 테스트를 작성하기 위한 자체 DSL을 제공해요.
  • 이를 통해, 사람이 읽기 쉽게 구성하여 테스트의 의도를 명확히 드러내요.
  • 테스트 메소드는 given, when, then, expect, where과 같은 블록으로 구성되요.
  1. given (혹은 setup) : 테스트 하기 위한 기본 설정 작업 (테스트 환경 구축)
  2. when : 테스트할 대상 코드를 실행 (Stimulus)
  3. then : 테스트할 대상 코드의 결과를 검증 (이 메소드 범위에선 한 줄 한 줄 자동 assert가 됩니다.)
  4. expect : 테스트할 대상 코드를 실행 및 검증 (when + then)
  5. where : feature 메소드를 파라미터로 삼아 실행시킵니다.

Junit vs Spock


항목JUnitSpock
철학단위 테스트 중심의 전통적 테스트 프레임워크BDD(Behavior-Driven Development) 및 단위 테스트 지원
언어 기반JavaGroovy (JVM 기반의 동적 언어)
테스트 스타일절차적, 명령형 테스트 작성선언적, 가독성 높은 테스트 작성

Spock은 Groovy를 기반으로 작성되어서, 이는 Java보다 간결하고 가독성이 높은 문법을 제공해요.

아주 간단한 예제를 살펴볼게요.

JUnit 예제 (Java)

@Test
void testAddition() {
    // given
    int a = 2;
    int b = 3;

    // when
    int result = a + b;

    // then
    assertEquals(5, result);
}

Spock 예제 (Groovy)

def "addition works correctly"() {
    given:
    def a = 2
    def b = 3

    when:
    def result = a + b

    then:
    result == 5
}
  • Spock은 given-when-then 스타일을 제공해 코드의 의도를 더 명확히 표현할 수 있어요.

Mocking 및 Stubbing

  • JUnit은 Mocking/Stubbing을 위해 별도의 라이브러리(예: Mockit, EasyMock)가 필요합니다.
  • Spock은 내장된 Mocking/Stubbing 기능을 제공하며, 문법도 간단합니다.

JUnit의 Mocking (Mockito 사용):

@Test
void testWithMock() {
    MyService service = mock(MyService.class);
    when(service.getData()).thenReturn("mocked data"); //mocking

    assertEquals("mocked data", service.getData());
}

Spock의 Mocking

groovy
코드 복사
def "test with mock"() {
    given:
    def service = Mock(MyService)
    service.getData() >> "mocked data"

    expect:
    service.getData() == "mocked data"
}

Junit 기반의 테스트 코드 문법과 Spock 기반의 테스트 코드 문법의 유사 사항

SpockJUnit
SpecificationTest class
setup()@Before
cleanup()@After
setupSpec()@BeforeClass
cleanupSpec()@AfterClass
FeatureTest
Feature methodTest method
Data-driven featureTheory
ConditionAssertion
Exception condition@Test(expected=…​)
InteractionMock expectation (e.g. in Mockito)

도입 방법


  • build.gradle

  • 위와 같이 groovy를 설정하면 Spock을 사용할 수 있는 환경을 구축할 수 있어요.

예제를 통해 Spock을 적용해보기


  • 이러한 요구사항이 있다고 가정해 봅시다.

취소 조건 검증

  • 충전 취소 가능 대상은 충전성공 상태의 거래건
  • 충전 후 해당 포인트 사용 이력이 없어야 함

취소 기한 검증

  • 충전결제일시 로 부터 7일 이내 충전 거래는 취소 가능하다.
  • 휴대폰결제의 경우 당월 충전 분에 한해 충전결제일시로 부터 7일 이내에 취소 가능하다.

기존에 저는 이렇게 테스트 했어요.


  • 위와 같은 조건을 테스트하기 위해 기존의 경우 테스트 데이터를 위에 맞게 개발계 서버에 세팅한 뒤 수동으로 테스트를 진행했어요.

ex) 취소 기한 검증을 위해 충전결제일자를 수기로 업데이트 합니다.

DEVAPP> UPDATE DEVAPP.CHG_PAY t SET t.CHG_PAY_DH = '20241231161257' WHERE t.CHG_PAY_NO LIKE 'BK250106161242110607
[2025-01-07 10:52:41] 1 row affected in 6 ms
DEVAPP> UPDATE DEVAPP.CHG_PAY t SET t.CHG_PAY_DH = '20241231161238' WHERE t.CHG_PAY_NO LIKE 'BK250106161155110605'
[2025-01-07 10:52:41] 1 row affected in 6 ms
DEVAPP> UPDATE DEVAPP._CHG_PAY t SET t.CHG_PAY_DH = '20241231160708' WHERE t.CHG_PAY_NO LIKE 'BK250106160644110603'
  • 업데이트 한 데이터를 바탕으로 검증 API에 수기로 요청을 보내요.
  • 로그로 결괏값들을 확인해가며 검증 로직이 정상 동작 했는지 확인해요.
💡 테스트 하기 위해 서버를 띄우고, 데이터를 조작하며, 확인 용도의 로그 찍기등 테스트 하기 위해 부가적으로 해야할 것들이 많이 보이지 않으신가요?

💡 물론 통합 테스트를 위해서 위의 명시한 내용은 모두 진행 해야 지만, 단위 테스트를 통해 기능의 정상 동작이 보장된다면 테스트 수행 시간을 줄이고 효율적으로 진행할 수 있는 기대감이 생기지 않을까요?

💡 기존에 테스트 했던 상황들을 생각해보아요.

Spock 으로 테스트 해보자


  • Spock을 활용해 테스트 코드로 테스트의 효율성을 높혀봅시다.
  • 예를 들어 앞서 말씀 드린 취소 기한에 대해 검증을 수행한다고 해볼게요.
	def "휴대폰 결제의 경우 당월 취소만 충전 취소가 가능하다."() {
        when:
        def isCancelable = service.isCancelAvailablePhonebillTxn(chgPayDh)

        then:
        isCancelable == result

        where:
        chgPayDh || result
        "20241231145000" || false
        "20251231145000" || false
    }

    def "충전결제 일자가 현재 일자로부터 7일 이상인지 판단한다."() {
        when:
        def isCancelable = service.isChgTxnOverSevenDays(chgDay)

        then:
        isCancelable == result

        where:
        chgDay || result
        "20241230102231" || true
        "20241231102231" || true
        "20250101102231" || false

    }

    def "휴대폰 결제의 경우 당월 충전 분에 한하여 7일 이내에 취소 가능하다."() {
        given:
        bubiCashChargeCancelAvailableTxnMapper.getChargePayMethod(_) >> createPhoneBillChgPayMethod()
        bubiCashChargeCancelAvailableTxnMapper.getChgTxnListWithinSevenDays(_) >> createChgCancelPhonebillTxnList()
        bubiCashChargeCancelAvailableTxnMapper.getTotalPayAmtWithinSevenDays(_, _, _) >> 2000

        when:
        def chgCancelAvaliableList = service.getChargeCancelAvailableTxnList(request)

        then:
        chgCancelAvaliableList.size() == 2;
    }

    def "결제 완료일로부터 7일이 지난 충전 거래라면 취소가 불가능하다."() {
        given:
        bubiCashChargeCancelAvailableTxnMapper.getChargePayMethod(_) >> createChargePayMethod()
        bubiCashChargeCancelAvailableTxnMapper.getChgTxnListWithinSevenDays(_) >> createChgCancelNotAvailableTxn()
        bubiCashChargeCancelAvailableTxnMapper.getTotalPayAmtWithinSevenDays(_, _, _) >> 4000

        when:
        def chgCancelAvailableList = service.getChargeCancelAvailableTxnList(request)

        then:
        chgCancelAvailableList.size() == 0
    }
  • 위와 같이 Spock의 경우 given - when - then 구조가 명시적으로 표기되어야 하는 것이 규칙이기 때문에 테스트 코드의 의도를 명확하게 파악할 수 있어요.
  • 인자값으로 설정된 _ 는 와일드카드로서, 메서드 호출 시 전달되는 특정 파라미터 값을 무시하거나 어떤 값이 전달 되더라도 매칭되도록 설정할 수 있어요
  • 또한, 테스트 케이스들을 where 내부에 다양하게 정의해보고 결괏값을 얻음으로써 간략하고 빠르게 테스트 결과를 확인할 수 있어요.
  • 테스트 케이스를 설계하고 Mocking을 통해 테스트 데이터 값을 세팅함으로써 원하는 케이스에 대해 테스트를 진행할 수 있어요.
  • 또한, Fixture 클래스를 만들어서 테스트에 필요한 데이터들을 세팅해둔 것을 알 수 있어요.

테스트를 도와주는 Fixture와 Mocking


💡 Fixture

test fixture is a device used to consistently test some item, device, or piece of software. Test fixtures are used in the testing of electronics, software and physical devices.

  • Test Fixture는 테스트를 수행하기 전에 필요한 상태나 환경을 설정하는 것을 의미합니다. 예를 들어, 데이터베이스 연결을 성립하거나 테스트 데이터를 생성하는 등의 작업이 Test Fixture에 해당합니다.
  • 이와 같이 간단하게 테스트 하기 위해 필요한 데이터들이 담긴 객체를 생성하여 테스트 대상으로 활용할 수 있어요.

💡 Mocking

주로 객체 지향 프로그래밍으로 개발한 프로그램을 테스트 할 경우 실제의 모듈을 "흉내"내는 "가짜" 모듈을 작성하여 테스트의 효용성을 높이는데 사용하는 객체이다. 사용자 인터페이스(UI)나 데이터베이스 테스트 등과 같이 자동화된 테스트를 수행하기 어려운 때 널리 사용된다.

  • Spock에서 Mock 객체의 반환값은 >>로 지정해요.
  • Mocking을 통해 특정 데이터의 반환 값을 가정하고 해당 데이터가 반환 되는 경우 어떻게 작동하는지 테스트 해볼 수 있어요.

정리 - 테스트 코드를 작성해야 하는 이유


  • 주관적이지만 정리해보았어요.
  • 서버를 띄울 필요도, 로그를 찍을 필요도, 데이터를 조작할 필요도 없어요.
  • 테스트 케이스들을 설계하고 이에 맞게 테스트 코드를 작성하면 수동 테스트에 비해 비교적 빠르게 테스트 결과를 확인할 수 있어요.
  • 단위 테스트가 보장된 통합 테스트는 테스트 수행 시간을 단축시킬 수 있다고 확신해요.

레퍼런스


https://jojoldu.tistory.com/228
https://techblog.woowahan.com/2560/
https://spockframework.org/spock/docs/1.0/spock_primer.html

profile
🖥 Junior Developer.

0개의 댓글