🧪

自動テストにおけるテストピラミッド

2023/07/16に公開

はじめに

本記事は、自動テストにおけるテストピラミッドについて、調べた内容をまとめたものである。
一部、調べた内容を踏まえた筆者の考えを含み、その場合は、記述の前にその旨を明示する。

自動テストにおける教訓

初めての自動テスト - Webシステムのための自動テスト基礎』では、自動化したUIテストを万能な自動テストとしたプロジェクトでの失敗から得た、以下の3つの教訓を示している。

  1. すべての自動テストは同等の力を持っているわけではない。あるテストをする場合に、特定の種類のテストの方が、他のテストより優れていることがある。
  2. ある種類のテストを「書くことができる」というだけでは、必ずしもそのテストを「書くべき」だという理由にはならない。
  3. スピードとフィードバックが重要。テストケースの実行に長い時間がかかるほど、開発サイクルは遅く、反復回数は少なくなる。

さらには、以下のような学びも示されている。

自動テストが1種類でどんな場合にも通用するような、万能薬ではない。テストにはさまざなまな種類があり、それぞれがテストするものが異なっている。

テストピラミッド

上記のような教訓は、度々さまざまなチームやプロジェクトで得られ、有用な概念として形成されたのが、テストピラミッドである。Succeeding with Agile: Software Development Using Scrum (Mike Cohn, 2009)で最初に提唱されたモデルである。以下の異なる3つのテストレベルの自動テストの「テストケース数の望ましい比率」をピラミッド型で表したものである。

テストピラミッド

テストピラミッドにおいて、ピラミッドの幅は、テストケース数を表す。一般に、上位のテストレベルであるほど、テストの記述および実行のコストとテストの忠実性(実際の挙動の反映度合い)が高くなる。一方で、テストの実行速度と、決定性(テストが毎回同じ結果を出力する度合い)は、低くなる。これらの指標をピラミッドの縦方向で示す。開発速度と信頼性の高いバランスが得られる自動テストのケース数の望ましい比率として、上位のテストレベルのテストケース数を少なくし、下位のテストレベルのテストケース数を多くすることが知られている。テストピラミッドは、自動テストにおけるテストレベルごとの理想的なテストケース数の比率を表している。
テストピラミッド ~自動テストの信頼性を中長期的に保つ最適なバランス~[1]では、「テストピラミッドが最も大事にしているのは、自動テストの信頼性を中長期的に保つこと」と説明している。すなわち、前述のようなピラミッドの各段の構成比を適切に保つことが必要となる。

テストレベル

ここでは、各テストレベルのざっくりとした定義と、各レベルでのスコープや役割について記述する。

  • ユニットテスト
    テスト対象をメソッドレベルなどの最小単位に分割し、その最小単位の振る舞いをテストする。ユニットテストの特長として、高速に実行でき、決定性が高く、迅速なフィードバックが得られることが挙げられる。また欠陥がある箇所を明確にできる。
    ユニットテストは、開発者がシステムに機能を追加するたびに書くものである。ユニットテストを書く上で、基本的な方針は「壊れる可能性のあるものはすべてテストする」ということである。extreme programmingの格言で、「妥当だと思える範囲、本当に重要な部分は可能な限りテストしなければならないが、一方で本当にすべてのテストはできないことを意識しておく」といったことを意味する。
  • インテグレーションテスト
    ソフトウェアもしくは、アプリケーションの複数の層が一つにつながって動いていることを確認するテスト全般のことを指す。テストレベルの定義としては、曖昧さがある。
    このテストレベルの観点は、ユニットテストではカバーしきれない隙間の部分と大まかなつながりを確認することである。
    ユニットテストでは、アプリケーションやソフトウェアの基盤を構成している個別のオブジェクトをテスト対象にしているのに対し、インテグレーションテストでは、それらのオブジェクトが互いにどのようにつながっているか(オブジェクト間の値の受け渡し)をテストする。
  • UIテスト
    システム全体をエンドツーエンドで操作し、ユーザーが実際にそのシステムもしくはアプリケーションを使用するときと同じ振る舞いをテストする。アプリケーションのすべての層をテストすることになるため、接続性のテストとして優れており、スモークテストとして使用される。一般に、欠陥がある箇所は、明確でない。また、テストの実行に時間がかかる。
    一般に、UIテストは、自動テストの中で最も構築や保守に手間がかかり、実行時間も最も長い。UIの開発がある程度落ち着いた時点で、UIテストを書くことが望ましい。UIの変更があるたびに書き直す必要があるため、UIの変更が頻繁に発生する開発の初期段階では、テストを書くべきではない。
    また、UIテストは、一般に決定性が低いため、テスト実行の動作が不安定な「不安定なテスト」(後述)となる。
    一方で、構築と保守にかかる多大なコストを勘定した上でもなお、状況によっては、UIテストは、極めて価値の高いものになる。例えば、継続的、かつ定期的にプロダクトをリリースするためには、同様のサイクルでテストが必要となり、手動でテストを実施するためには、多大なコストがかかる。または、高度な安全性を必要とするプロダクトの場合、たった一つのUIのミスにより、多額の損失につながる可能性がある。これらのような場合、UIテストは、非常に価値の高いものになる。

