Stub, Mock, Spyについて解説 (Spock)

まえがき

この記事では、Spockテストフレームにおける
Stub(スタブ), Mock(モック), Spy(スパイ)について解説していきます。

このあたり、テストダブルと呼ばれるものですが、
テストフレームワークによって、ニュアンスが多少異なることがあるそうです。

なので、今回はあくまで、Spockテストフレームワークに限定して、
話を進めていきたいと思います。

Stub/Mock/Spyの違いについて、
それぞれの簡単な使い方を説明していくので、Spockは使った事あるけど、
まだよくわかってない。テストコードどうやって書くのか想像つかない。
という人には参考になるかと思います。

Spockテストフレームワークとは

まず最初に、Spockのテストフレームワークについて
簡単に紹介しておきます。

Spockは、Javaアプリケーション用のテストフレームワークです。
JavaのテストフレームワークといえばJUnitですが、
テストに最適な美しい表現ができるように設計されています。

テストの実装は、Groovy言語で記述。
直感的で、簡素に書けるようになっています。

最初のテストサンプルはこんな感じ。

class MyFirstSpec extends Specification {
  def "let's try this!"() {
    expect:
    Math.max(1, 2) == 3
  }
}

Specificationというクラスを継承して、期待する式を書いているだけです。

あと、テーブルテストも下記のように表現できます。
whereに指定しているデータが、一行ずつ順番に変数に代入されテストが行われます。

class DataDrivenSpec extends Specification {
    @Unroll
    def "test Math.min(#a, #b)"() {
        expect:
        ret == Math.min(a, b)

        where:
        ret || a | b
        3   || 3 | 7
        4   || 5 | 4
        9   || 9 | 9
    }
}

偽装クラスの役割

単体テストをするにあたって必要となる技術。

依存するクラスを、テスト用の偽装クラスに差替えて、
単体テストをやり易くするテクニックです。

テストダブルと呼ばれる、Stub, Mock, Spyも偽装するという点では共通しています。
本来のクラスを装って動きつつ、
観測したり処理自体を変更することが可能になります。

テストダブルがあればテストできるのか?

Stub, Mock, Spyのようなテストダブルを利用すれば、
テストはし易くなります。

でも、まずはちゃんとテストできるコードにしておくのが大切です。
クラス設計、大丈夫でしょうか?

無駄な依存関係が複雑に絡み合ってるような場合は、
テストコード自体もカオスになります。

そもそも、テストできないなんて事もありえます。

クラス設計で注意する点

テスト対象クラスの依存性は排除して、
いつでも、どこからでも実行できる必要があります。

クラス設計をする際には、以下の点を意識します。

ひとつの事をうまくやるようにする。

ひとつの事をうまくやるは、UNIX哲学でもある話ですね。

一つのパッケージ、クラス、メソッドに
なんでもかんでも機能モリモリにするのは良くない。

複雑になりすぎるとテストするときも、
一体このクラスの何をテストしたらいいのか、不明瞭になります。

接合部を設ける。

挙動の差替えができるようにするポイントを作る事です。

本番で動くときと、テストやダミーで動く時に、
実際、DBに書き込みにいったり、外部にリクエストされるとまずい場合もありますよね。
乱数を扱う場合も、再現性がないとテストがしづらくなります。

そういった部分を、
接合部でうまく切替えができるように、設計しておくのです。

インターフェイスに依存させる。

interfaceを使えという事だけじゃなくて、
依存するのは最小限のシグネチャだけにしておこうという事です。

精査すると依存するクラス全部が必要なわけではない事が多いです。

特定のメソッドだけをコールしているだけというのであれば、
インターフェイスにするか、関数オブジェクトにする事も検討できます。

Stub (スタブ) とは

stubという単語自体の意味は、
木の切り株、切ったあとの端くれみたいな意味になります。

完成形とは違って、肝心のロジック部分が空っぽになった。
残りのガワ(シグネチャ)だけになっている。
というのが由来のようです。

シグネチャだけの存在

Stubは、本来のクラスではないけど、同じシグネチャをもって動作します。
同じシグネチャをもつだけで、実際に中身の処理はデフォルトで空っぽです。
コールしても実際何もしません。

返り値はデフォルト値を返す

返り値は、
int, long, Integer, Long, Double などの数値系であれば、0をデフォルトで返します。
Stringの場合も、空文字("")
Listの場合は空リスト
オリジナルのクラスであれば、そのStubオブジェクトを返します。

setup:
// Personクラスのスタブを生成
def stubPerson = Stub(Person)

expect:
"" == stubPerson.name()  // Stringのデフォルト値
0 == stubPerson.age()    // intのデフォルト値
"" == stubPerson.greet() // 処理が空っぽなのでなにもしない

Personというクラスのシグネチャは備えていますが、
本物のPersonオブジェクトではないのです。

挙動を定義できる (stubbing)

そして、挙動を追加で定義することもできます。

setup:
// Personクラスのスタブを生成
def stubPerson = Stub(Person)

