相互作用中心のテスト

相互作用中心のテストは2000年代前半に登場したExtreme Programming(XP)から生まれた設計、テストの手法です。オブジェクトの状態ではなく振る舞いに注目し、メソッドを呼び出した際にコラボレータとオブジェクトの間で、意図したインタラクション(相互作用)が行われるか検証します。

例えば複数のSubscriberにメッセージを送信するPublisherがあるとします。

class Publisher {
    List<Subscriber> subscribers
    void send(String message)
}

interface Subscriber {
    void receive(String message)
}

class PublisherSpec extends Specification {
    Publisher publisher = new Publisher()
}

どのようにPublisherをテストすればよいでしょうか?状態中心のテストではpublisherがsubscriberを追跡できているか検証します。しかし、ここで一番の関心事は、publisherが送ったメッセージがsubscriberで受信できるかです。これを検証するにはSubscriberの特別な実装を用意し、publisherとsubscriberのやり取りを監視します。このような実装をモックオブジェクトと呼びます。

subscriberのモック実装を開発者自身で用意することもできますが、インタラクションが複雑なメソッドが増えてくると、非常に手間になってきます。そこでモックフレームワークの登場です。このフレームワークはコラボレータがどのようにインタラクションすべきか定義することで、実際にその通りに動作したか検証することができます。

Javaの世界では、JMockEasyMockMockitoといったモックフレームワークが人気があり、非常に成熟しています。これらのモックフレームワークをSpcokで使用することもできますが、Spcokの言語仕様に合わせて、より自然に記述できるように、モックフレームワークをSpock自身で再構築することを決めました。この判断は、相互作用中心のテストを容易にするため、Groovyの機能を活用し、より読みやすく、より楽しく書けるようにしたいという思いからです。この章を読み終えた時に、その目標が達成されていると感じていただけると幸いです。

特に何も明示していない限り、すべての機能はJavaとGroovyで使用することができます。

モックオブジェクトの作成

モックオブジェクトはMockingApi.Mock()メソッドで作成します[1]。2つのsubscriberモックを作成してみましょう。

def subscriber = Mock(Subscriber)
def subscriber2 = Mock(Subscriber)

または、IDEのサポートが得られやすいように、よりJavaのシンタックスに近い形で記述することもできます。

Subscriber subscriber = Mock()
Subscriber subscriber2 = Mock()

この場合のモックの型は、左辺の変数の型から推論されます。

注釈

もしモックの型を左辺で指定した場合は、右辺で型を省略することができます(指定しても問題ありません)。

モックオブジェクトは文字通りその型の実装(クラスの場合はサブクラス)を作成します。別の言い方をすると、上記の例でsubscriberはis-a Subscriberです。これは、この型を期待する静的型付けのコード(Java)に、このオブジェクトを渡せることを意味しています。

モックオブジェクトのデフォルト動作

初期状態のモックは何の振る舞いもしません。このオブジェクトのメソッドを呼び出すことはできますが、戻り値の型に応じたデフォルト値を返すだけです(false0、またはnull)。ただしObject.equalsObject.hashCodeObject.toStringメソッドは例外です。モックオブジェクトはオブジェクト毎に一意のハッシュコードを持ち、自身との比較にのみ等しくなります。そしてObject.toStringの文字列表現では、モックした型が含まれます。これらのデフォルトの動作はスタビングによりオーバーライド可能です。詳細はスタビングで説明します。

モックオブジェクトを設定する

publisherとモックのsubscriberを作成した後に、publisherに作成したsubscriberを設定する必要があります。

class PublisherSpec extends Specification {
    Publisher publisher = new Publisher()
    Subscriber subscriber = Mock()
    Subscriber subscriber2 = Mock()

    def setup() {
        publisher.subscribers << subscriber // << is a Groovy shorthand for List.add()
        publisher.subscribers << subscriber2
    }
}

これで2つのオブジェクト間で行われるインタラクションについて記述する準備が整いました。

モッキング(Mocking)

モッキングとは、オブジェクトとそのコラボレータ間(で必須)の、インタラクションを定義するということです。次の例を見てください。

def "should send messages to all subscribers"() {
    when:
    publisher.send("hello")

    then:
    1 * subscriber.receive("hello")
    1 * subscriber2.receive("hello")
}