不安定なテスト

実行結果に信頼性をもつことができないテスト。すなわち、プロダクトコードに変更がないにも関わらず「実行するたびに異なる結果を返すテスト」や「ときどき失敗するテスト」のことを指す。
すべての自動テストは、一貫性が求められ、実行するたびに毎回まったく同じように、高い信頼性をもって動作する必要がある。
テスト結果が不安定な場合、失敗するたびに、実行結果を確認し、その真偽を確認したり、テストを再実行したりと、時間やコストを浪費してしまう。また、テストの信頼性の低下につながり、最悪のケースでは、テスト自体が実行されなくなる。
以下に不安定なテストへの対策を3つ挙げる。

  • テストを書きなおす。
  • 信頼性の高い下層のテストレベルに移動する。
  • 価値のないテストとみなし、他の方法でリスクをカバーできないか検討する。不安定さがあるテストを保守する価値があるとは限らない。その保守や改修作業に大きな負担かかるようになっているのであれば見直す。

逆ピラミッド

多くのUIテストを持ち、ユニットテストが存在しない、もしくはほんのわずかしか存在しないシステムのテストピラミッドは、逆三角形になる。このようなテストピラミッドを、逆ピラミッドやアイスクリームコーンと呼ぶ。
UIテストは、そのテスト内容がユーザー視点に近く、検証するコード範囲も最も広く書けるため、これらのテストだけですべての範囲を網羅できると錯覚してしまう。しかし、前述のとおり、UIテストは、決定性が低い不安定なテストであるため、失敗時の原因究明を困難にし、テストの実行結果を次第に信頼しなくなる。また、テストの実行結果を安定させるためにかかるコストも大きい。さらにテスト実行時間も長いため、テストの実行頻度が低くなり、自動テストのフィードバックまでに時間を要してしまう。
しかし、上位レベルのテストはすべて不要というわけではない。たとえばユニットテストだけが書かれている状況においては、開発したプログラム同士や外部サービスとの連携などが自動テストの範囲に入っていないことになる。この場合、ユニットテストがすべて成功しても、デプロイした本番環境ではシステムが望む動作を保証することができない。
つまり、自動テストの粒度において、すべての層でテストを書いていく必要があり、それらの望ましいバランスを考える必要がある。逆ピラミッドを解消するためには、上位のテストレベルでのテストを下位のテストレベルに移動させることが必要になる。新しくテストコードを追加する際も、ユニットテストのレベルで実施できないかをまず検討する。これにより、テストの実行時間が短縮され、テストの実行頻度が上がり、フィードバックが早くなる。また、テストの信頼性も高まり、テストの保守コストも下がる。

テストスコープ

