GradleのプロジェクトでGroovy Consoleを起動する

GradleのプロジェクトでGroovy Consoleを起動できると、gradleプロジェクトで依存するライブラリとプロダクトコードをクラスパスにロードした状態で、ちょっとしたコードの動作を試せるようになる。

build.gradleに以下を追加してgradle consoleを実行する。

task console(dependsOn: 'classes', type: JavaExec) {
    main = 'groovy.ui.Console'
    classpath = sourceSets.test.runtimeClasspath
}

classpathsourceSets.main.runtimeClasspathでも問題ないが、testにしておくとtestスコープにspockなどを含んでおき、spockを使用して動作確認をしたい場合に都合が良い。

参考: http://piraguaconsulting.blogspot.jp/2012/02/gradle-groovy-console.html

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 さんです。

Spock0.7で追加されたStubについて

Spock0.7でStubを作る機能が追加されました。
http://docs.spockframework.org/en/latest/interaction_based_testing.html#stubs

Mockとの違いはデフォルトで返す値が違うとのこと。mockはnullを返しますがstubでは

といった感じ。どんな値を返すかはorg.spockframework.mock.EmptyOrDummyResponseで定義されている。すごく小さいクラスなので、ざっとみるだけでもどんな動作をするのかわかると思う。
あとorg.spockframework.smoke.mock.StubDefaultResponses.groovyというテストクラスがあるので、それをみるとわかりやすい。

このStubDefaultResponses.groovyをもとにmockとの動作の違いを確認してみた。

package org.yamkazu

import org.spockframework.mock.MockDetector
import spock.lang.Specification
import spock.lang.Unroll

class MockStubDiffSpec extends Specification {

    @Unroll
    def "#methodの値は、mockの場合は#mockExpected、stubの場合は#stubExpected"() {
        given:
        def mock = Mock(TestInterface)
        def stub = Stub(TestInterface)

        expect:
        mock."$method" == mockExpected
        stub."$method" == stubExpected

        where:
        method           | mockExpected | stubExpected
        'byte'           | 0            | 0
        'short'          | 0            | 0
        'int'            | 0            | 0
        'long'           | 0            | 0
        'float'          | 0            | 0
        'double'         | 0            | 0
        'boolean'        | false        | false
        'char'           | 0            | 0

        'byteWrapper'    | null         | 0
        'shortWrapper'   | null         | 0
        'intWrapper'     | null         | 0
        'longWrapper'    | null         | 0
        'floatWrapper'   | null         | 0
        'doubleWrapper'  | null         | 0
        'booleanWrapper' | null         | false
        'charWrapper'    | null         | 0

        'bigInteger'     | null         | BigInteger.ZERO
        'bigDecimal'     | null         | BigDecimal.ZERO

        'charSequence'   | null         | ""
        'string'         | null         | ""
        'GString'        | null         | ""

        'primitiveArray' | null         | [] as int[]
        'interfaceArray' | null         | [] as IPerson[]
        'classArray'     | null         | [] as Person[]

        'iterable'       | null         | []
        'collection'     | null         | []
        'queue'          | null         | []
        'list'           | null         | []
        'set'            | null         | [] as Set
        'map'            | null         | [:]
        'sortedSet'      | null         | [] as Set
        'sortedMap'      | null         | [:]
    }

    def "インタフェースが戻り値のメソッドの場合はそのインタフェースのstubを返す"() {
        given:
        def mock = Mock(TestInterface)
        def stub = Stub(TestInterface)

        expect: "mockの場合はnull"
        mock.unknownInterface == null

        and: "stubの場合はそのインタフェースのstubを返す"
        with(stub.unknownInterface) { // 0.7から出来るwithという書き方
            new MockDetector().isMock(it)
            name == ""
            age == 0
            children == []
        }
    }

    def "デフォルトコンストラクタがあるクラスが戻り値の場合はそのクラスのインスタンスを返す"() {
        given:
        def mock = Mock(TestInterface)
        def stub = Stub(TestInterface)

        expect: "mockの場合はnull"
        mock.unknownClassWithDefaultCtor == null

        and: "stubの場合はそのクラスの本物(モックではない)のインスタンスを返す"
        with(stub.unknownClassWithDefaultCtor) {
            !new MockDetector().isMock(it)
            name == "default"
            age == 0
            children == null
        }
    }

    // デフォルトコンストラクタがないクラスのstubを作るのに以下が必要となる。
    // "cglib:cglib-nodep:2.2.2"
    // "org.objenesis:objenesis:1.2"
    def "デフォルトコンストラクタがないクラスが戻り値の場合はそのクラスのstubを返す"() {
        given:
        def mock = Mock(TestInterface)
        def stub = Stub(TestInterface)

        expect: "mockの場合はnull"
        mock.unknownClassWithoutDefaultCtor == null

        and: "stubの場合はそのクラスのstubを返す"
        with(stub.unknownClassWithoutDefaultCtor) {
            new MockDetector().isMock(it)
            name == ""
            age == 0
            children == []
        }
    }

    static interface TestInterface {
        byte getByte()
        short getShort()
        int getInt()
        long getLong()
        float getFloat()
        double getDouble()
        boolean getBoolean()
        char getChar()

        Byte getByteWrapper()
        Short getShortWrapper()
        Integer getIntWrapper()
        Long getLongWrapper()
        Float getFloatWrapper()
        Double getDoubleWrapper()
        Boolean getBooleanWrapper()
        Character getCharWrapper()

        BigInteger getBigInteger()
        BigDecimal getBigDecimal()

        CharSequence getCharSequence()
        String getString()
        GString getGString()

        int[] getPrimitiveArray()
        IPerson[] getInterfaceArray()
        Person[] getClassArray()

