テストコードにモックやスタブ利用時のポイントと注意点

はじめに

テスト対象の依存関係が複雑になると

テストコードではモックやスタブを利用します。




しかし、

これらを過剰に利用してしまうと、

テストコードが実装の詳細と結びつき

リファクタリングへの耐性を失うことになります。




今回はモックやスタブ利用時の注意点や、

利用すべきケースと利用すべきでないケース

についてまとめました。




モックとスタブの違い

プロダクションコードには含まれず、

テストでしか使われない偽りの依存として表現される全てのものを

包括的にテストダブルと呼びます。




テストの際に使われる依存を用意することや維持するのが難しい場合、

(例えば外部システムとのシステム間連携部分など)

実際の依存のかわりにテストダブルをテスト対象に使わせることで、

テストを容易に行えるようにします。




テストダブルには

大きくモックとスタブに分類できます。


モック

テスト対象から外部に向かうコミュニケーション(出力)を模倣し、

検証するのに使われます。

(例えばメールの送信部分をモックにするなどがあります)


スタブ

依存からテスト対象に向かうコミュニケーション(入力)を模倣するのに使われます。

(例えばデータベースからのデータ取得をスタブにするなどがあります)


モックとスタブ




モックとスタブ利用時の注意点

モックとスタブの違いについてここまで見てきましたが、

ここからはそれぞれの利用時のポイントや注意点について見ていきます。


勝手に変更できない依存関係との連携のみをモックする

テスト対象が依存しているオブジェクト(協力者オブジェクト)を

全て、もしくはテキトーにモックしてしまうと、

テストケースがリファクタリングへの耐性を失います。




ではどこをモックすれば良いかというと・・・

その依存との連携をテスト対象の都合で変更しても問題ない箇所をモックします。




テスト対象の都合で、

リファクタリング時にとある依存との連携部分を変更可能ということは、

実装の詳細として扱えるということで、

変更不可能ということは、

テスト対象の外から見た振る舞いとして守るべき契約となります。




実装の詳細をモック・スタブしたり検証し始めると、

リファクタリングへの耐性を失ってしまうので、

依存関係とのやり取りに着目します。




例えば以下のようなテストコードとテスト対象、

テスト対象の依存関係があったとすると・・・


依存関係いろいろ


例えばテスト対象は、ドメインモデルのクライアントになりますが、

内部のコントロールが可能です。

リファクタリングをしても誰にも迷惑はかかりません。




一方で、メールサービス連携をしているアプリケーションサービスが、

勝手にメールサービス呼び出しのリクエストパラメータを変更するような

リファクタリングをしてしまったら??

また、外部サービス呼び出し時のリクエストパラメータを勝手に変更すると??

もちろんそのようなことをしてはダメですね。




こんな感じで、

勝手に変更しても良いかどうかの観点で整理すると、

以下は勝手に変更するのが許されないことがわかります。

  • 外部サービス
  • メールサービス
  • DB(外部サービス公開)

で、これらをモックします。

これらはテスト対象の外から見た振る舞いであり、

テスト対象が動作した際に守るべき契約事項だからです。

これらとのやり取りは後方互換を保つ必要があり、

そこをモックして後方互換が保たれていることを検証します。




一方で、それ以外のドメインモデルとの連携部分や、

内部でしか利用しないデータベースとのやり取りはモックしません。

これらはリファクタリング時に変更しても、

テスト対象を外から見た振る舞いが変わらなければそれで問題にはならないからです。

(データベースを変更するとすでにリリース済みでデータ登録されていると・・・ってのは置いておきます)

これは、つまり実装の詳細になり、ここをモックしたり検証すると

リファクタリングへの耐性が失われ、テストコードの価値が下がってしまうので、

モックしてはいけません。


スタブで検証はしない!!

スタブの部分は、テスト対象の最終的な結果ではなく

最終結果を出すまでの途中経過であることが多いのが理由です。




途中経過(実装の詳細)を検証すると、

価値の高いテストが備えるべき、

リファクタリングへの耐性を備えさせることができません。




これを過剰検証と呼び、

テスト対象とその依存とのコミュニケーションを検証するが最も起きやすいので注意が必要です。


モックやスタブ利用時にテスト対象の設計ミスを察知する

コマンドクエリ分離(CQS)の原則が守れていない例

まずコマンドクエリ分離(CQS)の原則では、

全てのメソッドはコマンドかクエリのどちらかになるべきで、

両方の性質を持つべきではないということを提唱しています。




コマンドとは戻り値がなく副作用を起こすメソッドで、

クエリとは副作用がなく戻り値のみを返すメソッドのことで、

このような分離を明確に行うことでコードが読みやすくなります。




このコマンドクエリ分離の原則に従った場合、

コマンドの代わりとして使われるテスト・ダブルがモックであり、

クエリの代わりとして使われるテスト・ダブルがスタブになります。




もし、モックを作ろうとしたのに、

モック兼スタブになった。という場合は、

テスト対象とモック対象のコミュニケーション部分が

コマンドクエリ分離の原則に違反していることを疑い、

設計を見直しましょう。