文章としてコードを読んでみると「publisher が ‘hello’ のメッセージを send したとき、 それぞれの subscriber は 1回 message を receive すべき」になります。

フィーチャーメソッドが実行されると、then:ブロックで宣言したインタラクションにマッチする、when:ブロックでのモックオブジェクトへの呼び出しが全て記録されます。もし期待したインタラクションが満たされない場合はInteractionNotSatisfiedError(のサブクラス)がスローされます。この検証は自動的に行われ、開発者自身で検証のコードを記述する必要はありません。

インタラクション

then:ブロックの詳細を見ていきます。上記では2つのインタラクションを定義していますが、それぞれ多重度(cardinality)対象制約(target constraint)メソッド制約(method constraint)、そして引数制約(argument constraint)の4つの個別パートから成り立っています。

1 * subscriber.receive("hello")
|   |          |       |
|   |          |       argument constraint
|   |          method constraint
|   target constraint
cardinality

多重度

多重度は期待するメソッドの呼び出し回数を表しています。これは固定の数値、または範囲で指定できます。

1 * subscriber.receive("hello")      // exactly one call
0 * subscriber.receive("hello")      // zero calls
(1..3) * subscriber.receive("hello") // between one and three calls (inclusive)
(1.._) * subscriber.receive("hello") // at least one call
(_..3) * subscriber.receive("hello") // at most three calls
_ * subscriber.receive("hello")      // any number of calls, including zero
                                     // (rarely needed; see 'Strict Mocking')

対象制約

対象制約は呼び出しを期待するモックオブジェクトを表しています。

1 * subscriber.receive("hello") // a call to 'subscriber'
1 * _.receive("hello")          // a call to any mock object

メソッド制約

メソッド制約は呼び出しを期待するメソッドを表しています。

1 * subscriber.receive("hello") // a method named 'receive'
1 * subscriber./r.*e/("hello")  // a method whose name matches the given regular expression
                                // (here: method name starts with 'r' and ends in 'e')

呼び出しを期待するメソッドがゲッターの場合は、Groovyのプロパティ構文が使用できます。

1 * subscriber.status // same as: 1 * subscriber.getStatus()

セッターの場合は、通常のメソッドと同じように指定する必要があります。

1 * subscriber.setStatus("ok") // NOT: 1 * subscriber.status = "ok"

引数制約

引数制約は呼び出しを期待するメソッドの引数を表しています。

1 * subscriber.receive("hello")     // an argument that is equal to the String "hello"
1 * subscriber.receive(!"hello")    // an argument that is unequal to the String "hello"
1 * subscriber.receive()            // the empty argument list (would never match in our example)
1 * subscriber.receive(_)           // any single argument (including null)
1 * subscriber.receive(*_)          // any argument list (including the empty argument list)
1 * subscriber.receive(!null)       // any non-null argument
1 * subscriber.receive(_ as String) // any non-null argument that is-a String
1 * subscriber.receive({ it.size() > 3 }) // an argument that satisfies the given predicate
                                          // (here: message length is greater than 3)

複数の引数を持つメソッドに対しても問題なく使用できます。

1 * process.invoke("ls", "-a", _, !null, { ["abcdefghiklmnopqrstuwx1"].contains(it) })

また可変長引数のメソッドに対しても、同じようにインタラクションを定義できます。

interface VarArgSubscriber {
    void receive(String... messages)
}

...

subscriber.receive("hello", "goodbye")

Spock Deep Dive: Groovy Varargs

Groovyではメソッドの最後のパラメータを、配列として宣言すると可変長引数として扱います。可変長引数のメソッドと同じように、このようなメソッドに対してもインタラクションのマッチングが行えます。

すべてのメソッド呼び出しに対するマッチング

“何にでも”一致するという指定が便利な場合もあります。

1 * subscriber._(*_)     // any method on subscriber, with any argument list
1 * subscriber._         // shortcut for and preferred over the above

1 * _._                  // any method call on any mock object
1 * _                    // shortcut for and preferred over the above

注釈

(_.._) * _._(*_) >> _は有効なインタラクションの定義ですが、これは良いスタイルでもなければ、便利な場面があるわけでもありません。

Strictモッキング

上記のすべてのメソッド呼び出しに対するマッチングはどのような時に役立つでしょうか?よい例がstrictモッキングです。このモックのスタイルは明示的に定義したインタラクション以外を許容しません。

