[번역]Interaction Based Testing

Spock의 공식문서 중 Interaction Based Testing를 번역해 보았습니다.

보실 때 오역이 있음을 감안하시고, 번역하기에 애매한 부분은 원문 그대로 옮겼습니다.


상호작용 기반 테스팅은 2000년대 초반 Extreme 프로그래밍(XP) 커뮤니티에서 등장한

설계 및 테스팅 기술이다. 그것은 상태가 아닌 객체의 동작에 초점을 맞추고 명세안에서 객체가

메서드 호출을 통해 그들의 협력자들과 어떻게 상호작용 하는지 연구한다.

예를 들어, 구독자에게 메세지를 보내는 게시자가 있다고 가정해보자.

class Publisher {
  List<Subscriber> subscribers = []
  void send(String message){
    subscribers*.receive(message)
  }
}

interface Subscriber {
  void receive(String message)
}

class PublisherSpec extends Specification {
  Publisher publisher = new Publisher()
}

게시자를 어떻게 테스트 할 것인가? 상태기반 테스팅에서는, 게시자가 그의 구독자를 추적하는지

검증 할 수 있다. 더 흥미로운 질문은 게시자가 보낸 메세지를 구독자가 수신하는지 여부이다.

이 질문에 대답하려면, 게시자와 구독자간의 대화를 듣는 구독자의 특별한 구현이 필요하다.

이러한 구현을 mock 객체라고 부른다.

우리는 구독자의 가짜 구현을 손으로 만들 수 있지만,  이 코드를 만들고 유지보수하는 것은

메서드수와 상호작용의 복잡성이 증가함에 따라 괴로워 질 수 있다.

이곳이 mocking framework가 들어오는 곳이다.

그것은 명세안에서 객체와 그들의 협력자 사이의 예상되는 상호작용을 설명하는 방법을

제공하고, 이러한 예상을 검증하는 협력자의 가짜 구현을 만들 수 있다.

가짜 구현은 어떻게 생성되는가?

대부분의 자바 mocking framework와 마찬가지로 Spock은 런타임에 가짜 구현을 만들어내기

위해 JDK dynamic proxies(인터페이스를 mocking할 때)와 CGLIB proxies(클래스를

mocking할 때)를 사용한다.

Groovy  메타 프로그래밍을 기반으로 하는 구현과 비교하면 자바 코드를 테스트할 때도 유용하

다.  자바세계에서는 인기있고 성숙한 mocking framework(JMock, EasyMock, Mockito)이 많

다. 이 도구들 각각은 Spock과 사용할 수 있지만, 우리는 Spock의 명세언어와 긴밀하게 통합된

자체 mocking framework을 사용하기로 결정했다. 이 결정은 Groovy의 모든 기능을 활용하여

상호작용기반 테스트를 보다 더 쉽게 만들고, 더 읽기쉽고, 궁극적으로 더 재미있게 만들려는

의도에서 비롯되었다. 이 장의 끝에서 당신은 이러한 목표를 달성하는 데 동의하게 되기를 바란

다. 표시된 부분을 제외하고, Spock의 mocking framework의 모든 기능은 java 및 Groovy

코드 테스트를 위해 둘다 모두 작동한다.

Creating Mock Objects

Mock 객체는 Mock 메서드로 생성된다. 두개의 가짜 구독자를 만들어보자.

def subscriber = Mock(Subscriber)
def subscriber2 = Mock(Subscriber)

또는 자바와 비슷한 문법이 지원되므로 더 나은 IDE 지원을 제공할 수도 있다.

Subscriber subscriber = Mock()
Subscriber subscriber2 = Mock()

여기 mock 타입은 할당의 왼쪽에 있는 변수 타입으로부터 유추된다.

NOTE | mock 타입이 할당의 왼쪽에서 주어지면 (필수는 아니지만)오른쪽에서는 생략할 수

있다. Mock 객체는 말 그대로 타입을 구현(또는 클래스의 경우 확장)한다. 즉, 우리 예제의

subscriber는 Subscriber이다. 따라서 이 타입을 예상하는 정적타입(Java) 코드로

전달 할 수 있다.


Default Behavior of Mock Objects

Lenient vs. Strict Mocking Frameworks

Mockito에서처럼, 우리는 mocking framework가 기본적으로 관대해야 한다고 믿는다.

이것은 mock 객체에 대한 예상치못한 호출(다른 말로 현재 테스트중인 테스트와 관련없는

상호작용)이 허용되고 기본응답으로 대답함을 의미한다. 반대로, EasyMock 및 JMock 같은

mocking framework는 기본적으로 엄격하고, 예상치 못한 메서드 호출에 예외가 발생한다.

엄격함은 엄함을 강요하지만, 과한 명세로 이어질 수 있고, 결과적으로 다른 내부코드가

변경될때마다 테스트가 깨질 수 있다. Spock의 mocking framework를 사용하면 명세와 관련된

함정을 피하면서 상호작용과 관련된 내용만 쉽게 설명할 수 있다.

초기에 mock객체는 아무런 동작을 하지 않았다. 메서드 호출은 허용했지만 메서드의 리턴타입

(false, 0, 혹은 null)에 대한 기본값을 리턴하는 것외는 아무런 효과가 없었다.예외

는 Object.equals, Object.hashCode, and Object.toString 메서드같은 기본동작을 갖는다.

mock 객체는 그 자체로 동일하고 고유한 해시코드와 그것이 나타내는 타입의 이름을 포함한

문자열 표현을 가진다. 이 기본동작은 메서드를 stubbing하여 재정의할 수 있고,

Stubbing section에서 배울 것이다.


Injecting Mock Objects into Code Under Specification

게시자와 구독자를 만든 후에 우리는 게시자에게 구독자를 알릴 필요가 있다.

class PublisherSpec extends Specification {
  Publisher publisher = new Publisher()
  Subscriber subscriber = Mock()
  Subscriber subscriber2 = Mock()

