tools & libs/단위테스트(junit, spock)

[spock]기본 사용법

  • -

spock의 블록

Spock은 블록을 통해서 할 일들을 정리한다. 기본 블록은 아래와 같다.

  • given / setup
    • 테스트에 필요한 객체나 환경을 준비하는 블록으로 반드시 다른 블록들 보다 먼저 있어야 한다.
    • 키워드 자체는 생략가능하다.
  • when
    • 테스트 하고 싶은 상황을 만드는 영역으로 코드를 실행하는 영역이다.
  • then
    • 테스트 결과를 검증하는 블록으로 여기에 작성된 코드 한줄 한줄이 모두 assert에 해당하는 문장이다.
  • expect
    • when과 then이 합쳐진 형태로 작은 테스트 작성 시 유용하다.
  • cleanup
    • 필요 시 setup의 자원을 정리한다.
  • where
    • 일부 데이터만 바꿔가면서 여러 번 테스틀 할 수 있게 도와주는 영역이다.

 

given / when / then

given / when / then은 테스트에서 가장 기본이 되는 절차이다.

    def "가장 기본이 되는 블록 given/when/then"(){
        given:
        List<String> list = new ArrayList<>();

        when:
        list.add("Hello")

        then:
        list.size()==1
        list.get(0)=="Hello"

        when:
        list.remove("Hello")

        then:
        list.size()==0
    }

위 코드에서 하나의 given 상황에 대해 두 개의 when/then 블록이 설정되어있으며 when/then 블록은 순서대로 실행된다. 자바 개발자라면 내용이 너무 쉽게 이해되리라 생각되므로 설명은 생략한다.

 

expect

가끔은 간단한 테스트를 진행하면서 매번 when/then을 사용하기 번거러울 때가 있다. 이런 경우 expcet 블록을 사용한다.

    def "간단한 when/then일 경우는 expect를 사용해보자"(){
        expect:
        3 == Math.max(1, 3)
        5 == "Hello".length()
    }

 

where

where 블록은 좀 멋진 구석이 있다.  이 블록은 상위 블록에서 사용하는 값들을 변수로 지정하고 값을 공급할 수 있다. 예를 들어 문자열의 길이를 테스트 할 때 테스트할 문자열 마다 when/then 또는 expect를 작성하기는 매우 성가시다. 이럴 때 data pipes 형태로 연속된 값을 공급할 수 있다.

    def "where를 통해 연속된 데이터를 테스트해보자."(){
        given:
        String data = str

        when:
        int len = data.length()

        then:
        len>3

        where:
        str << ["Spock", "Is", "Easy"]
    }

given 블록에는 data에 할당된 값이 문자열이 아닌 변수 str이다. 이 녀석은 느닷없이 어디서 나온 것일까? 정답은 where 블록에 있다.

where에는 str이 선언되어있는데 우측의 리스트에서 << 기호를 통해서 값을 공급받고 있다. 이를 통해 Spock, Is, Easy에 대해 순차적인 검증이 시도된다.

Is는 2 글자이기 때문에 위 테스트는 실패할 것이고 아래와 같은 오류 메시지가 출력된다.

 

data pipe가 하나의 값을 리스트 형태의 데이터를 전달한다면 data table은 여러 개의 값을 테이블 형태로 전달한다.

    def "여러 값을 테이블 형태로 전달하기"()  {
        expect:
            Math.max(name.length(), age) == max
            
        where:
            name    | age  || max
            "Hi"    | 1    || 2
            "Spock" | 6    || 6
            "Java"  | 7    || 7
    }

위의 예는 expect 부분에 name, age, max 3개의 변수가 사용되고 있는데 이들을 선언하는 곳은 역시 where 블록이다. where에서는 | 기호를 이용해서 테이블 모양으로 데이터를 제공하고 있다. age와 max 사이에는 | |  처럼 2개가 사용되고 있는데 이는 단지 원인과 결과를 구분해서 가독성이 좋게 하기 위한 용도이다.

 

예외 처리

예외 상황을 테스트 하기 위해서는 3가지 메서드를 사용할 수 있다.

  • thrown(e): e 타입의 예외가 반드시 던져진다.
  • notThrown(e): e 타입의 예외는 던져지지 않는다.
  • noExceptionThrown(): 어떠한 형태의 예외도 던져지지 않는다.

주의할 점은 이 메서드들은 then 블록에서만 사용 가능하다는 점이다.

    def "예외가 발생하는 상황은 어떻게 처리할까요?"(){
        given:
        List<String> list = new ArrayList<>()
        
        when:
        list.get(0)
        
        then: "throw은 then 블록에서만 사용"
        def e = thrown(IndexOutOfBoundsException)
        e.message=="Index: 0, Size: 0"
        
        when:
        list.add(null)
        
        then: "null을 추가해도 NullPointerException은 발생하지 않는다."
        notThrown(NullPointerException)
        
        when:
        list.size()
        
        then:
        noExceptionThrown()
    }

자바에서 예외 타입을 받을 때는 thrown(XXException.class)와 같이 쓰겠지만 Groovy에서는 .class를 사용하지 않고 그냥 예외 클래스만 기입해도 된다.

첫 번재 then 블록에서는 thrown의 리턴 타입을 def로 할당 받아 예외 객체의 message를 추가로 검증하고 있다. then 블록 옆에 라벨을 달아서 어떤 동작을 처리하고 있는지 표시하는 것도 좋은 습관이다.

 

공통 객체의 설정

 

공통 객체의 설정

테스트를 진행하다보면 하나의 specification 클래스에서 한 두개의 클래스를 집중적으로 테스트하는 경우가 많다. 이때 각 feature에서 객체를 선언하다보면 아무래도 중복이 발생한다. 이때 클래스 레벨에 객체를 변수로 선언하고 생성해두면 각 feature가 동작할 때마다 새로운 객체가 생성된다.

    // 매번 테스트가 동작할 때마다 새로운 객체 생성
    private List newPerTest = []

    def "한번 불러보자 1"(){
        when:
        newPerTest.add(1)
        then:
        newPerTest.size()==1
    }

    def "한번 불러보자 2"(){
        when:
        newPerTest.add(1)
        then:
        newPerTest.size()==1
        when:
        newPerTest.add(1)
        then:
        newPerTest.size()==2
    }

즉 위의 테스트에서 살펴보면 각각의 테스트에서 newPerTest는 별도의 객체이다.

 

Specification 내에서 공유되는 객체

여러 테스트 간에 걸쳐서 공유되는 객체를 선언하기 위해서는 @Shared라는 애너테이션을 사용한다. 그런데 이렇게 작성하면 일반적인 테스트는 순서가 없기 때문에 공유 객체의 상태에 따라서 테스트 결과를 쉽게 예측할 수 없다. 

이런 경우 클래스 레벨에 @Stepwise 애너테이션을 추가하면 작성된 순서대로 테스트를 수행하게 된다.

package com.quietjun

import spock.lang.Shared
import spock.lang.Specification
import spock.lang.Stepwise

@Stepwise
class T04_SharedObj extends Specification{
    // 한 클래스 내의 모든 테스트에서 공유하는 객체 생성
    @Shared
    private List oneForAll = []

    def "한번 불러보자 3"(){
        when:
        oneForAll.add(1)
        then:
        oneForAll.size()==1
    }

    def "한번 불러보자 4"(){
        when:
        oneForAll.add(1)
        then:
        oneForAll.size()==2
        when:
        newPerTest.add(1)
        then:
        newPerTest.size()==3
    }
}

 

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.