when:
publisher.publish("hello")

then:
1 * subscriber.receive("hello") // demand one 'receive' call on `subscriber`
_ * auditing._                  // allow any interaction with 'auditing'
0 * _                           // don't allow any other interaction

このように0 *then:ブロック、またはメソッドの最後のインタラクションとして定義することで効果を発揮します。また_ *(任意の呼び出し回数)は、auditingコンポーネントに対して任意の呼び出し回数を許容することを表しています。

注釈

_ *はstrictモッキングを行う場合にのみ有効です。特にスタビングを行う場合は必要になることはありません。例えば_ * auditing.record(_) >> "ok"は単にauditing.record(_) >> "ok"と定義すべきです。

インタラクションの宣言場所

ここまで見てきた例ではインタラクションを、then:ブロックに定義してきました。 これはより自然にスペックが読めるという効果がありますが、インタラクションをwhen:ブロックので定義することもできます。これはsetupといったメソッドでも定義できることを意味しています。同じように他のヘルパークラスでインタラクションを定義し、スペッククラス内から呼び出すといったことも可能です。

モックオブジェクトへの呼び出しが発生すると、インタラクションは定義した順番に評価されます。もし、呼び出しが複数のインタラクションにマッチする場合はマッチの上限に達してない限り、先に定義したインタラクションが優先して評価されます。ただし例外がありthen:ブロックで定義したインタラクションは他のインタラクションよりも先に評価されます。これはsetupなど他のメソッドやブロックで定義したインタラクションよりもthen:ブロックで定義したインタラクションが先に評価されることを意味しています。

Spock Deep Dive: How Are Interactions Recognized?

(質問: どのように通常のメソッドではなくインタラクションの定義だと識別しているのか?)Spockではインタラクションの定義を識別するために、簡単なルールを持っています。これは、式があった場合に、乗算(*)、または左シフト(>>, >>>)のどちらか含まれていた場合に、インタラクションの定義として識別します。このような式は、本来もっている式の意味を変えることで、正しく動作してます。また、多重度の定義(モッキング)と、レスポンス定義(スタビング)のシンタックスを間違わないようにしてください。これらは必ずどちらかが、定義されなければなりません。foo.bar()といった単独の式が、インタラクションとして識別されることはありません。

モック作成時のインタラクション定義(New in 0.7)

もしモックが動的に変化しない”基本”となるインタラクションがある場合は、モック作成時にインタラクションを定義できます。

def subscriber = Mock(Subscriber) {
    1 * receive("hello")
    1 * receive("goodbye")
}

この機能はスタビングにより専用のスタブを作る際にも便利な機能です。ここで、インタラクションに対象制約を含んでいない(参照できない[2])ことに注意してください。これは作成するモックのコンテキストで動作することが明白なためです。

また、フィールドをモックで初期化する場合にも使用できます。

class MySpec extends Specification {
    Subscriber subscriber = Mock {
        1 * receive("hello")
        1 * receive("goodbye")
    }
}

インタラクションのグループ化(New in 0.7)

Specification.withブロックで対象制約を共有することで、インタラクションのグループ化が行えます。これはモック作成時のインタラクション宣言と同じように、対象制約を繰り返し指定する必要がなくなります。

with(subscriber) {
    1 * receive("hello")
    1 * receive("goodbye")
}

また、withブロックは特定の対象に対する、コンディションのグループ化にも使用できます。

インタラクションとコンディションの組み合わせ

then:ブロックにはインタラクションと、コンディションの両方を同時に含むことができます。厳密には必須ではありませんが、コンディションの前にインタラクションを定義するのが一般的です。

when:
publisher.send("hello")

then:
1 * subscriber.receive("hello")
publisher.messageCount == 1

文章としてコードを読んでみると「publisher が ‘hello’ のメッセージを send したとき、 subscriber は 1回 message を receive して、publisher の message count は 1 であるべき」になります。

明示的なインタラクションブロック

仕組み上、Spockは実行に定義された、インタラクション全ての情報を保持する必要があります。では、なぜthen:ブロックでインタラクションが定義可能なのでしょうか?内部的には、then:ブロックで定義したインタラクションは、実行前にwhen:ブロックの前に移動されます。これは多くの場合に問題なく動作しますが、特定の条件下では問題を引き起こす場合があります。