  def setup() {
    publisher.subscribers << subscriber // << is a Groovy shorthand for List.add()
    publisher.subscribers << subscriber2
  }

이제 두 당사자간의 예상되는 상호작용을 설명할 준비가 되었다.


Mocking

Mocking은 명세하에서 객체와 협력자들 사이의 상호작용을 기술하는 행위이다.

여기 예제가 있다.

def "should send messages to all subscribers"() {
  when:
  publisher.send("hello")

  then:
  1 * subscriber.receive("hello")
  1 * subscriber2.receive("hello")
}

게시자가 ‘hello’ 메세지를 보낼 때, 두 구독자들은 정확히 한번씩 메세지를 받아야 한다.

이 기능 메서드가 실행되면, when block에서 실행 중에 일어난 mock 객체들의 모든

호출이 then: block에서 기술된 상호작용과 일치될 것이다.

상호작용 중 하나가 충족하지 않으면, InteractionNotSatisfiedError가 발생할 것이다.

이 검증은 자동으로 수행되므로 어떤 추가코드도 필요하지 않다.

Interactions

상호작용은 단지 보통의 메서드 호출인가?

그렇지 않다. 상호작용은 보통의 메서드 호출과 유사해 보이지만, 그것은 메서드 호출이 일어날것

으로 예상되는 표현의 방법일 뿐이다. 상호작용에 대한 좋은 생각은 mock 객체로 들어오는

모든 호출과 서로 일치한다는 것이다. 상황에 따라, 상호작용은 0,1 또는 다수의 호출과

일치할 수 있다.

그럼 then: block에 대해 자세히 살펴보자. 두가지 상호작용은 각각 carinality, a target

constraint, a method constraint, an argument constraint 4개의 뚜렷한 파트를

포함한다.
1 * subscriber.receive("hello")
|   |          |       |
|   |          |       argument constraint
|   |          method constraint
|   target constraint
cardinality

Cardinality

상호작용의 cardinality는 얼마나 자주 메서드 호출이 발생할지를 설명한다.

그것은 고정된 번호이거나 범위로 주어질 수 있다.

1 * subscriber.receive("hello")      // exactly one call
0 * subscriber.receive("hello")      // zero calls
(1..3) * subscriber.receive("hello") // between one and three calls (inclusive)
(1.._) * subscriber.receive("hello") // at least one call
(_..3) * subscriber.receive("hello") // at most three calls
_ * subscriber.receive("hello")      // any number of calls, including zero
                                     // (rarely needed; see 'Strict Mocking')

Target Constraint

상호작용의 target constraint는 mock 객체가 어떤 메서드 호출을 받을 것으로 예상되는가를

설명한다.

1 * subscriber.receive("hello") // a call to 'subscriber'
1 * _.receive("hello")          // a call to any mock object

Method Constraint

상호작용의 method constraint는 어떤 메서드가 호출될 것인가를 설명한다.

1 * subscriber.receive("hello") // a method named 'receive'
1 * subscriber./r.*e/("hello")  // a method whose name matches the given regular expression
                                // (here: method name starts with 'r' and ends in 'e')

getter 메서드 호출이 예상될 때, Groovy 속성 구문은 메서드 구문 대신 사용할 수 있다.

1 * subscriber.status // same as: 1 * subscriber.getStatus()

setter 메서드 호출이 예상될 때, 오직 메서드 구문만 사용할 수 있다.

1 * subscriber.setStatus("ok") // NOT: 1 * subscriber.status = "ok"

Argument Constraints

상호작용의 argument constraints는 예상되는 메서드 인자값을 설명한다.

1 * subscriber.receive("hello")     // an argument that is equal to the String "hello"
1 * subscriber.receive(!"hello")    // an argument that is unequal to the String "hello"
1 * subscriber.receive()            // the empty argument list (would never match in our example)
1 * subscriber.receive(_)           // any single argument (including null)
1 * subscriber.receive(*_)          // any argument list (including the empty argument list)
1 * subscriber.receive(!null)       // any non-null argument
1 * subscriber.receive(_ as String) // any non-null argument that is-a String
1 * subscriber.receive({ it.size() > 3 }) // an argument that satisfies the given predicate
                                          // (here: message length is greater than 3)

Argument constraints는 여러개의 인자가 있는 메서드에 대해서도 예상대로 동작한다.

1 * process.invoke("ls", "-a", _, !null, { ["abcdefghiklmnopqrstuwx1"].contains(it) })

vararg 메서드를 다룰 때, 다음과 같은 상호작용에서도 varrag 구문이 사용된다.

interface VarArgSubscriber {
    void receive(String... messages)
}

...

subscriber.receive("hello", "goodbye")
Spock Deep Dive: Groovy Varargs

Groovy는 배열타입의 마지막 파라미터를 vararg style로 호출할 수 있다.따라서,

vararg 구문은 그러한 메서드와 일치하는 상호작용에 사용될 수 있다.

Matching Any Method Call

때때로 임의의 “어떤 것”과 일치할 때 다음과 같은 단어를 쓰면 유용하다.

1 * subscriber._(*_)     // any method on subscriber, with any argument list
1 * subscriber._         // shortcut for and preferred over the above

1 * _._                  // any method call on any mock object
1 * _                    // shortcut for and preferred over the above

(..) * .(*_) >> _는 유효한 상호작용 선언이지만, 특별히 좋은 스타일도 아니다.

Strict Mocking

어떤 메서드 호출과 일치하는 것이 유용할까? 좋은 예는 명시적으로 선언된 것 이외의

상호작용이 허용되지 않는 mocking 스타일의 엄격한 mocking이다.

when:
publisher.publish("hello")

then:
1 * subscriber.receive("hello") // demand one 'receive' call on 'subscriber'
_ * auditing._                  // allow any interaction with 'auditing'
0 * _                           // don't allow any other interaction

0 *은 then: block 또는 메서드의 마지막 상호작용에서 의미가 있다.

모든 호출은 auditing component와 상호작용할 수 있는 _ *를 사용해라

_ * 는 엄격한 mocking의 문맥에서만 의미가 있다. 특히, Stubbing 호출시에는 필요하지 않다.

예를 들어, _ * auditing.record() >> “ok”는 auditing.record() >> “ok”로 할 수 있다.

Where to Declare Interactions

지금까지 우린 then: block에 모든 상호작용을 선언했다. 이는 자연스럽게 스펙을 읽도록 해준다.

그럼에도,  그것들을 만족시키는 상호작용을 when: 블록전에 어느곳이든 넣을 수 있다.

특히, 이것은 상호작용이 setup 메서드에도 선언될 수 있음을 의미한다.

또한 동일한 specification 클래스의 어떤 helper 인스턴스 메서드에도 상호작용을 선언할 수

있다. mock 객체가 상호작용이 일어날 때, 상호작용은 상호작용이 선언된 순서와 일치한다.

만약 호출이 여러 상호작용과 일치한다면, 상위 호출한계에 도달하지 못한 가장 초기에 선언된

상호작용이 승리할 것이다. 이 규칙에 예외가 하나 있다.

then: block에 선언된 상호작용은 다른 상호작용보다 먼저 비교된다.

예를 들어, then block에 선언된 상호작용으로 setup 메서드에서 선언된 상호작용을

오버라이드 할 수 있다.

Spock Deep Dive: How Are Interactions Recognized?

달리 말하면, 정규 메서드 호출이 아니고 상호작용 선언을 표현하는 것이 무엇인가?

Spock은 간단한 구문 규칙으로 상호작용을 인식한다.

즉, 표현식이 statement 위치에 있고, * 또는 right-shift 연산인 경우, 상호작용으로 간주되어

구문 분석됩니다. 그러한 표현은 statement 위치에서 거의 가치가 없으므로, 의미를

바꾸는 것은 좋은 일이 될 것이다.

연산이 cardinality (mocking할 때) 혹은 응답 발생기(stubbing시)를 선언하는 구문과 일치하는

방법에 주목하라. 둘 중 하나는 항상 존재해야 한다.

foo.bar()는 혼자 상호작용으로 간주되지 않을것이다.

Declaring Interactions at Mock Creation Time (New in 0.7)

mock이 변하지 않는 기본 상호작용이 있을경우, mock 생성시 다음과 같이 선언할 수 있다.

def subscriber = Mock(Subscriber) {
   1 * receive("hello")
   1 * receive("goodbye")
}

이 기능은 Stubbing 및 전용 Stubs에 특히 유용하다. 상호작용은 target constraint를

가지지 못한다. 그들이 속한 mock 객체의 문맥에서 명확하다.

상호작용은 mock과 함께 인스턴스 필드를 초기화할 때 선언할 수 있다.

class MySpec extends Specification {
    Subscriber subscriber = Mock {
        1 * receive("hello")
        1 * receive("goodbye")
    }
}

Grouping Interactions with Same Target (New in 0.7)

동일한 대상을 공유하는 상호작용은 Specification.with block에서 그룹화할 수 있다.

Mock 생성시 상호작용 선언과 유사하게, 이것은 target constraint 반복이 불필요하다.

with(subscriber) {
    1 * receive("hello")
    1 * receive("goodbye")
}

with block은 또한 같은 대상에 한해 조건을 그룹화하여 사용할 수 있다.

Mixing Interactions and Conditions

then: block은 상호작용과 조건 둘다 포함한다. 그것이 꼭 필요하지 않더라도, 조건 전에

상호작용을 선언하는 것이 일반적이다.

when:
publisher.send("hello")

then:
1 * subscriber.receive("hello")
publisher.messageCount == 1

publisher가 ‘hello’ 메세지를 보내면, subscriber는 정확히 한번 메세지를 받고,

publisher의 메세지 카운트는 1이 될 것이다.

Explicit Interaction Blocks

내부적으로, Spock은 기능 실행 전 예상되는 상호작용에 대한 완전한 정보를 가지고 있어야한다.

then: block에서 선언된 상호작용이 어떻게 이루어질까? 대답은 when: block 진행전에

곧바로 then: block에 선언된 상호작용으로 이동한다.

대부분 이것은 잘 동작하지만 때때로 문제를 일으킬 수 있다.

when:
publisher.send("hello")

then:
def message = "hello"
1 * subscriber.receive(message)

여기 예상된 인자값을 위한 변수를 선언했다.(혹은 cardinality를 위한 변수선언을 했다)

하지만, Spock은 상호작용이 변수와 본질적으로 연결되어 있다고 말할 만큼 똑똑하지 않다.

그래서 상호작용으로 이동하면 MissingPropertyException이 발생할 것이다.

이 문제를 해결하는 한가지 방법은 when: block전으로 변수 선언을 옮기는 것이다.

(Data Driven Testing에선 변수를 where: block 안으로 옮길 수 있다)

예를 들어, 메세지를 보낼때 같은 변수를 사용하는게 더 이득이다.

다른 해결책은 변수 선언과 상호작용이 함께 속해있다는 사실을 명시하는 것이다.

when:
publisher.send("hello")

then:
interaction {
    def message = "hello"
    1 * subscriber.receive(message)
}

MockingApi.interaction block은 항상 전체적으로 이동하기 때문에, 코드는 정상동작한다.

Scope of Interactions

then: block안에 선언된 상호작용은 when: block 보다 선행된다.

when:
publisher.send("message1")

then:
subscriber.receive("message1")

when:
publisher.send("message2")

then:
subscriber.receive("message2")

이렇게 하면 subscriber가 message1을 수신하는 동안 when: block이 실행되고,

두번째 when: block이 실행되는 동안 message2를 수신받는다.

then: block 밖에 선언된 상호작용은 그들의 선언에서 기능 메서드 끝까지 활성화된다.

상호작용은 항상 특별히 기능 메서드에 범위가 지정된다.

따라서 상호작용은 static 메서드, setupSpec 메서드, cleanupSpec 메서드에는

선언될 수 없다. 달리말하면, mock 객체는 static이나 @Shared 필드에 저장될 수 없다.

Verification of Interactions

mock 기반 테스트가 실패할 수 있는 두 가지 주요 방법이 있다. 상호작용이 허용하는 것보다

더 많거나 더 적은 호출수와 일치할 수 있기 때문이다. 이전 케이스에서는 호출이 일어날 때

바로 감지되어 TooMayInvocationsError가 발생한다.

Too many invocations for:

2 * subscriber.receive(_) (3 invocations)

너무 많은 호출을 쉽게 인지할 수 있도록, Spock은 상호작용과 일치하는 모든 호출을

보여줄 것이다.

Matching invocations (ordered by last occurrence):

2 * subscriber.receive("hello")   <-- this triggered the error
1 * subscriber.receive("goodbye")

output에선, receive(“hello”) 중 하나가 TooManyInvocationsError를 일으켰다.

하나의 라인에 집계된 output으론 두 호출의receive(“hello”)를 구분할 수 없으므로,

receive(“goodbye”) 전에 첫번째 receive(“hello”)에 호출되었을것이다.

두번째 경우,(필요한 것보다 적은 호출의 경우) when block의 실행이 완료된 후

탐지할 수 있다.  (그때까진, 추가적인 호출이 일어날 수 있다)

그것은 TooFewInvocationsError를 발생시킨다.

Too few invocations for:

1 * subscriber.receive("hello") (0 invocations)

메서드가 전혀 호출되지 않았 든, 동일한 메서드가 다른 인자로 호출되었는지,

같은 메서드가 다른 mock 객체에서 호출되었는지 또는 다른 메서드가 이 메서드 대신

호출되었는지 여부는 중요하지 않다.

이 경우 모두 TooFewInvocationsError 에러가 발생합니다.

누락된 호출 대신 일어난 것을 쉽게 진단하기 위해, Spock은 상호작용과 일치하지 않는

상호작용의 유사성으로 정렬해서 모든 호출을 보여줄 것이다.

특별히, 상호작용의 인자를 제외한 모든 것과 일치하는 호출이 먼저 보여질 것이다.

Unmatched invocations (ordered by similarity):

1 * subscriber.receive("goodbye")
1 * subscriber2.receive("hello")

Invocation Order

종종, 정확한 메서드 호출순서는 관련이 없으므로 시간이 흐르면 변한다.

over-specification을 피하기 위해, Spock은 지정된 상호작용이 충족되면 어떤 호출순서도

허용한다.

then:
2 * subscriber.receive("hello")
1 * subscriber.receive("goodbye")

여기서 “hello” “hello” “goodbye”, “hello”, “goodbye” “hello” “goodbye” “hello” “hello”

같은 어떤 호출 순서도 ​​지정된 상호 작용을 충족시킨다.

호출 순서가 중요한 경우 상호 작용을 여러 개의 then: block으로 분할하여 순서를 지정할 수

있다.

then:
2 * subscriber.receive("hello")

then:
1 * subscriber.receive("goodbye")

지금 Spock은 “goodbye” 전에 “hello”가 수신되었는지 검증할 것이다.

달리 말하면, 호출순서는 then: block안이 아니고 사이에 적용된다.

then: block과 and: block을 구분하는 것은 어떤 순서도 적용되지 않는다.

and: 는 오직 문서 목적의 의미만 있고 다른 어떤 의미도 전달하지 않는다.

Mocking Classes

인터페이스 이외에도, Spock은 클래스 mocking을 지원한다. Mocking 클래스는

인터페이스 mocking과 같이 작동한다. 추가로 필요한 것은 cglib-nodep-2.2 혹은 그 이상

그리고 objenesis-1.2나 그 이상을 클래스패스에 넣는 것이다.

이 라이브러리 중 하나가 없다면, Spock은 너에게 부드럽게 알려줄 것이다.

Java8은 CGLIB 3.2.0부터 지원된다.

Stubbing

Stubbing은 협력자들이 메소드 호출에 특정 방식으로 응답하게하는 행위다.

메서드를 stubbing할 때, 메서드를 얼마나 많이 호출했는지 횟수는 신경 쓰지 않아도 됩니다.

호출 될 때마다 특정 값을 반환하거나 일부 부작용을 수행하기만 하면 됩니다.

다음 예제를 위해 구독자의 receive 메서드를 구독자가 메시지를 처리 ​​할 수 ​​있었는지 여부를

알려주는 상태 코드를 반환하도록 수정해보자.

interface Subscriber {
    String receive(String message)
}

이제 receive 메서드는 모든 호출마다 “ok”를 반환할 것이다.

subscriber.receive(_) >> "ok"

subscriber가 메시지를 받을 때마다 ‘ok’로 응답하도록 만들어라.

mocked된 상호작용과 비교해 stubbed 상호작용은 왼쪽 끝에 cardinality가 없지만

오른쪽 끝에 응답 발생기가 추가된다.

subscriber.receive(_) >> "ok"
|          |       |     |
|          |       |     response generator
|          |       argument constraint
|          method constraint
target constraint

stubbed된 상호 작용은 일반적인 위치에서 선언 할 수 있다. 즉,  then: 블록안 또는 when  block

이전에 선언 할 수 있다.  mock 객체가 stubbing에만 사용된다면, setup: block이나

mock 객체 생성시 선언하는 것이 일반적이다.

Returning Fixed Values

우린 이미 right-shift(>>) 연산자로 고정된 값을 반환하는데 사용하는 것을 보았다.

subscriber.receive(_) >> "ok"

다른 호출 시 다른 값을 반환하기 위해, 여러 상호작용을 사용할 수 있다.

subscriber.receive("message1") >> "ok"
subscriber.receive("message2") >> "fail"

이것은 message1을 수신받으면 ok를 반환하고, message2를 수신받으면 fail을 반환할 것이다.

메서드에 선언된 반환 타입과 호환되는 경우, 리턴되는 값에 제한은 없다.

Returning Sequences of Values

성공적인 호출시 다른 값을 반환하기 위해, triple-right-shift 연산자를 사용해라.

subscriber.receive(_) >>> ["ok", "error", "error", "ok"]

이것은 첫번째 호출시 ok를 리턴하고 두번째와 세번째 호출시는 error, 그리고 남은 모든

호출시에는 ok를 리턴할 것이다. 오른쪽은 Groovy가 반복하는 방법을 알아야 한다.

이번 예에서는 일반적인 리스트를 사용했다.

Computing Return Values

메서드의 인자를 기반으로 한 반환값을 계산하려면, closure와 함께 right-shift(>>) 연산자를

사용한다. closure에 타입이 지정되지 않은 단일 파라미터가 선언되면, 메서드의 인자 리스트로

전달한다.

subscriber.receive(_) >> { args -> args[0].size() > 3 ? "ok" : "fail" }

여기선 메세지의 길이가 3을 넘으면 ok, 아니면 fail을 반환한다.

대부분의 경우에 메서드의 인자에 직접 접근하는게 편리하다.

closure가 둘 이상의 파라미터 혹은 단일 타입 파라미터를 선언하면, 메서드 인자는

하나씩 closure 파라미터로 맵핑될 것이다.

subscriber.receive(_) >> { String message -> message.size() > 3 ? "ok" : "fail" }

이 응답기는 전과 동일하게 동작하지만, 틀림없이 읽기가 더 쉽다.

인자보다 메서드 호출에 더 많은 정보가 필요하다면,

org.spockframework.mock.IMockInvacation을 봐라.

이 인터페이스에 선언된 모든 메서드들은 그들 앞에 접두사를 쓸 필요없이 closure 안에서

사용할 수 있다. (Groovy 용어에선, closure는 IMockInvocation의 인스턴스에 위임한다.)

Performing Side Effects

때때로 반환값을 계산하는 것보다 더 필요한 것이 있을 수 있다. 전형적인 예로는

예외를 던지는 것이다. closure는 다음과 같다.

subscriber.receive(_) >> { throw new InternalError("ouch") }

물론, closure는 println 구문같은 더 많은 코드를 포함할 수 있다.

그것은 상호작용과 일치하는 매 호출마다 실행될 것이다.

Chaining Method Responses

메서드 응답은 체이닝할 수 있다.

subscriber.receive(_) >>> ["ok", "fail", "ok"] >> { throw new InternalError() } >> "ok"

이것은 3번째 호출까진 ok, fail, ok를 반환하고, 네번째 호출부터는 InternalError를 던지고

이후 호출에 대해서는 ok를 반환한다.

Combining Mocking and Stubbing

Mocking과 Stubbing은 함께 실행된다.

1 * subscriber.receive("message1") >> "ok"
1 * subscriber.receive("message2") >> "fail"

mocking과 stubbing이 같은 메서드에서 호출될 때, 그들은 같은 상호작용이 일어난다.

특별히, Mockito-style의 stubbing과 두개로 분리된 구문을 mocking하면 작동하지 않는다.

setup:
subscriber.receive("message1") >> "ok"

when:
publisher.send("message1")

then:
1 * subscriber.receive("message1")

상호 작용을 선언하는 곳에서 설명한 것처럼 수신 호출은 먼저 then : 블록의 상호 작용에 대해

일치시킨다. 이 상호 작용은 응답을 지정하지 않으므로 메서드의 반환 유형 (이 경우 null)의

기본값이 반환된다. (이것은 mocking에 대한 Spock의 관대 한 접근 방식의 또 다른 측면이다)

따라서 setup : 블록의 상호 작용은 절대로 일치 할 기회를 얻지 못한다.

동일한 메서드 호출을 Mocking하고 stubbing하는 것은 동일한 상호작용에서 발생해야 한다.

Other Kinds of Mock Objects (New in 0.7)

지금까지 MockingApi.Mock 메서드를 사용하여 mock 객체를 만들었다.

이 메서드외에도, MockingApi 클래스는 mock 객체의 더 특별한 종류를 만들기 위한

여러 다른 팩토리 메서드를 제공한다.

Stubs

stub은 MockingApi.stub 팩토리 메서드에서 만들어진다.

def subscriber = Stub(Subscriber)

mock은 stubbing과 mocking 모두에 사용할 수 있지만, stub은 stubbing에만 사용할 수 있다.

협력자를 stub으로 제한하면 그 역할을 specification의 독자들에게 전달할 수 있다.

stub 호출이 기본적인 상호작용(1 * foo.bar()와 같은)과 일치하면, InvalidException이

발생한다.

mock처럼, stub도 예상치못한 호출을 허용한다. 그렇지만, 그러한 경우에 stub에 의해

반환된 값들은 더 야심찬 것들이다.

