Spock Stub/Mock/Spyのデフォルト挙動

まえがき

この記事では、Spock (Java/Groovy用のテストフレームワーク) において、
Stub,Mock,Spyオブジェクトがスタビングしていない状態で、
デフォルトでどのような挙動と、値を返すのかについて解説していきます。

Stubと、Mockは、同じようにスタビングができるのですが、
デフォルトの返値が全然違うので、ハマる前に知っておくとよいです。

テスト用のコード

これから、実際の挙動を確認するために、
用意したインターフェイスです。

Stub, Mock用のインターフェイス

interface SampleInterface {
    // プリミティブ型
    int getPrimitiveInt()
    long getPrimitiveLong()
    double getPrimitiveDouble()
    boolean getPrimitiveBoolean()

    // 数値系のクラス
    Integer getInteger()
    Long getLong()
    Double getDouble()
    BigDecimal getBigDecimal()
    Boolean getBoolean()

    // 文字列
    String getString()

    // 配列
    int[] getIntArray()

    // コレクション
    List<Integer> getList()
    Set<Integer> getSet()
    Map<String, Integer> getMap()

    // enum
    SampleEnum getEnum()

    // カスタムクラス
    Person getPerson()
}

サンプル用のenumクラス

enum SampleEnum {
    FIRST,
    SECOND,
    THIRD;
}

サンプル用のカスタムクラス

class Person {
    String _name
    int _age

    Person(String name, int age) {
        _name = name
        _age = age
    }

    String name() {
        return _name
    }

    int age() {
        return _age
    }
}

スタブ (Stub)

Stubオブジェクトをスタビングしない状態で使用する場合、
処理自体は空っぽですが、返り値は、基本よしなに初期値が返ります。

詳しくは、
org.spockframework.mock.EmptyOrDummyResponse::respondの中身を見ると
デフォルトの返値を決める処理が実装されています。

プリミティブ型の返値

int, long, doubleなどは、ゼロを返します。

booleanは、falseを返す。

setup:
def stubObject = Stub(SampleInterface)

expect:
// プリミティブ型は初期値
0 == stubObject.getPrimitiveInt()
0L == stubObject.getPrimitiveLong()
0.0D == stubObject.getPrimitiveDouble()

// booleanはfalse
false == stubObject.getPrimitiveBoolean()

数値などの基本クラスの返値

Integer, Long, Double, BigDecimalなどの数値用のクラスは、ゼロを返します。

Booleanは、falseを返す。

setup:
def stubObject = Stub(SampleInterface)

expect:
// 数値系クラスは初期値
0 == stubObject.getInteger()
0L == stubObject.getLong()
0.0D == stubObject.getDouble()
0.0D == stubObject.getBigDecimal()

// Booleanはfalse
false == stubObject.getBoolean()

文字列の返値

Stringが返値の場合、空文字列("")が返ります。
他にも、StringBuilderなどのクラスの場合でも、
インスタンス化されたStringBuilderが返ってきます。

setup:
def stubObject = Stub(SampleInterface)

expect:
// String系は、空文字列
"" == stubObject.getString()

配列の返値

要素ゼロの空配列が返ります。
(nullではない)

setup:
def stubObject = Stub(SampleInterface)

expect:
// 配列は空配列
[] == stubObject.getIntArray()

コレクションの返値

List, Set, Mapなどのコレクション系は、空リスト([])や、空マップ([:])を返します。

setup:
def stubObject = Stub(SampleInterface)

expect:
// コレクション系は、空リスト、空マップ
List.of() == stubObject.getList()
Set.of() == stubObject.getSet()
Map.of() == stubObject.getMap()

Enumの返値

Enumの場合、一つ目のものが返ります。
もし、要素がゼロのEnumだった場合は、nullが返ります。

setup:
def stubObject = Stub(SampleInterface)

expect:
// enumは、最初の要素
SampleEnum.FIRST == stubObject.getEnum()

その他のカスタムクラス

上記で述べた以外の、クラスを返す場合、
Stubオブジェクトは、デフォルトでそれらのStubオブジェクトを生成して返します。

