モダンなテストレベル設計(ユニットテスト~システムテスト等をどう設計するか)の原則

 プロジェクト全体のテストを組み立てる際に重要な課題になるのが、テストレベル設計です。テストレベル設計は、ユニットテスト結合テストシステムテストといったテストレベルを、どのような責務・段取りで行うか分析・設計する活動です。

 このテストレベル設計ですが、ここ10年程度の間に望ましいアプローチが変わってきたと感じています。今回はこの変化と、変化後のモダンなテストレベル設計の原則について、考えていることを書き出したいと思います。

旧来のテストレベル設計のアプローチ

 旧来、このテストレベル設計では、Vモデルをベースしたアプローチや、自工程完結・品質積み上げをベースとしたアプローチがよく見られました。

 このうち一つ目のVモデルをベースとしたアプローチは、要求定義から設計までの上流工程への対応を観点に、テストレベルを設計するものです。
 (Vモデルが必須と明言しているわけではなく、極端な例ですが)例えば「要求定義工程があるので、その成果物の実現性を検証するためにシステムテスト工程を確保」「アーキテクチャ設計工程があるので、その成果物の実現性を検証するために結合テスト工程を確保」のように、開発工程と対となるテスト工程を用意するアプローチが、Vモデルを意識したプロジェクトの多くで取られます。
 またテストレベル設計は、IEC62304やA-SPICEなどのプロセス標準・プロセス規格に従って行う組織も多いと思います。そういったプロセスの標準・規格は大抵の場合Vモデルをベースにしているので、実質的にこのアプローチとなっていることが多いです。

 二つ目の自工程完結・品質積み上げをベースとしたアプローチは、「自分たちチームの成果物の品質は自分たちが責任をもって確保・保証する」「構成要素ごとに品質を確保・保証する」という考え方でテストレベルを設計するものです。
 例えばチームがシステムを構成する特定のコンポーネントを担当する場合、コンポーネントに対するユニットテスト結合テストを自チーム内に確保し、担当コンポーネントの品質確保・品質保証を自分たちでしっかり行うようにします。
 チームが実装工程を担当するならば、ユニットテスト工程を確保し、実装仕様の実現性保証や、実装に起因するバグの取りきりをチーム内で行います。
 このアプローチにより、分担した成果物の品質確保・品質保証をそれぞれの担当組織・構成要素で行い、品質を積み上げることで、全体の品質を向上させていくアプローチとなります。
 SoS開発や複数会社での開発では、組織構造に合わせてテストレベルを設計するアプローチが一般的に取られますが、それもこのアプローチの一種と言えます。

 こうした旧来のテストレベル設計では、テストレベルごとにある程度独立性をもって、それぞれのテストの責務を果たしていくアプローチが好まれます。

開発技術やプロセスの発展とテストレベル設計

 上記のアプローチは現在でも陳腐化したわけではありません。ただ開発技術やプロセスの発展で、より柔軟なアプローチを加えることが可能になっていると感じています。

 その発展の一つが、自動テスト手段の充実です。ここ十年、ユニットテスト結合テスト、End to Endテストのツールが多数生まれ・普及しています。Flutterのような便利な自動テスト機能を自身に持つ開発フレームワークも増えています。特に、最近は費用対効果の高い結合テスト・End to Endテストの自動化手段が、複数の分野で充実・普及してきました。
 これにより、テストレベルごとの有効性や費用対効果の差が明確に大きくなりました。特に、旧来の手動のシステムテストに対し、現代的な自動化を行ったシステムテスト結合テストの有効性、アジリティ、費用対効果がかなり大きくなりました。
 結果、Vモデルや自工程完結のアプローチだけでテストレベルを設計するだけでなく、費用対効果の高いテストレベルの責務を増やし、費用対効果の低いテストレベルの責務を減らす設計アプローチが、開発の生産性を高めるのに有効になりました。

 発展の二つ目は、継続的デリバリやDevOps、またはDORA Four Keysを追及するような、開発のリードタイムを重視する開発プロセスの普及です。
 この開発プロセスでは設計、実装、静的テスト、動的テスト、インテグレーション、デリバリまでの一連の開発のリードタイムを高速化し、ビジネスと開発のフィードバックサイクルを軽快に回せる体制・プロセス・環境を構築することで、プロダクトの価値を高めていきます。例えばユーザのニーズ・シーズの取り込みの速さ・バグ修正の速さを高め、プロダクトの競争力や高品質を保ちます。
 この開発プロセスにとっては、旧来の大規模な手動テストは、遅すぎ・コストがかかりすぎで、リードタイムを大きく悪化させ、プロダクトの価値を棄損します。
 そこで、リードタイムの短いテストレベルでなるべくテストを済ませ、遅いテストレベルはできるだけ排除するアプローチが採用されやすくなりました。

 最後の発展は、CI/CDによるデプロイメントパイプラインの充実です。
 デプロイメントパイプラインの作業を手動で行っていた時代では、工程移行時にバグが混入するリスクが少なくありませんでした。例えばユニットテスト後に、マージ・インテグレーションする作業でバグが混入し、後段のシステムテストで見つかる、といったものです。またテストレベルごとにブランチやバージョンが異なり、その差異から前段のテストでバグを見つけられない、といった状況もありました。こうしたリスクから、後段のテストの責務を前段のテストに移動させるアプローチには、バグ流出のリスクが付随しました。
 ただCI/CDのデプロイメントパイプラインが普及したことにより、同一のバージョン・ブランチに一括して複数のテストレベルを実行したり、工程移行時のバグ混入をなくしたりすることが可能になり、テスト責務の移動のリスクを軽減できるようになりました。
 これにより、テストレベルの本来の能力に基づいて、テストの責務を移動させるアプローチを柔軟に取れるようになりました。