when:
publisher.send("hello")

then:
def message = "hello"
1 * subscriber.receive(message)

この例では期待する引数を変数に受けています(多重度を変数に受ける場合も場合も同様)。ここでインタラクションを移動すると、インタラクションからこの後で宣言する変数を参照していることになり、もちろんこのような参照は許容されません。このように単純にインタラクションを移動しただけでは、実行時にMissingPropertyExceptionを引き起こします。

この問題を解決する一つの方法は変数宣言を(少なくとも)when:ブロックの前に移動することです(データ駆動テストが好きなユーザはwhere:ブロックに変数を移動するかもしれません)。上の例では、副作用として送信するメッセージと同じ変数を共有できるというメリットもあります。

もう一つの解決方法は、変数をインタラクションと共に使用することを明示することです。

when:
publisher.send("hello")

then:
interaction {
    def message = "hello"
    1 * subscriber.receive(message)
}

MockingApi.interactionブロックは、常にそのブロック全体が移動するため、このコードは意図した通りに動作します。

インタラクションのスコープ

then:ブロックで定義したインタラクションは、直前のwhen:ブロックのみがスコープの対象となります。

when:
publisher.send("message1")

then:
subscriber.receive("message1")

when:
publisher.send("message2")

then:
subscriber.receive("message2")

これは最初のwhen:ブロックでsubscriber"message1"を受信した確認を行い、次のwhen:"message2"を受信した確認を行なっています。

then:ブロックの外で宣言したインタラクションは、そのフィーチャーメソッドが完了するまで有効になります。

また、インタラクションは常に特定のフィーチャーメソッドに対してスコープを持ちます。そのため、インタラクションをstaticメソッド、setupSpec メソッド、またcleanupSpecメソッドで定義することはできません。このような理由により、static、または@Sharedフィールドにもモックオブジェクトを設定すべきではありません。

インタラクションの検証

モックベースのテストが失敗する理由は主に2つです。期待したインタラクション以上に呼び出しが発生するか、もしくは期待したインタラクションに呼び出しが満たいない場合です。前者の場合は、TooManyInvocationsErrorが発生し、実際に発生した呼び出し回数が表示されます。

Too many invocations for:

2 * subscriber.receive(_) (3 invocations)

このようなエラーが発生した場合は、期待した以上に呼び出しが発生した原因の解析が容易に行えるよう、呼び出しに関連する全てのインタラクションを表示します(new in Spock 0.7)。

Matching invocations (ordered by last occurrence):

2 * subscriber.receive("hello")   <-- this triggered the error
1 * subscriber.receive("goodbye")

この出力によるとreceive("hello")が原因となりTooManyInvocationsError発生しています。ここではsubscriber.receive("hello")の呼び出しでエラーがあったということに、情報が集約されてしまうことに注意してください。receive("hello")で先にエラーとなるとreceive("goodbye")にもエラーがあった場合と区別が付きません。

2つめの場合(期待したインタラクションに呼び出しが満たいない場合)は、whenブロックの実行が完了した時点でエラーを検出します(終了するまでは呼び出しの可能性があるため)。エラーの場合はTooFewInvocationsErrorが発生します。

Too few invocations for:

1 * subscriber.receive("hello") (0 invocations)

これはメソッドの呼び出しが全くなかったということではありません。同じメソッドが、他の引数で呼び出された、異なるモックオブジェクトで呼び出された、もしくは”別の”メソッドが呼び出されたといったことが考えられます。これらいずれの場合でも、TooFewInvocationsErrorのエラーが発生します。

なぜ期待した呼び出しが行われなかったかを簡単に解析できるように、インタラクションに一致しない呼び出しがあった場合は、呼び出しに類似している順に、全てのインタラクションを表示します(new in Spock 0.7)。具体的には引数が一致するインタラクションを最初に表示します。

Unmatched invocations (ordered by similarity):

1 * subscriber.receive("goodbye")
1 * subscriber2.receive("hello")

実行順序

多くの場合にメソッドの呼び出し順序は重要ではありません。また実行順序は時間と共に変更される可能性が高いものでもあります。Spockはデフォルトでは、over-specificationを避けるため、最終的に定義したインタラクションすべてが満たされていれば、どのような実行順序でも許容します。

