カテゴリー
コンピューター 文化

📖 読書感想文4。13章 テストダブル『Googleのソフトウェアエンジニアリング―持続可能なプログラミングを支える技術、文化、プロセス』Titus Winters、Tom Manshreck、Hyrum Wright 編、竹辺 靖昭 監訳、久富木 隆一 訳 https://amzn.to/3YrMBEn

『単体テストの考え方/使い方』Vladimir Khorikov https://amzn.to/3BCLytq を以前読んだ。

それでテストについて記されている11章12章を読んだ。引き続きテストに関連する13章の、テストダブル、を読もうと思う。2024年12月31日(火)スタートとする。

13章 テストダブル

そのような場合には、テストダブルがおあつらえ向きである。テストダブル (test double) (https://oreil.ly/vbpiU) は映画内でスタントマン (stunt double) が俳優の代役を務めるのと同じように、テスト内で本物の実装の代役を務めることのできるオブジェクトまたは関数である。

ユニットテストのことを学び始めた時は、モックと言う言葉から入ったような思い出がある。また、大事なのはオブジェクトまたは関数のスタントマンであるということだろう。オブジェクトだけではなく、関数。

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

テストダブルを利用すると、多少のトレードオフを要する複雑な問題が、ソフトウェア開発にいくつかもたらされる。 …略… テスト可能性 テストダブルを利用するには、コードベースがテスト可能なように設計されていなければならない。 …略… 応用性 テストダブルを適切に利用すれば、エンジニアリング速度に力強い推進力を加えることができるが、テストダブルを不適切に利用すると、テストが脆く、複雑で、効果の劣るものになる結果に陥りかねない。 …略… 忠実性 忠実性は、テストダブルの挙動を置換対象の本物の実装の挙動にどれだけ近いところまで見せられているかを指す。

テストダブルを使おうとするときに生まれるトレードオフは、テスト可能性、応用性、忠実性。

13.2 Googleでのテストダブル

苦労しながら我々が学んだ教訓に、モッキングフレームワークを使いすぎるのは危険であるというものがある。 …略… 何年か経ち、数え切れない量のテストが実行されて初めて、モッキングフレームワークを使ったテストの代償を我々は悟り始めた。つまり、それらのテストは、書くのは簡単だったとはいえ、バグを滅多に発見しない割に保守のための労力が定常的に必要となることを考え合わせると、我々の受けた損害は大きかった。Google の振り子は現在は別の方向に振れるようになり、多くのエンジニアがより現実的なテストを書く方を優先してモッキングフレームワークの利用は避けている。

テストダブル、モッキングフレームワークはできるだけ使わないようにし、仕方なく使うようにしている。

13.3 基本概念

テストダブルに関連する基本概念をいくつか扱っておこう。

楽しみ。

13.3.1 テストダブルの例

テスト内で本物のクレジットカードサービスを利用する事は実現不能だろう(テスト実行で生ずるまとまった額のトランザクション手数料を思い浮かべてみて欲しい!)。だが、本物のシステムの挙動をシミュレーションするために、代わりにテストダブルを利用できるだろう。

これはテストダブルのわかりやすい良い例だ。

13.3.2 シーム

シーム (seam: 継ぎ目。 https://oreil.ly/pFSFf) は、テストダブルを利用できるようにすることで、コードをテスト可能とする方法である。 …略… ディペンデンシーインジェクション (dependency injection/DI: 依存性の注入。 https://oreil.ly/og9p9) はシームを導入する一般的なテクニックである。

DIは日常的に活用するが、シームはなじみのない言葉だ。だが、シームの一部としてDIが存在するという事は理解できた。

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

Google では Java 向けには Mockito 、C++ 向けには Googletest の googlemock コンポーネント (https://github.com/google/googletest) 、Python 向けには unittest.mock (https://oreil.ly/clzvH) を用いている。

へえ〜。

13.4 テストダブル利用のためのテクニック

テストダブルの利用には、3つの主要なテクニックが存在する。

この3つを使いこなせるようになればかなり強くなれると思う。『単体テストの考え方/使い方』では、第5章 モックの利用とテストの壊れやすさ、で扱われているが、同じ言葉が違う意味で使われていたり、説明の切り口、観点が異なっていたりするので単純な紐付けはやめておこう。

13.4.1 フェイキング

フェイク(fake : 偽物)は、本番環境には適していないが、本物の実装同様に振舞うAPIの軽量な実装である。例えばメモリー内データベースだ。

実装が低速であったり、用意が難しい場合にフェイキングを使う。モッキングフレームワークを使っていなくても、フェイク用のクラスを別途用意しておけば実現できる。

そして、フェイクを書くのは難しい場合があり、それはフェイクが現在ならびに将来において本物の実装同様の挙動を持つことを担保しなければならないためだ。

ここが弱点。

13.4.2 スタビング

スタビング (stubbing : スタブ適用。 https://oreil.ly/gmShS)は、挙動を与えられなければそれ自体では何も挙動を持たないような関数に、挙動を与えるプロセスである。つまり、関数が返すべき正確な値を関数に対して指定するのだ (すなわち、返り値をスタブ [stub] する)。 … 略 … 訳注 : 一般的には、既存の別コードや、インターフェースのみ定義され実装が済んでいないソフトウェアコンポーネントの代役を務めることで依存関係を制御するコードを指すが、テストの文脈では、テストを通すために定義済みの決まったデータを提供するテストダブルを指す。ここでは動詞としてそのようなデータを返すことを意味している。

もしこの関数が呼ばれたら、こういう値を返してくれ、ってやる奴がスタビング。

13.4.3 インタラクションテスト

インタラクションテスト(https://oreil.ly/zGfFn) は、関数がどのように呼び出されるかを、実際にその関数の実装を呼び出すことなしに検証する方法である。関数が正しい方法で呼び出されない場合、テストは失敗すべきだ。例えば、関数が全く呼び出されない場合、呼び出される回数が多すぎる場合、誤った引数で呼び出される場合である。

関数の呼び出されるやり方の検証 ( = インタラクションテスト) であって、呼び出された後の結果を代替するもの ( = スタビング) ではない

13.5 本物の実装

テストダブルは非常に有用なテストツールとなり得るが、テストについての我々の第一の選択は、テスト対象システムの依存関係に本物の実装を利用することだ。

この章は、テストダブルについてだが、使わなくて済むならそれが1番良いと言う結論。

テスト内で本物の実装を優先する事は、古典的テスト (classical testing。 https://oreil.ly/OWw7h) として知られる。モック主義者テスト (mockist testing) として知られるテスト手法のスタイルもあり、このスタイルで優先されるのは、本物の実装の代わりにモッキングフレームワークを使うことである。

『単体テストの考え方/使い方』では古典学派とロンドン学派で説明されている。Googleは古典て学派であり、古典的テストを重視している。

13.5.1 分離より現実に即することを優先せよ

分離 = テストダブル。現実 = 本物の実装。

我々が現実的なテストを優先するのは、テスト対象システムが正常に動作しているという点での信頼をより多くもたらすのが、現実的なテストだからである。

テストダブルを使わないのに越したことのない理由は、テストの信頼性アップ (もしくは下げない) のため。

つまり、テストは実装がどのように構成されているかという観点からではなく、テストされるAPIの観点から書かれるべきだ。

実装の詳細をテストする、のではなく、振る舞いをテストする。

本物の実装を使うと、本物の実装にバグが存在する場合にテストの失敗を招く恐れがある。これは良いことだ!テスト失敗は本番環境でコードが正常動作しないことを示すため、そのような場合にはテストが失敗するのは望むところだ

こういった言葉は勇気をもらえる。しっかりとテストを書いて失敗すべきところでしっかり失敗するのは良いことだ。

ケーススタディ : @DoNotMock

Googleでは、テストがモッキングフレームワークに頼りすぎる例が相当数見られたため、それが Java での @DoNtoMock アノテーションを作成する動機となったほどだった。 …略… 何故 API のオーナーが気にかけるのか。要は、 API オーナーが長期的に API の実装を変更できる能力に、 API のモック化が著しい制約を課すのだ。

モックに引きずられて、本来のAPIがインターフェイスを変更できないことが発生してしまった。モックがAPIの足を引っ張る、こんなことってあるんだな。。。でもなるほど。

13.5.2 いつ本物の実装を使うべきか決める方法

本物の実装が優先されるのは、それが高速で、決定性で、持っている依存関係が単純である場合である。

逆にテスト対象は高速なのはとりあえず置いとくとして、決定性を持たせるように設計し、依存関係を単純にするように設計して、実装するとテストできるようになるといえる。

13.5.2.1 実行時間

結論として、本物の実装が遅い場合は、テストダブルが非常に有用となり得る。 …略… 追加でかかる時間が妥当であるかどうか判断に迷う状況では、遅すぎて使えなくなるまで本物の実装を使うのが比較的簡単な場合が多く、本物の実装が使えなくなったところで代わりにテストダブルを使うようにそのテストを更新すれば良い。

最初からテストダブルを使おうと考るのではなく、最初は本物を使う。その際、後でテストダブルに置き換えられるように作るというところがポイントになる

13.5.2.2 決定性

「テストが決定性 (https://oreil.ly/brxJl) である」というのは、あるバージョンのテスト対象システムに対し、そのテストが常に同じ結果に至る性質を指す。すなわち、そのテストが常に合格するか、常に失敗するかである。対照的に、「テストが非決定性 (https://oreil.ly/5pG0f) である」と言うのは、テスト対象システムが変化しないままである。にもかかわらず、テストの結果が変化することがある性質を指す。

例えるなら、関数型プログラミングのような冪等であるテストが決定性であるテスト。

信頼不能性がテストの健全性を害するのは開発者がテストの結果を疑って失敗を無視し始める場合である。

これは『単体テストの考え方/使い方』にも同様の記述があった。テストが無視されることに繋がるので成功したり失敗したりは駄目。

しかし、信頼不能性が頻繁に発生するなら、本物の実装をテストダブルで置換することでテストの忠実性が改善するので、置換を行うべき頃合いかもしれない。

そこでテストダブルの出番というわけか。

13.5.2.3 依存関係の構築

理想の解法は、テスト内のオブジェクトを手動で構築するのではなく、ファクトリーメソッド (factory method) もしくは自動ディペンデンシーインジェクションのように、本番環境向けコード内で利用されているのと同じオブジェクト構築コードを使うものだ。

テストに必要な部品を生成するのに依存関係が多い場合は、すごくテストを書くのが大変という所に起因している。

13.6 フェイキング

フェイクは本物の日と同様に振る舞うため、他のテストダブルのテクニックより好まれる、つまりテスト対象システムにとって本物の実装とやりとりしているのか、フェイクとやりとりしているのか、判別することさえできない状態であるべきだ。

この状態を作り出すために、ディペンデンシーインジェクションなど、本物またはフェイクを外から入れ替えるための手段があると理解している。

13.6.1 何故フェイクが重要なのか

すなわち、フェイクの実行は高速で、本物の実装を利用する際の欠点なしに効果的にコードをテストできるようにしてくれる。

また、他のテクニックよりも、不明確で脆く効果の低い、といったことがもたらされにくいと言及している。

13.6.2 どんな場合にフェイクが書かれるべきか

フェイクは本物同様に振る舞わなければならないため、作成に比較的多くの労力とドメイン経験とを要する。またフェイクには保守も必要である。 …略… フェイクを書くことをチームが検討しているなら、フェイクの利用から生ずる生産性の改善が、フェイクを書き保守するコストを上回るかどうかに関して、トレードオフを行わなければならない。 …略… 保守が必要なフェイクの数を減らすには、通常、テスト内での利用が現実的ではないコードが持つ呼び出しツリーの最上位ノード (ルート) においてのみフェイクを作成すべきだ。

テストが遅い等でフェイクを検討し始めたとき、ただ単に導入するのではなく、やはりチームでトレードオフがあるということを認識した上で導入することが大事になってくる。

13.6.3 フェイクの忠実性

フェイクの作成をめぐる最も重要な概念は、おそらく忠実性である。 …略… 完全な忠実性は必ずしも実現可能ではない。 …略… しかしまずは、フェイクは本物の実装の持つAPI契約への忠実性を維持すべきである。フェイクは、APIのどんな入力に対しても、そのフェイクに対応する本物の実装と同じ出力を返し、同じ状態変更を実行すべきである。

入出力を揃えると言う事。

また別の場所で記述があるが、想定外のケースでフェイクを利用したしようとした場合は、エラーにする。早々に失敗させるのが最善とあり、これはその通りと思った。

13.6.4 フェイクはテストされるべきである

フェイクは、対応する本物の実装のAPIに準拠していることを担保するために、それ自体のテストを備えなければならない。 …略… フェイク向けにテストを書く場合、APIの公開インターフェースに対してテストを書き、本物の実装とフェイクの双方に対してそのテストを実行することを伴うアプローチがある(このようなテストは契約テスト [https://oreil.ly/yuVlX] として知られている)。

理想だと思ったが、めんどくさいと思った。逆にこういった部分からサボり始め、ほころびが生じ始めるのではないか。

13.6.5 フェイクが利用できない場合はどうすべきか

APIのオーナーがフェイクの作成に乗り気でないかフェイクの作成ができない場合でも、自分用のフェイクを書けるかもしれない。その実行のために、そのAPIの呼び出し全てを単一のクラス内にラップし、それからそのAPIに通信しないフェイク版のクラスを作成すると言う方法がある。

外部APIとのインターフェースとなるのりしろ部分のようなクラスを別途用意しておくということだと思う。何かパターン名があったような気がしたが忘れた。生成AIに聞いたところ、「アダプター(Adapter)パターン」が近そうだった。

13.7 スタビング

スタビングは、テスト内の本物の実装を置き換えるための手っ取り早く簡単な手段であることが多い。

テスト対象の内部の詳細を知らないとできないので、あまり良くないということにつながる。

13.7.1 スタビングを使いすぎることによる危険

しかし、スタビングを使いすぎると、そのようなテストを保守する必要のあるエンジニアの生産性が大きく損われる恐れがある。

生産性が損われるのはテストを書いている時でもなく、保守の段階。もっと言えばおそらくテストが失敗する時であると(思う)いうのがポイント。

13.7.1.1 テストが不明確になる

スタビングのためには、スタブ化される関数の挙動を定義するために、余分なコードを書く必要がある。

それで不明確になる。

13.7.1.2 テストが脆くなる

スタビングは、コードの実装詳細をテスト内に漏洩する。

個人的にはこれが1番問題だと思う。

13.7.1.3 テストの効果が落ちる

スタビングの場合、スタブ化される関数が本物の実装のように振る舞うことを保証する。手立ては存在しない。 …略… さらにスタビングの場合、状態を保存するのは不可能で、そのためにコードの特定の面をテストするのが困難になり得る。

例としても挙げられていたが、スタビングではデータベースに保存ができないので、テスト対象実行後にデータベースに保存されているはずの値を取り出して検証するという事は無理。

13.7.1.4 スタビングを使いすぎている例

テストが短くなり、その実装詳細 (トランザクションプロセッサーがどのように利用されているか等) がテスト内で公開されてない様子に注目してほしい。

この文章の前に駄目な例があり、この文章の後に良い例がある。駄目な場合だとコードが長いという事と、実装詳細に踏み込みすぎということが駄目だと示唆している。

13.7.2 スタビングが適切なのはどんな場合か

本物の実装の包括的な置換ではなくスタビングが適切なのは、テスト対象システムがトランザクションの空でないリストを返すことを要求する例13-12のように、テスト対象システムをある状態に遷移させるために関数が特定の値を返さなければならない場合である。

「テスト対象システムをある状態に遷移させるために関数が特定の値を返さなければならない場合」をいつも念頭に置いておかないと、綻びそうなので注意。

13.8 インタラクションテスト

しかし、テストを有用で、読みやすく、変化に対して強靭な状態に保つには、インタラクションテストは必要な場合のみ実施することが大切だ。

今までの物よりももっと使わないようにすべき、と言っている。

13.8.1 インタラクションテストよりステートテストを優先せよ

インタラクションテストとは、対照的なものとして、ステートテスト (https://oreil.ly/k3hSR) を通じて行動テストする方が好ましい。 …略… Googleでは、ステートテストに重点を置く方がよりスケーラブルであることがわかっている。ステートテストは、テストの脆さを減少させ、コードの変更と保守が長期的には容易になる。 インタラクションテストの一番の問題は、テスト対象システムが正常に動作していることを知らせることができないことだ。インタラクションテストが検証できるのはある関数が期待通りに呼ばれている点だけである。

ステートテストは、前の本や今までのこの本の内容出てきたような良いテストで使われている内容のことと思えば良さそう。

13.8.2 インタラクションテストが適切なのはどんな場合か

本物の実装またはフェイクを使えないため、ステートテストを実施できない場合(例えば本物の実装が遅すぎるか、フェイクが存在しない場合)。

仕方なしに使うパターン。

関数への呼び出しの回数、または順序の違いが望ましくない挙動を引き起こすかもしれない。この挙動をステートテストで検証するのは難しいことがあるため、こうした場合もインタラクションテストが有用だ。

インタラクションテストでできること、つまり関数がどのように呼ばれるかを検証しなければならないときに使う。まさに本来の目的のための使い方。

ユニットテスト内でステートテストを実施できないなら、スタートテストを実施するさらに広範囲なテストを用いてテストスイートを補うことを積極的に検討すべきだ。

つまり、ユニットテストで本物の実装もフェイクもスタビングも使えないなら、インタラクションテストを検討する、のではなく、さらにその前にインテグレーションテストを追加したほうがまだマシだということだ。インタラクションテストの優先度はそれほどまでに低い。

13.8.3 インタラクションテストのベストプラクティス

13.8.3.1 状態変更型関数向けの場合のみインタラクションテストの実施を選べ

テスト対象システムの外の世界への副作用がある関数。例 : sendEmail()、 saveRecord()、 logAccess()

以上が状態変更型関数。

非状態変更型の関数に向けてインタラクションテストを実施すると、相互作用のパターンが変化するときは常にテストを更新しなければならなくなるので、テストが脆くなる。

ぴんとこない。。。

13.8.3.2 過剰な指定は避けよ

インタラクションテストを実施する場合も、同じ原則の適用を目指すべきであり、それには、どの関数と引数が検証されるかについて過剰な指定を避けるべきだ。

原則というのは、12章の「1つのテストで1つの挙動の検証を試みよ」だ。

13.9 結論

テストダブルは、コードの包括的なテストを支援でき、またテストが高速に実行されることを担保できるので、エンジニアリング速度にとって決定的に重要である。他方で、テストダブルの誤用は、テストを不明確で、脆く、効果の低いものにすることがあるため、生産性を大量に消耗させかねない。

テストダブルは諸刃の剣。

13.10 要約

  • テストダブルより本物の実装が優先されるべきである。
  • テスト内で本物の実装が利用できないなら、フェイクが理想的な解法である場合が多い。
  • スタビングを使いすぎると、不明確で脆いテストにつながる。
  • インタラクションテストはできるだけ避けるべきである。テスト対象システムの実装詳細を公開するため、脆いテストにつながるからだ。

良いまとめ。個人的には、インタラクションテストもスタビングも実装の詳細を公開することにつながりそうに思うので避けるべき。

おわりに。13章を読んで。

読んで印象的な箇所に下線を引くのは、1月10日(金)終わった。2024年内に読み終えられるかもとも思ったが、やはり今の生活スタイルからかそうはならなかった。

本章の鍵は、テストダブルには3種類の使い方、フェイキング、スタビング、インタラクションテスト、があり、これらの定義をまず押さえることだと思った。3種類なんだっけ、となりがちで何度も振り返る必要がある。どれを選ぶかの優先順位は、わかりやすいので思い出したときにちょっと振り返るくらいで確信が持てると思う。

  • フェイキング: 実装が低速であったり、用意が難しい場合にフェイキングを使う。モッキングフレームワークを使っていなくても、フェイク用のクラスを別途用意しておけば実現できる。
  • スタビング: もしこの関数が呼ばれたら、こういう値を返してくれ、ってやる奴がスタビング。
  • インタラクションテスト: 関数の呼び出されるやり方の検証 ( = インタラクションテスト) であって、呼び出された後の結果を代替するもの ( = スタビング) ではない

下線を引いた箇所にコメント書いていくことで理解を深めようとする勉強は、1月12日(日)終わった。これでこの章はおしまい。引き続きテストを扱っている14章へ進むつもり。

コメントを残す