決して、テストコードだけでその問題を回避しようとせず、

根本的なテスト対象の設計ミスを修正しましょう!!


単一責任の原則が守られていない例

次に、単一責任の原則では、

テスト対象のコードは

プロセス外依存とのやり取りを行うか、

ビジネス的に重要で複雑なロジックを取り扱うか、

どちらかになります。




この原則に従えば、

ドメインモデルの層とコントローラの層に自然と分かれ、

ドメインモデルに対する検証は単体テストで行われ、

コントローラに対する検証は統合テストで行われることになります。




モックに置き換えるのは管理下にない依存だけなので、

そのような依存を扱うのはコントローラだけになります。

つまり単一責任の原則にテスト対象が従っていれば

モックを使うのはコントローラのテストのみ(つまり統合テスト)になります。




しかし、単一責任の原則にテスト対象が従っていなければ、

単体テストの中でモックが使いたくなったり、

統合テストが複雑になったりします。




このようにテスト対象が悪い設計になっている場合、

(正しく価値の高い)テストコードを書こうとすると、

テスト対象の設計の歪な箇所を検知できるケースがあるので、

それを見逃さないようにしましょう。




モックの対象は自身のプロジェクトが所有する型のみ

モックを作成する際、

サードパーティ製のライブラリが提供するものを直接モックに置き換えるのではなく、

そのライブラリに対するアダプタを独自に作成し、

そのアダプタに対してモックを作成します。




理由は、

サードパーティ製のライブラリが

実際にどのように機能しているのかを深く知ることは滅多にないからです。




サードパーティ製のインタフェースをモックに置き換える対象にすると、

モックの振る舞いとサードパーティ製のライブラリの実際の振る舞いを

一致する保証があり、それはリスクです。




アダプタを挟むことで、

サードパーティ製のライブライに含まれる技術的な詳細を隠蔽でき、

自身のアプリケーションの用語を用いてライブラリとの関係を定義できるようになります。




例えば、ライブラリのバージョンを上げた際、

どのようにライブラリのコードが変更になるのかは予想できません。

もし、ライブラリのコードに変更があると、

自身のコードが全体的に影響を受けてしまう可能性が出てきます。




しかし、

このようにアダプタを挟んで抽象化層を追加しておけば

変更による影響をそのアダプタだけに抑えられるようになります。




モックとテストの壊れやすさとの関係

モックを利用することが有効なケースと、

モックを利用することが

テストの壊れやすさに繋がってしまうケースについてそれぞれみていきます。




モックの利用が有効なケース

テスト対象とその依存とのコミュニケーションは、

システム内コミュニケーションと

システム間コミュニケーションに分類できます。




システム内コミュニケーションは実装の詳細になります。

例えばコントローラーを利用するクライアントから見て、

コントローラーがドメインクラスを利用して1つのユースケースを実現する場合、

コントローラーとドメインクラスとのコミュニケーションは

システム内コミュニケーションかつ実装の詳細であり、

観察可能な振る舞いではありません。




一方で、

システム間コミュニケーションは観察可能な振る舞いになります。

例えばコントローラーを利用するクライアントから見て、

コントローラーが外部システムと連携して1つのユースケースを実現する場合、

コントローラーと外部システムとのコミュニケーションは、

システム間コミュニケーションかつ、観察可能な振る舞いの1つであり、

コントローラーが常に遵守しなければならない契約の一部になります。




システム内コミュニケーションの場合、

リファクタリングをして、

コントローラーとドメインクラスとのコミュニケーションが

変更になる、もしくはなくなったとしても、

コントローラーのクライアントからすると何も問題はありません。

(実装の詳細が変わってもクライアントから見た振る舞いが変わらないため)




そのため、

システム内コミュニケーションに対してはモックを利用しません。

実装の詳細と強く結びついてしまい、リファクタリングへの耐性が低くなるのが理由です。




一方、

システム間コミュニケーションの場合、

リファクタリングをして、

コントローラーと外部システムとのコミュニケーションが

勝手に変更になったら問題です。

(クライアントからみた振る舞いが変わるため)




例えば、メッセージバスに送信するメッセージの構造は

リファクタリング前後で同じである必要があり、

この部分に後方互換が必要になるのをイメージするとわかりやすいと思います。




そのため、

システム間コミュニケーションに対してはモックを利用します。

システム間の契約を違反しておらず、後方互換を保っていることを担保する必要があるためです。




おまけ

今回は話をシンプルにするために省略しましたが、

システム内コミュニケーションはさらに、

プロセス外依存とプロセス内依存に分類できます。

プロセス外依存の典型例としてはデータベースがあります。



このデータベースに対してはモックをすべきでしょうか??

詳細は別記事にまとめる予定ですが、

これは状況によります。

データベースを外部システムに公開している場合は、

システム間コミュニケーションと同じくモックすべきです。

一方で、

データベースは内部でしか利用しない場合は、

システム内コミュニケーションと同じくモックすべきではありません。



このように、

テスト対象の依存関係を正確に分類することは、

質の高いテストコードを書くためには必要です。


参考文献

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください