見出し画像

Googleのソフトウェアエンジニアリング(品質に関する部分)を読んだメモ・感想/13章 テストダブル

こんにちは。kubopです。

QA活動をしていく中で、Googleのソフトウェアエンジニアリングの11章〜14章を読みました。読書メモを書いていたのですが、忘れないよう、思い出しながら感想や、まとめを書きます。

※ まだまだ理解しきれていない部分があるため、解釈が異なる点があるかもしれません。

前回はこちら

今回は13章、テストダブルについて

テストダブル?

ユニットテストは単純なコード向けに書く分には簡単なこともあるが、コードの複雑性が増すと書くのが難しくなっていく。
例えば、外部サーバへのリクエストを送信後、レスポンスをDBに保存する関数…など。
それらはランダムなネットワーク障害や、別のテストデータを上書きするなどといった複雑性により信頼不能なテストになりかねない。

そういった場合にはテストダブルが有用になる。

ロジックに依存するコードが使えないときに、どのようにロジックを独立に検証するか?遅いテストを避けるには?

SUTが依存するコンポーネントを "テストに特化した同等品 "に置き換える。

実際の依存コンポーネント (DOC) を使用できない (あるいは使用しない) テストを作成する場合、それをテストダブルに置き換えることができます。Test Double は、本物の DOC とまったく同じように振る舞う必要はありません。単に、本物の DOC と同じ API を提供し、SUT がそれを本物だと思うようにしなければならないのです!

映画業界では、主役の俳優が演じるにはリスクが高く危険なものを撮影したい場合、そのシーンで俳優の代わりを務める「スタントダブル」を雇います。スタントダブルは、高度な訓練を受け、そのシーンの特定の条件を満たすことができる人物です。演技はできなくても、高いところから落ちたり、車をぶつけたり、どんなシーンでも対応できるように訓練されているのです。スタントダブルが俳優にどれだけ似ている必要があるかは、シーンの性質によります。通常は、俳優と同じような体格の人が代役を務めることができます。

http://xunitpatterns.com/Test%20Double.html

