読者です 読者をやめる 読者になる 読者になる

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
        }
    }
}

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