본문 바로가기

Programing/테스트

[Spock] when: 및 then: 블럭에서 with 사용하기

Spock에서 기본적인 테스트를 작성했다면 when: 블럭과 then: 블럭에 대해 알 수 있다.

when: 블럭에서의 with

Groovy 문법에 보면 with 라는 키워드로 같은 객체에 대해 동시에 배정(assignment)을 할 수 있다.

이 기본 그루비 문법은 when: 블럭에서 사용이 가능하다.

예. 과일

아래와 같은 Fruit 클래스와 내부에 Color 열거형(enum)이 있다.

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class Fruit {
    private String name;
    private Color color;
    private int quantity;

    @Override
    public String toString() {
        return quantity + "개의 " + color.toString() + " " + name;
    }

    public enum Color {
        RED("빨간"),
        GREEN("녹색"),
        YELLOW("노랑");

        Color(String name) {
            this.name = name;
        }

        @Getter
        private String name;

        @Override
        public String toString() {
            return name;
        }
    }
}

이 클래스를 테스트하는 Spock 테스트 코드가 있다. (Fruit의 toString() 메서드를 테스트 하고 있다.)

import spock.lang.Specification

class FruitTest extends Specification {
    def "Fruit의 toString()을 호출하면 색상과 이름과 수량이 출력된다."() {
        given: "Fruit 객체를 apple이라는 이름으로 생성한다."
        def apple = new Fruit()
        apple.name = "사과"
        apple.color = Fruit.Color.RED
        apple.quantity = 2

        when: "apple의 toString 메서드를 호출한다."
        String result = apple.toString()

        then: "결과는 '2개의 빨간 사과'라고 나온다."
        result == "2개의 빨간 사과"
    }
}

given: 블럭에 보면 같은 apple 객체에 대해 프로퍼티(내부적으로는 setter가 호출된다.)를 사용하고 있다.

아래와 같이 with를 이용하면 동일한 객체에 값을 설정하는 것으로 그룹핑(groupping)의 느낌이 있어 코드를 이해하기 쉬워진다.

import spock.lang.Specification

class FruitTest extends Specification {
    def "Fruit의 toString()을 호출하면 색상과 이름과 수량이 출력된다."() {
        given: "Fruit 객체를 apple이라는 이름으로 생성한다."
        def apple = new Fruit()
        apple.with {
            name = "사과"
            color = Fruit.Color.RED
            quantity = 2
        }

        when: "apple의 toString 메서드를 호출한다."
        String result = apple.toString()

        then: "결과는 '2개의 빨간 사과'라고 나온다."
        result == "2개의 빨간 사과"
    }
}

참고로 with { } 에서 괄호 부분은 문법적으로 클로저(Closure)라고 한다.

또한 아래처럼 다중 배정(multiple assignment)로 값 설정도 가능하다.

import spock.lang.Specification

class FruitTest extends Specification {
    def "Fruit의 toString()을 호출하면 색상과 이름과 수량이 출력된다."() {
        given: "Fruit 객체를 apple이라는 이름으로 생성한다."
        def apple = new Fruit()
        apple.with {
            (name, color, quantity) = ["사과", Fruit.Color.RED, 2]
        }

        when: "apple의 toString 메서드를 호출한다."
        String result = apple.toString()

        then: "결과는 '2개의 빨간 사과'라고 나온다."
        result == "2개의 빨간 사과"
    }
}

then: 블럭에서의 with

하지만 then: 블럭에서의 with는 기본 Groovy 문법으로 하면 안된다. 단정문(assert) 문이기 때문이다.

Spock에서는 기본 그루비 문법과 유사하기 with 블럭을 만들었다.

 

위에서 예를 들었던 Fruit에 객체를 복사하는 clone이라는 메서드를 추가하였다.

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class Fruit implements Cloneable {
    private String name;
    private Color color;
    private int quantity;

    @Override
    public String toString() {
        return quantity + "개의 " + color.toString() + " " + name;
    }

    @Override
    public Object clone() throws CloneNotSupportedException {
        Fruit clone = (Fruit)super.clone();
        clone.setName(this.name);
        clone.setColor(this.color);
        clone.setQuantity(this.quantity);
        return clone;
    }

    public enum Color {
        RED("빨간"),
        GREEN("녹색"),
        YELLOW("노랑");

        Color(String name) {
            this.name = name;
        }

        @Getter
        private String name;

        @Override
        public String toString() {
            return name;
        }
    }
}