モダンなテストレベル設計の原則

 この自動テストの発展、リードタイム重視の開発プロセスの普及、CI/CDデプロイメントパイプラインの充実により、テストレベル間でのテストの責務調整を柔軟に実施できるようにする後押し・環境が生まれました。
 これにより遂行可能になったのが、次の原則に基づいてテストレベル設計を行い、開発のアジリティや生産性を向上させるアプローチです。

  • もっとも効果的なテストレベルの責務を最大化する
  • それぞれのテストレベルの強みを最大化し、弱みを最小化・補完する
  • テストレベル間のテストの重複は、全体が良くなる方向で削除する

 一言で言うならば、「全体が良くなるように、個々のテストレベルの責務分担を最適化する」という原則です。

 例えば、CI/CDに組み込まれた自動テストとそれ以外では、変更対応を中心に、テストのコスト・アジリティ・費用対効果が大きく変わります。そこで、複雑な組み合わせやロジックのテストは自動テストで済ませ、手動テストは、ユーザビリティテストなどどうしても手動でなければならないテストを除いて、探索的テストで済ませるようなアプローチをとる、といった方針がこの原則に乗っ取った設計方針になります。

 当たり前に見える原則ではありますが、旧来と違うのは、テストの責務移動と、テストの責務重複の削除を大胆に推奨するということです。結合テストが一番効率的なら、組み合わせは結合テストでテストして、システムテストからは組み合わせテストを削除するといったものです。

 余談ですが原則中の「もっとも効果的なテストレベル」として、ユニットテストを選び、テストピラミッドのテストレベル設計を行う事例をよく見ます。ただ現代的な開発では、ユニットテスト以外も有力な候補になっています。マイクロサービスアーキテクチャのようなサービスの群体ならAPIテストが有力です。FlutterといったモダンなFEフレームワークを作っているなら、フレームワークが提供する結合テストが有力です。
 またユニットテストは、メリットを多く持つものの、次のようなデメリットも持ちます。

  • プロダクトの価値から離れる。魅力的なユーザビリティや、競争力のある高度な機能といった、プロダクト価値は、ユニットレベルのような細分化された世界では意識しにくくなります。プロダクト価値を意識せず、コードカバレッジを満たせばよい、のようなやり方を取ってしまう場合もあり得ます。
  • プロダクトコードに強結合する。コードの細部にテストが依存するため、変更・保守を行う手間が発生します。例えば機能の影響のないリファクタリングでもテストが壊れるといった状況対応です。

 このようなデメリットを加味して、注力するテストレベルを選択すべきといえます。テストピラミッドでなく、APIテスト等結合テストを主体としたテストトロフィーを志向したほうが良い開発も少なくありません。

テストレベル間でのテストの責務の移動を支える基礎

 上記のモダンなテストレベル設計の原則を実現するには、テストレベル間でテストの責務を安全に移動させるための基礎作りが不可欠になります。基礎作りの中で重要なものを次にいくつかピックアップします。

