モッククラスを使うべきか否か
モッククラスを使うべきか否か、というネタを拾ったので書いてみます。これまでにモックについて殆ど書いてないことに気づいて驚きつつ。
Short Answer
「モックに何をさせたいの?」
質問に質問で返すなという感じですが、モックに何をさせたいか答えてみれば、モックを使うべきか否かはだいたい決まるよなーと思うのです。
モックは道具で、使うことで何かを改善するものです。 「このことをテストしたいから、モックをこう使う。」という文脈なく使用すると、無用な複雑性を作り込むことになります。 モックの使用は負債なので、十二分に利益を見込める場合にのみ使用するものだと思っています。 これはモックに限った話ではなく、何かを便利にするために道具を追加する際は必ず付いて回るトレードオフです。
- Short Answer
- 露払い
- 使うべきか否か
- 捨てることを考える
- 実体を使用するリスク?
- 「モックに何をさせたいの?」
- モックというかテストダブルについて(宣伝)
- で、使うべきなの?
- 追記: 上位のテストで賄うようにしていくべきか?
露払い
うだうだ書く前に露払いしておきます。読み飛ばし可。
モック「クラス」なので、モックライブラリによって生成されるものや、テストのためだけに作成されたクラスを指すものに限定します。モックネタは MockMVC
とか MockRestServiceServer
とかについても書きたいと思ってるのだけど、クラスのモックだけで結構になったので別枠にします。=> 書いた。どこでモックを使おうか
元ネタの「単体テストの際」とありますが、単体って部分は無視します。と言うのも単体何ぞやって話もありますが、それはこの話の主要な要素ではないと思っているので。
モックの対となるのは実体と呼びます。元ネタからの採用。
例えば以下のように Hoge
と Fuga
があるとします。
@Component class Hoge { Fuga fuga; Hoge(Fuga fuga) { this.fuga = fuga; } } @Component class Fuga { }
ここでテストメソッドは次のように書かれているとします。
Fuga fuga = new Fuga(); Hoge sut = new Hoge(fuga);
この fuga
は実体でしょうか?実体っぽく見えますが、実は怪しいところです。Fuga
にプロパティがあるとどんどん怪しくなっていきます。
// 実体? Fuga dummyFuga = new Fuga("dummy"); // 実体?? Fuga fugaEx = new Fuga() { // ...何かオーバーライド };
dummyFuga
はぎりぎり実体っぽいですが、 fugaEx
はかなり怪しく見えます。
個人的にはテストコードでインスタンス化しているものは全部テストダブルと言いたい気分なんですが、そんな事言うと sut
すら怪しくなるし、テストしたいものに影響を与えなければどうでもいい話です。
なので本稿では前段の線引きとしておき、 dummyFuga
は実体で fugaEx
はモックです。
と言うことで、露払い終わり。
使うべきか否か
さて本題。モックを使うべき状況を考えていきましょう。
どちらでも差がない: 実体
モックを使うべきではありません。
モックを用意するコスト、モックのメンテナンスのコストが単純に増加します。 負債を負ったのに利益が増加しない状態です。
メンテナンスのコストはモックを準備するロジック以外にも、モックライブラリを使用する場合はその設定や他の使用箇所との兼ね合い、なぜそれを使用しているかを思い出したりキャッチアップする時間など、多岐に渡ります。これらは使用しなければ不要なコストです。
どちらでも差がない場合は、常に実体を使用します。
実体を使用すると時間がかかる: モック
モックを使用します。
許容できる時間は様々でしょうが、この文脈(テストコードのレベルでモックを使うかどうかを選択する)であればおそらく0.01秒とかで存分に遅いです。 モックを使うことでテスト実行時間を短縮し、得られる時間効率やフィードバックの速さを活かすことで負債の利息を相殺します。 つまり、この状況においては テストの実行時間が短縮されることに利益がないならば使うべきではありません。
モックも遅い?早いモックを作るのです。
遅くても実体使ったテストが必要?モックの準備が大変?おそらく構造に問題があります。
実体を使用するのが非常に手間である: モック
インスタンス生成のコード記述や設定が大変なクラスの場合、モックを使用します。
巨大なオブジェクトグラフを構成するようなインスタンスの生成はセットアップコードが膨れ上がってノイズになります。それくらいであればテスト用のユーティリティでなんとかなるかもしれませんが、環境への設定が必要になってきたりすると厄介ですし、テストの並列性を損なったりもするかもしれません。
実体をテストで使うのが手間な場合、設計に問題がある可能性も考えられたりします。 膨れ上がったセットアップのコードはもしかしたらテストではなく本体が持つべきものだったりとか、案外こういうところから気づけたりします。 本体で手間が解消されたら、この理由でモックを使う必要はなくなります。
依存コンポーネントへのインタラクションをテストしたい: モック
インタラクションの結果を知る手段があれば、実体を使用します。 結果を知る手段がないのであればモックを使用するしかありません。
とはいえ結果を知る手段があれば「インタラクションをテストしたい」なんて言い出さないので、だいたい後者でしょう。
ここで注意が必要なのは、「それほんとにテストする必要あるの?無理にやろうとしてない?」と問いかけたくなるようなテストになる傾向が強いことです。 テストできる手段を持つとなんでもテストしたくなりますが、インタラクションのテストは無意味だったり、意味的に重複したりしがちです。 ゴールデンハンマー症候群であり、作れば作るほどテストのメンテナンスコストが上がっちゃうパターンです。 「これもテストするほうが方がいいでしょ?」と純粋な目で言われても、困る。
外部サービスの呼び出しなどに使用するのは適切ですが、そうでないのであれば結合のテストを別途行う必要が出てくる事に注意してください。 「呼び出しの順番や回数、引数が合っている。」が示されたところで、自分でやった呼び出しが自分の思っているようにできていることを示しているに過ぎません。価値がないとは言いませんが、実体が自分の手の届くものであれば、過度の部品化が疑われます。
起こりづらい状況をテストしたい: モック
例外の発生や超時間のブロックなど、実体を使っていると通常操作では起こらない状況をテストしたい場合です。
モックを使用せずにこの状況を起こすのは非常にコストがかかるため、モックの使用を推奨するものです。 実体でも起こせなくはない状況を低コストで引き起こすと言うのがポイントです。 起こらない状況を起こすために使うのは杞憂です。まあ何をもって「起こらない」と断じるかとかは面倒なので割愛。
依存コンポーネントが存在しないか作成中である: モック
モックを使うしかありません。
依存コンポーネントが手の届くものであれば、モックで牽引して作り上げる方法も取れます。
手を出せないものであれば、出来上がるまで待ってもいいけど、プロジェクトの状況次第ですかねー。まあ出来上がってきたら別物とか、あるある。仕様を決めれないというのは人類の欠陥だから諦めて人類らしいアプローチをとるのがいいと思うんだ。
拡張を前提としている: モック
フレームワークやライブラリを作成しており、拡張を前提としているものはモックにせざるを得ません。標準の実装はあるかもしれないけれど、それだけだと不十分なはずなので。
実体が存在しないわけだから、契約の振る舞いをモックで作る感じ。 そうある状況じゃないし、この状況に遭遇する人は迷わないと思う。
依存コンポーネントの戻り値の制御が困難である: モック
モックを使用することで戻り値を制御下に置きます。Mockitoを使用する場合、テストメソッドごとにthenReturn
が違ったり、Answer
を作り込むのがパターンになります。
実体の戻り値が引数だけで定まるのであれば、実体を使用するのが良いでしょうが、レガシーコードに相対する場合などは依存コンポーネントの引数を制御するのすら困難だったりしますし、依存コンポーネントの戻り値がその引数だけでは定まらない場合などもあり、これらのコントロールを始めると泥沼を突き進む事になったりします。
モックを使用することでテストしたいことに集中したい好例ではありますが、その設計を見直すスメルでもあります。
実体を使用する副作用が許容できない: モック
これに足すことがあるとすれば、実態を使うとシステムに対する副作用がデカすぎる場合。特にエンティティ周り。 https://twitter.com/ishisaka/status/1151506206575353856
副作用がDBなどトランザクション制御されているものであればロールバックをすればいいかもしれません。でもできないこともあると思います。外部APIではAPIの呼び出し制限があることもあります。メールが飛んでしまったりするかもしれません。う、トラウマが……。
このパターンにはモックがよく刺さりますが、モッククラスかと言われると疑問があります。例えばDBであればH2などのインメモリDBを使用するなど、クラスレベルではモックを使用しない解決にすることもよくあります。この辺りは一大テーマですので、以下に別に書いています。
集計
実体 : モック = 1 : 8となりました!……そんな話ではない。
騙されないとは思いますが、集計なんてしちゃいけないものです。そもそも足すものじゃないですし、比較も成立しません。
要するに、よほどの事情がない限りは実体を使います。よほどの事情が先に挙げたものです。使うべき場面を見定めて使います。
たとえばRepositoryに依存するServiceのロジックをテストするとして。Repositoryをモックにするかどうかは「実体を使用すると時間がかかる」「実体を使用するのが非常に手間である」「依存コンポーネントの戻り値の制御が困難である」あたりを考慮します。おそらく「Serviceのロジック」なんてものをテストしたくなってるんだから、多分DBに関わる諸々は時間も手間も見合わないでしょう。きっとモックが妥当です。ただ「Serviceのロジック」をテストするわけではなく、「Serviceのテスト」であれば実体でやるようになったりします。 DBアクセスに伴う例外をServiceで処理しているならば「起こりづらい状況を引き起こしたい」でモックを使いますが、Serviceが例外処理していないのであればこれは対象外です。
捨てることを考える
モックは使い慣れてくると軽い気持ちで使いがちです。軽い気持ちでどんどん作っていいと思います。ただし捨てられるならば。
テストが充実してくると、実体を使用するテストやより上位の包括したテストで賄えるようになってきたりもします。 モックのテストは単純にメンテナンスコストがかかるだけの無駄なものになります。適切に捨てましょう。
「同じことをテストしているか?」は、テストが同じ原因で落ちるかどうかでわかります。ミューテーションテストとかやります。 モックを使用するテストだけを落とす条件があれば、そのテストはまだ現役です(モックの作りのせいでそれが起こっているならただのゴミですが)。一方で必ず他のテストとセットで落ちるのであれば、そのテストはもはや不要かもしれません。
他のテストで検出できたとしても、残すべきかもしれません。 他のテストよりも高速であれば先に実行すればfail fastに使えますし、他のテストよりもテスト失敗メッセージが充実していれば修正をスムーズに行えます。 このテストもう要らないのでは?と思っても、落とせばわかります。
不要になったテストは消しましょう。適切に消すのは結構難しいけれど、消せるようにならないと健全なテストは維持できないです。これはモックに限らず。
実体を使用するリスク?
実体を使うと、実体の変更の煽りを受けてテストが落ちることがあります。モックを使っていたら落ちないでしょう。
でも、これは大抵の場合は落ちるべきでしょう。
影響しないと思っていた依存コンポーネントの変更が影響したってテストがちゃんと機能してる証拠だし、入力に依存する振る舞いが変わったのであれば、テスト変えずに通る方がおかしい。 入力に依存しない振る舞いが変わったのであれば、単純に見直す機会。「実体を使用するのが非常に手間である」行きかもしれない。
下手にモックにしてて影響が検知できない方がまずいんじゃないかな。
「モックに何をさせたいの?」
モックはテストしたいこと以外に煩わされないようにするための道具です。
モックを使うことで容易にテストができそうに思えます。しかしその容易さのために余計な複雑さを持ち込んでないでしょうか。モックを使うという判断をするのであれば、捨てる準備もしておくのが良いと思います。心の準備でもいいです。
モックによりテストを容易にする必要があるものの中には、多くの組み合わせによるテストが必要なものが挙げられます。これは組み合わせは入力に依存するからであり、モックは依存コンポーネントからの入力(要するにモックメソッドの戻り値です)を制御できるからです。ですが、その組み合わせを擁するロジックを独立してテストできるように設計すれば、この目的でのモックは不要になります。モックを必要とする設計を見直しつつ、必要な時に必要なだけ使うのがいいかなと思うわけですよ。
モックというかテストダブルについて(宣伝)
あ、なんかモック否定派みたいに見えたかもしれませんが、モック好きです。記事書くくらいには。
1年半前になりますが、WEB+DB PRESS Vol.103の「Mockitoによるテストダブル入門」で書きました。
特集の「脆弱性の見つけ方&ふさぎ方」はホットですね?
記事の半分は陳腐化しないテストダブルの話で、読んでいただければ私のスタンスもわかるかと思います。 Mockitoなどコード部分は流石に古いですが、そこは適宜アップデートしてください。本稿を書きながらMockito3がリリースされてることに気づきました。数日前だからセーフ。たぶん別物レベルでの差はないです。
「テストダブル」の元は XUnit Test Patterns ですので、そちら当たるのもいいと思います。
で、使うべきなの?
条件設定無しで二択を迫るのであれば、使うべきではありません。
繰り返しになりますが、使うべき条件があり、それに合致するのであれば使うべきです。 そして要らなくなったら捨て、必要になったらまた使う。そういうものなんじゃないかな。
追記: 上位のテストで賄うようにしていくべきか?
より上位の呼び出し元のテストで包括できる、と言ってしていくと呼び出し元のテストが肥大化していって見通しが悪くなったりはしないか https://twitter.com/haiiro_shimeji/status/1151625085511319552
包括したテストで細粒度のテストを行おうとすると、スローテストをはじめとした様々な問題を引き起こします。 テストしたいこととテストコードの距離が離れて、ノイズが膨大になり、何をテストしているのかわからなくなりがちです。 テストはテストすべきところに寄り添って最小のコストで無理なく最大の成果を得るべきです。
単にテストのエントリーポイントを変えれば済むだけならモック云々は関係ないので割愛するとして、この「肥大化して見通しが悪くなる」状態が「実体を使用するのが非常に手間である」であれば、モックにするべきじゃないでしょうか。 モックにする場合に一つ意識しておいて欲しいのは、モックを使う状態に戻すのではなく、よりよい形のために改めてモックを使う、ということ。戻すってなると、最初から全部モックでいいじゃんという思考停止になりますので。
モックを使用しない上位のテストで包括できるような設計ができていればモックは不要になる(かもしれない)という話であって、上位の包括したテストで細やかな条件をテストするのがよい、ではありません。見た目上のテストケース数やテストのパターンを無理に減らしてもいいことはないのです。
なので、包括しに行ったらダメで。結果として包括できているのがいいと思う。複雑な組み合わせの条件分岐はどこかに閉じ込められて全体としてはシンプルに保てている、というような設計ができているならば、「依存コンポーネントの戻り値の制御が困難である」に適用されるモックは不要になっていく。はず。