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

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

14章 大規模テスト

結論から言うと、実際にはGoogleは大規模テストを多数用いており、大規模テストは健全なソフトウェアエンジニアリングに必要となるリスク緩和戦略を構成する重要な一要素である。

14.1 大規模テストとは何か

  • 大規模テストは遅いかもしれない。Googleでは、大テストには15分か1時間のデフォルトのタイムアウト時間があるが、何時間、それどころか何日間も実行されるテストもある。
  • 大規模テストは密閉されていないかもしれない。大テストは、他のテストやトラフィックと、リソースを共有しているかもしれない。
  • 大規模テストは非決定性かもしれない。大テストが密閉されていないなら、決定性を保証するのはほぼ不可能である。つまり、他のテストやユーザ状態が大テストに干渉するかもしれない。

このようなデメリットがあっても、なお信頼を提供できるのであれば、大テストの価値がある。

14.1.1 忠実性

本番環境自体は、当然ながらテストにおける忠実性が最高の環境だ。 …略… 本番環境からコピーされたテストデータは現実に対してかなり忠実だが(そのような方法で取得されたものなので)、しかし大きな課題となるのは、その新しいコードを初めてローンチする前に、現実に即したテスト用トラフィックを作成する方法である。

確かに用意できないものを用意することはできない。

14.1.2 ユニットテストでよくある不足部分

大規模なテストはまた、小規模なテストが機能しない場で必要となることがある。

以下、ユニットテストが苦手なところがリストアップされていく。

14.1.2.1 忠実でないテストダブル

しかし、そのエンジニアはたいていの場合、モックの対象となるものを書いたわけではないのであり、モック対象の実際の挙動について誤解していることもありうる。

…略…

さらに、モックは陳腐化する。モックに基づいたユニットテストが本物の実装の作者には認識されておらず、本物の実装が変化する場合、そのテスト(とテストされるコード)が本物の実装の変化に追随するように変更されるべきだと知らせるシグナルは存在しない。

つまり、モック化される対象について完璧に理解しているわけではないし、時間が経てばモック化した対象が変わっている事が起こり得るがそれに気づくこともできない、といった問題がある。

14.1.2.2 設定の問題

もしそれらの設定ファイルに問題があるか、またはそれらの設定ファイルの保存内容に定義された状態と当該バイナリにより定義された状態との間の互換性に問題がある場合、ユーザにとって大問題になりかねない。ユニットテストだけでは、この互換性の検証はできない。

ユニットテストはそれぞれの環境での設定が正しいかどうかを検証することには向いてない。

Googleでは設定変更が大障害の原因の第1位である。

どこでもそうなんだね。Googleでもそうなんだ。

14.1.2.3 負荷がかかると起こる問題

しかし、パフォーマンス、負荷、ストレスのテストでは、あるバイナリへの大容量のトラフィック送信が必要となることが多い。一般的なユニットテストのモデル内では、こうした大容量トラフィックはテストが難しくなる。

つまり、ロジック以外のことをテストしたい時は、ユニットテストは役に立たない。

14.1.2.4 予期しない挙動、入力、副作用

しかし、ユーザが製品に関して発見する問題は、大部分が予期しないものだったりする(さもなければ、その問題がエンドユーザーのところまで問題として行き着く見込みはないだろう)。この事実は、予期しない挙動のテストをするために、テストに別のテクニックが必要とされていることを示唆している。

問題が発見されたら、問題を再現するユニットテストを書き、そして問題を修正し、再度ユニットテストを実行して成功するように修正していく、という流れを思い出した。

14.1.2.5 不意に起こる挙動と「真空効果」

つまりユニットテストは、真空の空間内に安置され、現実世界の混乱の届かないところに巧妙に隠匿されている。その状態は速度と信頼性のためには素晴らしいが、その状態のために、ある欠陥のカテゴリーを捕捉し損ねている。

上述の「負荷がかかると起こる問題」もこれの一部分のように思える。

14.1.3 何故大規模テストを備えないのか

大規模テストの第二の課題は、標準化(あるいはその欠如)の一種だ。ユニットテストと違い、大規模テストは、それらのテストが書かれ、実行され、デバッグされる際に使うインフラストラクチャーとプロセスの面での標準化の欠如に苛まれている。

14.2 Googleの大規模テスト