契約による設計(またはそれと同等の責務設計)を推進する

 システムテストの責務を結合テストに移動させる場合に重要になるのが、システムの機能や処理を、コンポーネントのどこが責務として担っているのか明確にすることです。

 例えば以下の事例を考えます。

  • 複雑な入力バリデーション(入力の形式、数、フォーマット、組み合わせは正しいか等チェックする機能)をテストする。
  • 対象システムはフロントエンドレイヤとバックエンドレイヤで分かれている
  • システムテストと、フロントエンド結合テスト、バックエンド結合テストのテストレベルを実施している。入力バリデーションはシステムテストでテストしている。

 この入力バリデーションのテストの責務を結合テストに移動させる場合、入力バリデーションの責務がフロントエンドで行うか、バックエンドで行うか、設計で保証されている必要があります。
 仮に入力バリデーションの責務がフロントエンドにあると保証されているならば(契約による設計の言葉を使えば、入力バリデーション済みの状態が、フロントエンドの事後条件とバックエンドの事前条件に指定されているならば)、システムテストから、フロントエンド結合テストへ、入力バリデーションのテストを移動できます。
 しかし入力バリデーションの責務が曖昧だったり、レイヤ横断的に実装されている場合は、システムテストから結合テストへの移動は、リスクができず実施できなくなります。

 ですので、 契約による設計、またはそれと同等のアーキテクチャレベルの責務設計は、テスト責務を柔軟に移動させるアプローチにとって不可欠になります。

デプロイメントパイプラインの信頼性を挙げる

 CI/CDのデプロイメントパイプラインの信頼性を挙げて、安全にテストレベル間でテストの責務を移動できるようにするのも重要です。
 例えば各テストレベルで、各テストレベルのテスト条件(例えば対象のブランチ、バージョン、インテグレーション状態)を一致させるようにパイプラインを組むのは有効です。またテストレベルを移行する間のバグ混入を最小化するのも有効です。
 こうした信頼性向上策は、後段の長大なテストレベルの責務を前段のテストレベルに分散するアプローチを採用可能にします。

アーキテクチャレベルで凝集性を上げ、結合性を下げる

 システムテストのような包括的なテストレベルの責務を、結合テストユニットテストに移動させるアプローチでは、システムを結合することで発生するリスクを減らす対策が必要になります。
 これには、一般的に求められる凝集性を上げ(前述の契約による設計もこのためのアプローチになります)、結合性を下げるアーキテクチャ設計が有効です。例えば「コンポーネント間の結合部の並行処理のリスクを下げることで、システムを結合しないと再現できないバグを減らす」「コンポーネントの凝集度を上げ責務を明確化することで、多数のコンポーネントを横断させないとテストできない機能を減らす」といった設計の工夫の積み重ねが、テスト責務の移動を安全にします。

カオスエンジニアリングのプラクティス:スクイーズテスト(Squeeze Test)とは

カオスエンジニアリングや継続的ベリフィケーションの解説で目にすることのあるテストプラクティスに、スクイーズテスト(Squeeze Test、圧迫テスト)があります。ただこのスクイーズテストはカオスエンジニアリングの一例として名こそ取り上げられているものの、現時点で普及は一部にとどまっているため、それが何なのかの解説があまりなされていない状態です。

今回はこのスクイーズテストについて、オープンにされている文献と概要をまとめます。

一般的なスクイーズテストの用例について

本来、スクイーズテストは医療用語です。特定の部位を圧迫して対象者が痛みを感じるかで、靭帯や関節の損傷を推定する評価手法を指す言葉となります。
また、別の用例として、一定の力で絞った時の水の滲み具合から、対象の含水量を推定する評価方法の呼び名としても使われています。
今回のソフトウェアテストとしてのスクイーズテストの名前は、上記の一般的な用例のニュアンス(少し負荷を加えて得られた結果から、真に知りたい情報を推定する)を意識して命名されたと思われます。

スクイーズテストの解説文献について

