この記事は筆者のソロ Advent Calendar 2022 5日目の記事です。
前回まででSpockでのテストの書き方を色々紹介しましたが、今回はSpockにおけるMockを使用したテストの書き方を紹介します。
KotlinでSpock入門[基礎編]
KotlinでSpock入門[基礎編2]
KotlinでSpock入門[Data Driven Testを学んでデータ駆動テストを使い倒す]
KotlinでSpock入門[Data Driven Testを学んでデータ駆動テストを使い倒す2]
KotlinでSpock入門[mock編] <- 今ここ
KotlinでSpock入門[Spring boot実践編]
記事作成で使用したサンプルコードはこちら
Mocking
まず、以下のようなクラスとインターフェイスを用意します。
class Publisher {
List<Subscriber> subscribers = []
int messageCount = 0
void send(String message){
subscribers*.receive(message)
messageCount++
}
}
interface Subscriber {
void receive(String message)
}
PublisherにはリストでSubscriberが格納でき、sendメソッドで受け取ったメッセージを各Subscriberに送信し、保持しているカウントを増やすことができます。Subscriberはインターフェイスで用意しています。
Subscriberの実装クラスを用意していないため、Publisherクラスのテストが今のままだとできません。なのでSubscriberをモック化しPublisherに格納します。
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
}
}
ここで、Publisherのsendメソッドを呼んだ場合の振る舞いとしては各Subscriberのreceiveメソッドが1回づつ呼ばれ、かつ、メッセージとしてPublisherのsendメソッドで指定したメッセージを受け取ってることを期待します。これをテストで書くと以下のようになります。
def "should send messages to all subscribers"() {
when:
publisher.send("hello")
then:
1 * subscriber.receive("hello")
1 * subscriber2.receive("hello")
}
期待している振る舞いと違かった場合にはInteractionNotSatisfiedError
が発生します。thenブロックを詳しく見てみると以下のように4つに分類することができる。
1 * subscriber.receive("hello")
| | | |
| | | argument constraint
| | method constraint
| target constraint
cardinality
Cardinality
メソッドが何回呼ばれたか。これは固定値でも範囲でも指定することができる。
1 * subscriber.receive("hello") // 1回呼ばれていること。
0 * subscriber.receive("hello") // 1回も呼ばれていないこと。
(1..3) * subscriber.receive("hello") // 1-3回の範囲で呼ばれていること。
(1.._) * subscriber.receive("hello") // 最低1回は呼ばれること。
(_..3) * subscriber.receive("hello") // 最大で3回まで呼ばれること。
Target Constraint
どのモックオベジェクトに対してかを指定する。
1 * subscriber.receive("hello") // subscriberに対しての振る舞いを指定
1 * _.receive("hello") // どのモックでもok。
Method Constraint
どのメソッドが呼ばれるか。正規表現も使える。
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')
getterも指定できる。プロパティ呼び出しでok。
1 * subscriber.status // same as: 1 * subscriber.getStatus()
setterの場合はメソッド呼び出しのみok。
1 * subscriber.setStatus("ok") // NOT: 1 * subscriber.status = "ok"
Argument Constraints
引数の値に何が期待されるかを指定。可変長引数や2つ以上の引数指定も可能。引数の等価比較は==
による比較を行う。
1 * subscriber.receive("hello") // 引数として"hello"が指定されること
1 * subscriber.receive(!"hello") // 引数として"hello"が指定されないこと
1 * subscriber.receive() // 引数なし
1 * subscriber.receive(_) // 適当な値(nullを含む)
1 * subscriber.receive(*_) // 適当なlist(空を含む)
1 * subscriber.receive(!null) // nullでない
1 * subscriber.receive(_ as String) // String型にcastできる値
1 * subscriber.receive(endsWith("lo")) // Hamcrest matcher
1 * subscriber.receive({ it.size() > 3 && it.contains('a') }) //3文字以上かつaを含む
Code Constraint
引数にクロージャを渡すことで汎用性の高い検証をすることもできる。
以下の例ではlistにnameがtestというPersonオブジェクトが1回addされたことを検証してます。verifyAllは対象オブジェクトの複数の検証をする時に使えます。
class Person {
String name = ""
Person(String name) {
this.name = name
}
}
List<Person> list = Mock()
def "test"() {
when:
list.add(new Person("test"))
then:
1 * list.add({
verifyAll(it, Person) {
name == "test"
}
})
}
Mock Creation Time
モックは以下のように作成時に検証を指定することもできます。
Subscriber subscriber = Mock {
1 * receive("hello")
1 * receive("goodbye")
}
with
同じターゲットに対して複数の検証を書くときはwithを使用し以下のようにかける。
with(subscriber) {
1 * receive("hello")
1 * receive("goodbye")
}
Stubbing
スタブとはメソッドの呼び出しに対して指定の値で応答するようすることです。メソッドをスタブにする場合には以下のようにします。
Person person = Mock()
def "test stub person"() {
given:
person.hello() >> "test"
expect:
person.hello() == "test"
}
モックオブジェクトをスタブとしてのみ使用する場合は、 モックの作成時かgivenブロックの中でインタラクションを宣言するのが一般的です。
以下のように>>>
を使用し連続したスタブ設定をすることもできる。
def "test stub person2"(){
given:
person.hello() >>> ["test1", "test2", "test3"]
expect:
person.hello() == "test1"
person.hello() == "test2"
person.hello() == "test3"
}
メソッドの引数に応じてスタブを変更したい場合は以下のようにクロージャを使用して書くことができる。以下の例では指定された引数の文字数によって値を変更している。
def "test stub person3"() {
given:
person.say(_, _) >> { String word1, String word2 -> word1.size() + word2.size() > 5 ? "ok" : "fail" }
expect:
person.say("a", "b") == "fail"
person.say("aaaa", "bbbb") == "ok"
}
例外を発生させたい場合は以下のようにかける。
subscriber.receive(_) >> { throw new InternalError("ouch") }
メソッドの呼び出しは以下のように連鎖させることもできる。
def "test stub person4"(){
given:
person.hello() >>> ["test1", "test2"] >> { throw new InternalError() }
when:
def result1 = person.hello()
def result2 = person.hello()
person.hello()
then:
result1 == "test1"
result2 == "test2"
thrown(InternalError)
}
戻り値にあまり興味がないときは_
を使用することができる。_
を使用した場合はnullではない、デフォルトではモック自身を返す。このことを利用すると以下のようなビルダーパターンに適用できる。
given:
ThingBuilder builder = Mock() {
_ >> _
}
when:
Thing thing = builder
.id("id-42")
.name("spock")
.weight(100)
.build()
then:
1 * builder.build() >> new Thing(id: 'id-1337')
thing.id == 'id-1337'
builderのモックはデフォルトでモック自身を返すようになっているが、thenブロックで定義されたbuildメソッドの振る舞いは優先される。
上記のビルダーパターンの例は公式のドキュメントの例だが実際に動かすと以下のようなMissingMethodExceptionになってしまった。動かし方が悪いのか意図したように動かなかったのでわかる方いたらコメントください。
No signature of method: java.lang.Object.name() is applicable for argument types: (String) values: [spock]
Possible solutions: any(), wait(), dump(), any(groovy.lang.Closure), wait(long), each(groovy.lang.Closure)
groovy.lang.MissingMethodException: No signature of method: java.lang.Object.name() is applicable for argument types: (String) values: [spock]
Possible solutions: any(), wait(), dump(), any(groovy.lang.Closure), wait(long), each(groovy.lang.Closure)
まとめ
今回は以下の内容について紹介しました。
- SpockでMockを使用したテストについて
- SpockでMockのメソッドをスタブにして振る舞いを変える方法について
実際のプロジェクトで採用する場合、モックを使用したいケースは多いと思いますがSpockはモックの機能をデフォルトで備えています。加えて、Spockに対して言えることですが書き方が非常に柔軟でかなり直感的に書けます。JavaやKotlinでテストを書いてた人からすると最初は違和感を感じるかもしれませんが、慣れるとJUnitなどのテスティングフレームワークよりも書きやすいという人もいるのではないでしょうか!まだ使ったことのない方はぜひSpockを使ってみてください!