이 메서드를 테스트 해보는 코드는 아래와 같다.

import spock.lang.Specification

class FruitTest extends Specification {
    def "Fruit의 clone()을 호출하면 동일한 속성을 가진 객체로 복제가 된다."() {
        given: "Fruit 객체를 apple이라는 이름으로 생성한다."
        def apple = new Fruit()
        apple.with {
            (name, color, quantity) = ["사과", Fruit.Color.RED, 2]
        }

        when: "apple의 clone 메서드를 호출한다."
        Fruit resultOfAnotherApple = (Fruit)apple.clone()

        then: "결과는 만든 객체와 값과 동일하다."
        resultOfAnotherApple.name == "사과"
        resultOfAnotherApple.color == Fruit.Color.RED
        resultOfAnotherApple.quantity == 2

        and: "apple과 복제된 객체는 다른 객체이다."
        resultOfAnotherApple != apple
    }
}

코드에 한글로 설명이 달려있으므로 생략한다.

then: 블럭을 Spock의 with로 그룹핑을 해보자.

import spock.lang.Specification

class FruitTest extends Specification {
    def "Fruit의 clone()을 호출하면 동일한 속성을 가진 객체로 복제가 된다."() {
        given: "Fruit 객체를 apple이라는 이름으로 생성한다."
        def apple = new Fruit()
        apple.with {
            (name, color, quantity) = ["사과", Fruit.Color.RED, 2]
        }

        when: "apple의 clone 메서드를 호출한다."
        Fruit resultOfAnotherApple = (Fruit)apple.clone()

        then: "결과는 만든 객체와 값과 동일하다."
        with(resultOfAnotherApple) {
            name == "사과"
            color == Fruit.Color.RED
            quantity == 2
        }

        and: "apple과 복제된 객체는 다른 객체이다."
        resultOfAnotherApple != apple
    }
}

중복이 제거되면서 하나로 묶으니 훨씬 보기가 좋다. 물론 이 문법을 모른다면 처음에 보고 '이게 뭐지?'하면서 의아해 할 수 있다.

주의. with(객체) { } 를 실수로 객체.with { } 로 쓸 수 있는데 이렇게 하면 단정이 되지 않는다.

리팩터링1 - 도우미 단정문 추가

and: 블럭으로 연결된 단정문을 아래와 같은 도우미 단정문(helper assertion)을 도입할 수 있다.

import spock.lang.Specification

class FruitTest extends Specification {
    def "Fruit의 clone()을 호출하면 동일한 속성을 가진 객체로 복제가 된다."() {
        given: "Fruit 객체를 apple이라는 이름으로 생성한다."
        def apple = new Fruit()
        apple.with {
            (name, color, quantity) = ["사과", Fruit.Color.RED, 2]
        }

        when: "apple의 clone 메서드를 호출한다."
        Fruit resultOfAnotherApple = (Fruit)apple.clone()

        then:
        '결과는 만든 객체와 값과 동일하며 두 객체는 다르다.'(apple, resultOfAnotherApple)
    }

    private void '결과는 만든 객체와 값과 동일하며 두 객체는 다르다.'(Fruit original, Fruit cloned) {
        with(cloned) {
            name == "사과"
            color == Fruit.Color.RED
            quantity == 2
        }
        assert cloned != original
    }
}

then: 블럭이 한글로 되어 있어 이해하기도 쉽고 내용도 명료하다.

주의1 private void 대신 def 를 쓰고 싶은 욕망도 있을 수 있지만 이렇게 하면 동작하지 않았다. 그냥 void는 가능하다.

주의2 assert cloned != original 대신에 cloned != original 만 쓰고 싶을 수도 있지만 제대로 단정이 되지 않는다.

리팩터링2 - 하드코딩 값 대신 헬퍼의 인자를 이용한다.