現時点でのスクイーズテストのメジャーな日本語解説は、「カオスエンジニアリング 回復力のあるシステムの実践」(Casey Rosenthal他)に頼ることになります。この書籍では、カオスエンジニアリングや継続的ベリフィケーションを説明するための具体例としてスクイーズテストの名前を挙げています。
内容として、書籍では従来のテストやリアクティブな評価(発生した問題を見つける評価)の対に、実験やプロアクティブな評価(問題を推定・先見する評価)があり、そのプラクティスの一つがスクイーズテスト、のような説明を行っています。
また原文ではなく訳注ですが、スクイーズテストに次のような説明がなされています。

Netflix で行われる負荷テストの一種で、ベンチマークテストをはじめとする検証を行いながらパフォーマンスの変化を観察し、アプリケーションがどこまで耐えられるかを試算するもの。


次にすこし古い資料に以下の言及があります。

http://alexanderpodelko.com/blog/2014/02/11/load-testing-at-netflix-virtual-interview-with-coburn-watson/

One practice which isn’t yet widely adopted but is used consistently by our edge teams (who push most frequently) is automated squeeze testing. Once the canary has passed the functional and ACA analysis phases the production traffic is differentially steered at an increased rate against the canary, increasing in well-defined steps. As the request rate goes up key metrics are evaluated to determine effective carrying capacity; automatically determining if that capacity has decreased as part of the push.

前後の文も合わせて要約すると、スクイーズテストは本番環境上でのカナリアテストとして実施されるものであり、特定のトラフィックの負荷をかけ、その負荷の変動によってインフラリソースのメトリクスがどう変化するかを評価して、必要なリソース量を推定するもの、という説明がなされています。


更に古いプレゼンテーションの解説記録として、以下の文献があります。

http://blog.mattcallanan.net/2013/12/yow-netflix-workshop-11dec2013.html

New instances are also put through a squeeze test before full rollout to find the point at which the performance degrades. This is used to identify performance and throughput changes of each deployment.

前後の文も合わせて要約すると、スクイーズテストとは、パフォーマンスを悪化させるポイントを見つけるため、本番環境上で動かすインスタンスに負荷をかけてパフォーマンスやスループットの変化を調べるもの、という説明がされています。

スクイーズテストとは

ウェブで公開されている文献を総合するとスクイーズテストは以下のテストになります。

  • カナリアテストである。本番環境のインフラリソースは大丈夫か、対象のデプロイが本番環境のパフォーマンスに悪影響を及ぼさないかを評価することを目的とする。
  • リアクティブ(発生した問題を見つける)なテストというより、プロアクティブ(発生しそうな問題を推定する・先見する)な評価に位置づけられる。
  • 本番環境上の特定の領域に負荷をかけ、負荷をかけた時の性能効率性のメトリクスの変化量を計測する。変化量から、目的とするリソース量の必要量や、性能効率性の問題点を推定する。

スクイーズテストは、割と古くからあるものの、普及せず一部企業のみで使われてきたプラクティスです。ただカオスエンジニアリングが国内で認知され普及しつつある現状を見ると、その実践プラクティスの一つとして、国内でも使われ始める用語ではないかと思います。

自動テストを支えるテスト設計について講演

先日、JaSST'23 Tokaiというテストのイベントで「自動テストを活躍させるための基礎作りとテスト設計の工夫」と題して講演する機会を頂きました。

https://speakerdeck.com/goyoki/improvements-and-test-design-for-effective-test-automation

今回はシンポジウムの委員の一人の方から、テスト自動化に必要な基礎について話してほしい、特にテスト設計の必要性について話してほしいと要望いただいていたため、それをメインテーマに過去資料の内容をカスタマイズして話させていただきました。

なお自動テストを支えるテスト設計は、業界としてまだまだ改善の余地がある分野だと感じています。
ユニットテストをはじめとする開発者テストについては、どのようなテスト設計方針を示してテストを実現するか、すでに多数の実践や知見がたまっています。ただ一方で、システムテストや粒度の大きな結合テストについては、適切なテスト分析・設計をどう実現するかは迷っている現場が多いと感じています。そこでどうすべきかについて、今回の講演が何かしらの一助になれば幸いです。

SQuaRE、ISO/IEC 25010の製品品質モデルの改訂動向

SQuaRE、ISO/IEC 25010についてですが、標準規格の担当WGの方が、2022年の3月のタイミングで審議中の改訂情報に触れていました。

https://speakerdeck.com/washizaki/squareguan-lian-falsebiao-zhun-hua-falsequan-ti-dong-xiang-25010-25019gai-yao-ip-shan-jun-bo?slide=19