  • primitive 타입은 기본타입의 기본값이 반환된다
  • non-primitive 숫자값은(BigDecimal) 0이 반환된다.
  • non-numerical 값들은, 빈값이나 더미객체가 반환된다. 이것은 빈 문자열, 빈 collection
    기본생성자로부터 만들어진 객체, 혹은 기본 값을 반환하는 다른 stub일 수 있다.
    자세한건 org.spockframework.mock.EmptyOrDummyResponse 클래스를 보라.

stub은 종종 고정된 상호작용 세트가 있는 경우가 있다. mock 생성시 상호작용을 선언하는 것이

특히 매력적이다.

def subscriber = Stub(Subscriber) {
    receive("message1") >> "ok"
    receive("message2") >> "fail"
}

Spies

(이 기능을 사용하기 전에 두번 생각해라. 명세에서 코드의 디자인을 바꾸는 것이 더 나을 수 있다.)

spy는 MockingApi.spy factory 메서드로 만들어진다.

def subscriber = Spy(SubscriberImpl, constructorArgs: ["Fred"])

spy는 항상 실제객체를 기반으로 한다. 그래서 특정 생성자 인자와 함께 인터페이스 타입보다

클래스타입을 제공해야 한다.

인자없는 생성자가 제공되면, 타입의 기본 생성자가 사용될 것이다.

 

spy의 메서드 호출은 자동으로 실제 객체에게 위임된다. 마찬가지로, 실제 객체의 메서드로부터

반환된 값들은 spy를 통해 호출자에게 다시 전달된다. spy를 만들고 난 후에는 호출자와

실제객체 사이의 대화를 spy가 들을 수 있다.

1 * subscriber.receive(_)

receive가 정확히 한번 호출되는 것을 제외하고는 publisher와 스파이의 기초를 이루는

SubscriberImpl 객체간 대화는 변경되지 않는다.

spy에서 메서드를 stubbing하면 더 이상 실제 메서드가 호출되지 않는다.

subscriber.receive(_) >> "ok"

SuscriberImpl.receive 호출대신에, receive 메서드는 지금 간단히 “ok”를 반환할 것이다.

때때로, 실제 메서드를 실행하고 위임하는 것이 바람직하다.

subscriber.receive(_) >> { String message -> callRealMethod(); message.size() > 3 ? "ok" : "fail" }

여기선 실제 객체에게 메서드 호출을 위임하는 callRealMethod()를 사용했다.

message 인자값을 주지 않아도 자동으로 전달된다.

callRealMethod()는 실제 상호작용의 결과를 반환하지만, 이번 예에서는 우리가 만든 결과를

반환하기로 결정했다. 실제 메서드에서 다른 메세지를 전달해야 한다면, 우리는

callRealMethodWithArgs(“changed message”)를 사용할 수 있다.

Partial Mocks

(이 기능을 사용하기 전에 두번 생각해라. 명세에서 코드의 디자인을 바꾸는 것이 더 나을 수 있다.)

Spies는 또한 partial mocks를 사용할 수 있다.

// this is now the object under specification, not a collaborator
def persister = Spy(MessagePersister) {
  // stub a call on the same object
  isPersistable(_) >> true
}

when:
persister.receive("msg")

then:
// demand a call on the same object
1 * persister.persist("msg")

Groovy Mocks (New in 0.7)

지금까지 mocking 기능의 모든 것은 자바 혹은 그루비로 만든 것과 상관없이 동일하게 동작한다.

Groovy의 동적기능을 활용하여, Groovy mocks는 Groovy 코드를 테스트하기 위한 몇몇

특별한 부가기능을 제공한다. 그것은 MockingApi.GroovyMock(), MockingApi.GroovyStub()

MockingApi.GroovySpy() factory 메서드에서 만들어진다.

Groovy Mock은 정규 Mock보다 언제 더 좋은가? Groovy mock은 명세 하의 코드가

Groovy로 작성되고 고유한 Groovy mock 기능 중 일부가 필요할 때 사용해야 한다.

Groovy mocks는 Java 코드에서 호출 될 때 일반 mock처럼 작동한다.

명세하의 mocked 타입 코드가 Groovy로 만들어졌다고 Groovy mock을 사용할 필요는 없다.

Groovy mock을 사용해야하는 구체적인 이유가 없다면, 일반적인 mock을 써라.

Mocking Dynamic Methods

모든 Groovy mocks은 GroovyObject의 인터페이스를 구현한다. 그들은 물리적으로

선언된 메서드처럼 동적 메서드의 mocking과 stubbing을 지원한다.

def subscriber = GroovyMock(Subscriber)

1 * subscriber.someDynamicMethod("hello")

Mocking All Instances of a Type

(이 기능을 사용하기 전에 두번 생각해라. 명세에서 코드의 디자인을 바꾸는 것이 더 나을 수 있다.)

보통 Groovy는 일반 mocks와 같은 명세하에서 코드를 주입하는 것이 필요하다.

그럼에도 Groovy mock은 전역으로 생성되면 기능 메서드가 실행되는 동안 mocked된

모든 실제 객체가 자동으로 대체된다.

def publisher = new Publisher()
publisher << new RealSubscriber() << new RealSubscriber()

def anySubscriber = GroovyMock(RealSubscriber, global: true)

when:
publisher.publish("message")

then:
2 * anySubscriber.receive("message")

여기 실제 구독자를 구현한 두 객체와 게시자가 있다.

그때 우린 동일한 타입의 전역 mock을 만들 수 있다. 이것은 실제 구독자들의 모든

메서드 호출을 mock 객체로 변경한다. mock 객체의 인스턴스는 게시자에게 전달되지

않는다. 그것은 상호작용을 설명할때만 사용된다.

global mock은 클래스타입만 생성된다. 기능 메서드가 실행되는 동안 타입의 모든

객체들은 효율적으로 대체된다.

글로벌 mock은 글로벌 효과가 있기 때문에 GroovySpy와 함께 사용하는 것이 편리하다.

이는 상호 작용이 일치하지 않는 한 실제 코드가 실행되도록 해서 객체를 선택적으로 받고

필요한 경우 행동을 변경하도록 한다.

글로벌 Groovy Mock 어떻게 구현되는가?

Global Groovy mocks Groovy 메타 프로그래밍에서 강력한 힘을 얻는다.

보다 정확하게 말하자면, 전역적으로 mocked된 모든 유형에는 기능 메서드의 지속 기간 동안

맞춤 메타 클래스가 할당된다. 글로벌 Groovy mock은 여전히 CGLIB 프록시를 기반으로 하므로

Java 코드에서 호출 될 때 일반적인 mocking 기능 (super power는 아님)을 유지한다.

Mocking Constructors

(이 기능을 사용하기 전에 두번 생각해라. 명세에서 코드의 디자인을 바꾸는 것이 더 나을 수 있다.)

Global mocks는 생성자의 mocking을 지원한다.

def anySubscriber = GroovySpy(RealSubscriber, global: true)

1 * new RealSubscriber("Fred")

spy를 사용하면, 생성자 호출로 반환된 객체는 변하지 않는다.

생성될 객체를 변경하기 위해, 생성자 stub을 사용할 수 있다.

new RealSubscriber("Fred") >> new RealSubscriber("Barney")

일부 코드가 Fred라는 이름으로 subscriber를 만들려고 시도할 때, 우리는 대신 Barney라는

subscriber를 만들것이다.

Mocking Static Methods

(이 기능을 사용하기 전에 두번 생각해라. 명세에서 코드의 디자인을 바꾸는 것이 더 나을 수 있다.)

Global mock은 static 메서드의 mocking과 stubbing을 지원한다.

def anySubscriber = GroovySpy(RealSubscriber, global: true)

1 * RealSubscriber.someStaticMethod("hello") >> 42

동적 메서드에서도 마찬가지로 동작한다.

global mock이 생성자와 static 메서드를 mocking하는데만 사용되는 경우,

mock의 객체는 꼭 필요하지 않다. 이 경우엔 다음과 같이 사용할 수 있다.

GroovySpy(RealSubscriber, global: true)

Advanced Features (New in 0.7)

대부분 이 기능은 필요하지 않다. 하지만 그렇게 해야 한다면 그들을 가질 수 있다.

A la Carte Mocks

Mock (), Stub () Spy () 팩토리 메소드는 특정 구성을 사용하여 mock 객체를 만드는

사용할 있다.  mock 설정보다 세밀한 제어를 원할 경우

org.spockframework.mock.IMockConfiguration 인터페이스를 살펴봐라.

인터페이스 모든 속성은 명명 인자 Mock () 메서드에 전달할 있다. 예를 들어

def person = Mock(name: "Fred", type: Person, defaultResponse: ZeroOrNullResponse, verified: false)

여기서 우리는 Mock () ​​기본 반환 값과 일치하지만 호출이 검증되지 않은(Stub ()처럼)

모의 객체를 만든다.

ZeroOrNullResponse 전달하는 대신 예기치 않은 메서드 호출에 응답하기 위해

자체 customorg.spockframework.mock.IDefaultResponse 제공 ​​ 수 있다.

Detecting Mock Objects

특정객체가 Spock mock 객체인지 확인하려면,

org.spockframework.mock.MockDetector을 사용해라.

def detector = new MockDetector()
def list1 = []
def list2 = Mock(List)

expect:
!detector.isMock(list1)
detector.isMock(list2)

detector는 mock 객체에 대한 더 많은 정보를 얻는데 사용할 수 있다.

def mock = detector.asMock(list2)

expect:
mock.name == "list2"
mock.type == List
mock.nature == MockNature.MOCK
Advertisements

DbUnit 사용시 주의사항

최근에 테스트코드를 만들 때 spock-dbunit을 사용하면서 이상하게 동작하는 부분이 있어서

관련사항을 간단히 정리해 보았습니다.


1. spock-dbunit @DbUnit 데이터 초기화 문제

테스트코드 실행 전, DB의 데이터를 원하는 데이터로 초기화하기 위해 spock-dbunit

라이브러리의 @DbUnit 어노테이션을 아래와 같이 사용하였더니 예상과 다른 데이터가

들어갔습니다.

@DbUnit
def content = {
    User(id: 1, name: 'janbols')
    User(id: 2, name: 'bluepoet', ip: '127.0.0.1')

    Other_User(id: 1, name: 'bluepoet', ip: '127.0.0.1')
    Other_User(id: 2, name: 'janbols')
    Other_User(id: 3, name: 'tester', ip: '1.2.3.4')
}

Other_User 테이블은 3개 row의 id, name, ip 필드에 세팅한대로 값이 들어갔지만,

User 테이블의 2번 아이디의 ip필드의 “127.0.0.1”값은 들어가지 않았습니다.

관련해서 간단하게 테스트코드를 만들어 pr을 날렸고, 해당 pr이 merge되면서 프로젝트

운영자로부터 그것이 DbUnit의 결함이라는 피드백을 받았습니다.

참고 url : https://github.com/janbols/spock-dbunit/issues/4

%e1%84%89%e1%85%b3%e1%84%8f%e1%85%b3%e1%84%85%e1%85%b5%e1%86%ab%e1%84%89%e1%85%a3%e1%86%ba-2017-01-16-%e1%84%8b%e1%85%a9%e1%84%92%e1%85%ae-7-45-54

스크린샷 2017-01-16 오후 7.56.42.png

결론적으로, 내가 검증하고자 하는 필드는 첫번째 행에 모두 넣어야 합니다.

Other_User는 첫번째 행에 ip필드가 들어가 있어서 3번째 행에도 ip필드에 값이 정상적으로

들어가게 됩니다.

이와 관련한 DbUnit의 아래 내용을 읽어보시면 도움이 될 것 같습니다.

http://dbunit.sourceforge.net/faq.html#differentcolumnnumber

참고로, spring-test-dbunit은 동일한 테스트에서 위와 같은 문제가 발생하지 않았습니다.

@Test
@DatabaseSetup("sampleData1.xml")
public void tesNoValueLastName() throws Exception {
    List<Person> personList = this.personService.find("bl");
    assertEquals(1, personList.size());
    assertEquals("blue", personList.get(0).getFirstName());
    assertEquals("poet", personList.get(0).getLastName());
}
<!-- sampleData1.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
	<Person id="0" title="Mr" />
	<Person id="1" title="Mr" firstName="Mario" lastName="Zagar"/>
	<Person id="2" title="Mr" firstName="blue" lastName="poet"/>
</dataset>

참고 url :  http://bit.ly/2j7HEM4

2. spring-test-dbunit @ExpectedDatabase 반복사용 문제

spring-test-dbunit 1.3.0 이전버전을 사용하면, @ExpectedDatabase 어노테이션을

하나의 메서드에 반복 사용시, 마지막 어노테이션의 내용만 검증되는 버그가 있습니다.

그러므로 아래 코드의 expectedProductData.xml의 데이터가 틀려도 이 테스트는

성공하게 됩니다.

@ExpectDatabases 어노테이션도 동일하게 동작합니다.

참고 url : http://bit.ly/2jmqwTE

@Test
@DatabaseSetup("sampleData.xml")
@DatabaseSetup("ProductInitData.xml")
@ExpectedDatabase(value = "expectedProductData.xml", assertionMode = DatabaseAssertionMode.NON_STRICT) // verify error!
@ExpectedDatabase(value = "sampleData.xml", assertionMode = DatabaseAssertionMode.NON_STRICT)
public void testVerifyMultiTableValues() throws Exception {
}

스크린샷 2017-01-18 오후 3.17.44.png

이에 대해 override 설정을 “false”로 주면 해결되지만, 이에 대해 불편을 제기한 이슈가 있어서

1.3.0버전에 이 버그가 패치되었습니다.

1.3.0 버전에선 위 테스트가 원래 의도한 대로 실패하게 됩니다.

참고 url : https://github.com/springtestdbunit/spring-test-dbunit/issues/64
               http://bit.ly/2iGrbxi

 

 

 

[번역]Spock – Data Driven Testing

Spock의 공식문서 중 Data Driven Testing를 번역해 보았습니다.

보실 때 오역이 있음을 감안하시고, 번역하기에 애매한 부분은 원문 그대로 옮겼습니다.


흔히 다양한 입력 및 예상된 결과로 동일한 테스트 코드를 여러번 실행하는 것이 유용하다.
spock의 데이터 기반 테스팅 지원은 이것을 최고의 기능으로 만든다.

Introduction

Math.max 메서드의 동작을 지정한다고 가정하자:

class MathSpec extends Specification {
  def "maximum of two numbers"() {
    expect:
    // exercise math method for a few different inputs
    Math.max(1, 3) == 3
    Math.max(7, 4) == 7
    Math.max(0, 0) == 0
  }
}

이 접근법은 이와 같이 단순한 경우에는 좋지만, 몇가지 잠재적인 단점이 있다.

