『単体テストの考え方/使い方』Vladimir Khorikov https://amzn.to/3BCLytq を以前読んだ。
それでテストについて記されている11章を読んだ。引き続き12章を読もうと思う。前の章を読み終わったのが2024年12月1日(日)くらいなのでこの日にスタート、とする。
12章 ユニットテスト
要点を繰り返すと、規模が指すのは、テストにより消費されるリソース並びにテストが実行を許可されてる内容であり、範囲が指すのは、テストが検証を意図するコードの量である。テスト規模についてはGoogleは明確な基準を持つが、それに比べテスト範囲はやや曖昧となりがちである。
他人に規模と範囲って何?と聞かれたときに簡潔に答えられる内容。練られて醸成された、シャープさを感じる。
バグの防止に続くテストの最重要目的は、エンジニアの生産性の改善である。
バグ防止 = 退行への保護。なるほど、バグ防止はわかりやすい表現。それゆえに埋もれがちそう。
ユニットテストはエンジニアの生活もそれほど大きな部分を占めているため、Googleはテストの保守性に対してかなり重点を置いている。保守性のあるテストとは「とにかく動作する」ものだ。つまりテストを書いた後でエンジニアはテストが失敗するまではそのテストについて再度考える必要は無い。そしてテストの失敗は明確な原因のあるバグが実在することを示す。
動かなくなった、つまり失敗するテストは保守性がなく、イコール、バグがプログラムにある、という状態にしたい、目指し続けているということだな。
12.1 保守性の重要さ
要は、将来担当するエンジニアの負担とならないように、駄目なテストはチェックイン前に修正されなければならないのだ。大まかに行って、Maryが遭遇した問題は2つのカテゴリに入る。第一にMaryが作業していたテストは脆いものだった。要するに、実際のバグを全く持ち込んでいない無害かつ無関係な変更に反応して破綻したのだ。第二に、そのテストは不明確だった。つまりテスト失敗後に、間違っている点、修正方法、そしてそのテストがそもそも何を実行することになっているかという点を、確定するのが難しかった。
脆さ、不明確さ、を無いように保守性を保つことが大変であり、だからこそ重要。
12.2 脆いテストを防ぐ
今定義したようにもろいテストとは本番環境のコードへの無関係な変更で、実際のバグは何も持ち込まないものに直面した際に失敗してしまうテストのことだ。(注1)
(注1): これは信頼不能テストとは少々異なることに注意すべきで、信頼不能テストとは本番環境のコードへの変更が何もなくても非決定性に失敗するテストである。
脆いテスト → 偽陽性 (false positive)
信頼不能テスト → フレーキーなテスト。これも偽陽性 (false positive)
第8回 脆いテスト ~継続的な変更と改善を阻むテストの原因と対策~ | gihyo.jp
12.2.1 変化しないテストを目指す
したがって、理想のテストとは変化しないテストである。つまり、テスト対象システムの要件が変化しない限り、書かれた後は二度と変更の必要がないテストだ。
じゃぁ変化とは何かと言うので、次の4つを挙げている。1つ目、純粋なファクタリング。2つ目、新機能。3つ目、バグ修正。4つ目、挙動の変更。挙動の変更以外のケースにおいて、テストに変更は無い、テストへの更新が必要ないのが理想というのはなんかすごい。
以上を踏まえて覚えておくべきことは、テストを書いたら、システムをリファクタリングし、バグを修正し、新機能を追加する際に、そのテストに再度触れなければいけないというのは何かが間違っているということである。この了解こそが、スケールするシステムを扱えるようにしてくれるものだ。
この部分を読むと、確かになと腑に落ちるのと同時に、良質なテストを書くというのは、本当に高等な技術なのだと実感させられる。
12.2.2 公開API経由のテスト
このことを保証するのに群を抜いて最も重要な方法は、テスト対象システムのユーザが呼び出すのと同じ方法でシステムを呼び出すテストを書くことである。 …略… おまけにそのようなテストは、ユーザにとって有用なコード例ならびにドキュメンテーションの役目を果たせる。
実装に対してではなく、契約に対してテストコードを書くべしと確か前の本にあった気がする。『単体テストの考え方/使い方』5.2 観察可能な振る舞い(observable behavior)と実装の詳細(implementation detail)、が近いかなぁ。
12.2.3 相互作用ではなく、状態をテストせよ
一般に、テスト対象システムが期待通りの挙動を行うことを検証するには2つの方法が存在する。ステート(state:状態)テストでは、システム自体を観察して、システムのメソッドを呼び出した後にシステムがどのような状態になっているかを見る。それに対してインタラクション(interaction: 相互作用)テストでは、システムが呼び出しに応じて期待される一連の動作を協調動作の対象に対し行ったかチェックする(https://oreil.ly/3S8AL)。多くのテストは、状態と相互作用の検証を組み合わせて実施する。
つまり、インタラクションテストがどのように(how)システムがその結果に到達したかをチェックするのに対し、通常は結果が何(what)であるかだけを意識すべきだ。
インタラクションテストに問題がある場合、その問題の理由として最もありふれたものは、モッキングフレームワークへの過度の依存である。 …略… そのため我々は、本物のオブジェクトが高速で決定性である限りは、モックオブジェクトよりも本物のオブジェクトを使うことを好む傾向がある。
Googleは、古典学派だなぁ。
12.3 明確なテストを書く
脆さが入る事態を完全に免れたとしても、遅かれ早かれテストは失敗するだろう。失敗は良いことだ。テストの失敗はエンジニアに有用なシグナルを提供し、またユニットテストが価値を提供する方法の時主要なものである。
テストでの失敗は良いことだと言っているその姿勢がいい。
テストスイートが長期にわたりスケールし有用であり続けるためには、そのテストスイート内の各個別テストが可能な限り明確であることが重要である。本節では、テストが明確性を達成するようにするためのテクニックと考え方を探る。
明確なテストがもたらす利益はスケール。
12.3.1 テストは完全かつ簡潔にせよ
テストが明確性を達成するのを助ける、2つのハイレベルな属性は、完全性と簡潔性である(https://oreil.ly/lqwyG)。テストは、そのテストがどのようにその結果に到達するか理解するために読者が必要とする全情報をその本体部分が含んでいる場合に、完全である。テストは、他の紛らわしいか無関係な情報が含まれていない場合に、簡潔である。
明確=完全+簡潔。完全=自立。簡潔=必要最小限
とりわけより明確なテストにつながるのであれば、DRY(Don’t Repeat Yourself : 繰り返しをやめる)原則に違反する価値がありうる場合が多いという考え方だ。
個人的には完全性を達成するために、DRY原則をあえて破ることが多そうだなぁと感じる。
12.3.2 メソッドではなく、挙動をテストせよ
つまり、テストされるメソッドが複雑になるにつれて、そのテストの複雑性も増大し、実際は何をやっているのか、推論することが難しくなるのだ。
話はずれるが、だからこそ、実装の詳細に対してユニットテストを書くのではなく、インターフェースに対して言うとテストを書くと言うことだと思う。
各メソッド向けにテストを書くより、各々の挙動(behavior)向けにテストを書くべきだ。挙動は、システムが特定の状態にある間に、一連の入力へどう反応するかについて、システムが行う任意の保証である。
『単体テストの考え方/使い方』4.1.4 実装の詳細ではなく、最終的な結果を確認する P106 、6.1.1 出力値ベース・テストとは? P168 に対応すると思う。
12.3.3 テストにロジックを入れるな
システムがどのように構成されるかを定義する「〜という前提条件下で」の部分、システムに対して行われる動作を定義する「〜場合」の部分、結果を検証する「その場合は〜」の部分だ(注8)。 …略…
(注8) これらの部分は「準備する(arrange)」「行動する(act)」「真であることを表明する(assert)」とも呼ばれる。
Google でも AAA パターンを使っているようだ。
このようなテストを書く場合、うっかり複数の挙動を同時にテストしていないか慎重に確かめるようにしなければならない。各テストは単一の挙動のみを対象とすべきであり、ユニットテストの圧倒的大部分では、必要な「〜場合」と「その場合は〜」のブロックは1つだけだ。
『単体テストの考え方/使い方』3.1 単体テストの構造 P58 が詳しい。同じ主張をしていると思った。
12.3.4 明確な失敗メッセージを書け
理想的な世界ではエンジニアは、テスト自体を全く見る必要なしに、ログかリポート内のテスト失敗メッセージを読むだけで問題の原因を究明できる。 …略… つまり失敗メッセージは、期待される結果、実際の結果、関連する要因全てを明確に表現するべきである。
ユニットテスト失敗したときの出力も完全で簡潔にすることが大事になってくる。
12.4 テストとコード共有:DRYではなくDAMP
良いテストとは変化しないように設計されるものであり、そして実際、テスト対象システムが変化する際にはテストが破綻することが通常は**望ましい。**したがってテストコードに関しては、本番環境向けコードの場合ほどDRYの恩恵は無い。
DRYの恩恵は薄いので、こだわる必要もなく、もっと大事にした方が良いことを重視する。
テストコードは完全にDRYになるのではなく、DAMP (https://oreil.ly/5VPs2) を目指すべきであることが多い。つまり「説明的かつ意味がわかりやすい言い回し (Descriptive And Meaningful Phrases) 」を推奨すると言うことだ。少々の重複は、それがテストを単純かつ明確なものにする限り、問題ない。
テストが失敗したときに、そのテストだけをみたいので、テスト内で完全かつ簡潔になっている事を確保するためにドライじゃなくてダンプにする。この章にはスクロールをして行ったり来たりしながら、テストを読むのは苦痛ってことも書いてあった。上から下に読み下して、内容がわかるようにする必要がある。
DAMP は DRY を置き換えるものではなく、補うものである。
両立し得る。
重要な点は、そのようなりファクタリングは、テストを説明的で意味がわかりやすいものにすることを重視して行われるべきであり、単に繰り返しを減らすと言う名目のみにおいて行われるべきではないということだ。
DRY を使うとしても、システム構築時とユニットテストを書くときでは目指すものが違うところがポイント。『単体テストの考え方/使い方』3.3 テストフィクスチャの準備方法、にあたる。
12.4.1 共有値
エンジニアが通常共有の定数の利用に心惹かれるのは、各テスト内で個別の値を構築するのは冗長となり得るからだ。このゴールを達成する最も良い方法は、テスト作者が関心のある値のみ作者の指定を要求し、他の全ての値に妥当なデフォルト値を設定するヘルパーメソッド(例12-22を参照)を用いてデータを構築することである。
これは例えばまさに、Laravel でいうテストのファクトリメソッドに当たる!
12.4.2 初期設定の共有
初期設定メソッドを使う際のリスクは、テストが初期設定で使われる特定の値に依存するようになると、テストが不明確になりかねないことだ。
テストの読書がよくわからんなぁと言って、調べ始めたら負け。
明示的に特定の値に関心持つこうしたテストは、そのような値を直接表明し、必要なら初期設定メソッド内で定義されたデフォルト値をオーバーライドすべきだ。結果としてできるテストは、例12-24に示されるように繰り返しを少し多く含むが、結果がはるかに説明的かつ意味がわかりやすくなっている。
Laravel ファクトリーメソッドでも、後から値を変えることができ、本節と同じことが実現可能と思う。
12.4.3 ヘルパーメソッドと検証メソッドの共有
しかし、より対象を絞った検証メソッドは依然として有用となりうる。 …略… それは、検証している条件が、概念的には単純であるものの、テストメソッドの本体部分に含まれると明確性を低下させるであろうループ処理や条件分岐ロジックを実装に要する場合だ。
内容が難しいのだが、おそらく検証の部分を動的にするとかそういうことと思う。for ループの中にassertを書くとか、そういったことはできるだけ避けるべきだが、単純な場合は良いということを言っているのだと思う。
12.4.4 テストインフラストラクチャーを定義する
そのようなライブラリーは、膨大な数が利用できるため、組織内でのライブラリー標準化が、なるべく早くかつ全部署で起こるべきだ。
これは難しい話ではなく、テストライブラリーを決め、それを全体で使おうと言う話だ。
12.5 結論
ユニットテストは、ソフトウェアエンジニアにとって予期せぬ変更に直面しながらも長期的にシステムが動作し続けることを担保するための最も強力なツールである。
長くシステムを運用して変更し続けるために必須なものとしてユニットテストを挙げているのが面白い。
そして、ユニットテストをいい加減に使うことでできあがる可能性のあるシステムは、保守と変更にかかる手間が増えるにもかかわらず、当該システムの信頼が実際に向上する事はない。
12.6 要約
- 変化しないテストを目指せ。
- 公開API経由でテストせよ。
- 相互作用ではなく、状態をテストせよ。
- テストを完全かつ簡潔にせよ。
- メソッドではなく、挙動をテストせよ。
- 挙動に重点を置いて、テストを構成せよ。
- テスト対象の書道日なんでテストに命名せよ。
- テストにロジックを入れるな。
- 明確な失敗メッセージをかけ。
- テスト用コードを共有する場合、DRYよりDAMPに従え。
良いまとめ。DRYよりDAMPは、テスト用コードを共有する場合、と要約にはあり、本文中でも同じことを言っていたのだろうが、気が付かなかった。ただ、その通りだとは思う。
おわりに。12章を読んで。
今これを書いているのは12月29日(日)。だが、12月下旬入る頃には読み終わっていた。一方で気になった箇所のまとめは途中だ。まとめは12月30日(月)終わったので、やっと読み終わった。
印象に残っている点としては次。
- DAMP (Descriptive And Meaningful Phrases) という言葉を初めて知った。
- 脆いテストと不明確なテストを避ける。脆いテスト → 偽陽性 (false positive)のあるテストで、公開API経由のテストとすることで防ぐ。明確=完全+簡潔。完全=自立。簡潔=必要最小限
次の読書は引き続きテストを扱った13章『テストダブル』を読もうと思う。