C/J Build (Googleの初の継続的ビルドフレームワーク) は、ローンチ時にはユニットテストと他のテストを区別しなかったが、2つの決定的に重要な事情が新たに出てきたためにテスト種類の分割に至った。第一にGoogleは、ユニットテストを重視したこと。テストのピラミッドを奨励し、書かれるのテストの圧倒的大部分がユニットテストであることが保証されるよう望んだためだ。第二に、TAPがGoogleの正式な継続的ビルドシステムとして C/J Build に取って変わった際、TAPがその役目を果たせたのはTAPの適格要件を満たすテスト向けのみだったこと。

ユニットテストを重視するというポリシーのもと、大規模テストをどうするか模索していった、という流れ。

14.2.1 大規模テストと時間

開発の最初の数日以内にユニットテストを構築してテストピラミッドの形に向けて進み、その後は自動インテグレーションテストの導入と手動のエンドツーエンドテストからの脱却によりテストピラミッドを完成させることが、長期的な健全性のためには決定的に重要だ。我々の場合は、ユニットテストをリポジトリへのコード提出のための要件にすることで成功したが、長期的な健全性のためにはユニットテストと手動テストの間の間隙への対処が必要となる。

コードの存続期間に応じて、例えばユニットテストが必要か不要かといったことが決まってくる。存続期間が長いとみなして必ずユニットテストを書くスタンスがGoogleのようだ。

14.2.2 Googleスケールでの大規模テスト

したがって、このスケールでうまく動作する方式で大規模テストを実装しつつも、忠実性を適度に高く維持することが、決定的に重要となる。

忠実性はすぐ下がってしまうものなので、それはテストダブルによって、下げないように努力することがとても大事ということはわかった。説明のための文章がちょっとよくわからなかった。

ヒント : 「可能な限り最小のテスト」

インテグレーションテストにとってさえ、小さいことは良いことである。すなわち、巨大なテスト1つより、いくつかの大テストの方が好ましい。

…略…

連鎖させるというのは、テストの実行のみを指しているわけではなく、全体のシナリオを表す、対になった複数の比較的小さなインテグレーションテストを作成するということだ。

これによって忠実性を保つということだと思う。

14.3 大テストの構造

大テストは通常、以下の段階があるワークフローで構成される。

  • テスト対象システムを取得する
  • 必要なテストデータをシードとして与える
  • テスト対象システムを用いて動作を実行する
  • 挙動を検証する

大テストというよりも、ユニットテストよりも大きい規模のテストに当てはまるという感じがする。

14.3.1 テスト対象システム

SUTの各形式は、2つの主要な要因に基づいて判定可能である。

密閉性

これは、当該テスト以外の部品の利用と相互作用からSUTが分離されている度合いだ。

忠実性

SUTが、テスト対象の本番環境システムを反映する正確さ。

この2つの項目が、それぞれどれくらいあるかでテスト対象システムを分類している。

14.3.1.1 密閉されたSUTの利点

つまり、本番環境内テストは、それ自体では本番環境へのテスト対象コードのリリースを阻止できないということだ。つまりそのSUTは本質的に手遅れなのである。

プログラムコードを本番環境にデプロイしない限り、本番環境でのテストはできないわけだからバグが見つかって嬉しいとしても、本番環境でバグを出したくない観点から見れば手遅れ、ということになる。

次のステップはクラウド分離またはマシン密閉SUTへの対応だ。そのような環境は、コードリリースのため競合と予約要件を回避することで状況を改善する。

何言っているのか理解できていない。。。

14.3.1.2 問題の境界でSUTの規模を縮小する

避けるに値するかもしれない、特に苦労の多いテスト境界が存在する。ユーザインターフェイス(UI)のテストだ。UIテストの信頼性のなさとコストの高さは悪名高く、そのためフロントエンドとバックエンドの両方に関与するテストは悩ましいものとなる。

  • UIの変化は、UIのテスト脆くするが、実際にはUIの基底にある挙動には影響しないルックアンドフィール面で起こることが多い。
  • UIにはテストが難しい非同期の挙動があることが多い。

これはとてもよくわかる。バックエンドをAPIとして切り出しそちらだけ自動テストするようにし、フロントエンド側は何もテストしない。これが現在の自分の環境での状態だ。

忠実性とコスト/信頼性とのトレードオフを特定し、また妥当な境界を特定することが、ここでの鍵だ。

結局はコスト、労力、とのトレードオフになる。