  • 코드와 데이터가 섞여 있어 쉽게 독립적으로 변경할 수 없다.
  • 데이터를 외부 소스에서 쉽게 자동생성하거나 가져올 수 없다.
  • 동일한 코드를 여러번 실행하려면, 중복되거나 별도의 메서드로 분리해야 한다.
  • 실패한 경우, 실패를 일으킨 입력값이 무엇인지 바로 알 수 없을 수도 있다.
  • 동일한 코드를 여러번 실행하는 것은 별도의 메서드를 실행하는 것과 동일한 격리로 인해
    이익을 얻지 못한다.

spock의 데이터 기반 테스팅 지원은 이러한 문제를 해결하려고 한다.
시작하려면, 코드를 data-driven feature method로 리팩토링하자.
첫번째, 하드코딩된 integer 값들을 대체할 세개의 메서드 파라미터(데이터 변수라고 불리는)를
소개한다.

class MathSpec extends Specification {
  def "maximum of two numbers"(int a, int b, int c) {
    expect:
    Math.max(a, b) == c
    ...
  }
}

우리는 이 테스트로직을 끝냈지만, 여전히 사용할 데이터 값을 제공할 필요가 있다.
이것은 항상 메서드의 끝에 위치한 where: 블록에서 수행된다.
가장 단순한(가장 일반적인) 경우에, where: 블록은 데이터 테이블이 있다.


Data Tables

데이터 테이블은 고정된 값 집합을 사용하여 기능 메서드를 실행하는 편리한 방법이다.

class MathSpec extends Specification {
  def "maximum of two numbers"(int a, int b, int c) {
    expect:
    Math.max(a, b) == c

    where:
    a | b | c
    1 | 3 | 3
    7 | 4 | 7
    0 | 0 | 0
  }
}

테이블 헤더라 불리는 테이블의 첫번째 행은 데이터 변수를 선언한다. 테이블 행이라 불리는
다음 행은 해당하는 값이 있다. 각 행에 대해 기능 메서드는 한번씩 실행된다.
이것을 메서드의 반복이라 부른다. 반복이 실패하더라도, 나머지 반복은 실행될 것이다.
모든 실패는 보고될 것이다.

데이터 테이블은 적어도 두개의 열이 있어야 한다. 단일 열 테이블은 다음과 같이 작성할 수 있다.:

where:
a | _
1 | _
7 | _
0 | _

Isolated Execution of Iterations

반복은 별도 기능 메서드와 동일한 방법으로 서로 분리된다. 각 반복은 specification class의
자체 인스턴스를 가져오고, setup과 cleanup 메서드는 각각 반복 전후에 호출된다.


Sharing of Objects between Iterations

반복간에 객체를 공유하려면, @Shared 또는 static 필드에 보관해야 한다.

NOTE  | @Shared와 static 변수는 where: 블록내에서 접근할 수 있다.

이러한 객체는 다른 메서드와도 공유된다.  현재 동일한 메서드의 반복 사이에서 객체를 공유하는
좋은 방법은 없다.  이 문제를 고려한다면, 각 메서드를 별도 스펙에 넣는 것을 고려하라, 모든 스펙은
동일한 파일에 보관될 수 있다. 이것은 일부 상용구 코드를 사용하여 더 나은 격리를 얻을 수 있다.


Syntactic Variations

이전 코드는 몇가지 방법으로 조정할 수 있다.
첫째, where: 블록은 이미 모든 데이터 변수를 선언하므로, 메서드 파라미터는 생략할 수 있다.
둘째, 입력과 예상된 출력은 이중 파이프 기호(||)를 사용하여 시각적으로 구분할 수 있다.
이것을 사용하면 코드는 다음과 같이 된다.

class MathSpec extends Specification {
  def "maximum of two numbers"() {
    expect:
    Math.max(a, b) == c

    where:
    a | b || c
    1 | 3 || 3
    7 | 4 || 7
    0 | 0 || 0
  }
}

Reporting of Failures