// メソッドの挙動を設定
stubPerson.name() >> "Ken"
stubPerson.age() >> 20

expect:
"Ken" == stubPerson.name()
20 == stubPerson.age()

上記サンプルでは、クロージャの括弧と、return文が省略されていますが、
構文としては下記のとおりです。

stubObject.method(引数) >> { 
    処理
}

Mock (モック) とは

なにかを模したもの、真似したようなもの。
模型みたいなイメージかな〜と思います。

Stub同様に同じシグネチャを備えることができるので、
本来のクラスの置換えとして使える代役です。

ただ、Stubとは少し挙動が異なります。

デフォルトの返り値がStubとは違う

setup:
// Personクラスのモックを生成
def mockPerson = Mock(Person)

expect:
null == mockPerson.name()  // Objectはnull (Stubなら""が返っていた)
0 == mockPerson.age()      // プリミティブのデフォルト値

先程と同じ例ですが、Mockを使った場合、
メソッドがデフォルトで返す値がStubとは異なります。

プリミティブ型はデフォルトのゼロや、false等が返りますが、
Object型はすべてデフォルトでnullを返すようになります。

インタラクションの監視ができる

あと、Mockではインタラクションを観測する事ができます。
観測というのは、メソッドが何回呼ばれたか、
どういった引数で呼ばれたかなどを記録までしています。

なので、依存する本来のクラスの代わりに、
Mockオブジェクトを渡す事で、テスト対象のクラス内部で、
想定通りのメソッドがコールされて使われているかどうかなどを、
チェックする事ができます。

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

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

then:
// 引数"hello"で、Receiver::receiveメソッドが、1回コールされていること
1 * mockReceiver.receive("hello")

1 *の部分が、コール回数をチェックする意味になります。

この書き方にはもっと細かくチェックする書き方もあって、
ゼロを使って逆にコールしてないことをチェックしたり、
どんな引数が渡ってきた場合とか、細かくマッチさせる事が可能です。

詳しくはドキュメントを参考にすると良いでしょう。
>> Spock Interactions

Mockはstubbing機能を持っているが、微妙に挙動は違う。
インタラクション(やりとり)を観測する事もできる。

Spy (スパイ) とは

スパイはその文字通り、
その人たちになりすまして、潜入してしまうようなイメージです。

StubとMockの機能も兼ね備えています。

本物のオブジェクトを利用している

特徴的なのは、本物クラスのインスタンス化したオブジェクトを内包して実際に利用することです。
Spyは、デフォルトで本物オブジェクトのメソッドをそのまま使用します。

setup:
// Spyは、クラスじゃなくてインスタンス化したオブジェクトを渡す。
def spyPerson = Spy(new Person("Ken", 20))

expect:
// デフォルトで、本物のオブジェクトのメソッドが使われる
"Ken" == spyPerson.name()
20 == spyPerson.age()

メソッドの挙動変更もできる。(stubbing)

本物のオブジェクトをwrapしていますが、
必要であればメソッドの挙動も変更が可能。

setup:
def spyPerson = Spy(new Person("Ken", 20))
// 挙動変更
spyPerson.greet() >> "Hello, this is a Spy"

expect:
// 変更したメソッドの結果になる
"Hello, this is a Spy" == spyPerson.greet() 

インタラクションの観測もできる。

Mockと同じように、メソッドのコールを観測できます。

setup:
def spyPerson = Spy(new Person("Ken", 20))

when:
// Person::greetは内部で、name, ageメソッドをコールしている。
String msg = spyPerson.greet()

then:
"Hello, this is Ken. I'm 20 years old." == msg
1 * spyPerson.name()
1 * spyPerson.age()

テスト用に継承クラス作らなくてもよくなる

Spyを使うと、テスト用に元のクラスを継承して、
メソッドオーバーライドして〜という、テスト用の偽装クラスを別途実装しなくても、
手軽に同じことが実現できるようになります。

ただ、ちょっと書き方に癖があるのと、
途中で処理を切り替えようとすると、ハマるポイントがあるので注意。

まとめ

Stub, Mock, Spyは、テストする際の偽装を手助けしてくれるツール。
なくてもテスト自体はできますが、
使うとより便利にテストができるようになります。

機能 デフォルトのメソッド挙動 デフォルトの返り値
Stub stubbing からっぽ デフォルト値 (0, false, "", [])
Mock stubbing and mocking からっぽ デフォルト値 (0, false) or null
Spy object, stubbing and mocking 本来のオブジェクトの挙動 本来のメソッドの返り値

どれも本来のクラスのシグネチャを再現してくれますが、
挙動や返り値が、異なってきます。

スパイだけが、
本来のクラスのインスタンス化したオブジェクトが必要になります。

機能的には、Spyで全部できる!と思うかもしれませんが、
役割と何をテストしたいかで、判断しましょう。

本来のオブジェクトでテストできるなら、Stubじゃなくて本物を使おう。
Stubで事足りるなら、Mockじゃなくて、Stubを使いましょう。
Mockで事足りるなら、Spyじゃなくて、Mockを使いましょう。

コメントを残す