魅惑的なテスティングフレームワーク Spock (Mocking API編)

f:id:Naotsugu:20150411014232p:plain

Spock では Groovy の動的な特性を生かした柔軟な Mocking テストが実現できます。


blog1.mammb.com

前回に続いて、Spock のモック機能について見ていきましょう。



まずは公式サンプルからの例で、以下のようなイベントの Publisher と Subscriber のインタラクションに対するテストを見ていきます。

class Publisher {
  def subscribers = []
  def send(event) {
    subscribers.each {
      try {
        it.receive(event)
      } catch (Exception e) { 
        println "Exception: " + StackTraceUtils.extractRootCause(e).message
      }
    }
  }
}
interface Subscriber {
  def receive(event)
}


Mock を作成する

JUnit などとは異なり、Spock は自身で Mocking 機能を提供しています。

Subscriber のモックを作成するのは以下のようにするだけです。

import spock.lang.*

class PublisherSpec extends Specification {
    def pub = new Publisher()
    def sub1 = Mock(Subscriber)
    def sub2 = Mock(Subscriber)
}

作成したモックを Publisher に設定します。

    def setup() {
        pub.subscribers << sub1 << sub2
    }

前回登場した fixture メソッド setup() にて作成したモックを Publisher に登録しています。


インタラクションを検証する

定義したモックを利用した feature メソッドを定義してみましょう。これは JUnit で言うところのテストメソッドとなります。

    def "delivers events to all subscribers"() {
        when:
        pub.send("event")
        then:
        1 * sub1.receive("event")
        1 * sub2.receive("event")
    }

Publisher からイベント"event"が発行された場合、sub1sub2 がそれぞれ "event" を1回受信する というケースになります。

Spock では N * モック.メソッド() の形式でモックに対するインタラクションの検証が行えます。


Mock の振舞いを定義する

Subscriber への通知は、途中なんらかの例外が発生したとしても、登録されている全ての Subscriber へ通知を行わなければなりません。

モックとして定義した sub1 の receive() メソッドが呼び出された場合に例外を throw するように振舞いを定義するには以下のようにします。

    def "can cope with misbehaving subscribers"() {
        sub1.receive(_) >> { args -> throw new Exception(args[0]) }
        when:
        pub.send("event1")
        pub.send("event2")
        then:
        1 * sub2.receive("event1")
        1 * sub2.receive("event2")
    }

Spock ではモックのメソッドに対して >> で振舞いを定義できます。

sub1 に対する receive() メソッド呼び出しは、全て例外を throw するように定義しています。 throw する例外には引数で得たものを例外メッセージとして設定しました。

実行すると以下のようになります。

f:id:Naotsugu:20150412224502p:plain

sub1 への通知は例外となりコンソールにメッセージが表示されています。 例外が発生しても sub2 への通知は行われています。

ここまでを合わせると以下のようになります。

class Publisher {
    def subscribers = []
    def send(event) {
        subscribers.each {
            try {
                it.receive(event)
            } catch (Exception e) { 
              println "Exception: " + StackTraceUtils.extractRootCause(e).message 
            }
        }
    }
}
interface Subscriber {
    def receive(event)
}

class PublisherSpec extends Specification {
    def pub = new Publisher()
    def sub1 = Mock(Subscriber)
    def sub2 = Mock(Subscriber)
    def setup() {
        pub.subscribers << sub1 << sub2
    }
    def "delivers events to all subscribers"() {
        when:
        pub.send("event")
        then:
        1 * sub1.receive("event")
        1 * sub2.receive("event")
    }
    def "can cope with misbehaving subscribers"() {
        sub1.receive(_) >> { args -> throw new Exception(args[0]) }
        when:
        pub.send("event1")
        pub.send("event2")
        then:
        1 * sub2.receive("event1")
        1 * sub2.receive("event2")
    }
}

では、これからMocking API の詳細について見ていきましょう。


Mock オブジェクトの生成

モックの定義は MockingApi.Mock() を使い、以下のように行います。

  def subscriber1 = Mock(Subscriber)
  def subscriber2 = Mock(Subscriber)

モックの作成は以下のように書くこともできます。

  Subscriber subscriber = Mock()

こちらの表記の方がIDEのサポートが良く機能します。


インタラクション回数条件の定義

1 * subscriber1.receive("event") のような形式でインタラクションの回数条件を指定できます。

他に以下のような指定ができます。

回数の指定例 説明
1 * subscriber.receive("event") 正確に1回
0 * subscriber.receive("event") ゼロ回
(1..3) * subscriber.receive("event") 1〜3回
(1.._) * subscriber.receive("event") 少なくとも1回
(_..3) * subscriber.receive("event") 多くても3回
_ * subscriber.receive("event") 任意回数(ゼロ回含む)(strict mock で必要)


引数のマッチング定義

引数のマッチングは以下のような指定ができます。

引数の指定例 説明
1 * subscriber.receive("event") 引数が "event" の呼び出し
1 * subscriber.receive(!"event") 引数が "event" でないの呼び出し
1 * subscriber.receive() 空引数リストの呼び出し
1 * subscriber.receive(_) 全ての引数1つの呼び出し(null含む)
1 * subscriber.receive(*_) 全ての引数リスト(空引数リスト含む)
1 * subscriber.receive(!null) 全て非null引数呼び出し
1 * subscriber.receive(_ as String) String型の非null引数呼び出し
1 * subscriber.receive({ it.size() > 3 }) 述語を満たす引数呼び出し