then:
2 * subscriber.receive("hello")
1 * subscriber.receive("goodbye")

ここで呼び出し順序は "hello" "hello" "goodbye""hello" "goodbye" "hello"、または "goodbye" "hello" "hello" のいずれの場合も定義したインタラクションを満たしています。

実行順序が重要な意味をもつ場合は、インタラクションを複数のthen:ブロックに分割することで、順序の検証が行えます。

then:
2 * subscriber.receive("hello")

then:
1 * subscriber.receive("goodbye")

これは"goodby"を受信する前に、"hello"を2回受信することを検証しています。このようにthen:ブロックでは実行順序の検証を行いませんが、then:ブロックを複数に分割することで、ブロックの実行順序の検証が行えます。

注釈

then:ブロックをand:を使用して分割した場合は、順序の検証を行なっていることにはなりません。and:は単なるドキュメンテーション目的のために存在しており、実行時の動作には何も影響を与えません。

クラスのモッキング

Spockではインタフェースの他に、クラスのモッキングもサポートしています。クラスのモッキングも、インターフェースと同じように使用できます。ただしクラスをモッキングする場合はcglib-nodep-2.2以上、objenesis-1.2以上が、クラスパスに含まれている必要があります。もしこれらのライブラリが、クラスパスに含まれていない場合はSpockが警告を出力します。

スタビング(Stubbing)

スタビングとは特定のメソッド呼び出しに対するレスポンスを定義することです。メソッドをスタビングする場合は、そのメソッドが何回呼ばれるかを気にせず、いつ呼び出されても特定の値を返したり、何かの副作用が働くようにします。

例を示すために、Subscriberreceiveメソッドの処理が完了した場合に、ステータスコードを返すように変更してみましょう。

interface Subscriber {
    String receive(String message)
}

ここでreceiveメソッドが常に"ok"を返すようにします。

subscriber.receive(_) >> "ok"

文章としてコードを読んでみると「subscriber が メッセージ を 受信 するたびに ‘ok’ を返す」になります。

モック時のインタラクションと比べると、スタブのインタラクションは左に多重度がない代わりに、右にレスポンスジェネレータを指定します。

subscriber.receive(_) >> "ok"
|          |       |     |
|          |       |     response generator
|          |       argument constraint
|          method constraint
target constraint

スタブのインタラクションは、then:ブロック内やwhen:ブロックの前など、どのような場所でも宣言することができます(詳細はインタラクションの宣言場所を参照)。もし、モックオブジェクトをスタブとしてのみ使用する場合は、モック作成時setup:ブロック内でインタラクションを定義するのが一般的です。

固定の値を返す

すでにここまでの例の中で出てきましたが、固定の値を返すには算術右シフト(>>)演算子を使用します。

subscriber.receive(_) >> "ok"

呼び出し毎に異なる値を返すには、それぞれ個別のインタラクションとして定義します。

subscriber.receive("message1") >> "ok"
subscriber.receive("message2") >> "fail"

これは"message1"を受信すると"ok"を返し、"message2"を受信すると"fail" を返します。返す値に制限はありませんが、メソッドの戻り値型の制約を外れることはできません。

一連の値を返す

連続した呼び出しに対し異なる値を返すには、論理右シフト演算子(>>>)を使用します。

subscriber.receive(_) >>> ["ok", "error", "error", "ok"]

これは初めの呼び出しに対し"ok"を返し、2回目3回目には"error"、そして4回目以降は"ok"を返します。右辺に指定する値はGroovyがイテレーション方法を知っている値である必要があります。この例では単なるリストを使用しています。

動的に値を返す

メソッドの引数に応じて動的に値を返すには、算術右シフト(>>)演算子とクロージャを使用します。もしクロージャの引数が、型指定なしで1つの場合は、引数のリストが渡されます。

subscriber.receive(_) >> { args -> args[0].size() > 3 ? "ok" : "fail" }

これはメッセージの長さが3文字以上の場合は"ok"を返し、それ以外は"fail"を返します。

しかしほとんどのケースでは、メソッドの引数に直接アクセスできたほうが便利でしょう。クロージャの引数を型付きで1つ以上定義した場合は、メソッドの引数がそれぞれクロージャの引数にマップされます。[4]

subscriber.receive(_) >> { String message -> message.size() > 3 ? "ok" : "fail" }

