テスト
『Google のソフトウェアエンジニアリング』第11〜14章を要約したもの。本文の引用はしていない。 書籍由来の主張には (書籍11章) のように出典を付けている。
手元の判断基準(テストスイートの健全さの見方、依存の置き換え方)は テストの方針 にある。
なぜ自動テストを書くか
根っこにある理由は1つで、変化できる状態を保つため。 テストは正しさの証明ではなく、変化の許可証として持つ。技術も市場も顧客の好みも変わるので、 素早く適応できること自体が製品の競争力になる。(書籍11章)
テストの価値は、エンジニアがそれをどれだけ信頼しているかで決まる。 信頼できないテストスイートは、テストがまったくない状態より害が大きい。(書籍11章)
そこから実務上の理由が導かれる。
| 目的 | 何がなくなるか |
|---|---|
| テスト手順の記載と実施の時間を最小限にする | 全機能を毎回手動でテストすること。同じ手順を何度も書くこと |
| 変化時のテストの時間を最小限にする | 大規模リファクタ・大規模設計変更のたびの手動テスト |
| 仕様とユースケースを明文化する | 実装と食い違ったドキュメント。「どういうケースがあるか」の口伝 |
| バグを早期に見つける | 緊急リリース、緊急リバート、本番で初めて気付くバグ |
| 属人性を排除する | 人によって OK / NG が変わること |
| 同じテストを共通化する | 同一内容のテスト手順の重複 |
副次的な効果として、テストを書く行為そのものが設計を教えてくれる。 テストしにくい新規コードは、たいてい担当機能が多すぎるか依存関係が扱いにくい。(書籍11章)
書きやすさは規模の問題でもある。何百万行のコード、何百のライブラリ、 言語 × 国 × デバイスの掛け算、そこにアクセシビリティとセキュリティが乗る。 これを手で回す前提は最初から成り立たない。(書籍11章)
効果はどこに出るか
- 欠陥の大半がリポジトリへの提出前に捕まる。存続期間を通じて欠陥が減る
- 依存関係側の変更で壊れたことがすぐ検出され、本番に出る前にロールバックできる
- リファクタリングが促進される。 挙動を保つ変更なら、理想的には既存テストに一切手を入れなくていい
- 1つの挙動だけを対象にした明確なテストは、実行可能なドキュメントとして機能する
- レビュアーが各ケースを頭の中で追う必要が減り、テストが通っていることを確認すればよくなる
(以上 書籍11章)
逆に、信頼できないテストは持たない方がまし
脆いテスト — 期待結果を過剰に指定する、大きくて複雑な定型コードに依存する — は変化の妨げになる。 無関係な変更でも失敗するので、5行の修正で何十個ものテストが壊れる。 これが続くと、コードベースを健全に保つためのリファクタリングをチームが避けるようになる。(書籍11章)
脆さの最大の発生源はモックオブジェクトの誤用。 Google はモッキングフレームワークの誤用で相当な害を受け、 「モックは二度と使わない」と言い出すエンジニアが出るほどだったという。(書籍11章)
遅さも同じ問題を生む。エンジニアは遅いテストを待たないので、遅いほど実行されなくなる。
並列化や高速なハードウェアで底上げしても、遅い個別ケースが大量にあれば埋もれる。
sleep() や setTimeout() による不要な待ちは、状態遷移の細かいポーリングとタイムアウトの組み合わせに置き換える。(書籍11章)
決定的でも高速でもないテストスイートは、いずれ必ず生産性の障害になる。 だから盤石なテストを用意したことにも、機能をローンチしたのと同じだけの評価を与える。 遅いテストや大して重要でないテストはリファクタリングする。 サポートするフレームワークとツールの数自体も絞る。(書籍11章)
いいテストの条件
漏れがない
実際のユースケースやパターンを網羅している
すべてのパターンだと多すぎる場合は実際にあり得るケースを網羅できているか
レアパターンでも実際あり得るパターン
必須項目がすべてあるパターン
必須項目がすべてないパターン
各種バージョンアップをしたときに、テストの失敗を修正するだけで
手動テストが不要なくらい信頼性があるかどうか
通過や分岐としてのカバレッジ100%だけではなく、実際のユースケースやパターンを網羅している
網羅の基準はカバレッジではない。壊れてほしくないものを全部テストする、が基準。 挙動の正しさだけでなく、パフォーマンス・アクセシビリティ・セキュリティ、 そして障害時の振る舞いも対象に入る。一般的な障害を模擬する自動テストは書いておく。(書籍11章)
裏返しの厳しさもある。無関係なインフラ変更で自分たちの製品が壊れたとき、 自分のテストが全部通っていたのなら、壊れた箇所を直してテストを足すのは自分の側の仕事になる。(書籍11章)
カバレッジは行が呼ばれたことを測るだけで、その結果何が起きたかは測らない。 しかも他のメトリクス同様、すぐにそれ自体が目的化する。 テストされていない場所を知る手がかりにはなるが、 「十分にテストできているか」を批判的に考えることの代わりにはならない。代わりに問うべきなのは、(書籍11章)
顧客が動作を期待するものが全部動くという確信があるか
依存関係の破壊的変更を捕捉できる自信があるか
テストは安定していて信頼できるか
速い
実行が速い
関係しているコンポーネントのパフォーマンスが良いためsleepが不要
大量に存在する遅い個別テストケースが少ない
並列で実行ができる
DB関連のテストはトランザクション内でテストして、ロールバックされる
本物のオブジェクトでテストが遅い場合はパフォーマンス・メモリ溢れ等問題のある可能性が極めて高い
大きなテストを2つの小さなテストに割って並列に動かすのが、速度改善として最も効くことが多い。(書籍14章)
安定している(決定的である)
入力が同じ場合は常に同じ結果になる
テストは実行順など外部環境に前提を置かず、環境の構築・実行・解体に必要な情報を自分で持つ。(書籍11章)
明確
なぜそのケースがあるのか明確で誤って削除されない
どういう場合なのか明確で再現方法が明確
失敗時のパラメーター、ユースケースが明確
失敗したケースだけを実行しやすい
結果は変数展開や動的に作成されていない
何がテストされているかのコメントが残されている
スタブデータの詳細がどのユースケースなのかコメントが残されている
contextのネストを深くしすぎない
異常系は先に単一のcontextにする
互いに影響する場合のみマトリックスのcontextにする
互いに影響しないものは単一のデータの各ケースをcontextにする
初めて見る壊れたテストを直す羽目になったとき、 読みやすく書いてあることのありがたみが分かる。コードは書かれるより読まれる回数の方がずっと多い。(書籍11章)
実装・レビューが楽
テストの実装が楽で速くできる
テストのレビューが楽
無理やり共通化をしていなく、修正しやすい
同じ内容の挙動のテストは共通化できている
テスト内で条件分岐やループを使っていない
テストのためのテストが必要ない
不必要にライブラリ・フレームワーク・言語の機能を使って複雑になっていないか
テスト自体にはテストがない。 だからテスト内の制御フロー(条件分岐・ループ)は強く非推奨。 複雑なフローはそれ自体がバグを抱えうるし、失敗原因の特定を難しくする。(書籍11章)
何をテストするか
メソッドではなく挙動をテストする。実装詳細ではなく公開 API をテストする。 ここが崩れると、脆さのほとんどがここから生える。
理屈はこう。テスト対象システムのユーザーが呼ぶのと同じ方法で呼べば、 テストを壊す変更はユーザーも壊す変更ということになる。だからテストが現実的になり、脆さが減る。 おまけにそのテストは、ユーザーにとってのコード例とドキュメントを兼ねる。 逆に内部にテストが食い込んでいなければ、内部のリファクタリングは好きなだけやれる。(書籍12章)
メソッドと挙動は多対多で、複数メソッドの相互作用に依存する挙動もある。 だからメソッド単位ではなく挙動単位でテストを書き、1つのテストは1つの挙動だけを対象にする。(書籍12章)
見るのは結果が**何(what)**であるかであって、どうやって(how)そこに至ったかではない。(書籍12章)
| テスト対象として妥当 | 妥当でない |
|---|---|
| 検証している挙動 / 壊れてほしくない挙動すべて | 実装詳細部分 / 実行されているコード |
| 顧客が動作を期待するものすべて | 自分が書いたクラス |
| 公開 API に対する呼び出し | コードの内部構造についての前提条件 |
| システムを使う側(顧客)の挙動・視点 | システムを実装した側の実装詳細 / そのクラスやメソッドを使うエンジニアの視点 |
| 挙動 | メソッド / どのメソッドを呼び出すか |
| 実装から独立している | 実装に依存している |
| 依存関係のクラスの破壊的変更の捕捉も対象 | 破壊的変更の捕捉は対象外 |
| 1ケース1挙動 | 1ケースに複数の挙動が混在 |
| 設定のバグを拾える | 設定のバグを拾えない |
「範囲が狭い」というのは実行されるコードではなく検証されるコードが狭いという意味。 クラスが多数の依存を持ち、テスト中にそれらが呼ばれるのは普通のこと。(書籍11章)
サポートライブラリー(オーナーだけがアクセスできるが、一定の文脈で汎用的な機能を提供するもの)も ユニットとみなして直接テストする。使う側が減ったときにカバレッジの穴を開けないため。(書籍12章)
相互作用ではなく状態をテストする
DBに保存されることではなく、データを取得した結果が正しいかを見る
状態を見るテストの方がスケールする。脆さが減り、長期的にコードの変更と保守が楽になる。(書籍13章) ステートテストが書けない場合に限ってインテグレーションテストを書く。インタラクションテストに逃げない。
インタラクションテストを書かざるを得ない場合も、 どの関数がどの引数で呼ばれるかを過剰に指定しない。呼び方を変えただけで失敗するテストが減る。(書籍13章)
依存をどう扱うか
優先順位は 本物 > フェイク > スタブ。モックは使わない。
第一の選択肢は本物の実装。本番で動くのと同じ形で動かせば、テストの忠実性が上がり、 「ちゃんと動いている」という信頼が得られる。 テストダブルに寄りかかったユニットテストは、同じ信頼を得るために 結局インテグレーションテストか手動確認を追加させることになる。(書籍13章)
モックを使いすぎると、本物の実装と同期が取れなくなってリファクタリングを妨げるコードが増える。 モック化は、API のオーナーが長期的に実装を変える能力にも強い制約をかける。(書籍13章)
本物の実装は「遅すぎて使えなくなるまで」使い続けられることが多い。 テストで本物のクレジットカードサービスを叩くような、使うこと自体が非現実的なものだけが例外。(書籍13章)
スタブの作り方
スタブデータは実データに即しているかどうか
スタブ化のソースはテストコードには書かずにライブラリで吸収する
スタブの内容が重複しないように結果は一時変数においてから各キーを比較する
時間や日付も現実に即したスタブにする
時間や日付のスタブが古くなりすぎたら最新にする
関係する箇所を修正する度に最新にする
他システムの変化とスタブは一致させる
スタブと実際のソースの切り替えが楽に切り替えれるかどうか
スタブは定期的に最新のものに更新できるのが楽かどうか
外部の複数のシステムに対する操作の順序の検証はモックではなく
postした後にgetしてその都度確かめる
スタブの危うさは3点。実装詳細がテストに漏れるので本番コードの内部が変わるとテストも直す羽目になる。 スタブが本物と同じように振る舞う保証がない。 そして保守するエンジニアの生産性を大きく削る。 スタブが妥当なのは、テスト対象をある状態に遷移させるために特定の戻り値やエラーが必要なときだけ。(書籍13章)
フェイクを使う場合は、本物の挙動が変わったら必ず追随させる。 一致していないフェイクを使ったテストは役に立たない。 公開インターフェイスに対してテストを書き、本物とフェイクの両方に同じテストを流すやり方がある。(書籍13章)
テストの規模
範囲(ユニット / インテグレーション)では分けない。速度と決定性で分ける。 規模を決めるのは行数ではなく、そのテストが何をしてよいか、どれだけリソースを食うか。 そしてどの機能に対しても、書けるかぎり小さいテストを書く。(書籍11章)
| 規模 | 許されること | 備考 |
|---|---|---|
| 小 | ネットワークもディスクも触れない | 遅さと非決定性の主要因へのアクセスを断つ。該当する依存はテストダブルで軽量なプロセス内依存に置き換える |
| 中 | 単一マシン内に収まる。DB インスタンスの実行、Web UI とサーバーコードの組み合わせ | WebDriver 等が必要になりやすい。柔軟だが非決定性が入るリスクがある |
| 大 | 複数マシン、外部トラフィック | E2E とテストダブルが使えないレガシー向け。開発者のワークフローを妨げないよう、ビルドとリリースの間だけで動かす |
規模の定義を厳密に決めたことで、それを機械的に強制するツールが作れた。 小テストとタグ付けされたテストがネットワーク接続を張ろうとしたら、そのテストは落ちる。(書籍11章)
範囲と規模は独立している。テストダブルでプロセス外依存を全部代役させれば、 サーバーの構文解析・リクエスト検証・ビジネスロジックを全部通る広いテストでも小テストになる。 逆に、単一メソッドを対象にした狭いテストが中テストになることもある。(書籍11章)
良いテストスイートは、局所的な現実に即したテストから組織全体の現実に即したテストまでが混在している。 別チームが作った2つのシステム間の契約のような相互作用は、ユニットテストでは検証できない。(書籍11章)
なぜ大規模テストがいるのか
大規模テストは忠実性のために持つ。バグ捕捉の主要手段ではない。(書籍11章・14章)
ユニットテストだけでは埋まらない穴がある。(書籍14章)
- データを手で作るので、扱う範囲が狭く、書いた人のバイアスに寄る
- モック対象の実際の挙動を誤解していることがありうる。しかも本物が変わっても、 追随すべきだと知らせてくれるシグナルは存在しない
- 書いた人の想像力の範囲を超えられない
- 公開 API に明示されていない観察可能な挙動を、ユニットテストだけで押さえるのは難しい
- 大量のトラフィックを流すパフォーマンス・負荷・ストレスのテストは、ユニットテストの型に収まらない
- 製品の意図についての誤解は、コードとユニットテストの両方に同じように入り込む。 そうなるとテストは「意図通りに動いている」ではなく「意図通りに実装されている」ことしか確かめられない
巨大なテスト1つより、いくつかの大テストに分けた方がいい。 1つのテストの出力を永続化して次のテストの入力にする、という連鎖のさせ方がある。(書籍14章)
外部サービスについては、本物のサードパーティ API を自動テストから叩かせるのは非推奨。 代わりに、大規模テスト実行時に外部へのトラフィックを記録しておき、小規模テストで再生する手がある。(書籍14章)
テストデータは、質と量の両方で現実に即した一式を用意する。本番からコピーできることが多い。 必要最小限のデータで最大のカバレッジを狙う「スマートサンプリング」という考え方もある。 ただしテスト用データはエンドユーザーに発見されうる前提で扱う。(書籍14章)
A/B 差分テスト
異なる2環境での出力の差分が確認できる
リファクタ時の出力に変化がないことを確認
パフォーマンスの変化も確認できる
公開 API にトラフィックを流し、新旧バージョンのレスポンスを比較する。 コーナーケースの差異を捕まえられるだけのシナリオを含める必要がある。 人気がある理由は、検証ステップの保守にかかる人的コストが低いこと。 失敗時には、何を測っていて、なぜその挙動が怪しいのかが出力から分かるようにする。(書籍14章)
E2E テスト
コードがシンプルでコンパクトのため、E2Eはタグの読み込みによるタイムアウトやjsエラーが発生しない
タイムアウトの上限値はユーザーが待つであろう時間を超えない
本番のユーザーは待ち時間が短いことを望むので、テストも本番のユーザーと同じように反応するのがよい。 エンドユーザーが受忍できる挙動(最大許容タイムアウトは n 秒、など)を定義しつつ、 実行の不安定さも扱えるバランスを見つけるのが鍵。 アサーションは合否のシグナルを明確に出し、原因の切り分けに役立つエラー出力を返す。(書籍14章)
テストコードの書き方
テスト本体は、理解に必要な情報を全部含み、それ以外は何も含まない。(書籍12章)
| 良い | 悪い |
|---|---|
| 重要ではない情報や紛らわしい情報を全く含まない | 含んでいる |
| 理解するのに必要な情報を全部含んでいる | 含んでいない |
| ロジックが含まれていない | 含まれている |
| 文字列が動的に作成されない | 動的に作成される |
| テストの共通コードがある | ない |
DRY より DAMP
共通化して無理にDRYにするよりは重複を許容して明確にする
テストが分かりやすくなるように共通化されていて、テストの意図はぱっと見で分かりやすいかどうか
重複と明確化のトレードオフのセンス
テストが説明的で分かりやすくなるなら、ある程度の重複は許容してよい。 ただし DAMP は DRY を置き換えるものではなく補うもので、 ヘルパーやテストインフラは今も有効。判断基準は 「説明的で分かりやすくなるか」であって「繰り返しが減るか」ではない。(書籍12章)
検証ヘルパーは、入力について単一の概念的事実が真であることを表明するものが最良。 テスト本体に置くと明確性を損なうループや条件分岐を、実装側に押し込みたいときに効く。 そしてテストインフラ自体は常に自分のテストを持つ。(書籍12章)
自動化しないもの
自動テストが向かない領域は残る。人間に任せる。(書籍11章)
検索結果の品質テスト(人間による判定が必要)
電話やビデオ通話システムのパフォーマンス評価(音声と映像品質の微妙な差異)
複雑なセキュリティ脆弱性の探索(人間の方が自動システムより上手)
人が欠陥を見つけて理解した後は、自動セキュリティテストに追加して継続的に探索させる。 こうした、あらかじめ何を探すか決まっていないテストは探索的テストと呼ばれる。 アプリケーションを解くべきパズルとして扱い、予期しない手順や予期しないデータを入れて、 見過ごされてきたコードパスや異常な応答を炙り出す。(書籍11章)
逆に、手動テストに回してはいけないもの。
改修の影響範囲のユースケース
自動テストでできる内容
同じテストを何回も
※エンジニアがちゃんと手動テストをしないとバグがすり抜け放題
関連
- テストの方針 — 手元の判断基準
- コードレビュー(書籍第9章) — 同じ書籍の第9章
- 静的解析 — 機械が指摘できるものはテストを書く前に潰す
- 継続的インテグレーション — どのテストをどの段階で走らせるか
- 分散システム — 障害注入は大規模テストの領域
- 性能を測る — 挙動だけでなく性能も回帰しうる
- アプリケーションセキュリティ — 探索的テストで見つけたものを自動テストに戻す
- アクセシビリティの検証 — 機械が見られるのは形式だけ、の具体例