相互作用中心のテスト¶
相互作用中心のテストは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が受信できたか」です。この問いに答えるには、publisherとsubscriberのやり取りを監視しするSubscriber
の特別な実装が必要です。この特別な実装はよくモックオブジェクトと呼ばれます。
subscriberのモック実装を自前で作成することもできます。しかし、メソッドの数が多かったり、相互作用が複雑なメソッドが増えてくると、このコードを書いたりメンテナンスするのが煩わしくなります。そこで、モックフレームワークの出番です。このフレームワークは、仕様対象のオブジェクトとコラボレータの間に期待する相互作用を宣言する方法を提供します。さらに、この期待する相互作用を検証する、コラボレータのモック実装を生成できます。
Javaの世界では、JMock、EasyMock、Mockitoといった、人気があり成熟したモックフレームワークに事欠きません。これらのモックフレームワークを、Spockと共に使用することもできます。しかし、Spockの言語仕様に合うように、モックフレームワークをSpock自身で再開発することを決めました。この判断は、Groovyの能力を最大限活用して、より簡単に相互作用中心のテストを書けるようにし、より読みやすく、また最高に楽しいものにしたいという思いからです。この章を読み終えた時に、その目標が達成されていると感じていただけると幸いです。
特に明示していない限り、モックフレームワークのすべての機能はJavaコードとGroovyコードのテスト両方で使用できます。
モックオブジェクトの作成¶
モックオブジェクトはMockingApi.Mock()
メソッドで作成します[1]。2つのsubscriberモックを作成してみましょう。
def subscriber = Mock(Subscriber)
def subscriber2 = Mock(Subscriber)
または、Javaに近いシンタックスもサポートしています。これは、IDEのサポートがより受けやすくなるかもしれません。
Subscriber subscriber = Mock()
Subscriber subscriber2 = Mock()
この場合のモックの型は、左辺の変数の型から推論されます。
注釈
もしモックの型を左辺で指定した場合は、右辺で型を省略できます(指定しても問題ありません)。
モックオブジェクトは文字通りその型の実装(クラスの場合は継承)します。別の言い方をすると、上記の例のsubscriberはis-a Subscriber
です。つまり、この型を期待する静的型付けのコード(Java)に、この実装を渡せるということです。
モックオブジェクトのデフォルト動作¶
初期状態のモックは何の振る舞いもしません。このオブジェクトのメソッドを呼び出すことは可能ですが、戻り値の型に応じたデフォルト値(false
、0
、またはnull
)を返す以外は何もしません。ただしObject.equals
、Object.hashCode
、Object.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)¶
モッキングとは、仕様対象のオブジェクトとそのコラボレータ間の、(必須の)インタラクション(訳注: ここまで相互作用と表記してきましたが、Spockではインタラクションを用語として使用しているため、以降インタラクションと表記します)を宣言する活動です。次の例を見てください。
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 すべき」になります。(訳注: コードが英語の文章のように読めると言っている)
このフィーチャメソッドを実行すると、when:
ブロック内で発生するモックオブジェクトへのすべての呼び出しが、then:
ブロックで宣言したインタラクションと照合されます。もし、インタラクションが1つでも満たされない場合はInteractionNotSatisfiedError
(のサブクラス)が投げられます。この検証は自動的に行われます。開発者自身で検証コードを記述する必要はありません。
インタラクション¶
それでは、then:
ブロックについて詳しく見ていきましょう。上記の例では2つのインタラクションを宣言しています。この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
といったメソッドで宣言できるという意味です。同様に、ヘルパークラスのメソッドで宣言するといったことも可能です。
モックオブジェクトへの呼び出しが発生すると、宣言した順番でインタラクションを照合します。もし、呼び出しが複数のインタラクションにマッチする場合は、インタラクションが実行上限に達してない限り、先に宣言したインタラクションが優先されます。ただし1つ例外があります。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:
ブロック内でのインタラクション宣言を可能にしているのでしょうか? 答えはSpockの内部にあります。Spockは、then:
ブロックのインタラクション宣言を、when:
ブロックを処理する直前へ移動します。これは多くの場合に問題なく動作します。しかし、特定の条件下では問題が起きます。
when:
publisher.send("hello")
then:
def message = "hello"
1 * subscriber.receive(message)
ここでは、期待する引数を変数に受けています(多重度を変数に受ける場合も同様)。しかし、Spockはインタラクションが変数宣言へ参照を持っていると把握するほど賢くありません。そのため、インタラクションをそのまま移動し、実行時に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)
なぜマッチした呼び出しが多すぎたのか解析しやすいように、Spockは問題となるインタラクションにマッチしたすべての呼び出しを表示します(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")
の呼び出しの1つがTooManyInvocationsError
を引き起こしています。ここで、subscriber.receive("hello")
のような2つの呼び出しは、1行に集約されて表示されます。最初のsubscriber.receive("hello")
はreceive("goodbye")
の前に発生していたかもしれませんが、この出力からは区別できません。
2つめの場合(意図したよりも実行が少ない場合)は、when
ブロックの実行が完了した時点でのみ検出できます(終了するまでは呼び出しの可能性があるため)。検出した場合はTooFewInvocationsError
を発生させます。
Too few invocations for:
1 * subscriber.receive("hello") (0 invocations)
これはメソッドの呼び出しが、一切なかったということではありません。同じメソッドが、他の引数で呼び出された、異なるモックオブジェクトで呼び出された、もしくは、”別の”メソッドが呼び出された、といったことが考えられます。これらいずれの場合でも、TooFewInvocationsError
が発生します。
なぜ意図しない呼び出しが代わりに起きたのか解析しやすいよう、Spockはどのインタラクションにもマッチしなかったすべての呼び出しを表示します。この呼び出しは、問題のあるインタラクションに類似している順番で表示されます(new in Spock 0.7)。特に、インタラクションの引数以外がすべて一致する呼び出しが最初に表示されます。
Unmatched invocations (ordered by similarity):
1 * subscriber.receive("goodbye")
1 * subscriber2.receive("hello")
実行順序¶
多くの場合に、正確なメソッドの実行順序に意味はありません。また、この実行順序は時間と共に変わる可能性があります。over-specificationを避けるため、Spockはデフォルトで、最終的に宣言したインタラクションすべてが満たされれば、どのような実行順序も許容します。
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)¶
スタビングとは特定のメソッド呼び出しに対するレスポンスを宣言することです。メソッドをスタビングする場合は、そのメソッドが何回呼ばれるかを気にせず、いつ呼び出されても特定の値を返したり、何かの副作用が働くようにします。
例を示すために、Subscriber
のreceive
メソッドの処理が完了した場合に、ステータスコードを返すように変更してみましょう。
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
参考文献¶
相互作用中心のテストについて、次の情報を参照することをおすすめします。
Endo-Testing: Unit Testing with Mock Objects
モックオブジェクトの概念を説明した、XP2000カンファレンスの資料
-
どのように適切にモッキングするか説明した、OOPSLA2004カンファレンスの資料
-
モックに関するMartin Fowlerの資料
Growing Object-Oriented Software Guided by Tests
TDDのパイオニアであるSteve FreemanとNat Pryceが、実際にどのようにモックを活用しながら、テスト駆動開発を行うのか詳細に説明している
注釈
[1] | モックオブジェクトを作成する他の方法については、他のモックオブジェクト(New in 0.7)とアラカルトモックを参照してください。 |
[2] | 同じステートメントの一部として宣言しているため、クロージャから |
[3] |
[4] | このクロージャの引数の代入はGroovyの動作によるものです。 |
[5] | モックの構成はイミュータブルであるため、インタフェースにはプロパティのゲッターのみが含まれています。 |