숫자야구게임 TDD 연습

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

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

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

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

Advertisements

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

[Spock]spock-dbunit 초간단 예제

기존 스프링 프레임워크 기반에서 어플리케이션 개발시, DB와 연관된 테스트코드를 만들기 위해

spring-test-dbunit 라이브러리를 사용했습니다.

최근 프로젝트에서 스프링부트를 사용하면서, 다시 DB에 대해 테스트코드를 만들어야 되는 일이

생겼는데요. 테스트 프레임워크는  전부터 즐겨쓰던 groovy기반의 spock을 사용하고 있습니다.

spock 역시 dbunit관련 라이브러리가 있을까 살펴보던 중, spock-dbunit을 발견해서

간단히 CRUD 예제를 만들어 보았습니다.

전에 spring-test-dbunit을 사용할 때는, 별도의 xml파일을 만들어야 되서 상당히 귀찮고

오타도 많아 테스트코드 작성시 어려움이 많았었지만, spock-dbunit의 모토가

그 어려움과 귀찮음을 피하는 것이라고 하니 개인적인 입장에서는 상당히 호감이 갔습니다.

그리고 무엇보다 sql query를 소스상에 직접 작성해서 테스트코드를 만들 수 있는 점도

큰 장점으로 생각되네요.

물론, 개인개발자가 1인이 만들었고 처음 만든게 2013년 중반임에도 아직 버전이 0.2라서

spring-test-dbunit보다 상대적으로 기능상 쓰기에 더 불편 할수도 있습니다.

package net.bluepoet.exercise

import be.janbols.spock.extension.dbunit.DbUnit
import groovy.sql.Sql
import org.apache.tomcat.jdbc.pool.DataSource
import spock.lang.Specification

/**
 * Created by bluepoet on 2016. 11. 17..
 */
class SpockDBUnitTest extends Specification {

    DataSource dataSource

    @DbUnit
    def content = {
        User(id: 1, name: 'bluepoet', age: 20, 'phone_number': '010-0000-0001')
        User(id: 2, name: 'tester', age: 10, 'phone_number': '010-0000-0002')
    }

    def setup() {
        dataSource = new DataSource()
        dataSource.driverClassName = 'org.h2.Driver'
        dataSource.url = 'jdbc:h2:mem:'
        dataSource.username = 'sa'
        dataSource.password = ''
        new Sql(dataSource).execute("CREATE TABLE User(id INT PRIMARY KEY, name VARCHAR(255), age INT, phone_number VARCHAR(20))")
    }

    def cleanup() {
        new Sql(dataSource).execute("drop table User")
    }

    def "총 유저수가 몇명인지 테스트한다."() {
        when:
        def result = new Sql(dataSource).firstRow("select count(*) as cnt from User")

        then:
        result.cnt == 2
    }

    def "특정 유저정보를 가져와 확인한다."() {
        expect:
        new Sql(dataSource).firstRow("select * from User where id = " + id) == result

        where:
        id | result
        1  | [ID: 1, NAME: 'bluepoet', AGE: 20, PHONE_NUMBER: '010-0000-0001']
        2  | [ID: 2, NAME: 'tester', AGE: 10, PHONE_NUMBER: '010-0000-0002']
    }

    def "특정 유저정보를 수정하고 결과를 확인한다."() {
        when:
        new Sql(dataSource).executeUpdate("update User set age=12, phone_number='010-1111-2222' where id=1")
        def result = new Sql(dataSource).firstRow("select * from User where id=1")

        then:
        result.age == 12
        result.phone_number == '010-1111-2222'
    }

    def "특정 유저를 삭제하고 총 유저수를 확인한다."() {
        when:
        new Sql(dataSource).executeUpdate("delete from User where id=2")
        def result = new Sql(dataSource).firstRow("select count(*) as cnt from User")

        then:
        result.cnt == 1
    }
}

Github url은 https://github.com/bluepoet/spock-dbunit-exercise 를 참고하세요.