import spock.lang.Specification

class FruitTest extends Specification {
    def "Fruit의 clone()을 호출하면 동일한 속성을 가진 객체로 복제가 된다."() {
        given: "Fruit 객체를 apple이라는 이름으로 생성한다."
        def apple = new Fruit()
        apple.with {
            (name, color, quantity) = ["사과", Fruit.Color.RED, 2]
        }

        when: "apple의 clone 메서드를 호출한다."
        Fruit resultOfAnotherApple = (Fruit)apple.clone()

        then:
        '결과는 만든 객체와 값과 동일하며 두 객체는 다르다.'(apple, resultOfAnotherApple)
    }

    void '결과는 만든 객체와 값과 동일하며 두 객체는 다르다.'(Fruit original, Fruit cloned) {
        with(cloned) {
            name == original.name
            color == original.color
            quantity == original.quantity
        }
        assert cloned != original
    }
}

리팩터링3 - 객체 만드는 것도 도우미 메서드로 추출한다.

단정 메서드만 뺄 수 있는 것이 아니다. 이 글에 언급된 테스트는 동일한 Fruit 객체를 만들고 있다.

method extract를 할 수 있다.

import spock.lang.Specification

class FruitTest extends Specification {
    def "Fruit의 toString()을 호출하면 색상과 이름과 수량이 출력된다."() {
        given:
        def apple = "Fruit 객체를 apple이라는 이름으로 생성한다."()

        when: "apple의 toString 메서드를 호출한다."
        String result = apple.toString()

        then: "결과는 '2개의 빨간 사과'라고 나온다."
        result == "2개의 빨간 사과"
    }

    def "Fruit의 clone()을 호출하면 동일한 속성을 가진 객체로 복제가 된다."() {
        given:
        def apple = "Fruit 객체를 apple이라는 이름으로 생성한다."()

        when: "apple의 clone 메서드를 호출한다."
        Fruit resultOfAnotherApple = (Fruit)apple.clone()

        then:
        '결과는 만든 객체와 값과 동일하며 두 객체는 다르다.'(apple, resultOfAnotherApple)
    }

    private Fruit "Fruit 객체를 apple이라는 이름으로 생성한다."() {
        def apple = new Fruit()
        apple.with {
            (name, color, quantity) = ["사과", Fruit.Color.RED, 2]
        }
        return apple
    }

    private void '결과는 만든 객체와 값과 동일하며 두 객체는 다르다.'(Fruit original, Fruit cloned) {
        with(cloned) {
            name == original.name
            color == original.color
            quantity == original.quantity
        }
        assert cloned != original
    }
}

참고로, 리팩터링 하기 전의 테스트 코드는 아래와 같다. (위의 코드와 비교해 보라. 어느 것이 눈에 잘 들어오는지?)

import spock.lang.Specification

class FruitTest extends Specification {
    def "Fruit의 toString()을 호출하면 색상과 이름과 수량이 출력된다."() {
        given: "Fruit 객체를 apple이라는 이름으로 생성한다."
        def apple = new Fruit()
        apple.name = "사과"
        apple.color = Fruit.Color.RED
        apple.quantity = 2

        when: "apple의 toString 메서드를 호출한다."
        String result = apple.toString()

        then: "결과는 '2개의 빨간 사과'라고 나온다."
        result == "2개의 빨간 사과"
    }

    def "Fruit의 clone()을 호출하면 동일한 속성을 가진 객체로 복제가 된다."() {
        given: "Fruit 객체를 apple이라는 이름으로 생성한다."
        def apple = new Fruit()
        apple.with {
            (name, color, quantity) = ["사과", Fruit.Color.RED, 2]
        }

        when: "apple의 clone 메서드를 호출한다."
        Fruit resultOfAnotherApple = (Fruit)apple.clone()

        then: "결과는 만든 객체와 값과 동일하다."
        resultOfAnotherApple.name == "사과"
        resultOfAnotherApple.color == Fruit.Color.RED
        resultOfAnotherApple.quantity == 2

        and: "apple과 복제된 객체는 다른 객체이다."
        resultOfAnotherApple != apple
    }
}

더 읽을 거리