このモックは先程の例とまったく同じように動作しますが、より読みやすくなっています。

もし、メソッドの引数以上に実行中のメソッドについて、情報が必要な場合はorg.spockframework.mock.IMockInvocationを参照してください。このインターフェース内の全てのメソッドは、クロージャ内からプレフィックス指定なしに呼ぶことができます。(Groovyの用語で言うとクロージャはIMockInvocationのインスタンスへデリゲートします。)

副作用の実行

場合によっては動的に値を返す以外の処理が必要になることがあります。たとえば例外のスローです。このような場合にもクロージャを使用します。

subscriber.receive(_) >> { throw new InternalError("ouch") }

もちろんクロージャの中にはprintlnといった、さまざまなコードを含むことができます。この例ではメソッドの呼び出しが、インタラクションと一致するたびに例外をスローします。

メソッドのレスポンスをチェーンする

メソッドのレスポンスをチェーンすることができます。

subscriber.receive(_) >>> ["ok", "fail", "ok"] >> { throw new InternalError() } >> "ok"

これは最初の3回の呼び出しに対し"ok", "fail", "ok"を返し、4回目にはInternalErrorをスロー、それ以降の呼び出しにはokを返します。

モッキングとスタビングの組み合わせ

モッキングとスタビングを組みわせて使用できます。

1 * subscriber.receive("message1") >> "ok"
1 * subscriber.receive("message2") >> "fail"

モッキングとスタビングを同じメソッドに対して行う場合は、同じインタラクションとして定義する必要があります。これは、Mockitoのようにモッキングとスタビングを、別々に定義することができないということです。

setup:
subscriber.receive("message1") >> "ok"

when:
publisher.send("message1")

then:
1 * subscriber.receive("message1")

インタラクションの宣言場所で説明したようにreceiveが呼ばれると、then:ブロックで定義したインタラクションが始めに評価されます。このインタラクションは、特定のレスポンスを定義していないため、メソッドのデフォルトの値(この場合はnull)を返します(これはSpockがlenientなモックのアプローチを採用しているためです)。このため、setup:ブロックで定義したスタビングのインタラクションは評価されることがありません。

注釈

同じメソッド呼び出しに対するモッキングとスタビングは、同じインタラクションとして定義する必要があります。

他のモックオブジェクト(New in 0.7)

ここまではMockingApi.Mockメソッドを使用してモックを作成してきました。MockingApiクラスは、これ以外にも特別なモックを作成するファクトリメソッドを提供しています。

スタブ

スタブMockingApi.Stubメソッドで作成します。

def subscriber = Stub(Subscriber)

モックはモッキング、スタビングの両方を行うことができましたが、スタブではスタビングのみが行えます。これはインタラクションの機能を制限することで、読み手に対し役割をより明確にできるメリットがあります。

注釈

もしスタブでインタラクションを定義した場合は(1 * foo.bar()のように)、InvalidSpecExceptionがスローされます。

モックと同じように、スタブも予期していないメソッド呼び出しを許容しています。しかし、スタブはより実際の値に近い値を返す点が異なります。

  • プリミティブ型は、プリミティブ型のデフォルト値を返します。

  • 非プリミティブ数値(BigDecimalのような)の場合は、ゼロを返します。

  • 数字以外の値は”空”や”ダミー”オブジェクトを返します。これは、空文字や、空のコレクション、デフォルトコンストラクトから生成したオブジェクト、またはデフォルトの値を持ったスタブオブジェクトを返すということ意味します。詳細はorg.spockframework.mock.EmptyOrDummyResponseクラスを参照してください。

スタブはモック作成時のインタラクション宣言と同じように、インタラクションの固定セットを定義することができます。

def subscriber = Stub(Subscriber) {
    receive("message1") >> "ok"
    receive("message2") >> "fail"
}

スパイ

(この機能を使用する前に一度考えなおしてください。もしかするとコードの設計自体を見なおしたほうが良いかもしれません。)

スパイMockingApi.Spyのファクトリメソッドで作成します。

def subscriber = Spy(SubscriberImpl, constructorArgs: ["Fred"])

スパイは常に本物のオブジェクトである必要があります。そのため、インタフェースではなくクラスの型をコンストラクタの引数に指定してください。もしコンストラクタの引数を指定しなかった場合は、デフォルトコンストラクタが使用されます。