setup:
def stubObject = Stub(SampleInterface)

expect:
// personは、新たに生成されたスタブオブジェクト
def person = stubObject.getPerson()

// そのままスタブとして動作する
"" == person.name()
0 == person.age()

モック (Mock)

Mockオブジェクトをスタビングしない状態で使用する場合、
処理自体は空っぽですが、返り値は、プリミティブ型なら、ゼロやfalseが返りますが、
それ以外はnullが返ります。

プリミティブ型の返値

int, long, doubleなどは、ゼロを返します。
booleanは、falseを返す。

setup:
def mockObject = Mock(SampleInterface)

expect:
// プリミティブ型は初期値
0 == mockObject.getPrimitiveInt()
0L == mockObject.getPrimitiveLong()
0.0D == mockObject.getPrimitiveDouble()

// booleanはfalse
false == mockObject.getPrimitiveBoolean()

その他のクラス

Integer, Long, Double, BigDecimal, Booleanなどのクラスも、すべてnullを返します。

Stringも、スタブでは空文字列("")を返しましが、Mockオブジェクトではnullを返します。

コレクションなどの、
List, Set, Mapなどのコレクション系も、すべてnullを返します。

カスタムクラスを返す場合も、もちろんデフォルトでnullを返します。

setup:
def mockObject = Mock(SampleInterface)

expect:
// 数値系クラスはすべてnull
null == mockObject.getInteger()
null == mockObject.getLong()
null == mockObject.getDouble()
null == mockObject.getBigDecimal()
null == mockObject.getBoolean()

// String系は、null
null == mockObject.getString()

// 配列は、null
null == mockObject.getIntArray()

// コレクション系は、null
null == mockObject.getList()
null == mockObject.getSet()
null == mockObject.getMap()

// enumは、null
null == mockObject.getEnum()

// カスタムクラスは、null
null == mockObject.getPerson()

スパイ (Spy)

Spyオブジェクトは、本来のオブジェクトを内包するので、
スタビングしない場合、本来のオブジェクトの処理が実行されます。
(スタビングした場合、元のメソッドの代わりにできる。)

なので、基本同じ挙動だけど、環境に依存するような特定の箇所だけ、
スタビングで書き換えてテストするような事ができます。

スタビングしないと元の挙動

setup:
def spyPerson = Spy(new Person("Ken", 20))

expect:
// 元のオブジェクトの挙動
"Ken" == spyPerson.name()
20 == spyPerson.age()

スタビングしたものだけ挙動が変更される

setup:
def spyPerson = Spy(new Person("Ken", 20))

// スタビング
spyPerson.name() >> "Bob"
spyPerson.age() >> 23

expect:
// スタビングで書き換えた挙動
"Bob" == spyPerson.name()
23 == spyPerson.age()

まとめ

Stub, Mock, Spyオブジェクトに、スタビングしていない状態だと、
どのようなデフォルト挙動と、値を返すのか説明してきました。

Stubは、基本よしなに初期値を返します。
String, Collectionも、空文字列や、空リストを返してくれます。
自分で実装したクラスを返す場合、
それらのStubオブジェクトを新たに生成して返します。

Mockは、プリミティブ型であれば、初期値を返しますが、
オブジェクト型を返す場合は、すべてnullを返します。

Stringを返す場合も、スタビングしていない状態だとnullを返す。

Spyは、デフォルトで内包している本来のオブジェクトのメソッドが実行されます。
変更したい部分だけ、スタビングで変更する事ができるので便利。
ただし、本物のインスタンス化が必要なので、そもそもインスタンス化するのが大変な場合は使うのが難しい。

各オブジェクトの使い分けについて
本物 > Stub > Mock > Spy の順 で使うかどうかは検討しよう。

本物が使用できるなら、本物。
次にStubで事足りるならStubを使うというふうに。

Spyはなんでもできるが故に、それって本当にテストになってる?
という状況に陥りやすい。
本当に必要な場合に限って、利用するのが望ましい。

コメントを残す