Spock インタラクション(モッキング/スタビング)について

まえがき

この記事では、
Spockテストフレームワークのインタラクションについて、解説します。

前回、Stub, Mock, Spyについては解説しましたが、
今回は、それらのオブジェトに対して、
スタビング/モッキングする為のインタラクションについての記事になります。

また、スタビングの上書き挙動など、
初めて触れる人にとっては、ハマりやすい点も説明したいと思います。

interaction (インタラクション) とは

Spockではインタラクションという用語が使われます。

スタビングやモッキングで宣言するものが、インタラクションになります。

単語の意味自体は、「お互いのやりとり」、「相互作用」といった意味。

相互作用ベースのテスト

オブジェクトの状態をテストするのではなく、
オブジェクト同士がどういったやり取りをするのか、に注目してテストする方法です。

オブジェクトのメソッドに、インタラクション宣言する事で、
想定どおりのコミュニケーションが行われているのかをテストする事ができます。

スタブ、モックなどに対して宣言する

インタラクションは、
スタブや、モックオブジェクト(もちろんスパイにも)に対して宣言します。

インタラクションに記述できる項目は以下のとおり。

  • 実行回数
    このメソッドは何回実行されるべきか。

  • 引数
    このメソッドの引数はどう渡されるべきか。

  • 処理内容と返値
    依存オブジェクトを、テスト用に仮処理に差し替える、想定する返値を設定する。

mocking (モッキング) とは

モッキングは、モックオブジェクトに対して、
インタラクションを宣言する事をいいます。

ただ、モッキングでは、特定のメソッドの実行回数という部分だけを扱います。

モッキングの構文

  1 * mockObject.greet("hello")
  |       |        |      |
count     |        |      |
        object     |      |
                 method   |
                       arguments

上記の例では、mockObjectのgreeメソッドが、
引数("hello")で1回だけ実行されるべき。
とインタラクション宣言(モッキング)しています。

各ファクターには、ワイルドカードのような記述が可能です。
例えば、引数はひとつだけ受け取るけど、
値はなんでもよい場合、 _ を使って表現できます。

// 引数1の値はなんでもいいから、greetメソッドが1回コールされること
1 * mockObject.greet(_)

モッキングの記述場所

モッキングは、setupブロック、もしくはthenブロックで記述します。

なにかの処理がされた時、
モックオブジェクトはこのメソッドが○回コールされているはず!
という感じで使うので、thenブロックに記述するのが自然な形だと思います。

特に理由がない場合は、
thenブロックに書いておくのがよいでしょう。

thenブロックに記述した例

setup:
def mockReceiver = Mock(Receiver)
def publisher = new Publisher(mockReceiver)

when:
// Publisherは内部で、Receiver::receiveメソッドをコールする。
publisher.send("hello")

then:
// Publisher::sendがコールされた時に、
// Receiver::receiveが引数"hello"で、1回コールされていること
1 * mockReceiver.receive("hello")

setupブロックに記述した例

def mockReceiver = Mock(Receiver) {
    // receiveメソッドが引数"hello"で、1回コールされること
    1 * receive("hello")
}

def publisher = new Publisher(mockReceiver)

expect:
// Publisherは内部で、Receiver::receiveメソッドをコールする。
publisher.send("hello")

stabbing (スタビング) とは

スタビングは、スタブオブジェクトに対して、インタラクション宣言する事。
といっても、Spockでは、Mock,Spyもスタブ機能を持ってるので、同様に宣言できます。

スタビングは、処理と返り値を指定できる

スタブオブジェクトの特定メソッドが、とある引数でコールされた時は、
こんな処理をして、特定の返値を返す。
といったことをインタラクションで宣言できます。

stubObject.doSomething("hello") >> { msg -> return true }
    |           |         |                   |
  object        |         |                response
              method      |
                       arguments

Groovyでは、クロージャの括弧と最後のreturn文は省略できるので、
下記のように書く事もできます。

stubObject.doSomething("hello") >> true

スタビングの記述場所

スタビングはどこにでも記述する事ができます。
setupブロックに書くのが一番わかり易くて良いと思います。

setupブロックに記述した例

setup:
def boss = new Boss()

def stubPerson = Stub(Person) {
    // hogeと言われたら、falseを返す
    reply("hoge") >> false

    // piyoと言われたら、trueを返す
    reply("piyo") >> true
}

expect:
// Person::replyがtrueを返したら、
// Boss::tellメソッドも、trueを返します。
false == boss.tell(stubPerson, "hoge")
true == boss.tell(stubPerson, "piyo")