テストレベル間での重複

上位のテストレベルのテストは、下位のテストレベルのテストの上位集合のため、同じ機能をテストすることになる。一見テストが重複しているように見える。しかし、各テストレベルでの意図や目的が異なれば、同じ機能に対するテストが重複していても全く問題ない。

異なる立場からみた自動テストの目的

自動テストの目的に対する認識は、開発者とQA担当者という2つの異なる立場からみると、以下の表のように異なる。

開発 QA
自動テストに求めること スピード 正確性
自動テストを用いる場面 開発 検証
自動テストで網羅したい範囲 必要最低限 すべて
精神 チャレンジを許す・攻めの姿勢 冒険をしない・守りの姿勢

テストの分類

テストレベルによる分類の問題点

UIテスト、インテグレーションテスト、ユニットテストなど、各テストレベルのどのようなテストを実施するかの認識、解釈が人やチーム、組織によって異なる。「⁠1つの対象」を検証する狭いテストをユニットテスト、単体テスト、コンポーネントテストなどと呼ぶが、これらをほぼ同じものと言う人も、異なると言う人もいる。「⁠1つの対象」も関数、メソッド、クラス、モジュール、パッケージ、振る舞い、1つの画面とバラバラである。複数のレイヤ、たとえばコントローラとモデルをまたいで検証するテストをインテグレーションテストと呼ぶ人もいれば、それもユニットテストと呼ぶ人もいる。ユニットテストは、データベースに触ってもよい/触ってはいけないとそれぞれの考えの人がいる。
これらの解釈が間違っているということではないが、明らかに定義はブレ、矛盾しているものも多くある。これらのテストレベルの認識の違いは、テストレベル(検証の対象となるプロダクトコードの範囲や粒度)によるテストの分類において混乱をもたらす。

テストサイズ

テストサイズとは、Google 社内から広まり始めたテストの分類[2]である。テストレベルによる分類に比べ、あいまいさの少ない一貫した基準である。自動テストに使用されるリソースの量や実行場所、実行時間に着目した分類で、Small, Medium, Largeの3つのサイズに分類される。分類の指標を、以下の表に示す。

Feature Small Medium Large
ネットワークアクセス × localhost のみ
データベース ×
ファイルシステムアクセス ×
外部システムの使用 × 非推奨
マルチスレッド ×
sleep 文 ×
システムプロパティ ×
Time limit (seconds) 60 300 900+
  • Small
    単一のプロセス内で動作するテスト。非常に高速に動作し、かつスケールするが、単一プロセス内で動作させるため外部リソースを用いない。プロセス外への通信はテストダブルで置き換える。
  • Medium
    単一のマシンに閉じた環境内であれば、外部リソースの利用を許容する。
  • Large
    自動テストを実行するマシンから他のマシンへの接続を許容する。本番環境やそれと同等環境を利用したテストなどが相当する。

図に表すと、以下のようになる。

テストサイズ

(私見) 以下の記事がテストサイズの理解に役立つかもしれない。

よりシンプルな分類

シンプルなテストピラミッドの提案 ~ テストを有効活用するためのイロハでは、テストの実行スピードを開発の生産性にもっとも寄与するものとして、「遅い」と「速い」という2つで分類することを提案している。

おわりに

(私見) チームや組織によって、テストの分類の認識、解釈が異なることを避けるために、テストレベルによる分類を用いるよりも、あいまいさの少ない一貫した基準をもつテストサイズによる分類を用いた方がよいと思われる。
重要なことは、チームや組織内で、あいまいさのない包括的な基準が設けられ、メンバーが共通認識として把握し、それに従ってテストの分類が行われていることである。また、それぞれのテストのスコープについても、明確な定義が設けられていることが望ましい。

脚注
  1. テストピラミッドの説明が非常にわかりやすい。本調査でも非常に参考になった。 ↩︎

  2. Test Sizes - Google Testing Blogで紹介されている。 ↩︎

GitHubで編集を提案

Discussion