スパイのメソッド呼び出しは、自動的に本物のオブジェクトに委譲されます。同様にメソッドの戻り値は、本物のオブジェクトからスパイを経由して呼び出し元に返ります。

スパイを作成すると、スパイを通して行われた呼び出し元と実際のオブジェクトとのやり取りを監視することができます。

1 * subscriber.receive(_)

このようにpublisherとSubscriberImplを変更することなく、スパイを使用してreceiveが1回呼ばれたことを確認できます。

またスパイでメソッドのスタビングを行うと、本物のメソッドが呼ばれなくなります。

subscriber.receive(_) >> "ok"

これは本物のSubscriberImpl.receiveが呼ばれる代わりに、receiveメソッドが単に"ok"を返すようになります。

さらに、場合によっては、任意のコード実行と、本物のメソッドへの委譲の両方を組み合わせたい場合もでしょう。

subscriber.receive(_) >> { String message -> callRealMethod(); message.size() > 3 ? "ok" : "fail" }

このように本物のメソッドに処理を委譲する場合はcallRealMethod()メソッドを使用します。ここで、呼び出す際にmessage の引数を設定していないことに注意してください。このメソッドの引数は、Spockが自動的に設定します。この例ではcallRealMethod() の呼び出しに本当の処理結果が返りますが、この値を使用せず値を差し替えています。また、呼び出しと異なる引数を、本物のメソッドに設定したい場合はcallRealMethodWithArgs("changed message")を使用できます。

パーシャルモック

(この機能を使用する前に一度考えなおしてください。もしかするとコードの設計自体を見なおしたほうが良いかもしれません。)

スパイはパーシャルモックとしても使用できます。

// this is now the object under specification, not a collaborator
def persister = Spy(MessagePersister) {
  // stub a call on the same object
  isPersistable(_) >> true
}

when:
persister.receive("msg")

then:
// demand a call on the same object
1 * persister.persist("msg")

Groovyモック(New in 0.7)

これまで説明してきたモックの機能は、呼び出し元のコードがJava、またはGroovyのどちらでも問題なく動作します。Groovyモックは、Groovyの動的な性質を活用したコードをテストするための、Groovy固有のテスト機能をサポートしています。これらはMockingApi.GroovyMock()MockingApi.GroovyStub()MockingApi.GroovySpy()のファクトリメソッドを使用して作成できます。

When Should Groovy Mocks be Favored over Regular Mocks?

(質問: どのような場合に通常モックではなくGroovyモックを使用すべきか?)内部の実装にGroovyを使用していて、かつGroovy固有のモック機能が必要な場合に使用してください。もしJavaのコードからGroovyモックが呼び出された場合は、Groovyモックは通常のモックと同じように振舞います。ただしこのような場合は、そもそもGroovyモックを使用する必要がありません。Groovyモックは、Groovyで書かれたコードに対して、Groovy固有のテスト機能を追加しており、Javaから利用する際はこの機能が意味を持たないためです。もし、Groovyモックを使用する特別な理由がない限り、通常のモック機能を使用してください。

動的メソッドのモッキング

すべてのGroovyモックはGroovyObjectのインタフェースを実装しています。これらは通常のメソッドと同じように、動的メソッドへのモッキングとスタビングをサポートしています。

def subscriber = GroovyMock(Subscriber)

1 * subscriber.someDynamicMethod("hello")

任意の型すべてのインスタンスをモッキング

(この機能を使用する前に一度考えなおしてください。もしかするとコードの設計自体を見なおしたほうが良いかもしれません。)

通常のモックと同様に、Groovyモックも実際のインスタンスの値をモックで差し替える必要があります。しかしGroovyモックをグローバルなモックとして作成した場合は、フィーチャーメソッドの実行の間、モックした型の全てのインスタンスを、自動的に差し替えることができます。[3]

def publisher = new Publisher()
publisher << new RealSubscriber() << new RealSubscriber()

def anySubscriber = GroovyMock(RealSubscriber, global: true)

when:
publisher.publish("message")

then:
2 * anySubscriber.receive("message")