ISO/IEC 25010の製品品質モデルに限定して抜粋すると、主に以下のような改訂予定が説明されています。

  • 主特性にSafetyを新設。FailsafeやHazard warning、Safe integrationなどを副特性に配置
  • 移植性(Portability)をFlexibilityに変更。Scalabilityを副特性に追加
  • 使用性(Usability)をInteraction Capabilityに変更。Self-descriptivenessを副特性の追加
  • その他適宜の副特性の追加や名称変更を実施(Maturity→Faultlessnessの名称変更など)

このうち、移植性の改訂は、時代の変更に沿った良い改訂だと思います。

上記資料には10年スパンで改訂しているとありましたが、ここ10年で確実にソフトウェア開発を変えたのがインフラ関連の分野です。実行環境はAWSなど数が絞られたIaaSに構築することが当たり前になり、外部のSaaSをサービスの構成機能として活用することも普通になりました。Dockerやk8sといった仮想化技術が発達し、実行環境のコンテナ化も広く普及しています。例えば最近技術界隈で24時間以内にインフラ総取り換えをしてサービスを復旧させた事例が話題になっていましたが、これはインフラ分野が進化した現代ならではの出来事だと感じました。

振り返ると、現ISO/IEC 25010のインフラ関係の品質特性は、カスタマイズされた環境にカスタマイズしたプロダクトを設置する、旧来のスタイルに沿ったモデルになっていると感じます。仮想化で重視される冪等性や、IaaSやコンテナ・オーケストレーションで重要になるスケーラビリティといった、前述のインフラ環境の変遷を踏まえた品質観点では使いづらさがあります。上記で提示された改訂は、この使いずらさの是正に有効なものだと思います。
ただ、現在の実情を鑑みるとインフラ関連の改訂の程度がまだまだ甘い・不足しているようにも感じますので、この改訂ではさらに変更が大きくなる可能性もあると思います。

一方Safetyの追加は、日本人のWGらしいなあという印象です。存在意義は感じるものの、既存の信頼性と区別が曖昧な部分があり、それをどう整理してくるのかが気になりました。

10年スパンで改訂と資料中で説明されているため、改訂版は今年出てくるのではと思います。現場にとって使いやすいモデルになりそうなので、キャッチアップする価値があると感じます。

探索的テストの力を引き出す段取り、そしてTEXの改善

 これまで色々な立場で、色々なテスト案件を経験してきましたが、その中で一番生産性が高いテストアプローチが、精鋭テストエンジニア達による探索的テストでした。
 適切なタイミングで、必要な環境と必要な人材が揃った探索的テスト部隊がテストを始めると、猛烈な勢いでバグが見つけられ、品質リスクが潰されていきます。
 ただ探索的テストは流動的な要素が大きく、段取りや準備によって、効果やコスト・労力が大きく変動するのにも注意が必要です。
 今回は、そうした探索的テストの効果を引き出すための段取りや準備の経験則をまとめます。

探索的テストの効果を引き出す段取り

テスト環境確保を工夫する/テスト環境に応じてテストアプローチを工夫する

 探索的テストのスコープは、テスト環境の制約で制限されることが経験的に多いです。例えば機材不足や本番環境との差異でテストできないといった状況です。探索的テストのスコープを広げ、その効果を確保するためには、そのテスト環境の制約の緩和が不可欠になります。
 この実現アプローチとしては、早期にテスト分析を行って必要な環境を洗い出し、その環境を適時に使用できるように計画を組むのが重要になります。その手段の一つとして、スクリプトテストと探索的テストを並走させ、前者のテスト分析を早期から実施する形をよく実践します。
 
 また事前に段取りや準備に力を入れても、テスト環境に制約が出る場合があります。その際は、環境制約に応じた探索的テストの段取りの工夫が有効になります。例えば「本番環境の確保が終盤まで難しく、試作環境に頼らなければならない」のような状況なら、各環境ごとにどのようなテストをすべきかの段取りを事前に工夫することで、探索的テストの活躍どころを確保できます。