        Iterable getIterable()
        Collection getCollection()
        Queue getQueue()
        List getList()
        Set getSet()
        Map getMap()
        SortedSet getSortedSet()
        SortedMap getSortedMap()

        IPerson getUnknownInterface()
        Person getUnknownClassWithDefaultCtor()
        ImmutablePerson getUnknownClassWithoutDefaultCtor()
    }

    static interface IPerson {
        String getName()
        int getAge()
        List<String> getChildren()
    }

    static class Person implements IPerson {
        String name = "default"
        int age
        List<String> children
    }

    static class ImmutablePerson extends Person {
        ImmutablePerson(String name, int age, List<String> children) {
            this.name = name
            this.age = age
            this.children = children
        }
    }
}

わかれば意外と素直な動作。

Groovy2.0で追加された@NotYetImplemented

元ネタ http://blog.andresteingress.com/2012/03/04/using-notyetimplemented-in-test-cases/

@NotYetImplementedとそのテストメソッドは失敗しなくてはいけなくて、逆に成功してしまうと検証エラーとして扱われるみたい。

class EchoService {

    def echo(arg) {
        // Not Yet Implemented
    }

}

というプロダクトコードがあって、以下の様なテスコトード。Spockでやってみた。

import groovy.transform.NotYetImplemented
import spock.lang.Specification

class NotYetImplementedSpec extends Specification {

    @NotYetImplemented
    def "失敗しないとダメ"() {
        given:
        def service = new EchoService()

        when:
        def result = service.echo('hello')

        then:
        result == 'hello'
    }

}

これを実行するとグリーンになる。この状態で実装してみる。

class EchoService {

    def echo(arg) {
        arg
    }

}

テスト実行する。成功してしまうと

junit.framework.AssertionFailedError: Method is marked with @NotYetImplemented but passes unexpectedly
	at org.yamkazu.NotYetImplementedSpec.失敗しないとダメ(NotYetImplementedSpec.groovy:8)

という感じ。

あと現時点(2012/7/6)でGroovy2.0でSpockを使う場合はSNAPSHOTバージョンを使う必要があります。
http://code.google.com/p/spock/wiki/SpockVersionsAndDependencies

Groovy2.0で追加されたMatcher#matchesPartially

元ネタ http://mrhaki.blogspot.fr/2012/06/groovy-goodness-partial-matches.html

APIはこのへん。
http://groovy.codehaus.org/groovy-jdk/java/util/regex/Matcher.html#matchesPartially()

なかなか説明が難しいのですが、とある文字列があって、その文字列に続く文字によってマッチする可能性があるものをtrueとして扱い、もうその文字列に続く文字がなんでもあってもマッチする可能性がないというものをfalseとして扱うという感じでしょう。

def regex = /(090|080)-\d{4}-\d{4}/

def matcher = '090-1234-5678' =~ regex
assert matcher.matchesPartially()

matcher = '080-0987-6543' =~ regex
assert matcher.matchesPartially() 

matcher = '090-09' =~ regex
assert matcher.matchesPartially()

matcher = '0' =~ regex
assert matcher.matchesPartially()

matcher = '07' =~ regex
assert matcher.matchesPartially() == false

matcher = '090-98765' =~ regex
assert matcher.matchesPartially() == false

Groovy2.0でListに追加されたwithDefault、withEagerDefault、withLazyDefault

withDefaultはwithLazyDefaultのエイリアスなので機能的にはwithEagerDefault、withLazyDefaultが追加されました。今までmapには似たようなのがありましたが、今回Listにも追加されました。

それぞれ要素を取得した際の要素のパディング方法や、nullの扱いが若干違います。

まずwithLazyDefaultから。要素を取得したタイミングで初期化されnullの要素が特別な意味(初期化対象)を持っています。

def items = [1, 2].withLazyDefault { it * it }

assert items == [1, 2]

assert items[4] == 16
// 4を取ると間の要素をnullでパディング
assert items == [1, 2, null, null, 16]

assert items[3] == 9
// パディングされたnullの要素を取得するとそのタイミングで初期化されている
assert items == [1, 2, null, 9, 16]

// 自分でnullを入れてみる
items[1] = null
// 自分で入れたnullでも取得するとそのタイミングで初期化される
assert items[1] == 1
assert items == [1, 1, null, 9, 16]

次にwithEagerDefault。先ほどと違いパディングする際にその間の値も初期化されるのと、nullがnullとして扱われる点がEagerと異なります。

def items = [1, 2].withEagerDefault { it * it }

assert items == [1, 2]

assert items[4] == 16
// 4を取ると間の要素もあわせて初期化される
assert items == [1, 2, 4, 9, 16]

// 自分でnullを入れてみる
items[1] = null
// 自分で入れたnullはnullとして扱われる
// withLazyDefaultと違いnullを取得する際にその要素が初期化されたりしない
assert items[1] == null
assert items == [1, null, 4, 9, 16]

Groovy2.0のtakeWhileとdropWhile

元ネタ http://blog.bloidonia.com/post/26065074691/whats-new-in-groovy-2-0-takewhile-and-dropwhile

特定の条件を満たしている間までの要素をコレクションとして取得するtakeWhileと、特定の条件を満たしている間までの要素を落としたコレクションを取得するdropWhileというのが追加されました。

はじめtakeWhileはfindAllと何が違うのかと思いましたが、findAllは全要素をイテレートするので、途中でイテレートを中断するというところがtakeWhileとの違い。

def items = [1, 3, 9, 12, 8, 19, 2]

assert items.takeWhile { it < 10 } == [1, 3, 9]
assert items.findAll { it < 10 } == [1, 3, 9, 8, 2]

assert items.dropWhile { it < 10 } == [12, 8, 19, 2]