はじめに2つの本物のsubscriber実装インスタンスを持つpublisherを準備します。次に、この本物のsubscriber実装の型を指定して、グローバルモックを作成します。このようにすることで本物のsubscriberに対する全てのメソッド呼び出しが、モックオブジェクトへ送られるようになります。また、ここでモックオブジェクトのインスタンスをpublisherに設定していません。このモックオブジェクトは、インタラクションを定義するためだけに使用します。

注釈

グローバルモックはクラスの型にのみ使用できます。これはフィーチャーメソッドが実行中の間、その型の全てのインスタンスが差し替わります。

グローバルモックは全てのインスタンスに影響を与えますが、グローバルに使用する場合はGroovySpyも非常に便利です。これは、必要な部分の動作だけ変更を行い、インタラクションが一致しない場合は実際のメソッドを実行することが可能です。また、呼び出しの確認にも使用できます。

コンストラクタのモッキング

(この機能を使用する前に一度考えなおしてください。もしかするとコードの設計自体を見なおしたほうが良いかもしれません。)

グローバルモックはコンストラクタのモッキングをサポートしています。

def anySubscriber = GroovySpy(RealSubscriber, global: true)

1 * new RealSubscriber("Fred")

ここではスパイを使用しているため、コンストラクタの振る舞いは変更されずに、モックオブジェクトが作成されます。コンストラクタの振る舞い変更するには、コンストラクタをスタビングします。

new RealSubscriber("Fred") >> new RealSubscriber("Barney")

このようにすることでFredという名前のsubscriberを構築するたびに、Barneyという名前のsubscriberが代わりに構築されます。

Staticメソッドのモッキング

(この機能を使用する前に一度考えなおしてください。もしかするとコードの設計自体を見なおしたほうが良いかもしれません。)

グローバルモックはstaticメソッドの、モッキングとスタビングをサポートしています。

def anySubscriber = GroovySpy(RealSubscriber, global: true)

1 * RealSubscriber.someStaticMethod("hello") >> 42

これは動的なstaticメソッドに対しても使用できます。

もしグローバルモックをコンストラクタ、またはstaticメソッドのモックにのみ使用している場合は、モックインスタンスは必要がありません。このような場合は、以下のように書くこともできます。

GroovySpy(RealSubscriber, global: true)

高度な機能(New in 0.7)

ほとんどの人にとってはこの機能は必要がありません。しかし、一部のユーザにとっては非常に有益な機能なはずです。

アラカルトモック

これが最後の話題になります。Mock()Stub()、そしてSpy()のファクトリメソッドは、任意の構成でモックオブジェクトを作成するための方法が、あらかじめ用意されています。もしこの構成をより細かく制御したい場合は、まずorg.spockframework.mock.IMockConfigurationインタフェースを参照してください。このインタフェース[5]の全てのプロパティは、Mock()メソッドへ名前付き引数として設定することができます。例えば以下のように使用します。

def person = Mock(name: "Fred", type: Person, defaultResponse: ZeroOrNullResponse, verified: false)

ここで作成したモックは、通常のMock()で作成した場合と同じデフォルト値を返しますが、(Stub()のように)呼び出しの確認を行いません。また、ZeroOrNullResponseを設定する代わりに、予期しないメソッドの呼び出しに対応するといった、独自のorg.spockframework.mock.IDefaultResponse実装を設定することもできます。

モックオブジェクトの検出

オブジェクトがSpockのモックオブジェクトであるか調べるには、org.spockframework.mock.MockDetectorを使用します。

def detector = new MockDetector()
def list1 = []
def list2 = Mock(List)

expect:
!detector.isMock(list1)
detector.isMock(list2)

また、detectorはモックオブジェクトの詳細情報を取得することもできます。

def mock = detector.asMock(list2)

expect:
mock.name == "list2"
mock.type == List
mock.nature == MockNature.MOCK

参考文献

相互作用中心のテストについて、次の情報を参照することをおすすめします。

注釈

[1]

モックオブジェクトを作成する他の方法については、他のモックオブジェクト(New in 0.7)アラカルトモックを参照してください。

[2]

同じステートメントの一部として宣言しているため、クロージャからsubscriberの変数を参照することができません。

[3]

このような動作をするGroovyのMockForStubFor をご存知かも知れません。

[4]

このクロージャの引数の代入はGroovyの動作によるものです。

[5]

モックの構成はイミュータブルであるため、インタフェースにはプロパティのゲッターのみが含まれています。