探索的テストの活躍どころを計画で確保する

 探索的テストにとって適時性は重要です。自分の経験として、早すぎるタイミングで探索的テストを実施してバグを大量に見つけた結果「作りかけだからそのテストは無駄」「バグを報告しないでほしい」などとフィードバックされたことあります。まだ逆に遅すぎるタイミング(最終盤のデバッグフェーズの空き時間でテストするなど)で探索的テストを実施した結果、そこで見つけたバグのデバッグが間に合わず、プロジェクトが遅延するといったこともありました。

 探索的テストの適時性を確保するためには、開発と連携して、適切なタイミングで適切なスコープの探索的テストを実行する期間を確保し、それを明示的に計画に組み込むのが重要になります。

 そこで探索的テスト期間は開発スケジュール中で明示的を設けるのが有効です。スクリプトテストのみを工数確保し、探索的テストは合間時間にやる計画を組むと、探索的テストの工数が流動的になるほか、理解が深まっていない間は遅延時に探索的テストを無駄な作業とみなす圧力が外部からかけられる場面に遭遇しがちです。

必要な知識と能力をチームとして確保する/学習機会を確保しチームを育てる

 探索的テストの効果は属人的であり、テスト対象や、それを取り巻くドメイン、ビジネス、品質についての、属人的な知識や能力に依存します。例えば派生開発なら派生元のプロダクトの知識が重要になります。
 そのため探索的テストを活用しようとすると、相応の知識・能力を持った人材確保が必要です。必要なすべての能力と知識を持つ万能人材を獲得するのが難しい場合は、様々な人を集めて、探索的テストチーム総体として能力・知識を確保していくアプローチをとります。

 さらに、知識・能力ある人の獲得のほか、プロジェクト中に必要な知識・能力を高める機会を設けて、探索的テストチームを育てていく段取りも有効です。
 例えば上流仕様のレビュー機会を設けて仕様理解を深める、プロトタイピングや反復開発で早期からテストして必要な知識・能力のフィードバックを得る、職種をローテーションして設計知識を深める、といった段取りです。そうした段取りを開発計画に組み込むと、探索的テストチームの知識・能力が高まり、探索的テストの効果向上につながります。

正しい方向にテストを方向付けする/フィードバックサイクルを回して方向性を正す

 探索的テストは流動的で、様々な要因で効果が変動します。悪い方向に倒れれば、モンキーテストと変わらなくなり、時間やコストを浪費する場合もあり得ます。
 そのため探索的テストの実施では、適切なテストへの方向付けが求められます。この方向付けには、次の2つがあります:

  • テスト分析を実施してテストチャータを作成する、セッションベースを取り入るといった形で、テスト実施の方向性を明示化する。
  • 実施したテストを評価して、それに基づいて方向を正しい方に改善する改善フィードバックサイクルを回す。テストを繰り返し実施したり、エラーシーディングを行ったりして、欠陥流出や生産性を評価し、自分たちのテストが妥当だったか確認して、問題があれば是正していく。

テストへのニーズ・シーズを継続的に把握する

 探索的テストは様々なテストのニーズやシーズに柔軟に対応できます。テストのニーズ・シーズを的確に把握すると、探索的テストを活躍させるチャンスを増やせます。

 テストのニーズやシーズは、例えば「開発中に特定のコンポーネントの品質が悪いことがわかり、それ起因のトラブルに困っている」「ユーザ要求を正しく認識できなかった可能性が出てきた」「特定のコードから不吉な臭いを感じた」などと、プロジェクト進展中にどんどん生まれ、移り変わっていきます。
 そのため、開発ライフサイクルを通じて、テストのニーズ・シーズを継続的に監視し、逐次探索的テストを投入していくアプローチが有効になります。

効率化のためのテスト技術を蓄積する

 探索的テストの適用可能な領域は広く、それに付随して様々な効率化手段を活用できる余地があります。
 その主な手段に自動化があります。煩雑な作業を自動化して、テストエンジニアの作業をより重要なテストに集中させる、手動で困難な作業を自動化で実現して探索的テストで活用する、といったアプローチは、探索的テストの過程で数多く活用できます。
 そのため、探索的テストを効率化する技術や手段を蓄積し、探索的テストで活用していくアプローチを組むと、技術蓄積に応じて、探索的テストのスコープをより広く、生産性をより高く改善できるようになります。