複数の引数がある場合には以下のように、それぞれのパラメータに対して条件を定義できます。

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


メソッドのマッチング定義

メソッドのマッチングは以下のように指定できます。

引数の指定例 説明
1 * subscriber./r.*e/("event") 正規表現にマッチするメソッド名の呼び出し
1 * subscriber._(*_) 全てのメソッド呼び出し
1 * subscriber._ 全てのメソッド呼び出し(上記のショートカット)
1 * _._ 全てのモックオブジェクトの全ての呼び出し
1 * _ 全てのモックオブジェクトの全ての呼び出し(上記のショートカット)


マッチングの順序

複数の条件にマッチするメソッド呼び出しが発生した場合、最初に定義したものが採用されます。

例外として then: ブロックで定義したものは、他で定義したものに先立ってマッチングが行われます。 これは、例えば setup() で設定された定義を then: ブロック内の定義により無効化できることを意味します。


Strict Mocking

Spock のモックは Lenient(寛容) です。つまりモックで定義されていないメソッド呼び出しを行っても例外にはなりません。

Strict なモックが必要な場合には以下のようにします。

  when:
  publisher.publish("event")

  then:
  1 * subscriber.receive("event")
  _ * auditing._
  0 * _

この例では subscriber と auditing に対する特定のインタラクションを定義し、0 * _ とすることで、それ以外のインタラクションが発生していないことを条件としています。


モック生成時にインタラクション条件を指定する

モックにベースとなる振舞いがある場合には、モック生成時に定義することができます。

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

以下のような書き方もできます。

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


モックとのインタラクション順序を検証する

これまでの例では、メソッドの呼び出し条件は規定しないものでした。

呼び出し順序を検証する必要がある場合には以下のようにします。

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

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

then: ラベルで分けて書くことで "goodbye" の前に "hello" を受信することが規定できます。


スタブの作成

モックをスタブ化、つまり戻り値を返すようにするには >> を使います。

Subscriber の receive メソッドが戻り値を返すように以下のように変更しておきましょう。

interface Subscriber {
    String receive(String message)
}

メソッドが "ok" を返却させるには以下のようにします。

  subscriber.receive(_) >> "ok"

任意引数の receive 呼び出しにより、戻り値として "ok" を返却する response generator を生成しています。

引数に応じて戻り値を変えるには以下のように定義します。

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

スタブ定義は then: ブロックの中、when: ブロックの前など、ほとんどの場所で定義できます。モックオブジェクトをスタブ化のためだけに使うなら、setup: ブロックの中や、以下のようにモックの作成時に定義できます。

def subscriber = Mock(Subscriber) {
   receive(_) >> "ok"
}


スタブから順序値を返却する

呼び出し順序により戻り値を変えるには以下のように >>> で戻り値オブジェクトのリストを与えます。

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


スタブから生成した値を返却する

引数の内容に応じて戻り値を変化させるには以下のように合成します。

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

対象のメソッドが固定された型で、引数が1つの場合には以下のように定義することができます。

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

例外の throw も同じように closure で指定するだけです。

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

順序を考慮して、4回目に例外などは以下のように定義できます。

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


モックとスタブを合わせて使う

モックとスタブの定義を組み合わせるには以下のように書くことができます。

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

以下のようにモックの定義とスタブの定義を分けて定義した場合には上手く動かないでしょう。

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

when:
publisher.send("message1")

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

先に述べた通り、then: での定義は setup: での定義に先立ってマッチングが行われるため、subscriber.receive("message1") の戻り値は常に null となります。


スタブ化に特化したStubsを使う

MockingApi.Mock ではモックとスタブの双方の機能を提供していましたが、スタブに特化したオブジェクトを MockingApi.Stub() で生成できます。

def subscriber = Stub(Subscriber)

または以下のようにもできます。

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

明確に Stub としておくことで読み手に意図を伝えられるとともに、Mock にはない以下の特徴があります。

  • プリミティブ型のデフォルト値を返却する
  • 非プリミティブ型の数値(BigDecimalなど)がゼロを返却する
  • 非数値型が空またはダミーインスタンスを返す
    • Stringの場合は空文字
    • コレクションの場合は空のコレクション
    • デフォルトコンストラクタで構築さえたオブジェクト


スパイ

MockingApi.Spy により Spy を生成できます。

Spy は実際のオブジェクトを使うため、クラスとコンストラクタ引数を指定する必要があります。

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

作成した Spy を以下のようにすることで、呼び出し元とのインタラクションを傍受できます。

1 * subscriber.receive(_)

以下のようにスタブ化することで特定メソッドからの戻り値を変更できます。

subscriber.receive(_) >> "ok"

そして、実際のメソッドに処理を委譲し、その結果を加工して返却することもできます。

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

callRealMethod() により、引数も含めて実際のオブジェクトに転送されます。

Spy で partial mock にすることもできます。

def persister = Spy(MessagePersister) {
  isPersistable(_) >> true
}

when:
persister.receive("msg")

then:
1 * persister.persist("msg")