※SUT=System Under Test(「テストしているものなら何でも」の略

つまり、インターフェースの振る舞いを模したクラスを代わりに作成し、テストに利用する。

テストダブルの利用は、モッキング(mocking)とい呼ばれることがあるが、この用語はテストダブルのさらに個別具体的な側面を指すため、あえてテストダブルとは分けて利用する。

小テストは大規模開発において占めるべき割合を多くするべきと前章に書いてあったが、複数プロセス、またマシン間通信の実装がある場合は小テストに課される制約にはおさまらないことが多い。

テストダブルのソフトウェア開発への影響

テストダブルでは、以下の効果がトレードオフになる。

  • テスト可能性

    • テストダブルを利用するにあたって、コードベースがテスト可能なように設計されていなければならない。すなわち、本番コードはテストダブルと取り替えられるようになっているべき。 -> リスコフ置換の原則と近い?

    • そのため、コードベースがテストを念頭に設計されていない場合は、テストダブル利用に際してリファクタリングを要される。

  • 応用性

    • テストダブルを不適切に利用すると、テストが脆く、複雑で、効果が劣るものになり得る。

  • 忠実性

    • テストダブルの挙動を本物の実装に近づけると、本物の実装自体に変更がされた時に追従できないことがある。

つまり、テストダブルを利用する場合には、テスト可用性が十分に配慮された本番コードであり、適切な利用方法で、本番の挙動とほぼ同一のインターフェースを提供しなければならない。

Googleのテストダブル

モッキングフレームワークを利用するのには、危険が伴う。
それらのテストは書くのは簡単な一方でバグをあまり発見せず、保守のための労力が定常的に必要となる。

テストダブルの基礎概念

シーム(継ぎ目)

コードはそのコード向けにユニットテストをかけるような形式で書かれている場合に、テスト可能であるといわれる。

テストを書くためのコツはありません。あるのはテスト可能なコードを書くためのコツだけです。

https://testing.googleblog.com/2008/08/by-miko-hevery-so-you-decided-to.html

→シームについては1記事書けそう。

シーム(継ぎ目
シームとは、その場所で編集することなく、プログラムの動作を変更できる場所のことです。
シームがあるということは、振る舞いを変更できる場所があるということです。テストするためだけに、その場所に行ってコードを変更するわけにはいきません。

レガシーコードをテストする際の最大の難関のひとつは、依存関係を断ち切ることです。幸運な場合、依存関係は小さく局所的ですが、病的な場合、依存関係は多く、コードベース全体に広がっています。ソフトウェアのシームビューは、コードベースに既に存在する機会を見るのに役立ちます。

もし、継ぎ目で動作を置き換えることができれば、テストにおいて依存関係を選択的に除外することができます。また、コード内の条件を感知し、その条件に対してテストを書きたい場合は、その依存関係があった場所で他のコードを実行することができます。多くの場合、この作業によって、より積極的な作業をサポートするのに十分なだけのテストを準備することができます。

https://www.informit.com/articles/article.aspx?p=359417&seqNum=2

DI(Dependency Injection)依存関係の注入は、シームを導入する一般的なテクニック。
テスト可能なコードを書くには、先行投資が必要になる。
テスト可能性は、考慮されるのが後になるほど、コードベースへの適用が困難となる。

モッキングフレームワーク

前述したモッキングフレームワーク。
テストダブルをテスト内部で用意に作成してくれるソフトウェアライブラリーのことであり、オブジェクトをモックとして置き換えられるようにする。

モックとは、その挙動がテスト内部で、その場で指定されるテストダブルである。

テストダブル利用テクニック

  1. フェイキング

  2. スタビング

  3. インタラクションテスト

フェイキング

fake(偽物)は、本番環境に適さないが、本物実装のように振舞うAPIの軽量実装のこと。
例えばメモリ内データベース。
テストダブルを使わなければならない場合、フェイクを利用するのが理想的である。しかしながら、フェイクを書く場合は本物同様の挙動を持つ事を担保し続けなければならない。

フェイクは本物の実装同様に振舞うために、他のテストダブル利用テクニックより最良の選択になる。本物の実装そのものと置換可能なフェイクを利用することが良い。

フェイクは、その忠実性(どの程度本物の実装に近づけるか)が重要になる。そのため、そのフェイク自体にテストが必要。このようなテストを契約テストという。

スタビング

stubbing(スタブ適用)は、挙動を与えられなければそれ自体では何も挙動をもたないような関数に挙動を与えるプロセス。
つまり、関数が返すべき正確な値を関数に対して指定する。
(when リンゴ… and_return 赤い 的な事を予め渡すこと)

Sketch Test Stub embedded from Test Stub.gif

テストスタブの仕組み
まず、SUT が依存するインターフェイスのテスト固有の実装を定義する。この実装は、SUT からの呼び出しに応答して、SUT 内の未試験コードを実行するための値(または例外)が設定されます。
SUT を実行する前に、テストスタブをインストールし、SUT が実際の実装の代わりにテストスタブを使用するようにする。
テストの実行中に SUT から呼び出されると、テストスタブは事前に定義された値を返します。その後、テストは通常の方法で期待される結果を検証することができます。

使用するタイミング
テストスタブを使用する主な理由は、SUT の間接的な入力を制御できないことによる未テストコードの発生です。テストスタブをコントロールポイントとして使用することで、様々な間接入力によるSUTの振る舞いを確認することができ、間接出力を検証する必要がなくなります。また、テストスタブを使用して、テスト環境では利用できないソフトウェアを呼び出す際に、そのソフトウェア の特定のポイントを通過するための値を注入することができる。

http://xunitpatterns.com/Test%20Stub.html

スタビングを利用しすぎると、スタブ化される関数の挙動を定義するためのコードを余分に書く。それはテストの意図が不明確になる
スタビングは適していない場合は、スタブされている関数の理由を理解するために本番コードを頭の中で考えなくてはならなくなった時。

また、本番コードの実装詳細をテストコードが知ってしまい、コードの振る舞いが変更する場合はテストの変更も必要になる。(本来はAPIの変更にのみ反応するべき。

以下のようなハードコードされているテストは本番コードと同じように振る舞うかを保証しない。

when(stubCalculator.add(1, 2)).thenReturn(3)

スタビングが適切なのは、テスト対象システムをある状態に遷移させるために関数が特定の値を返さなければならない時
例えば、1+2="3"である場合に、trueとなるオブジェクトがあり、それをテストしたい時など?(認識間違っているかも)

インタラクションテスト

インタラクションテスト(相互作用テスト)は、関数がどのように呼び出されるかを実際にその関数を呼び出す事なしに検証すること。

インタラクションテストを使い過ぎているテストを、冗談ぽく変更検知器(change-detector)と呼んだりする。

SUTが依存関係の対象に対して関数呼び出しを行う場合は2つのカテゴリーのいずれかに属する。インタラクションテストは、状態変更型に対して行うべき。

  • 状態変更型

    • SUT外へ副作用がある関数(sendEmail, saveRecord

  • 非状態変更型

    • 副作用のない関数(findResult, getUser

モック?スタブ?フェイク?

モックやスタブやフェイクで混乱してきたので、整理。

誰かが「テストスタブ」や「モックオブジェクト」と言ったとき、何を意味しているのか混乱することはありませんか?相手が全く違う定義を使っていると感じることはありませんか?それはあなただけではありません。

http://xunitpatterns.com/Mocks,%20Fakes,%20Stubs%20and%20Dummies.html

- Fake を使用するのは、実際の実装に近い形で再利用可能な具象実装をテスト全体で使用したい場合です (例: In Memory Database など)。
- ハードコードされたレスポンスや実装をテスト間で再利用したい場合は スタブ を使用します。
- 個々のテストに対して動的なレスポンスが必要で、 かつテスト間での再利用は必ずしも必要でない場合にモックを使用します。

https://dotnetcoretutorials.com/2021/06/19/mocks-vs-stubs-vs-fakes-in-unit-testing/

本物の実装

本番向けコードで利用されているのと同じ実装を利用すると、テストの忠実性は高まる。

Googleでは、モッキングフレームワークを使いすぎると、本物の実装と同期が取れなくなってリファクタリングが難しくなる。

古典的テスト classical testing

テスト内で本物の実装を優先すること。

モック主義者テスト mockist testing

本物の実装の代わりにモッキングフレームワークを利用すること
これらのテストでは、テスト対象システムを設計する際に厳格なガイドラインに従うことが求められる。(面倒くさそうだし、既存システムへの適用は難しそう。

ユニットテストがテストダブルに依存しすぎると、同じレベルの信頼を得るにはテストダブルが本物の実装と同期しているか、正しいかを検証するしかなくなってしまう。

テストダブルではなく、本物の実装を使うべき部分

本物の実装が優先されるのは、それ自体が高速で、決定性で、持っている依存関係が単純である場合。
例えば、値オブジェクト(DDD
金額や日付、地理的所在地、リストやマップなどのコレクションクラス。

※決定性
テスト結果が、常に同じ結果に至る性質のこと。
テストにおける非決定性は、信頼が失われてしまい、テストが失敗すること自体に関心を持たなくなる。
例えばマルチスレッドを利用したテストでは、実行順で結果が変わってしまう・・・など

結論・まとめ・思ったこと

テストダブルは、誤用するとテストを不明確で脆く、効果を低くすることがある。

かなり気軽に利用していた気がするので、しっかり意識して利用しないとなぁ…

テストダブルより、利用できるなら本物の実装が優先されるべき。

スタビングを利用しすぎると、不明確で脆いテストになる。

インタラクションテストよりは、ステートテストを利用するべき。

テスト対象システムの実装詳細はテストコード自体に漏洩させるべきではなく、APIの振る舞いが変更された時のみテストが壊れるようにする。



この記事が気に入ったらサポートをしてみませんか?