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

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

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

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

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