 max 메서드의 구현에 결함이 있어서 반복 중 하나가 실패한다고 가정하자.

maximum of two numbers   FAILED

Condition not satisfied:

Math.max(a, b) == c
    |    |  |  |  |
    |    7  0  |  7
    42         false

분명한 질문은 어떤 반복이 실패했고 데이터 값은 무엇인가이다. 이 예에서는, 두번째 반복에서
실패했다는 것을 알기가 어렵다. 다른 경우에 이것은 더 어렵거나 불가능할 수도 있다.
어떤 경우엔, spock이 실패를 보고하는 것보다 반복이 실패하는 것을 크고 분명하게 만들면
좋을 것이다. 이것이 @Unrolling 어노테이션의 목적이다.


Method Unrolling

@Unroll로 어노테이션된 메서드는 반복이 독립적으로 보고된다.

@Unroll
def "maximum of two numbers"() {
...
@Unroll이 기본이 아닌 이유는 무엇인가?
@Unroll이 기본이 아닌 한가지 이유는 일부 실행환경(특히 IDEs)은 테스트 메서드의 수를 미리
알려주고 실제 수가 다를 경우 특정문제가 발생하길 기대하기 때문이다.

다른 이유는 @Unroll이 보고된 테스트 수를 크게 바꿀 수 있기 때문이고 이것은 항상 바람직하지 않다.
unrolling은 메서드 실행에 아무런 영향을 주지 않는다. 그것은 단지 보고의 대안일 뿐이다.
실행환경에 따라 출력은 다음과 같다.

maximum of two numbers[0]   PASSED
maximum of two numbers[1]   FAILED

Math.max(a, b) == c
    |    |  |  |  |
    |    7  0  |  7
    42         false

maximum of two numbers[2]   PASSED

이것은 두번째 반복(인덱스 1 포함)이 실패했음을 알려준다. 조금만 더 노력한다면, 더 잘할 수 있다.

@Unroll
def "maximum of #a and #b is #c"() {
...

메서드 이름은 선행 해시 기호(#)로 표시된 placeholders를 사용하여 데이터 변수 a, b 및 c를
나타낸다. 결과에서 placeholders는 구체적인 값으로 변경된다.

maximum of 3 and 5 is 5   PASSED
maximum of 7 and 0 is 7   FAILED

Math.max(a, b) == c
    |    |  |  |  |
    |    7  0  |  7
    42         false

maximum of 0 and 0 is 0   PASSED

이제 우리는 max 메서드 입력 7과 0에 대해 실패한 것을 한 눈에 알 수 있다.
자세한 내용은 More on Unrolled Method Names를 참조하라

@Unroll 어노테이션은 스펙에 배치할 수도 있다. 이는 스펙의 각 데이터 주도 기능 메서드에
배치하는 것과 동일한 효과를 발휘한다.


Data Pipes

데이터 테이블은 데이터 변수에 값을 제공하는 유일한 방법은 아니다. 사실, 데이터 테이블은
하나이상의 데이터 파이프에 대한 syntactic sugar이다.

...
where:
a << [1, 7, 0]
b << [3, 4, 0]
c << [3, 7, 0]

왼쪽 시프트 연산자(<<)로 표시된 데이터 파이프 연산자는 데이터 변수를 데이터 공급자에게 연결한다.
데이터 공급자는 반복마다 하나씩 변수에 대한 모든 값을 보유한다.
반복하는 방법을 알고 있는 Groovy의 어떠한 객체도 데이터 공급자로 사용될 수 있다.
이것은 Collection, String, Iterable 및 Iterable 계약을 구현한 객체를 포함한다.
데이터 공급자가 반드시 데이터일 필요는 없다(Collection의 경우에)
그들은 텍스트 파일, 데이터베이스, 스프레드시트같은 외부 소스에서 데이터를 가져오거나
임의로 데이터를 생성할 수 있다. 데이터 공급자는 필요할 때만(다음 반복전에) 다음 값으로 질문된다.


Multi-Variable Data Pipes

데이터 공급자가 반복당 여러 값(Groovy가 반복하는 방법을 알고 있는 객체)을 반환하면,
동시에 여러 데이터 변수에 연결할 수 있다. 문법은 Groovy multi-assignment와 비슷하지만
왼쪽에는 parentheses 대신 brackets를 사용한다.

@Shared sql = Sql.newInstance("jdbc:h2:mem:", "org.h2.Driver")

def "maximum of two numbers"() {
  expect:
  Math.max(a, b) == c

  where:
  [a, b, c] << sql.rows("select a, b, c from maxdata")
}

관심이 없는 데이터는 밑줄(_)로 무시할 수 있다.

...
where:
[a, b, _, c] << sql.rows("select * from maxdata")

Data Variable Assignment

데이터 변수에는 직접 값을 할당할 수 있다:

...
where:
a = 3
b = Math.random() * 100
c = a > b ? a : b

할당은 모든 반복마다 재평가된다. 위에 표시한 것처럼, 할당의 오른쪽은 다른 데이터 변수를
참조할 수 있다.

...
where:

where:
row << sql.rows("select * from maxdata")
// pick apart columns
a = row.a
b = row.b
c = row.c

Combining Data Tables, Data Pipes, and Variable Assignments

필요에 따라 데이터 테이블, 데이터 파이프 및 변수 할당을 결합할 수 있다.

...
where:
a | _
3 | _
7 | _
0 | _

b << [5, 0, 0]

c = a > b ? a : b

Number of Iterations

반복횟수는 사용가능한 데이터 양에 따라 다르다. 동일한 메서드를 연속적으로 실행하면
반복 횟수가 달라질 수 있다.데이터 공급자의 peer가 다른 값보다 빨리 실행되면,
예외가 발생할 것이다. 변수 할당은 반복횟수에 영향을 주지 않는다.
where: 블록은 할당이 하나만 있으면 정확히 하나의 반복을 만든다.


Closing of Data Providers

모든 반복이 완료된 후, 인자가 없는 close 메서드는 이 메서드가 있는 모든 데이터 제공자에서
호출된다.


More on Unrolled Method Names

 unroll 메서드 이름은 Groovy GString과 비슷하지만, 다음과 같은 차이가 있다.

  • 표현식은 $대신에 #으로 표시되고, ${…} 문법에는 해당되지 않는다.
  • 표현식은 속성 접근 및 인자가 없는 메서드 호출만 지원한다.

이름과 나이 속성을 가진 Person 클래스와 Person 타입의 person 데이터 변수가 주어지면
유효한 메서드 이름은 다음과 같다.

def "#person is #person.age years old"() { // property access
def "#person.name.toUpperCase()"() { // zero-arg method call

위의 #person과 같은 String 아닌 값은 Groovy semantics에 따라 String으로 변환된다.

다음은 잘못된 메서드 이름이다.

def "#person.name.split(' ')[1]" {  // cannot have method arguments
def "#person.age / 2" {  // cannot use operators

필요하다면, 복잡한 표현식을 유지하기 위해 추가 데이터 변수를 도입할 필요가 있다.

def "#lastName"() { // zero-arg method call
  ...
  where:
  person << [new Person(age: 14, name: 'Phil Cole')]
  lastName = person.name.split(' ')[1]
}

[번역]JUnit A Cook’s Tour

예전에 토비의 스프링의 저자 이일민님에게 뜬금없이 아래와 같은 질문을 한 적이 있습니다.

스프링 프레임워크 소스를 분석하면서 객체지향 공부를 하려고 하는데 어떻게 해야 할까요?

이에 대한 답으로 JUnit A Cook’s Tour를 읽어보라고 하셔서 공부할 겸 번역해 보았습니다.

오역이 있음을 감안하시고, 번역하기에 애매한 부분은 원문 그대로 옮겼습니다.


Note : 이 기사는 Junit 3.8.x를 기반으로 한다.


1. Introduction

이전 기사에서 우리는 간단한 프레임워크를 사용해 반복가능한 테스트를 만드는 방법에 대해 설명했다.
이번 기사에서 우리는 프레임워크를 살펴보고 그것이 어떻게 구성되었는지 보여준다.

우리는 JUnit 프레임워크를 주의깊게 연구하고 우리가 그것을 어떻게 구성했는지를 반영했다.
우리는 다양한 수준에서 교훈을 발견했다. 이 기사에서 우리는 한 번에 모든 것을 전달하려고
노력하겠지만, 적어도 검증된 가치를 지닌 소프트웨어의 설계 및 구성을 보여주는 맥락에서
이를 진행할 것이다.

우리는 프레임 워크의 목표에 대한 토론으로 시작한다. 목표는 프레임워크 자체가 보여지는 동안 많은 작은 세부 사항에서 다시 나타난다. 이후, 우리는 프레임워크의 설계와 구현을 제시한다.
설계는 세련된프로그램으로 구현된 패턴(놀람, 놀라움)의 관점에서 설명 될 것이다.
우리는 프레임워크 개발에 대한 몇 가지 선택적인 생각으로 결론을 맺는다.

2. Goals

Junit의 목표는 무엇인가?

첫째,  개발시 다음과 같은 가정을 해보자. 프로그램 기능에 자동화 된 테스트가없는 경우 작동하지 않는다고 가정한다. 개발자가 우리에게 프로그램 기능이 작동한다는 것을 보증한다면 현재와 영원히 작동한다는 일반적인 가정보다 훨씬 안전하다.

이 관점에서 개발자는 코드를 만들거나 디버깅할 때 이것이 이뤄지지 않으면, 프로그램이 동작함을 입증하는 테스트들을 만들어야 한다. 그럼에도 불구하고, 모두는 바쁘고, 그들은 할일이 많고,  충분한 시간을 할애하지 못한 채 테스트를 하며 보낸다. 난 이미 너무 많은 코드를 작성했는데 어떻게 또 테스트코드를 만들까요? 대답해주세요. Mr. Hard-case Project Manager.

그래서 목표 중 하나는 개발자들이 실제로 테스트코드를 만들 희망이 조금 있는 프레임워크를 만드는 것이다. 프레임워크는 친숙한 도구를 사용하므로 새로 익힐 것이 거의 없다. 그것은 테스트를 만드는 데 절대적으로 필요한 것 이상의 작업을 요구하면 안된다. 그것은 중복된 노력을 제거해준다.

모든 테스트들이 동작해야 할 경우, 디버거에 표현식을 작성하면 된다. 그러나 이것은 테스트를 하기에 충분하지 않다. 당신의 프로그램 작동이 나에게 도움이 되지 않는다. 왜냐하면 그것은 통합이 끝난 뒤 1분 후에도 프로그램이 정상작동하리라고 보증하지 않기 때문이다. 그리고 그것은 당신이 오랫동안 자리를 비웠을 때 5년뒤에도 정상적으로 동작한다고 보증하지 못한다.

그래서 테스트의 두번째 목표는 시간이 지나도 가치를 유지하는 테스트를 만드는 것이다. 때때로 테스트를 만든 사람이 아닌 다른 사람이 테스트를 실행하고 결과를 해석할 수 있어야 한다.  다양한 개발자들의 테스트를 결합하고 간섭의 두려움없이 그것들을 함께 실행할 수 있어야 한다.

마지막으로,  기존 테스트를 활용하여 새로운 테스트를 만들 수 있어야 한다. setup이나 fixture를 만드는 것은 값비싼 비용이 들고 프레임워크는 다른 테스트들을 실행하기 위해 fixture들을 재사용할 수 있어야 한다. 오, 그게 다야?

3. The Design of JUnit

JUnit의 디자인은 (“패턴 생성 아키텍처”, Kent Beck 및 Ralph Johnson, ECOOP 94 참조)에서 처음 사용된 스타일로 제공된다.  아이디어는 시스템의 아키텍처를 가질 때까지 아무것도 시작하지 않고 하나하나씩 패턴을 적용하여 시스템 설계를 설명하는 것이다. 우리는 해결할 아키텍처 문제를 제시하고, 이를 해결하는 패턴을 요약한 다음 패턴이 JUnit에 어떻게 적용되었는지 보여줄 것이다.

3.1 Getting started- TestCase

먼저 기본개념인 TestCase를 표현할 객체를 만들어야 한다.
개발자는 종종 테스트 사례를 염두에 두지만 여러 가지 방법으로 테스트 사례를 실현한다.

  • 구문 출력
  • 디버거 표현식
  • 테스트 스크립트들

조작이 쉬운 테스트를 만들고 싶다면 객체를 만들어야 한다.
이는 은연중에 개발자가 테스트를 받아들이고 구체적으로 만들어, 시간이 지남에 따라 가치를 유지하는 테스트를 만드는 목표를 뒷받침한다.
동시에 객체 개발자는 객체 개발에 익숙해지기 때문에 객체로 테스트를 하는 결정은 테스트 작성을
더 매력적으로 만드는 (또는 적어도 덜 강요하는) 목표를 뒷받침한다.
커맨드 패턴 (see Gamma, E., et al. Design Patterns: Elements of Reusable Object-Oriented Software, Addison-Wesley, Reading, MA, 1995)은 우리의 필요를 아주 잘 맞춘다.
의도를 인용하면 “요청을 객체로 캡슐화하여 큐 또는 로그 요청 …”명령은 작업에 대한 객체를 만들고 “execute”메소드를 제공한다. 다음은 TestCase의 클래스 정의 코드이다.

public abstract class TestCase implements Test {
    …
}

이 클래스는 상속을 통해 재사용 될 것으로 기대하기 때문에 “public abstract”이라고 선언한다.
지금은 Test 인터페이스를 구현한다는 사실을 무시하라. 현재 설계의 목적을 위해 TestCase를
하나의 class로 생각할 수 있다.
모든 TestCase는 이름으로 작성되므로 테스트가 실패하면 실패한 테스트를 식별 할 수 있다.

public abstract class TestCase implements Test {
    private final String fName;

    public TestCase(String name) {
        fName= name;
    }

    public abstract void run();
        …
}

JUnit의 발전을 설명하기 위해, 아키텍처의 스냅 샷을 보여주는 다이어그램을 사용한다.
우리가 사용하는 표기법은 간단하다. 연관된 패턴을 포함하는 음영 처리된 상자로 클래스에 주석을
붙인다. 패턴에서 클래스의 역할이 분명하면 패턴이름만 표시된다. 역할이 명확하지 않은 경우
음영 처리된 상자는이 클래스가 해당하는 참가자의 이름으로 보강된다.
이 표기법은 다이어그램의 혼란을 최소화하기 위해 (see Gamma, E., Applying Design Patterns in Java, in Java Gems, SIGS Reference Library, 1997)에 처음으로 나타났다.
그림 1은 TestCase에 적용된 표기법을 보여준다. 우리는 하나의 클래스를 다루고 있으므로
모호하지 않고 패턴이름만 보여진다.

%e1%84%89%e1%85%b3%e1%84%8f%e1%85%b3%e1%84%85%e1%85%b5%e1%86%ab%e1%84%89%e1%85%a3%e1%86%ba-2016-12-03-%e1%84%8b%e1%85%a9%e1%84%8c%e1%85%a5%e1%86%ab-10-20-22

3.2 Blanks to fill in- run()

해결해야 할 다음 문제는 개발자에게 fixture 코드와 테스트 코드를 넣는 편리한 장소를
제공하는 것이다. abstract로 TestCase를 선언하면 개발자는 subclassing으로
TestCase를 재사용하는 것으로 예상한다. 그럼에도, superclass에 하나의 변수가 있고
기능이 제공되지 않는다면 테스트를 쉽게 만드는 우리의 첫번째 목표를 충족하지 못한다.

다행히도, 모든 테스트에 test fixture를 설정하고 fixture에 대한 코드를 실행하고
결과를 확인한 다음, fixture를 정리하는 공통적인 구조가 있다.
이것은 각 테스트가 새로운 fixture로 실행되고 한 테스트의 결과가 다른 테스트의
결과에 영향을 줄 수 없다는 것을 의미한다.
이것은 테스트의 가치를 극대화하는 목표를 지원한다.

템플릿 메서드는 우리의 문제를 아주 잘 해결해준다. 의도로부터 인용하자면,
“연산에서 알고리즘의 골격을 정의하고, subclasses로 일부 단계를 연기한다.
템플릿 메서드는 알고리즘 구조를 변경하지 않고 subclasses가 알고리즘의
특정단계를 재정의할 수 있다” 이것은 정확히 맞다.
우리는 개발자가 fixture(set up and tear down) 코드를 만드는 방법과
테스트 코드를 만드는 방법을 개별적으로 고려할 수 있기를 바란다.
그럼에도, 이 순서의 실행은 fixture 코드나 테스트 코드를 만드는 방법과
관계없이 모든 테스트에서 동일하게 유지될 것이다.

여기 템플릿 메서드가 있다.

public void run() {
    setUp();
    runTest();
    tearDown();
}

이 메서드의 기본 구현은 아무것도 하지 않는다.

protected void runTest() {
}

protected void setUp() {
}

protected void tearDown() {
}

setUp과 tearDown은 재정의 될 것이지만, 그들은 프레임워크에 의해 호출될 것이므로
protected로 선언한다. 이번 여행의 두번째 스냅샷은 그림2에서 보여주고 있다.

Figure 2 TestCase.run() applies Template Method

3.3 Reporting results- TestResult

TestCase가 forest에서 실행되면 결과에 대해 아무도 신경쓰지 않는가?
물론, 당신은 테스트를 실행하여 실행여부를 확인해야 한다.
테스트가 실행된 후, 작동여부에 대한 요약을 담은 기록이 필요하다.

테스트가 성공 혹은 실패할 확률이 같거나 하나의 테스트만 실행한 경우,
TestCase 객체에 대한 flag를 설정하고 테스트 완료시 flag를 살펴볼 수 있다.
그럼에도, 테스트는 아주 불균형한데 보통은 작동한다.
따라서, 실패와 성공에 대해 간략히 압축한 요약정보를 기록하기를 원한다.

스몰토크 모범사례패턴은 (see Beck, K. Smalltalk Best Practice Patterns, Prentice Hall, 1996)
적용가능한 패턴을 가지고 있다. 이를 Collecting Parameter라고 부른다.
여러가지 방법으로 결과를 수집해야 할 때, 메서드에 파라미터를 추가하고 결과를 수집할
객체를 전달해야 한다. 테스트 결과를 수집하기 위해 TestResult라는 새로운 객체를 만들어야 한다.

public class TestResult extends Object {
    protected int fRunTests;

public TestResult() {
       fRunTests= 0;
    }
}

간단한 버전의 TestResult는 실행된 테스트 숫자만 계산한다.
이를 사용하려면, TestCase.rum() 메서드에 파라미터를 추가하고, TestResult에게
테스트가 실행중임을 알려라.

public void run(TestResult result) {
    result.startTest(this);
    setUp();
    runTest();
    tearDown();
}

그리고 TestResult는 실행된 테스트의 숫자를 추적해야 한다.

public synchronized void startTest(Test test) {
    fRunTests++;
}

우리는 TestResult의 startTest메서드를 synchronized로 선언하여 테스트가 다른 쓰레드에서 실행될 때 단일 TestResult가 안전하게 결과를 수집할 수 있도록 한다.
마지막으로,  우리는 간단한 TestCase의 외부 인터페이스를 유지하기를 원하므로
자체적으로 TestResult를 만드는 파라미터 없는 run() 버전을 만든다.

public TestResult run() {
    TestResult result= createResult();
    run(result);
    return result;
}

protected TestResult createResult() {
    return new TestResult();
}

Figure 3 shows our next design snapshot.

Figure 3: TestResult applies Collecting Parameter

테스트가 항상 올바르게 실행되면, 테스트를 만들지 않아도 된다.
실패하는 테스트는 흥미로운데, 특히 실패할 것으로 예상하지 못하는 경우가 그렇다.
게다가 테스트는 예를 들어 잘못된 결과를 계산하여 예기치 못한 방식으로 실패할 수 있고,
예를 들어 배열의 범위를 벗어나게 테스트를 만들면 실패할 수 있다.
테스트가 실패하는 것과 상관없이 우리는 이어지는 테스트를 실행하길 바란다.

Junit은 실패와 오류를 구분한다. 실패의 가능성은 예상되고 assertions와 함께 확인된다.
오류는 ArrayIndexOutOfBoundsException과 같은 예상치 못한 문제이다.
실패는 AssertionFailedError 오류로 표시된다. 예상치 못한 오류와 실패를 구분하기 위해서
추가된 catch절 (1)에서 실패를 잡는다.
두번째 절 (2)에선 모든 다른 예외를 잡아서 테스트가 계속 실행됨을 보장한다.

public void run(TestResult result) {
    result.startTest(this);
    setUp();
    try {
        runTest();
    }
    catch (AssertionFailedError e) { //1
        result.addFailure(this, e);
    }    catch (Throwable e) { // 2
        result.addError(this, e);
    }
    finally {
        tearDown();
    }
}

AssertionFailedError는 TestCase에서 제공하는 assertion 메서드에 의해 유발된다.
Junit은 서로 다른 목적을 위해 assertion 메서드의 세트를 제공한다.
여기 가장 간단한 방법이 있다.

protected void assertTrue(boolean condition) {
    if (!condition)
        throw new AssertionFailedError();
}

AssertionFailedError는  클라이언트(TestCase내부의 테스트 메서드)에 의해 잡히진 않지만
Template Method TestCase.run() 내부에서 발견할 수 있다.
따라서 Error에서 AssertionFailedError를 파생시킨다.

public class AssertionFailedError extends Error {
    public AssertionFailedError () {}
}

TestResult에서 오류를 수집하는 방법은 아래와 같다.

public synchronized void addError(Test test, Throwable t) {
    fErrors.addElement(new TestFailure(test, t));
}

public synchronized void addFailure(Test test, Throwable t) {
    fFailures.addElement(new TestFailure(test, t));
}

TestFailure는 실패한 테스트와 나중에 보고를 위한 예외를 함께 묶은 프레임워크 내부
helper class이다.

public class TestFailure extends Object {
    protected Test fFailedTest;
    protected Throwable fThrownException;
}

collecting parameter의 정규형은 각 메서드에 collecting parameter를 전달해야 한다.
이 조언에 따르면, 테스트 메서드 각각은 TestResult를 위한 파라미터가 필요하다.
이것은 이러한 메서드 signatures의 “오염”의 결과가 된다.
실패에 예외를 사용하는 친절한 부작용으로 이러한 signature 오염을 피할 수 있다.
테스트 케이스 메서드 혹은 그것으로부터 호출된 helper 메서드는 TestResult를 알 필요없이
예외를 던질 수 있다. 여기 refresher는 우리의 MoneyTest suite의 샘플 테스트 메서드이다.
그것은 테스트 메서드가 TestResult에 대해 알 필요가 없다는 것을 보여준다.

public void testMoneyEquals() {
    assertTrue(!f12CHF.equals(null));
    assertEquals(f12CHF, f12CHF);
    assertEquals(f12CHF, new Money(12, “CHF”));
    assertTrue(!f12CHF.equals(f14CHF));
}

JUnit은 TestResult의 다른 구현이 있다. 기본 구현은 실패 및 오류 숫자를 계산하고 결과를 수집한다.
TextTestResult는 결과를 수집하여 텍스트 형식으로 표시한다.
마지막으로, UITestResult는 그래픽 테스트 상태를 업데이트 하기 위해 JUnit Test Runner의
그래픽 버전을 사용한다.

TestResult는 프레임워크의 확장 지점이다. 클라이언트는 자신만의 TestResult  클래스를
정의할 수 있다. 예를 들어 HTMLTestResult는 HTML문서로 결과를 보여준다.

3.4 No stupid subclasses – TestCase again

우리는 Command를 테스트에 적용했다. Command는 execute(TestCase안에 run()이라 불리는)
와 같은 단일 메서드를 사용하여 호출한다. 이 간단한 인터페이스는 동일한 인터페이스를 통해
Command의 다른 구현을 호출할 수 있다.

우리는 일반적으로 테스트를 실행하기 위한 인터페이스가 필요하다.
그러나, 모든 테스트 케이스는 동일한 클래스에서 다른 메서드로 구현된다.
이것은 클래스의 불필요한 확산을 피하게 한다.
주어진 테스트 케이스는 여러 메서드를 구현할 수 있고, 각각은 하나의 테스트 케이스를 정의한다.
각 테스트케이스는 testMoneyEquals나 testMoneyAdd와 같은 설명이 포함된다.
이 테스트 케이스는 간단한 command 인터페이스를 따르지 않는다. 동일한 command 클래스의
다른 인스턴스는 다른 메서드로 호출해야 한다. 그러므로 우리의 다음 문제는 테스트 호출자의
관점에서 모든 테스트 케이스가 동일하게 보이도록 하는 것이다.

사용가능한 디자인 패턴으로 해결된 문제를 검토하면 Adapter 패턴이 마음에 든다.
Adapter에는 “클래스의 인터페이스를 클라이언트가 기대하는 다른 인터페이스로 변환”이라는
의도가 담겨있다. 이것은 잘 맞는 것처럼 보인다. Adapter는 이 작업을 수행하는 다른 방법들을
알려준다. 그 중 하나는 subclassing을 사용하여 인터페이스를 조정하는 클래스 adapter이다.
예를 들어, testMoneyEquals를 runTest에 적용하려면 MoneyTest의 하위클래스를 구현하고runTest를 재정의하여 testMoneyEquals를 호출한다.

public class TestMoneyEquals extends MoneyTest {
    public TestMoneyEquals() { super(“testMoneyEquals”); }
    protected void runTest () { testMoneyEquals(); }
}

하위클래스를 사용하려면 각 테스트 케이스에 대한 하위클래스를 구현해야 한다.
이것은 테스트에게 추가적인 부담을 준다. 이것은 프레임워크가 테스트케이스를 추가하는 것을
가능한 단순하게 해야한다는 JUnit목표에 반하는 것이다. 또한, 각 테스트 메서드에 대한
하위클래스를 만들면 클래스가 늘어난다. 단일 메서드를 가진 많은 클래스들은 그 비용에 대한
가치가 없고 의미있는 이름을 찾기가 어려울 것이다.

Java는 익명내부클래스를 제공해서 클래스이름 지정 문제에 대한 흥미로운 java관련 솔루션을
제공한다. 익명 내부클래스를 사용하면 클래스 이름을 만들 필요없이 Adapter를 만들 수 있다.

TestCase test= new MoneyTest(“testMoneyEquals “) {
    protected void runTest() { testMoneyEquals(); }
};

이것은 완전한 서브클래스보다 더 편리하다. 그것은 개발자에게 약간의 부담을 주면서 컴파일타임
타입 체크를 한다. 스몰토크 모범사례 패턴은 pluggable 행동의 공통적인 표제하에 다르게
행동하는 서로 다른 인스턴스의 문제에 대한 또 다른 솔루션을 설명한다.
아이디어는 서브클래스를 필요로 하지 않고 다른 로직을 수행하기 위해 매개변수화 될 수 있는
단일 클래스를 사용하는 것이다.

pluggable 행동의 가장 간단한 형태는 Pluggable Selector이다. Pluggable Selector 는
인스턴스 변수에 스몰토크 메서드 선택자를 저장한다. 이 아이디어는 스몰토크에 국한되지
않는다. 그것은 자바에도 적용된다.  자바에선 메서드 선택자에 대한 개념이 없다.
그러나, 자바 reflection API를 사용하면 메서드 이름을 나타내는 문자열에서 메서드를 호출
할 수 있다. 이 기능을 사용하여 자바에서 pluggable selector를 구현할 수 있다.
여담이지만, 우리는 일반적으로 어플리케이션 코드에서 reflection을 사용하지 않는다.
우리의 경우에 인프라 프레임워크를 다루고 있으므로 reflection 모자를 쓰는 것이 좋다.

JUnit은 클라이언트에게 pluggable 선택자를 사용하거나 혹은 익명 adapter 클래스를 구현
할 수 있는 옵션을 제공한다. 이렇게 하기 위해, 우리는 pluggable 선택자를 runTest 메서드의
기본 구현으로 제공한다. 이 경우 테스트 케이스 이름은 테스트 메서드의 이름과 일치해야 한다.
다음과 같이 리플렉션을 사용하여 메서드를 호출한다. 먼저, 메서드 객체를 찾는다.
메서드 객체가 생기면 그것을 호출하고 인자를 전달할 수 있다. 우리의 테스트 메서드는 인자가
없기 때문에 빈 인자 배열을 전달 할 수 있다.

protected void runTest() throws Throwable {
    Method runMethod= null;
    try {
        runMethod= getClass().getMethod(fName, new Class[0]);
    } catch (NoSuchMethodException e) {
        assertTrue(“Method \””+fName+”\” not found”, false);
    }
    try {
        runMethod.invoke(this, new Class[0]);
    }
    // catch InvocationTargetException and IllegalAccessException
}

The JDK 1.1 리플렉션 API는 public 메서드만 찾을 수 있다. 이러한 이유로 테스트 메서드는
public으로 선언해야 하고, 그렇지 않으면 NoSuchMethodException이 발생할 것이다.

다음은 Adapter와 Pluggable Selector가 추가된 디자인 스냅샷이다.

Figure 4: TestCase applies either Adapter with an anonymous inner class or Pluggable Selector

3.5 Don’t care about one or many – TestSuite

시스템 상태에 확신을 얻으려면 많은 테스트를 실행해야 한다.
지금까지 JUnit은 단일 테스트 케이스를 실행하고 TestResult로 결과를 보고했다.
우리의 다음 도전은 다양한 테스트를 실행하도록 확장하는것이다.
이 문제는 테스트 호출자가 하나 이상의 테스트 케이스를 실행하는지 여부를 신경 쓸 필요없이
쉽게 해결 할 수 있어야 한다. 이러한 상황을 벗어날 수 있는 가장 인기있는 패턴은 Composite이다.
의도를 인용하면 “객체들을 트리구조로 구성해서 부분-전체 계층구조로 표현하면 Composite를
통해 클라이언트는 개별 객체와 객체의 구성을 균일하게 처리할 수 있다”
부분-전체 계층구조의 요점은 여기에 있다. 우리는 suites of suites of suites of tests를 지원하려고 한다.

Composite의 다음 참가자를 소개한다

  • Component: 테스트와 상호작용하는 데 사용할 인터페이스를 선언한다.
  • Composite: 인터페이스를 구현하고 테스트 컬렉션을 유지 관리한다.
  • Leaf: Component 인터페이스를 따르는 composition의 테스트케이스를 나타낸다.

이 패턴은 단일 혹은 복합 객체에 대한 일반적인 인터페이스를 정의하는 추상클래스를 소개한다.
클래스의 기본 목적은 인터페이스를 정의하는 것이다. 자바에서 Composite을 적용할 때는
추상 클래스가 아닌 인터페이스를 정의하는 것을 더 선호한다. 인터페이스를 사용하면
테스트를 위해 JUnit을 특정 기본 클래스에 맡기지 않아도 된다. 테스트들은 이 인터페이스를
따르는 것이 전부다. 따라서 패턴 설명을 조정하고 테스트 인터페이스를 도입한다.

public interface Test {
    public abstract void run(TestResult result);
}

테스트 케이스는 Composite의 Leaf에 해당하며 위에 보았듯이 인터페이스를 구현한다.

다음으로 Composite 참가자를 소개한다. 우리는 TestSuite 클래스의 이름을 지정한다.
TestSuite는 Vector안에 자식 테스트들을 가지고 있다.

public class TestSuite implements Test {
    private Vector fTests= new Vector();
}

run()메서드는 자식에게 위임한다.

public void run(TestResult result) {
    for (Enumeration e= fTests.elements(); e.hasMoreElements(); ) {
        Test test= (Test)e.nextElement();
        test.run(result);
    }
}
Figure 5: TestSuite applies Composite

마지막으로, 클라이언트는 suite에 테스트를 추가할 수 있어야 하고, addTest 메서드로
그것을 할 수 있다.

public void addTest(Test test) {
    fTests.addElement(test);
}

위 코드는 모두 Test 인터페이스에만 의존하고 있다. TestCase와 TestSuite는 Test 인터페이스를
따르므로 test suites를 재귀적으로 구성할 수 있다.
모든 개발자는 자신의 TestSuites를 만들 수 있다.
우리는 저 suites로 구성된 TestSuite를 만들어서 실행할 수 있다.

여기 TestSuite를 만드는 예제가 있다.

public static Test suite() {
    TestSuite suite= new TestSuite();
    suite.addTest(new MoneyTest(“testMoneyEquals”));
    suite.addTest(new MoneyTest(“testSimpleAdd”));
}

이것은 정상적으로 작동하지만, 모든 테스트를 수동으로 suite에 추가해야 한다.
Junit의 얼리어댑터는 우리에게 이것이 바보같다고 말했다.
새로운 테스트 케이스를 만들때마다 정적 suite() 메서드에 그것을 추가해야 한다는 걸
기억해야 한다. 그렇지 않으면 그것은 실행되지 않을 것이다.
우리는 테스트 케이스 클래스를 인자로 가지는 편리한 생성자를 TestSuite에 추가했다.
그 목적은 테스트 메서드를 추출하고 이를 포함하는 suite를 만드는 것이다.
테스트 메서드들은 접두어 “test”로 시작하고 인자가 없는 간단한 규칙을 따라야 한다.
편리한 생성자는 이 규칙을 사용하여, 테스트 메서드를 찾는 리플렉션을 사용해서 테스트 객체를
생성한다. 이 생성자를 사용하면 위의 코드가 다음과 같이 단순화된다.

public static Test suite() {
    return new TestSuite(MoneyTest.class);
}

원래 방법은 테스트 케이스의 하위 집합을 실행하려는 경우에도 유용하다.

3.6 Summary

JUnit의 요리사 여행은 끝났다. 다음 그림은 패턴으로 설명된 JUnit의 디자인을 한 눈에 보여준다.

Figure 6: JUnit Patterns Summary

프레임워크의 추상화 중심인 TestCase가 4개의 디자인패턴과 어떻게 관련되어 있는지 주목하라.
성숙한 객체 디자인의 그림은 이와 동일한 “패턴 밀도”를 보여준다.
디자인의 스타는 지원하는 플레이어와 풍부한 관계를 맺고 있다.

JUnit의 모든 패턴을 보여주는 또 다른 방법이 여기 있다. 이 스토리보드에는 차례대로
각 패턴의 효과를 추상적으로 표현한 것을 볼 수 있다.
따라서 Command 패턴은 TestCase 클래스를 만들고, Template Method 패턴은
run 메서드를 생성한다.(스토리보드 표기법은 모든 텍스트가 삭제된 그림 6의 표기법이다)

Figure 7: JUnit Pattern Storyboard

스토리보드에 대해 알아야 될 점 중 하나는 Composite을 적용할 때 그림의 복잡성이 어떻게
증가하는지에 대한 것이다. 이것은 Composite이 강력한 패턴이라는 직관에 대해 그림으로
확증되었지만, 그것은 “그림을 복잡하게 한다”. 따라서 주의해서 사용해야 한다.

4. Conclusion

결론을 내리기 위해 몇가지 일반적인 관찰을 해보자

* Patterns

우리는 프레임워크를 개발할 때와 다른 사람에게 설명하려고 할 때 모두 패턴의 관점에서
디자인을 매우 중요하게 논의하는 것을 발견했다. 이제는 패턴이 있는 프레임워크를 설명하는
것이 효과적인지 판단할 수 있는 완벽한 위치에 있다. 위의 토론이 마음에 들면, 자신의
시스템에 동일한 스타일의 프리젠테이션을 시도해봐라.

* Pattern density

JUnit의 핵심 추상화인 TestCase의 주변에는 “밀도”패턴이 있다. 패턴 밀도가 높은 디자인은
사용하기 쉽지만 변경하기 어렵다. 핵심 추상화와 같은 높은 패턴밀도는 성숙한 프레임워크의
일반적이라는 사실을 발견했다. 미숙한 프레임워크에 대해서는 그 반대가 되어야 한다.
그들은 패턴의 밀도가 낮아야 한다. 실제로 어떤 문제를 발견하여 해결하고자 한다면,
솔루션을 압축하여 밀도를 높이고  밀도가 높은 패턴의 필드를 활용하게 할 수 있다.

* Eat your own dog food

기존 유닛 테스팅이 구현되자마자 우리는 그것을 적용했다. TestTest는 프레임워크가 오류,
성공, 실패에 대한 올바른 결과를 보고하는지 검증한다.
우리는 프레임워크의 디자인을 계속적으로 발전시켜 가면서 이 값진 것을 발견했다.
우리는 JUnit의 가장 어려운 어플리케이션이 자체 동작을 테스트하고 있다는 것을 발견했다.

* Intersection, not union

프레임워크 개발에는 가능한 모든 기능을 포함하려는 유혹이 있다. 결국, 프레임워크를 최대한
가치있게 만들고 싶다. 그러나 개발자가 프레임워크를 사용하기로 결정해야 하는 중립적인 힘이
있다. 프레임워크의 기능이 적을수록, 배우기 쉽고, 개발자가 사용할 가능성이 높다.
JUnit은 이 스타일로 작성되어있다. 테스트 실행에 절대적으로 필요한 기능만 구현한다
– 테스트 suites의 실행, 각 테스트 실행의 격리, 테스트 자동실행
물론 우리는 몇가지 기능을 추가하는 것을 거부할 수 없었지만, 우리는 그것을 자신의
확장패키지에(test.extensions)넣을 때 주의해야만 했다.
이 패키지의 주목할만한 구성요소는 테스트 전후에 추가코드를 실행할 수 있는
TestDecorator이다.

* Framework writers read their code

우리는 JUnit을 만드는 것보다 JUnit코드를 읽는 데 더 많은 시간을 썼고, 새로운 기능을
추가하는데 소요된 시간만큼 중복기능을 제거했다. 우리는 우리가 상상할 수 있는 다양한
방식으로 디자인을 적극적으로 실험하고, 새로운 클래스들을 추가하고, responsibility를
옮겼다. 우리는 JUnit에 대한 지속적인 통찰력(테스팅, 객체 설계, 프레임워크 개발 및 추가
기사의 기회)으로 우리의 monomania에 대한 보상을 받았따(그들은 여전히 보상을 받고 있다)

JUnit의 최신버전은 http://www.junit.org에서 다운받을 수 있다.

5. Acknowledgements

주의깊게 읽고 부드럽게 수정해준 John Vlissides, Ralph Johnson, and Nick Edgar에게 감사한다.

숫자야구게임 TDD 연습

최범균님이 TDD로 숫자야구게임을 만든 영상을 보고 spock으로 따라 만들어 보았습니다.

최범균님이 만든 자바코드와 비교해서 보시면 재밌으실 것 같습니다.

추가로, 랜덤하게 3자리 숫자를 생성하는 생성기가 몇번만에 게임넘버를 맞추는지

테스트하는 RandomGameNumberTest.groovy를 추가했습니다.

xUnit 실습

켄트벡의 “테스트 주도 개발(Test-Driven Development)“책에 있는 xUnit예제를

groovy로 만들어 보았습니다.

이왕이면, 자신의 주력언어 이외 현재 관심이 있거나 공부해보고 싶은 프로그래밍 언어로

만들어보면 더욱 재미있을 것 같습니다.

실행은 TestCaseTest.groovy를 참고하시면 됩니다.

package net.bluepoet.xunit

/**
 * Created by bluepoet on 2016. 11. 25..
 */
class TestCaseTest {
    static void main(String[] args) {
        testTemplateMethod(setUp())
        testResult(setUp())
        testFailedResult(setUp())
        testFailedResultFormatting(setUp())
        testSuite(setUp())
    }

    static def setUp() {
        return new TestResult()
    }

    static def testTemplateMethod(result) {
        def test = new WasRun("testMethod")
        test.run(result)
        assert test.log == 'setUp testMethod tearDown'
    }

    static def testResult(result) {
        def test = new WasRun("testMethod")
        test.run(result)
        assert result.summary() == '1 run, 0 failed'
    }

    static def testFailedResult(result) {
        def test = new WasRun("testBrokenMethod")
        test.run(result)
        assert result.summary() == '1 run, 1 failed'
    }

    static def testFailedResultFormatting(result) {
        result.testStarted()
        result.testFailed()
        assert result.summary() == '1 run, 1 failed'
    }

    static def testSuite(result) {
        def suite = new TestSuite()
        suite.add(new WasRun("testMethod"))
        suite.add(new WasRun("testBrokenMethod"))
        suite.run(result)
        assert result.summary() == '2 run, 1 failed'
    }
}

[번역]Spock Primer

Spock의 공식문서 중 Spock Primer를 번역해 보았습니다.

보실 때 오역이 있음을 감안하시고, 번역하기에 애매한 부분은 원문 그대로 옮겼습니다.


이번 챕터는 당신이 Groovy와 unit testing에 기본적인 지식을 가지고 있음을 가정한다.

만약 당신이 자바 개발자인데 Groovy에 대해 듣지 못했더라도 걱정하지 말라.

Groovy는 당신에게 매우 친숙하게 느껴질 것이다.

사실 , Groovy의 주요 설계목표 중 하나는 자바와 함께 scripting language가 되는 것이다.

따라서 Groovy 문서를 참고하기만 하면 언제든지 따라 할 수 있다.

이번 챕터의 목표는 현실에서 Spock 명세를 사용하고 당신이 더 많은 것을 원한다면 Spock을

충분히 가르치는 데 있다.

Groovy에 대해 더 배우고 싶다면 http://groovy-lang.org/로 가라.

unit testing에 대해 더 배우고 싶다면 http://en.wikipedia.org/wiki/Unit_testing로 가라.

전문 용어(Terminology)

몇가지 정의들로 시작해보자. Spock은 system of interest에 의해 나타난 예상된 기능

(properties, aspects)을 설명하는 명세를 작성할 수 있다.

The system of interest는 단일 클래스와 전체 application간에 있을 수 있으며,

system under specification(SUS)라 불린다.

기능설명은 SUS와 해당 협력자들의 구체적인 snapshot으로부터 시작된다.

이 snapshot은 기능의 fixture로 불린다.

이어지는 섹션에서 당신은 Spock 명세가 구성된 모든 building blocks을 살펴볼 것이다.

일반적인 명세는 그 중 일부만 사용한다.

Imports

 import spock.lang.*
 

spock.lang Package는 명세를 작성하기 위한 가장 중요한 타입들을 포함한다.

Specification

 class MyFirstSpecification extends Specification {
 // fields
 // fixture methods
 // feature
 // methods
 // helper methods
 }
 

specification은 spock.lang.Specification을 상속하는 Groovy class를 나타낸다.

specification의 이름은 보통 시스템 혹은 specification에 의해 나타난 시스템 운영과 관련된다.

예를 들어, CustomSpec, H264VideoPlayBack 그리고 AspaceshipAttackedFromTwoSides

모두 specification을 위한 합리적인 이름이다.

Class Specification은 명세작성을 위한 다수의 유용한 메서드들을 포함한다.

게다가 그것은 Spock의 Junit runner인 Sputnik와 함께 명세를 실행하라고 Junit에게

지시한다. Sputnik에게 감사하고, Spock specifications는 대부분 요즘 자바 IDEs와 빌드툴에

의해 실행될 수 있다.

Fields

 def obj = new ClassUnderSpecification()
 def coll = new Collaborator()
 

instance 필드들은 명세의 fixture에 속한 객체들을 저장하기 좋은 위치이다.

그것은 선언지점에서 바로 초기화 하는 것이 좋다

(의미적으론, 이것은 setup() method의 가장 처음에 그들을 초기화 하는 것과 같다)

instance 필드에 저장된 객체는 feature method 사이에 공유되지 않는다.

대신, 모든 feature method는 자신만의 객체를 얻는다.

이것은 각각의 feature method들을 분리하는데 도움을 주고, 그것은 가장 바람직한 목표이다.

 @Shared res = new VeryExpensiveResource()
 

때때로 feature method들 사이에 객체를 공유하는 것이 필요하다.

예를 들어, 객체 생성비용이 크거나 혹은 feature method들 각각이 상호작용을 해야 될 때가 있다.

이것을 하기 위해, @Shared 필드를 선언한다.

그것은 선언하는 시점에서 필드를 초기화하는 것이 가장 좋다.

(의미적으로 이것은 setupSpec() method의 가장 처음에 필드를 초기화 하는 것과 같다)

 static final PI = 3.141592654
 

static fields는 상수를 위해 사용되어진다.

그렇지 않으면 공유된 필드들을 더 선호한다. 왜냐하면 공유하는 측면에서 그들의 의미가

더 잘 정의되었기 때문이다.

Fixture Methods

 def setup() {} // run before every feature method
 def cleanup() {} // run after every feature method
 def setupSpec() {} // run before the first feature method
 def cleanupSpec() {} // run after the last feature method
 

Fixture methods는 feature method가 실행되는 설정과 환경을 정리하는 역할을 맡는다.

보통 모든 feature method를 위해 setup() 그리고 cleanup() method 등에 해당하는

신선한 fixture를 사용하는 것은 좋은 생각이다.

모든 fixture methods는 선택이다.

때로는 feature method가 fixture를 공유하는 것이 의미가 있으며, 이는 setupSpec() 및

cleanupSpec() 메소드와 함께 공유 필드를 사용하여 수행된다.

setupSpec()이나 cleanupSpec()은 그들이 @Shared와 함께 어노테이션되지 않았다면

instance fields를 참조할 수 없다.

fixture methods가 specification subclass안에서 대체되면 superclass의 setup()은

subclass의 setup()전에 실행될 것이다. cleanup()은 역순으로 실행된다.

subclass의 cleanup()이 superclass cleanup()보다 먼저 실행될 것이다.

setupSpec()이나 cleanupSpec()은 같은 방식으로 실행한다.

spock은 상속계층의 모든 레벨에서 fixture methods들을 자동으로 찾아 실행하므로 명시적으로

super.setup()이나 super.cleanup()을 호출할 필요가 없다.

Feature Methods

 def "pushing an element on the stack"() {
 // blocks go here
 }
 

Feature Methods는 명세의 중심이다.

그것은 명세에 따라 시스템에서 찾을 것으로 예상되는 features(속성, 측면)을 설명한다.

관례상, feature methods는 String literals과 함께 명명되어진다.

당신의 feature methods를 위해 좋은 이름을 선택하도록 노력하라.

당신이 좋아하는 어떤 문자든 자유롭게 사용할 수 있다.

개념상, 하나의 feature method는 4개의 단계로 구성되어 있다.

1. feature’s fixture의 설정
2. 명세에 따라 시스템에 실행 제공
3. 시스템으로부터 예상된 반응 설명
4. feature’s fixture 정리

첫번째와 마지막 단계가 선택적임에도, 실행과 반응단계는 항상 존재한다.

(상호작용하는 feature methods 제외), 그것은 여러번 일어날 수 있다.

Blocks

Spock은 feature method의 각각의 개념적인 단계를 구현하기 위한 지원이 내장되어 있다.

이를 위해 feature methods는 소위 blocks라 불리는 것으로 구성된다.

Blocks는 라벨과 함께 시작하고, 다음 블락의 시작이나 method의 끝으로 확장된다.

blocks는 6가지 종류가 있다. setup, when, then, expect, cleanup, where blocks.

method 시작과 첫번째 명시적인 블록 사이에 어느 구문들은 암시적으로 setup block에 속한다.

feature method에는 적어도 하나의 명시적(레이블이 지정된)인 블록이 있어야한다.

실제로 명시적 블록이 있으면 method를 feature method로 만드는 것이다.

블록은 method를 별개의 부분으로 나누고 중첩 될 수 없습니다.

우측(편의상 아래)의 그림은 blocks이 feature method의 개념적인 단계와 어떻게 맵핑되는지

보여준다.

%e1%84%89%e1%85%b3%e1%84%8f%e1%85%b3%e1%84%85%e1%85%b5%e1%86%ab%e1%84%89%e1%85%a3%e1%86%ba-2016-11-23-%e1%84%8b%e1%85%a9%e1%84%92%e1%85%ae-11-18-30

where block은 특별한 역할을 가지고 있고, 곧 보여질 것이다.

그러나 첫번째로, 다른 blocks를 더 자세히 살펴보자.

Setup Blocks

 setup:
 def stack = new Stack()
 def elem = "push me"
 

setup block은 설명하는 기능에 대한 설정작업을 수행하는 곳이다.

그것은 다른 블록에 의해 선행될 수도 없고, 반복될 수도 없다.

setup block은 어떤 특별한 의미를 가지지 않는다.

setup: label은 선택적이고 생략될 수 있으므로, 결과적으로 암시적인 setup block이 된다.

given: label은 setup:의 별칭이고 때때로 더 가독성 있는 feature method 설명으로 이어진다

(문서는 Specificatons을 참고하라)

When and Then Blocks

 when: // stimulus
 then: // response
 

when과 then blocks는 항상 같이 나타난다. 그들은 실행하고 예상된 결과를 설명한다.

when blocks이 임의의 코드를 포함함에도, then blocks은 상태,예외 상태, 상호작용,

그리고 변수정의로 제한된다. feature method는 여러쌍의 when-then blocks를 포함할 수 있다.

Conditions

Conditions은 Junit의 단정문과 매우 흡사한 예상되는 상태를 설명한다.

그럼에도 불구하고 Conditions는 일반적인 boolean expressions으로 작성되므로,

단정문 API가 필요없다. (더 정확하게는, condition은 또한 boolean value가 아닌 것을

만들 수 있으며, 그 다음 Groovy truth에 따라 평가된다)

실제 몇가지 conditions를 살펴보자

 when:
 stack.push(elem)

then:
 !stack.empty
 stack.size() == 1
 stack.peek() == elem
 

TIP : feature method당 conditions의 수를 가급적 작게 유지하라.

하나에서 5개 정도가 좋은 가이드라인이다. 만약 당신이 저것보다 더 많이 가지고 있다면,

한번에 여러 관련없는 기능들을 넣었는지 스스로에게 자문해보라.

대답이 맞다면, 몇몇 작은 것들로 feature method를 분리하라.

당신의 conditions가 오직 그들의 값들에 대해서만 다르다면, data table 사용을 고려해보라

condition이 잘못되었을 때 Spock이 제공하는 피드백은 어떤 종류가 있을까?

두번째 condition을 2로 바꿔보자. 여기 우리가 얻을 수 있는 것이 있다.

 Condition not satisfied:

stack.size() == 2
 | | |
 | 1 false
 [push me]
 

당신이 보는것처럼, Spock은 condition을 평가하는 동안 만들어진 모든 값들을 캡쳐하고

그들을 쉽게 이해할 수 있는 형태로 보여준다. 좋지 않은가?

Implicit and explicit conditions

Conditions는 then과 expect blocks에서는 필수적인 재료이다.

상호작용으로 분류된 method와 표현식은 void method를 호출하는 경우를 제외하고,

이 블록안에서 모든 최상위 표현식들은 conditions로 암시적으로 처리된다.

다른 위치에서 conditions를 사용하기 위해선, 당신은 Groovy의 assert keyword와

함께 그들을 지정하는 것이 필요하다.

 def setup() {
 stack = new Stack()
 assert stack.empty
 }
 

만약 명시적인 조건이 잘못됐다면, 그것은 암시적인 조건과 동일한 멋진 진단메세지를

만들어 낼 것이다.

Exception Conditions

예외 조건은 when block에서 예외를 던지는 것을 설명할 때 사용된다.

그들은 thrown() method 사용으로 정의되며, 예상된 예외타입을 전달한다.

예를 들어, 빈 스택에서 가져오는 것을 설명할 때 EmptyStackException을 던질 수 있다.

당신은 아래와 같이 쓸 수 있다.

 when:
 stack.pop()

then:
 thrown(EmptyStackException)
 stack.empty
 

당신이 보는 것처럼, 예외조건은 다른 조건들(및 다른 블록)이 이어질 수 있다.

이것은 특별히 예외의 예상된 내용을 명시하는데 유용하다.

예외에 접근하기 위해, 첫번재로 변수에 그것을 결합해라

 when:
 stack.pop()

then:
 def e = thrown(EmptyStackException)
 e.cause == null
 

이 문법은 두 가지 이점이 있다.

첫번째로, 예외변수는 강력하게 형식화되어, IDE가 코드완성을 제공하기가 더 수월해진다.

두번째로, 조건은 조금은 문장과 비슷하게 읽힌다.(그리고 EmptyStackException이 던져진다)

예외타입이 thrown() method에 전달되지 않으면, 왼쪽의 변수 타입으로 유추한다.

때때로 우리는 예외가 던져질 수 없음을 전달할 필요가 있다.

예를 들어, HashMap에 null key를 넣는다고 해보자.

 def "HashMap accepts null key"() {
 setup:
 def map = new HashMap()
 map.put(null, "elem")
 }
 

이것은 작동하지만 코드의 의도가 드러나지 않는다. 이 메서드 구현을 끝내기 전에

개발을 끝낸 누군가가 있는가? 결국, 조건은 어디에 있는가?

다행히도 우리는 더 잘 할 수 있다.

 def "HashMap accepts null key"() {
 setup:
 def map = new HashMap()

when:
 map.put(null, "elem")

then:
 notThrown(NullPointerException)
 }
 

notThrown()을 사용함으로써, 우리는 NullPointerException이 던져지지 않는다는 것을

분명히 할 수 있다.

(Map.put()의 명세에 따라, 이것은 null keys를 지원하지 않는 맵을 위해선 옳은 것이

될 것이다) 그럼에도 불구하고, 이 메서드는 어떤 다른 예외가 던져진다면 실패할 것이다.

Interactions

조건들은 객체의 상태를 설명함에 비해,  상호작용은 객체들이 서로 어떻게 협력하는지 보여준다.

상호작용은 전체 챕터에서 다루므로 우리는 여기서 간단한 예제를 보여주겠다.

우리는 publisher으로부터 그것의 subscribers에게 가는 이벤트들의 흐름을 설명한다고

가정해보자. 여기 코드가 있다.

def "events are published to all subscribers"() {
  def subscriber1 = Mock(Subscriber)
  def subscriber2 = Mock(Subscriber)
  def publisher = new Publisher()
  publisher.add(subscriber1)
  publisher.add(subscriber2)

  when:
  publisher.fire("event")

  then:
  1 * subscriber1.receive("event")
  1 * subscriber2.receive("event")
}

Expect Blocks

expect block은 조건과 변수 정의만 할 수 있다는 점에서 then block보다 더 제한적이다.

단일 표현에서 실행과 예상되는 반응을 설명하기 더 자연스러운 상황에서 유용하다.

예를 들어 Math.max() method를 설명하는 두가지 시도를 비교해보자

when:
def x = Math.max(1, 2)

then:
x == 2
expect:
Math.max(1, 2) == 2

두 코드는 의미적으로 비슷함에도 불구하고, 두번째가 분명히 더 바람직하다.

가이드에 따르면, side effects가 있는 methods를 설명할 때는 when-then block을

순전히 기능 methods를 설명할때는 expect를 사용해라.

TIP : any()나 every()같은 Groovy JDK 메서드들을 사용하여 보다 표현적이고 간결한 조건을

만들어라.

Cleanup Blocks

setup:
def file = new File("/some/path")
file.createNewFile()

// ...

cleanup:
file.delete()

cleanup block은 where block 다음에 오고, 반복되지 않는다.

cleanup 메서드처럼, feature method에 의해 사용된 어떤 자원들을 해제하고,

feature 메서드에서 예외가 발생되도 실행한다.

그 결과로, cleanup block은 최악의 경우에 feature 메서드의 첫번째 실행문에서 예외를 던져도

모든 지역변수들이 그들의 기본 값들을 갖도록 정상적으로 처리되어야한다.

TIP : Groovy의 안전한 방어 연산자(foo?.bar())는 방어코드 작성을 단순화한다

객체레벨 명세에서는 보통 cleanup 메서드가 필요하지 않다.

그들이 소비하는 자원은 garbage collector에 의해 자동으로 반환되는 메모리이다.

좀 더 조잡한 명세에서는 그럼에도 불구하고, 파일시스템을 정리할 때 cleanup block을

사용하거나 database connection을 닫을 때, 혹은 network service를 셧다운 시킬때

사용한다.

TIP : 모든 feature 메서드들이 같은 자원을 필요로 하도록 설계되었을 경우,

cleanup() method를 사용하라. 그렇지 않으면 cleanup block을 선호해라.

이와 같은 절충은 setup() methods와 setup blocks에도 적용된다.

Where Blocks

where block은 항상 메서드의 마지막에 위치하고, 반복되지 않는다.

그것은 데이터 기반 feature method에 사용된다..

이것이 어떻게 동작하는지 알려주기 위해, 이어지는 예제를 보자.

def "computing the maximum of two numbers"() {
  expect:
  Math.max(a, b) == c

  where:
  a << [5, 3]
  b << [1, 9]
  c << [5, 9]
}

where block은 feature method의 두가지 버전을 효율적으로 만든다.

하나는 a가 5, b가 1, c가 5이고 다른 하나는 a가 3, b가 9, c가 9이다.

마지막으로 선언되었지만 feature method가 실행되기 전에 where 블록이 평가된다.

where 블록은 Data Driven Testing 장에서 자세히 설명한다.

Helper Methods

때로는 feature method가 커지거나 중복된 코드가 많이 포함될 수 있다.

그러한 경우 하나 이상의 helper method를 도입하는 것이 타당하다.

helper method의 두 가지 좋은 후보는 setup/cleanup logic과 복잡한 조건들이다

전자를 분석하는 것은 간단하므로 조건들을 살펴보겠다.

def "offered PC matches preferred configuration"() {
  when:
  def pc = shop.buyPc()

  then:
  pc.vendor == "Sunny"
  pc.clockRate >= 2333
  pc.ram >= 4096
  pc.os == "Linux"
}

당신이 컴퓨터를 매우 좋아한다면(geek), 당신이 선호하는 PC 구성이 매우 상세해서,

많은 다른 상점의 제안을 비교하길 원할 것이다. 그러므로 조건을 분석해보자

def "offered PC matches preferred configuration"() {
  when:
  def pc = shop.buyPc()

  then:
  matchesPreferredConfiguration(pc)
}

def matchesPreferredConfiguration(pc) {
  pc.vendor == "Sunny"
  && pc.clockRate >= 2333
  && pc.ram >= 4096
  && pc.os == "Linux"
}

새로운 helper method matchesPreferredConfiguration()는 결과를 반환하는

단일 boolean 표현식으로 구성된다.(return keyword는 Groovy에서 선택이다)

부적절한 제안이 제시되는 것을 제외하면 이것은 괜찮다.

Condition not satisfied:

matchesPreferredConfiguration(pc)
|                             |
false                         ...

이것은 별로 도움이 되지 않는다. 하지만 운좋게도 우리는 더 잘할 수 있다.

void matchesPreferredConfiguration(pc) {
  assert pc.vendor == "Sunny"
  assert pc.clockRate >= 2333
  assert pc.ram >= 4096
  assert pc.os == "Linux"
}

helper method로 조건을 분해 할 때는 두 가지 점을 고려해야한다.

첫째, 암시적 조건은 assert 키워드를 사용하여 명시적 조건으로 변환해야한다.

둘째, helper method에는 반환 형식 void가 있어야 한다.

그렇지 않으면 Spock은 반환 값을 실패한 조건으로 해석 할 수 있다.

이는 우리가 원하는 것이 아니다.

예상한대로, 개선된 helper method는 우리에게 잘못되었다고 말해준다.

Condition not satisfied:

assert pc.clockRate >= 2333
       |  |         |
       |  1666      false
       ...

마지막 조언 : 코드 재사용은 일반적으로 좋은 것이지만, 너무 과하게 사용하지마라.

fixture와 helper methods의 사용이 feature methods간의 결합도가 증가된다는 것을

알아야 한다.

만약 재사용을 너무 많이 하거나 잘못한다면, 당신은 깨지기 쉽고 개선하기 어려운 명세가

될 것이다.

Using with for expectations

위의 helper method 대신에, 당신은 with(target, closure) method로 검증해야 될 객체와

상호작용할 때 사용할 수 있다. 이것은 특별히 then과 expect block에 유용하다

def "offered PC matches preferred configuration"() {
  when:
  def pc = shop.buyPc()

  then:
  with(pc) {
    vendor == "Sunny"
    clockRate >= 2333
    ram >= 406
    os == "Linux"
  }
}

당신이 helper method를 좋아하지 않는다면, 적절한 에러 리포팅을 위한 명시적인 선언문이

필요없다. mocks를 검증할 때, with 구문은 자세한 검증구문을 생략할 수 있다.

def service = Mock(Service) // has start(), stop(), and doWork() methods
def app = new Application(service) // controls the lifecycle of the service

when:
app.run()

then:
with(service) {
  1 * start()
  1 * doWork()
  1 * stop()
}

Specifications as Documentation

잘 쓰여진 명세는 귀중한 정보원이다. 특히 개발자 (건축가, 도메인 전문가, 고객등)보다

광범위한 고객을 대상으로하는 고급명세의 경우 명세 및 기능의 이름뿐 아니라 자연스러운

언어로 더 많은 정보를 제공하는 것이 좋다.

따라서 Spock은 블록에 텍스트 설명을 첨부하는 방법을 제공한다.

setup: "open a database connection"
// code goes here

블록의 개별부분은 and:로 설명할 수 있다.

setup: "open a database connection"
// code goes here

and: "seed the customer table"
// code goes here

and: "seed the product table"
// code goes here

and : label 뒤에 description을 붙이는 것은 메소드의 의미를 변경하지 않고 feature 메소드의

어떤(최상위)위치라도 삽입 할 수 있다.

Behavior Driven Development (행위 주도 개발)에서는 고객 지향적인 기능은(스토리라고 함)

given-when-then 형태로 설명된다.

Spock은 given: 라벨과 같은 명세의 스타일을 직접 지원한다.

given: "an empty bank account"
// ...

when: "the account is credited $10"
// ...

then: "the account's balance is $10"
// ...

앞에서 언급했듯이 주어진 given:은 setup의 별칭이다.

블록 설명은 소스 코드에 존재할뿐만 아니라 Spock 런타임에서도 사용할 수 있다.

블록 설명의 계획된 사용은 강화된 진단 메시지 및 모든 이해 관계자가 동일하게 이해하는

텍스트 보고서이다.

Extensions

지금까지 보았 듯이 Spock은 명세 작성을 위한 많은 기능을 제공한다.

그러나 다른 어떤 것이 필요할 때가 항상 있습니다.

따라서 Spock은 차단 기반 확장 메커니즘을 제공합니다.

확장 기능은 지시문이라 불리는 어노테이션에 의해 활성화됩니다.

현재 Spock에는 다음과 같은 지침이 포함되어 있습니다.

@Timeout
feature 및 fixture method 실행을 위한 타임아웃 설정

@Ignore
feature method실행 무시

@IgnoreRest
이 어노테이션을 가지고 있지 않은 모든 feature method는 무시된다.

빠르게 단일 메서드를 실행하고 싶을 때 유용하다

@FailsWith
feature method가 갑자기 완료 될 것으로 기대한다. @FailsWith에는 두 가지 사용 사례가 있다.

첫째, 즉시 해결할 수없는 알려진 버그를 문서화하는 것이다.

둘째, 특정 상황에서 예외 조건을 바꿀 수 있다 (예외 조건의 동작 지정과 같이).

다른 모든 경우에는 예외 조건이 바람직하다.

자신의 지시문과 확장을 구현하는 방법을 배우려면 Extensions 장으로 이동하라.

Comparison to JUnit

Spock은 다른 용어를 사용하지만 JUnit에서 영감을 얻은 많은 개념과 기능이 있다.

다음은 대략적인 비교이다.

%e1%84%89%e1%85%b3%e1%84%8f%e1%85%b3%e1%84%85%e1%85%b5%e1%86%ab%e1%84%89%e1%85%a3%e1%86%ba-2016-11-24-%e1%84%8b%e1%85%a9%e1%84%8c%e1%85%a5%e1%86%ab-12-31-11