Googleのソフトウェアエンジニアリング(品質に関する部分)を読んだメモ・感想/13章 テストダブル
こんにちは。kubopです。
QA活動をしていく中で、Googleのソフトウェアエンジニアリングの11章〜14章を読みました。読書メモを書いていたのですが、忘れないよう、思い出しながら感想や、まとめを書きます。
※ まだまだ理解しきれていない部分があるため、解釈が異なる点があるかもしれません。
前回はこちら
今回は13章、テストダブルについて
テストダブル?
ユニットテストは単純なコード向けに書く分には簡単なこともあるが、コードの複雑性が増すと書くのが難しくなっていく。
例えば、外部サーバへのリクエストを送信後、レスポンスをDBに保存する関数…など。
それらはランダムなネットワーク障害や、別のテストデータを上書きするなどといった複雑性により信頼不能なテストになりかねない。
そういった場合にはテストダブルが有用になる。
※SUT=System Under Test(「テストしているものなら何でも」の略
つまり、インターフェースの振る舞いを模したクラスを代わりに作成し、テストに利用する。
テストダブルの利用は、モッキング(mocking)とい呼ばれることがあるが、この用語はテストダブルのさらに個別具体的な側面を指すため、あえてテストダブルとは分けて利用する。
小テストは大規模開発において占めるべき割合を多くするべきと前章に書いてあったが、複数プロセス、またマシン間通信の実装がある場合は小テストに課される制約にはおさまらないことが多い。
テストダブルのソフトウェア開発への影響
テストダブルでは、以下の効果がトレードオフになる。
テスト可能性
テストダブルを利用するにあたって、コードベースがテスト可能なように設計されていなければならない。すなわち、本番コードはテストダブルと取り替えられるようになっているべき。 -> リスコフ置換の原則と近い?
そのため、コードベースがテストを念頭に設計されていない場合は、テストダブル利用に際してリファクタリングを要される。
応用性
テストダブルを不適切に利用すると、テストが脆く、複雑で、効果が劣るものになり得る。
忠実性
テストダブルの挙動を本物の実装に近づけると、本物の実装自体に変更がされた時に追従できないことがある。
つまり、テストダブルを利用する場合には、テスト可用性が十分に配慮された本番コードであり、適切な利用方法で、本番の挙動とほぼ同一のインターフェースを提供しなければならない。
Googleのテストダブル
モッキングフレームワークを利用するのには、危険が伴う。
それらのテストは書くのは簡単な一方でバグをあまり発見せず、保守のための労力が定常的に必要となる。
テストダブルの基礎概念
シーム(継ぎ目)
コードはそのコード向けにユニットテストをかけるような形式で書かれている場合に、テスト可能であるといわれる。
→シームについては1記事書けそう。
DI(Dependency Injection)依存関係の注入は、シームを導入する一般的なテクニック。
テスト可能なコードを書くには、先行投資が必要になる。
テスト可能性は、考慮されるのが後になるほど、コードベースへの適用が困難となる。
モッキングフレームワーク
前述したモッキングフレームワーク。
テストダブルをテスト内部で用意に作成してくれるソフトウェアライブラリーのことであり、オブジェクトをモックとして置き換えられるようにする。
モックとは、その挙動がテスト内部で、その場で指定されるテストダブルである。
テストダブル利用テクニック
フェイキング
スタビング
インタラクションテスト
フェイキング
fake(偽物)は、本番環境に適さないが、本物実装のように振舞うAPIの軽量実装のこと。
例えばメモリ内データベース。
テストダブルを使わなければならない場合、フェイクを利用するのが理想的である。しかしながら、フェイクを書く場合は本物同様の挙動を持つ事を担保し続けなければならない。
フェイクは本物の実装同様に振舞うために、他のテストダブル利用テクニックより最良の選択になる。本物の実装そのものと置換可能なフェイクを利用することが良い。
フェイクは、その忠実性(どの程度本物の実装に近づけるか)が重要になる。そのため、そのフェイク自体にテストが必要。このようなテストを契約テストという。
スタビング
stubbing(スタブ適用)は、挙動を与えられなければそれ自体では何も挙動をもたないような関数に挙動を与えるプロセス。
つまり、関数が返すべき正確な値を関数に対して指定する。
(when リンゴ… and_return 赤い 的な事を予め渡すこと)
スタビングを利用しすぎると、スタブ化される関数の挙動を定義するためのコードを余分に書く。それはテストの意図が不明確になる
スタビングは適していない場合は、スタブされている関数の理由を理解するために本番コードを頭の中で考えなくてはならなくなった時。
また、本番コードの実装詳細をテストコードが知ってしまい、コードの振る舞いが変更する場合はテストの変更も必要になる。(本来はAPIの変更にのみ反応するべき。
以下のようなハードコードされているテストは本番コードと同じように振る舞うかを保証しない。
when(stubCalculator.add(1, 2)).thenReturn(3)
スタビングが適切なのは、テスト対象システムをある状態に遷移させるために関数が特定の値を返さなければならない時。
例えば、1+2="3"である場合に、trueとなるオブジェクトがあり、それをテストしたい時など?(認識間違っているかも)
インタラクションテスト
インタラクションテスト(相互作用テスト)は、関数がどのように呼び出されるかを実際にその関数を呼び出す事なしに検証すること。
インタラクションテストを使い過ぎているテストを、冗談ぽく変更検知器(change-detector)と呼んだりする。
SUTが依存関係の対象に対して関数呼び出しを行う場合は2つのカテゴリーのいずれかに属する。インタラクションテストは、状態変更型に対して行うべき。
状態変更型
SUT外へ副作用がある関数(sendEmail, saveRecord
非状態変更型
副作用のない関数(findResult, getUser
モック?スタブ?フェイク?
モックやスタブやフェイクで混乱してきたので、整理。
本物の実装
本番向けコードで利用されているのと同じ実装を利用すると、テストの忠実性は高まる。
Googleでは、モッキングフレームワークを使いすぎると、本物の実装と同期が取れなくなってリファクタリングが難しくなる。
古典的テスト classical testing
テスト内で本物の実装を優先すること。
モック主義者テスト mockist testing
本物の実装の代わりにモッキングフレームワークを利用すること
これらのテストでは、テスト対象システムを設計する際に厳格なガイドラインに従うことが求められる。(面倒くさそうだし、既存システムへの適用は難しそう。
ユニットテストがテストダブルに依存しすぎると、同じレベルの信頼を得るにはテストダブルが本物の実装と同期しているか、正しいかを検証するしかなくなってしまう。
テストダブルではなく、本物の実装を使うべき部分
本物の実装が優先されるのは、それ自体が高速で、決定性で、持っている依存関係が単純である場合。
例えば、値オブジェクト(DDD
金額や日付、地理的所在地、リストやマップなどのコレクションクラス。
結論・まとめ・思ったこと
テストダブルは、誤用するとテストを不明確で脆く、効果を低くすることがある。
かなり気軽に利用していた気がするので、しっかり意識して利用しないとなぁ…