補足:TEX(Test Engineer eXperience)と探索的テスト

 上記で上げた段取りは、いうなればTEX(テストエンジニア体験:Test Engineer eXperience)を改善するための施策ともいえます。
 探索的テストの効果は、テストエンジニアの能力を発揮させることで生まれます。そのため、テストエンジニアの能力の発揮しやすさ、すなわちTEXは、探索的テストの効果や生産性に直結すると言ってよいと思います。
 そのため上記で上げた段取りの工夫に限らず、TEXを向上させることが、探索的テストの効果向上の基本と言えます。

テスト自動化の事後(影響、評価、ネクストステップ)について講演

先日、QuesというソフトウェアQAをテーマにした勉強会に「テスト自動化の成果をどう評価し、どう次につなげるか」と題して登壇させていただきました。

docs.google.com

「テスト自動化をした後」をテーマにしてほしいとの要望を頂いていたため、今回は以下の3パート構成で話させていただきました。

  • テスト自動化によってどうなるのか
  • テスト自動化をどう評価するか
  • テスト自動化をどう次につなげるのか

参加者として日頃から楽しんでいたイベントに、登壇側として参加できたのは感慨深かったです。運営者の方、参加者の方、大変ありがとうございました。

GoogleTestとSanitizerを組み合わせて動的解析

Calendar for ソフトウェアテストの小ネタ | Advent Calendar 2022 - Qiita」の記事です。

C++のメジャーなテスティングフレームワークGoogleTestは、gccやclangに組み込まれたSanitizerと連動することで、不正なメモリ操作や不適切なスレッド間データ共有、リスクある未規定処理の実行などの異常を、テスト上で検出できるようになります。今回はそのGoogleTestとSanitizerの連携で、コードレベルの動的解析の環境を構築する例を解説します。

Sanitizerとの連携

GoogleTestのテストコード上で以下の関数を定義することで、Sanitizerがエラーを検出したときの処理をHookできるようになります。

  • address sanitizer :void __asan_on_error()
    • 不正なメモリ操作を検出
  • behavior sanitizer : void __ubsan_on_report()
    • 致命的なエラーや例外を発生させる不正な動作を検出
  • thread sanitizer : void __tsan_on_report()
    • スレッド間の不適切なデータ共有を検出

例えばGoogleTestのテスト実行中、Address Sanitizerが異常なメモリ操作を検出した際に、特定の文字列を出力するとともにテストをFailさせたいならば、以下をテストコード上で定義します。

extern "C" {
    void __asan_on_error() {
      FAIL() << "address sanitizer error!!!!";
    }
} // extern "C"

注意点として、Sanitizerが異常を検出したタイミングでテストコードは強制終了されます。その際、--gtest_outputによる結果ファイル出力は動作しません。そのため、CIなどで自動実行する際は、XMLなどの出力ファイルでなく、コンソールログ解析でテスト結果を判定する必要があります。

実装例

以下のテスト対象をテストするとします。範囲外への不正なメモリアクセスを行っています。

int target() {
    int a[5];
    return a[5];
}

通常のユニットテストでは、上記を実行した時のふるまいは不定です。ただ特に何もなくテストが終了する場合もあります。
ここで上記のような不正なメモリアクセスをSanitizerで見逃しなく検出させる場合、テストコードを次のように記述します。

//test_hoge.cpp
#include <gtest/gtest.h>

...

extern "C" {
    void __asan_on_error() {
        FAIL() << "Encountered an address sanitizer error";
    }
}  // extern "C"

TEST(TestCase, dummy) {
    EXPECT_EQ(0, target());//サンプル例示のための適当な確認
}

このテストコードをビルドする際に、Address Sanitizerを有効化します。例えば次のようなオプションで記述します。

g++ test_hoge.cpp -lgtest_main -lgtest -lpthread -fsanitize=address -o test_hoge

これを実行すると、a[5]にアクセスしたタイミングで、「Encountered an address sanitizer error」が出力され、テストがFailします。具体的には以下のようなメッセージが出力されます。

[==========] Running 1 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 tests from TestCase
[ RUN      ] TestCase.dummy
=================================================================
test_hoge.cpp:8: Failure
Failed
Encountered an address sanitizer error
==468==ERROR: AddressSanitizer: stack-buffer-overflow on address
... 以下、不正メモリアクセスを検出した旨のSanitizerの出力メッセージ ...

上記のようなアプローチで、Sanitizerの機能を使った動的解析を、GoogleTestのテストコードとして記述できるようになります。