14.3.1.3 記録/再生の代理機能

Googleが行っているのは少々違ったことだ。Googleで最も人気のあるアプローチ(https://oreil.ly/-wvYi 。このために公開APIがある)では、大規模テスト生使って小規模テストを生成する大規模テストを実行する際に、外部サービスのトラフィックを記録し、小規模テストを実行する際に、それを再生することにより小規模テストを生成している。

わかりそうでわからない。未知の領域。

14.3.2 テストデータ

データは、以下のように様々な方法で生成されうる。

手動作成のデータ

…略…

コピーされたデータ

通常は本番環境からデータをコピーできる。 …略…

標本として抽出されたデータ

データをコピーすると、提供されるデータが多すぎて適切に扱えないことがありうる。データから標本を抽出することでデータ量を減らすことができ、それによってテストの時間が短くなり、テストが実際には何を行っているのかを推論することが容易になる。

…略…

生成され方の整理はなるほどそうだと思った。標本として抽出するメリットとして、テスト時間が短くなり、推論が容易になるという2点はなるほどと思った。

14.3.3 検証

手動

…略…

アサーション

…略…

A/B比較(差分)

…略… 意図された挙動は明示的には定義されていない。つまり人間が手動で差分を調べ、あらゆる変更が意図通りであることを確かめなければならない。

想像以上に、自動ではなく手動だった。

14.4 大規模テストの類型

ソフトウェア設計の一部は、テスト計画の草案を書くことであり、テスト計画の鍵となる部分は、どの類型のテストが各々どれだけの量必要かについての戦略的概要である。このテスト戦略が特定するのは、主要なリスクのベクター(vector: 侵入経路)とそれらのリスクベクターの影響の緩和に必要なテストのアプローチである。 Googleには「テストエンジニア」の専門職があり、優れたテストエンジニアとなるためには、Google製品用のテスト戦略の概要を策定する能力が資質として求められる。

Googleでもテストエンジニアとは大規模テストを縦横無尽に操作できるような役割が求められているとわかった。それだけ大規模テストは属人的であり、自動化が難しいというのがこれまでの内容からもわかる。

以下、類型ごとに文章があるが、まだ自分には使いこなせない箇所でもあるので、タイトルだけ残し、下線を引いた部分の抜粋やそれに対するコメントはやめておく。

14.4.1 相互に作用し合う1つ以上のバイナリの機能テスト

14.4.2 ブラウザーとデバイスのテスト

14.4.3 パフォーマンス、負荷、ストレスのテスト

14.4.4 デプロイ設定のテスト

14.4.5 探索的テスト

14.4.6 A/B差分リグレッションテスト

14.4.7 ユーザー受け入れテスト(UAT)

14.4.8 プローバーとカナリア分析

14.4.9 障害復旧とカオスエンジニアリング

14.4.10 ユーザー評価

14.5 大テストと開発者ワークフロー

誰が大テストを行うべきかについては語ってこなかった。 大規模テストを開発者ワークフローに統合する場合の場所として標準的なユニットテストのインフラストラクチャーは適していないかもしれないが、それでも開発者ワークフローへの統合は決定的に重要だ。

開発フローから独立したとしても大規模テストはやるべきと強く薦めている、ということだと思う。

14.5.1 大テストの作成

ユニットテストは、ネイティブ言語サポート(JUnitはかつて難解なものだったが現在は主流となっている)のおかげで容易に書ける。我々は、機能的インテグレーションテストにこうしたアサーションライブラリーを再利用している。

テスト範囲のより広いインテグレーションテストでもJUnitのようなライブラリーを使っている、ということだと思う。

14.5.2 大テストの実行

大規模テストがエンジニアにとって馴染み深い方法で実行されるようにしようと、我々は可能な限り試みてきた。 …略… しかし多くの大テストは専用設計であるがゆえに、必要に応じて実行する方法についての具体的なドキュメンテーションを必要とする。これが馴染みのないエンジニアを苛立たせる原因となることがある。

結局大規模テストに共通フレームワークはなく、教育コストが高い。

14.5.2.1 テストの高速化

内部システムのタイムアウトと遅延を減らす

…略…

テストのビルド時間を最適化する

目新しさは感じないが、これくらいしか打ち手はない、または、これが有効だとして生き残ってきたのだろう。

14.5.2.2 信頼不能性を排除する

信頼不能性はユニットテストにとっては既に十分に有害だが、大規模テストにおいては、信頼不能性のせいで利用不能になりうるほどの影響を受ける。 …略… 信頼不能性の最小化は、テストの範囲を狭めることから始まる。

クラスやメソッドの責任範囲を明確にして狭めて単一責任にしていくことに通ずると感じた。

14.5.2.3 テストを理解可能にする

開発者ワークフローへのテスト統合が難しくなりうる具体的な場合として、テストを実行しているエンジニアがテストにより生成される結果を理解できなくなっている状態がある。 …略… しかし大規模テストでは、こうした混乱は克服しようがないものになりかねない。

難しいものは強いエンジニアでも難しい、多分計算の難しさ系というより人間関係の難しさ系に近いのではないか。

失敗しているのが何なのか明確に特定するメッセージを備える

…略…

優れたエラーというものは、テストを実行する者がコードに馴染みがないことを予期し、背景情報を与える以下のようなメッセージを提供する。

…略…

不一致の根本原因を特定するために要する労力を最小化する スタックトレースは大規模テストには有用ではない。関数呼び出しチェーンが複数のプロセス境界にまたがる可能性があるからだ。

…略…

サポートとコンタクトの情報を提供する

テストから得られるヒント、人間からのサポートを最大化する。

14.5.3 大規模テストのオーナーとなる

大規模テストには、ドキュメントに記載されたオーナーが存在しなければならない。

…略… 然るべきオーナーシップを欠くと、テストは以下の問題の犠牲となる。

  • テストへのコントリビューターにとって、テストの変更と更新が難しくなる。
  • テストの失敗の解決に時間が長くかかるようになる。 そしてそのテストは朽ち果てる。

…略…

誰であれテストのオーナーとなる者は、テストの全体的な健全性を担保する権限を与えられていなければならず、またテストの保守をサポートする能力ならびにサポートを行うインセンティブの双方かなければならない。

いくつもテストを朽ち果てさせたのだろうか。

テストオーナーに裁量と評価を与えるべきとあるが、Googleでおこなわれていないというよりも広がっていない浸透していないと感じている、ということではないか。

14.6 結論

キーワードを抜き出すだけでも面白い。

「忠実性」「対象」「大規模テストが必要だ」

「複雑化し」「遅く」「適切なオーナー」「保守」

「可能な限り小さく」

14.7 要約

  • 大規模テストはユニットテストが扱えないものを扱う。
  • 大テストは、「テスト対象システム」、「データ」、「動作」、「検証」から構成されている。
  • 優れた設計には、リスク並びにそのリスクの軽減のための大規模テストを見極めるテスト戦略が含まれている。
  • 大規模テストが開発者ワークフローの中で摩擦を生むことを防ぐために、大規模テストには余分に労力を割かなければならない。

この上は特になじみがないが故に難しいと感じた。要約を転記してちらちらと見返せるようにしておく。

おわりに。14章を読んで

読むこと自体は2025年1月18日(土)に終わった。この章は普段の自分に馴染みがなく、納得感や現実に問題に直面したこととして捉えることが難しかった。よって、抜書きも、文章に対して思ったことというよりも、どちらかといえば要約となってしまうと思う。下線を引いた箇所へのコメントは、今から取り組む。

本書の、テスト、とりわけユニットテスト、自動テスト、に関わる章はこれで読み終わったと思う。他にも気になる章はいくつかある。ドキュメンテーションが特に気になるが、一旦別の本を読む、でもいいかなと考えている。

読書の今のやり方、印書的な部分に下線を引きコメントをつけるやり方について。わからないことに対して理解しようとするための読書方法、発見する、未知と遭遇しそれを深めていく、ということには不向きな読書方法と思った。

そうではなく、なんとなくわかっている事、経験した事、それでいてうまく言語化できていなかった事、を言語化し整理し納得し深めていく、ということはできると思った。

そして、引いた下線を抜書きしてコメントをつくのは、2月9日(日)に終わった。6日で読み、約20日を、読んだ後の記録に残す作業にかけたことになる。時間がかかりすぎと感じる一方で、下線をひいたところの前後を読み、そして内容を思い出そうとしたり、感想述べたりすることが自分自身の学習に役に立っているとも思える。今後の学習をどう進めていくか悩む。時間がかかりすぎている感覚からどうにか納得いく形に変化していければ良いと思うが、アイデアはない。

コメントを残す