何でも"モック"と呼んでない?mock, stub, fake, dummy, spyの違い
テストコードを書くとき、本当のオブジェクトの代わりに使うものは何でも"モック"と呼んでいましたが、実はそうではないことを教わりました。
ユビキタス言語などもそうですが、メンバーとの認識のズレを発生させないためには正しい用語を使うことは重要です。という訳で Martin Fowler の Mocks Aren't Stubs の用語の違いの章を読んでみました。
結論
モックを「本物のオブジェクトの代わりに置き換わって使われるオブジェクトの総称」みたいな意味合いで使っている場合は、それは正しくは テストダブル と呼びます。
※ダブルを辞書で調べると「代役」という意味に当たる。「2」のイメージから連想できるね。
そしてテストダブルは以下の種類に分かれます。
Mocks:モック
モックは「呼び出され方を事前定義することができるオブジェクト」で、関数が呼び出される回数や呼び出されたときの引数の値といった、期待値・期待動作を定義し、それをテストできるテストダブル。
モックを使うことで、「テストしたいオブジェクトが期待通りに呼び出されるか(どのようにモックオブジェクトを呼び出しているか。振る舞いと呼ばれる)」をテストすることができる。
objects pre-programmed with expectations which form a specification of the calls they are expected to receive.
モックの例(https://martinfowler.com/articles/mocksArentStubs.html より)
public class OrderInteractionTester extends MockObjectTestCase {
private static String TALISKER = "Talisker";
public void testFillingRemovesInventoryIfInStock() {
// setup - data
Order order = new Order(TALISKER, 50);
Mock warehouseMock = new Mock(Warehouse.class);
// setup - expectations
// warehouse の hasInventory() メソッドは 1 度だけ呼び出され、そのときの引数には TALSKTER, 50 が入っていること。なおそのときは true を固定で返す
warehouseMock.expects(once()).method("hasInventory")
.with(eq(TALISKER),eq(50))
.will(returnValue(true));
// warehouse の remove() メソッドは 1 度だけ呼び出され、そのときの引数には TALSKTER, 50 が入っており、 hasInventory() の後に呼び出されること。
warehouseMock.expects(once()).method("remove")
.with(eq(TALISKER), eq(50))
.after("hasInventory");
// exercise
// ※ order.fill() は引数に Warehouse 型を受け取って中でごにょごにょするメソッド。
order.fill((Warehouse) warehouseMock.proxy());
// verify
// 定義したとおりに実行されたかどうかを検証する
warehouseMock.verify();
order.fill()
を実行した時に、引数で渡される warehouse
オブジェクトが期待通りに呼び出されるのかを warehouse のモックを使ってテストしている(order.fill()
をモックを使ってテストしている)ということですね。
Stubs:スタブ
スタブはテスト中の呼び出しに対して返す値が事前準備されているテストダブル。
インターフェースを実装して置き換え可能にしておけば、そこでスタブを使って固定値を返すようにし、テストを実現できる。
Stubs provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test.
スタブの例
type warehouse interface {
HasInventory() bool
}
// スタブ実装
type stubWarehouse struct{}
func (s stubWarehouse) HasInventory() bool {
return true // DB 確認などのもろもろのロジックを簡易化して固定値を返す
}
// warehouse を利用するコード
func orderFill(w warehouse) error {
...
w.HasInventory()
...
}
// 本番コード
w := 本番Warehouse{}
orderFill(w)
// テストコード(テストは書いていないのでご留意を)
w := stubWarehouse{}
orderFill(w)
よく見かけるケース。スタブはスタブ自身をテストしているわけではなく、呼び出し元(hoge())が正しく動作するかをテストしているので注意
Spies:スパイ
スパイはスタブの機能に加えて、どのように自身が呼び出されたかに基づいて何か追加情報(おそらく呼び出し回数や引数)を保持するテストダブル。例えば、本当はメールを送る処理だが、メールを送ったふりをしつつ何回メール送信が呼ばれたかの情報も保持し、それをテストするなど。
Spies are stubs that also record some information based on how they were called. One form of this might be an email service that records how many messages it was sent.
Fake:フェイク
フェイクはテストのためのワークアラウンドの実装を持っているテストダブル。例えば、実際はDBとやり取りを行う処理だが代わりに内部にHashMapなどを持っていてインメモリDBのように振る舞うなど。
Fake objects actually have working implementations, but usually take some shortcut which makes them not suitable for production (an in memory database is a good example).
Dummy:ダミー
ダミーは実引数として渡される何もしない。ただ引数を埋めるために使われるテストダブル。
Dummy objects are passed around but never actually used. Usually they are just used to fill parameter lists.
正直なところ、似ているものがあってややこしいという印象を持ちました。なので t_wada さんのツイートからも参照されている goyoki さんの記事を見てみました。
Mocks と Spies の違い
なおMock ObjectとTest Spyは両方とも間接出力を検証するためのTest Doubleです。ただ「Mock ObjectはMock Object内で間接出力結果を評価する」のに対し、「Test Spyは間接出力を保持するだけで、間接出力結果の評価は後からテストコード上で行う」という違いがあります。
この文章を踏まえた上で、さらに Mockito の @Spy の例を見ると完全に理解できました。
@Spy
List<String> spyList = new ArrayList<String>();
@Test
void givenUsingSpyAnnotation_whenSpyingOnList_thenCorrect() {
spyList.add("one");
spyList.add("two");
verify(spyList).add("one");
verify(spyList).add("two");
assertThat(aSpyList).hasSize(2);
}
「add()
が呼ばれた時にその引数が"one"だったか」のテストは、呼び出しに関する情報をスパイの中で保持していないとできないテストです。スパイは情報を保持している、というのが非常によく伝わりました。
ただ、スパイは定義しなかった関数に関してはテスト対象オブジェクトの関数をそのまま呼び出す、などの機能もありましたので、上記の5つのテストダブルの違いを押さえつつも、利用するテストライブラリの動作を確認したほうが良さそうです。
Stabs と Fake の違い
テストの範囲内で本物と同じように動作するTest DoubleはFake Object。
スタブは返す値が事前に定義されていますが、フェイクの場合は簡素化されつつも本物と同じように動作をする、という違いがありそうです。
本物と同じように動作をする、というのは以下の記事を見ると分かりやすかったです。
フェイクはDBの部分が HashMap<String, User>
に置き換わっていますが、呼び出し元のテスト対象クラスからするとDBのように見えていて、どのような User をフェイクに渡してもうまく動きます。
スタブは事前定義された値しか返さないので、スタブ化したオブジェクトがどんな処理を行うかには一切興味がなく、特定の値を返す動きだけをして欲しいというわけです。そのため、スタブではなくフェイクを使うということは、フェイクに値を返す以上の何らかを期待していることが多いと思います。
また、スタブはライブラリの機能を使うことができるので関数の置き換えコストが低いのに対し、フェイクは置き換えのためにインターフェースをすべて満たすように独自実装したうえでオブジェクトを生成しないといけないので、実装コストが大きくなります。
どれを使うべきなのか?
すみません見出しを作っていますけど書けないです。
将来書くぞという意味を込めて置いておきます。
終わりに
今日を境に語彙力が5倍になりました。(モック -> モック、スタブ、スパイ、フェイク、ダミー)
用語を正しく使うのは意思疎通をスムーズに行うために重要なので使い分けていこうと思いつつ、テストを書く際にどれを使えば良いのかについての知見を次はどんどん貯めていきたいと思います。
Discussion