ほかにも、when、thenプロックでも宣言する事ができますが、
インタラクションの挙動をよくわかっていないと混乱するポイントがあります。

インタラクションのつまずきポイント

Spockのスタビングを使うようになると、
setupブロックでスタビングしてるものを、
あとから上書きしたいような事がしばしばあります。

しかし、whenブロックや、thenブロックで書いたスタビングが反映されない。
といったところで悩んでる人は多いと思います。

これらは、インタラクションの挙動を理解していない為で、
下記のポイントをおさえておくと理解ができるようになります。

インタラクションは基本、追加宣言

スタビングも、モッキングも、実はインタラクションの宣言を追加しているだけです。
インタラクションは、最初に追加されたものから、
条件にマッチするかどうかが評価される。

最初に設定したインタラクションを取っ払うような事はできません。

スタビングの追加例

setup:
def stubObject = Stub(Something)
stubObject.foo() >> 1
stubObject.foo() >> 2 // 後ろに追加されている
stubObject.foo() >> 3 // 後ろに追加されている

expect:
1 == stubObject.foo()
1 == stubObject.foo() // 何度コールしても、最初のインタラクションにマッチする為、1が返る。
1 == stubObject.foo() // 何度コールしても、最初のインタラクションにマッチする為、1が返る。
1 == stubObject.foo() // 何度コールしても、最初のインタラクションにマッチする為、1が返る。

モッキングも同様にインタラクション宣言を追加している事になります。
回数を指定する事で、指定回数以降は次のインタラクションにマッチさせるような事もできます。

モッキングの追加例

setup:
def mockObject = Mock(Something)
1 * mockObject.foo() >> 1 // 1回目
1 * mockObject.foo() >> 2 // 2回目    後ろに追加されている
_ * mockObject.foo() >> 3 // 3回目以降 後ろに追加されている (_ * は省略可能)

expect:
1 == mockObject.foo() // 1個目のインタラクションにマッチ
2 == mockObject.foo() // 2個目のインタラクションにマッチ
3 == mockObject.foo() // 3個目のインタラクションにマッチ
3 == mockObject.foo() // 3個目のインタラクションにマッチ

thenブロックのインタラクションは例外的に先頭に挿入される

もし、スタビングを上書きするようなことがしたいのであれば、
thenブロックに記述します。

インタラクションには、特殊なルールがありthenブロックで宣言すると、
対になるwhenブロックより前に実行され、
しかも、インタラクション宣言を先頭に挿入する事ができます。

thenブロックでスタビング記載した例

setup:
def stubObject = Stub(Something)
stubObject.foo() >> 1

when:
int ret = stubObject.foo()

then:
// スタビングの挿入
// whenブロックが始まる前にここだけ先に実行されます。
stubObject.foo() >> 2

// インタラクションが挿入されている為、2が返る。
2 == ret

上書きしているわけではなく、先頭に挿入されているのです。

whenの前にthenブロックの一部だけが先に実行されているので、
直感的に混乱すると思いますが、覚えておきましょう。

thenブロックのインタラクションの有効範囲

thenブロックで挿入したインタラクションは、
対になるwhenブロック内でのみ有効です。

whenブロックを抜けると、挿入分は失われて、前の状態に戻ります。

thenブロックのスタビング挿入の有効範囲

setup:
def stubObject = Stub(Something)
stubObject.foo() >> 1

when:
// 対になるwhenブロックでは、thenブロックのスタビングが先に評価される
// setupブロックのスタビングは無視される
2 == stubObject.foo()

then:
// スタビングの挿入
// whenブロックが始まる前にここだけ先に実行されます
stubObject.foo() >> 2

// whenブロック以外では、挿入分は既に失われ
// もとのsetupブロックで宣言したスタビングに戻る
1 == stubObject.foo()

まとめ

Spockでは、
スタビング・モッキングで、インタラクション宣言をしている。

モッキングは、実行回数を評価する為、基本的にはthenブロックで記述する。
ただ、スタビングとまとめて記述する事も可能なので、setupブロック等で記述することも可能。

インタラクションを使う事で、相互作業ベースのテストが可能になる。
オブジェクトの状態ではなく、やりとりに注目したテスト。

スタビングでは、依存オブジェクトの処理内容や、返値を差し替える事ができる。

インタラクション宣言は、スタビングもモッキングも原則、追記である。
上書きというものはなく、追加した順でマッチしたものが使われる。

thenブロックで宣言した場合だけ、例外的に先頭に挿入される。
挿入したインタラクションは、対になるwhenブロックを抜けると失われる。

コメントを残す