SpockでビルトインされているExtensionsとかそのへん

G* Advent Calendar 2012 12日目担当のyamkazuです。こんにちは。

今日はみんな大好きSpockでビルトインされている機能拡張について、いくつかピックアップして紹介します。機能拡張にカテゴライズされないものもあるかもしれませんが、その辺はゆるやかに。

また、この記事はSpock0.7を元に記述していますが、バージョンが変わるとアノテーションが存在しないとかありますので、新しいバージョンが出た場合はそのへんを注意してお読みください。

それではさっそく。

@Ignore

これは説明不要だと思いますが、Ignoreを付与すると指定したフィーチャの実行がスキップされます。アノテーションに理由を書くような使い方もできます。

@Ignore
def "xxx"() { expect: true }

@Ignore('hogehogeのため')
def "yyy"() { expect: true }

スペックに指定すると全体がスキップされます。

@Ignore
class IgnoreSpec extends Specification { ... }

@IgnoreRest

IgnoreRestはIgnoreとは対照的に、IgnoreRestが付与されたフィーチャのみを実行します。IDEを使っている場合は対象のフィーチャを決め打ちで実行するのは比較的容易なのですが、コンソールからtestを実行する場合する場合などはそうではありません。このアノテーションを使用すると実行したいフィーチャメソッドを簡単に指定することができます。

class IgnoreRestSpec extends Specification {
    def "xxx"() { ... }

    @IgnoreRest
    def "yyy"() { ... }

    @IgnoreRest
    def "zzz"() { ... }
}

上記のように複数指定することもでき、この例ではyyy、zzzのみが実行されます。

@IgnoreIf

IgnoreIfは指定されたクロージャの実行結果がtrueの場合にフィーチャの実行がスキップされます。クロージャの中では暗黙的に以下の変数が使用可能です。

  • env - System.getenv()のショートカット
  • properties - System.getProperties()のショートカット
  • javaVersion - Javaのバージョン

以下のように使用します。

@IgnoreIf({ true })
def "trueなので実行されない"() { expect: false }

@IgnoreIf({ false })
def "falseなので実行される"() { expect: true }

@IgnoreIf({ 1 < 2 })
def "1 < 2 はtrueなので実行されない"() { expect: false }

@IgnoreIf({ 1 > 2 })
def "1 > 2 はfalseなので実行される"() { expect: true }

@IgnoreIf({
    def a = 1
    def b = 1
    a + b == 2
})
def "closureをcallしているだけなので複数行書いても良い"() { expect: false }

@IgnoreIf({ javaVersion > 1.6 })
def "javaVersionでJVMのバージョンが参照できる"() { expect: false }

@IgnoreIf({ env["LANG"] != 'C' })
def "envがSystem.getenv()のショートカットになっている"() { expect: false }

@IgnoreIf({ properties["os.name"] == 'Mac OS X' })
def "propertiesがSystem.getProperties()のショートカットになっている"() { expect: false }

@FailsWith

Spockでは例外のテストを行う際は、以下のように

then:
MyException e = thrown()

thrown()を使用することができますが、FailsWithはいわゆるJUnit4の@Test(expected = MyException.class)のような記述の仕方を可能にするアノテーションで、指定した例外でフィーチャが失敗することを宣言できます。

@FailsWith(MyException)
def "xxx"() { expect: throw new MyException() }

@FailsWith(value = MyException, reason = "hogehogeのため")
def "yyy"() { expect: throw new MyException() }

スペックに付与することも可能です。

@FailsWith(MyException)
class FailWithSpec extends Specification { ... }

この場合は、スペック上のすべてのフィーチャが指定した例外で失敗することを宣言しています。

@Timeout

Timeoutはフィーチャの実行時間のタイムアウト値を指定することができます。このタイムアウト値を超過した場合はorg.spockframework.runtime.SpockTimeoutErrorがスローされます。

@Timeout(1)
def "1秒以内に終わる"() {
    expect: Thread.sleep 500
}

@FailsWith(SpockTimeoutError)
@Timeout(1)
def "1秒以内に終わらない"() {
    expect: Thread.sleep 1100
}

デフォルトでは単位は秒に設定されています。単位を変更したい場合はunit属性を指定します。

@Timeout(value = 500, unit = TimeUnit.MILLISECONDS)
def "500ミリ秒以内に終わる"() {
    expect: Thread.sleep 250
}

@FailsWith(SpockTimeoutError)
@Timeout(value = 250, unit = TimeUnit.MILLISECONDS)
def "500ミリ秒以内に終わらない"() {
    expect: Thread.sleep 300
}

@Unroll

通常Spockではwhereのパラメタライズテストを実行すると、そのフィーチャに対し1つの実行結果が出力されます。

def "x + y の合計を計算する"() {
    expect:
    x + y == sum

    where:
    x | y || sum
    1 | 2 || 3
    3 | 4 || 7
    5 | 6 || 11
}

実行結果

Test                    Duration    Result
x + y の合計を計算する  0.001s      passed

Unrollはこのパラメタライズテストをそれぞれの独立したフィーチャとして実行してくれます。また以下のように#でパラメータをフィーチャ名に埋め込むことができます。

@Unroll
def "#x + #y の合計は #sum になる"() { ... }

実行結果

Test                       Duration    Result
1 + 2 の合計は 3 になる    0s          passed
3 + 4 の合計は 7 になる    0s          passed
5 + 6 の合計は 11 になる   0s          passed

#形式での参照は引数なしのメソッドあれば、メソッドをチェインして参照することも可能です。詳細はリファレンスを参照してください。

@Shared

通常フィールドで宣言したフィクスチャはフィーチャの実行毎に初期化されます。

def counter = 0

def "counterをインクリメントする"() {
    expect:
    counter++ == expectedCounter

    where:
    expectedCounter << [0, 0, 0]
}

Sharedはフィーチャ間でフィクスチャを共有するSharedFixtureを実現してくれます。生成コストが高いオブジェクトをフィーチャ間で共有したい場合に便利です。

@Shared
def counter = 0

def "counterをインクリメントする"() {
    expect:
    counter++ == expectedCounter

    where:
    expectedCounter << [0, 1, 2]
}

@AutoCleanup

AutoCleanupはフィールドに設定することで自動で後処理をしてくれます。デフォルトではcloseメソッドが自動で呼びされます。

@AutoCleanup
def closeable = new Closeable()

明示的に呼び出す後処理のメソッドを指定することもできます。

@AutoCleanup("shutdown")
def shutdownable = new Shutdownable()

後処理の中に例外が発生した場合に発生した例外を握りつぶしたい場合はquiet属性にtrueを指定します。デフォルトはfalseです。

@AutoCleanup(value = 'shutdown', quiet = true)
def shutdownable = new Shutdownable()

これらの後処理はフィーチャ実行毎に実行されますが、@Sharedが付与されたフィールドでは全フィーチャの実行後に1度だけ実行されます。

@Stepwise

Stepwiseを使用すると一連のフィーチャをそれぞれ定義した順に実行してくれます。フィーチャが一連のシナリオとして実行されるイメージで、途中のテストが失敗すると以降のテストが実行されません。

@Stepwise
class StepwiseSpec extends Specification {
    def "first"() { expect: true }
    def "second"() { expect: false }
    def "third"() { expect: true }
}

上記の例ではfirst、second、thirdの順に実行されるはずですが、secondで失敗するため、thirdは実行されません。

おわりに

今回は紹介できませんが独自のアノテーションを定義して拡張するといったことも容易に出来るようになっています。きっと誰か書いてくれるはず。誰も書かなかったらそのうち書きます。

それでは明日は @irof さんです。