テストピラミッドという考え方について

エクストーンの豊田です。今回はソフトウェアの品質を上げるためのテスト戦略について、今自分が取り入れている考え方について簡単に紹介させていただきます。

ここでのテストは機械によって自動的に実行される自動テストのことを指しています。自動テストを行うためにはテストコードを記述する必要があるのですが、何をテストするかによってテストコードの書きやすさが変わってきます。テストコードを書く際にもコストが発生するので、それに見合う効果がないとテストコードを書く文化も根付かないと思っています。

今回はMike CohnさんのSucceeding with Agileで書かれている「テストピラミッド」という考え方と、エクストーンでどのように実践しているかについて紹介します。

テストピラミッドとは

テストピラミッドとはテストの手法とテストの総量の関係を表した概念的な図、または概念そのものを指します。

テストピラミッド

上記ではテストを以下の3つに分類しています。

  • UI Test: 画面上から操作し、実際のシステムの挙動を確認するテスト
  • Integration Test: システム内の複数のユニットを結合して、一連の処理が正しく行われているかどうかのテスト
  • Unit Test: システム内の個々のユニットの挙動を独立して確認するテスト

上記のピラミッドでは、下に行くほどテストの記述コストや実行コストが低く、多くのテストが行われているということを指しています。シンプルに言えば記述コスト・実行コストの高いUIテストや結合テストを少なく、逆にユニットテストを多く記述するということを表しています。

場合によってはUIテストの自動化環境を構築するのはコストが高いため、UIテストは自動化せず、結合テスト・ユニットテストを中心にテストコードの記述を行い、テストの自動化環境を構築するようにすることもあります。

こちらは品質保証において、どのようにテストを記述・実行することでテストのコストを最適化できるかという戦略につながるため、テストピラミッドという考え方を開発チームや品質保証チームと共有したうえで、実際のテストをどのようにして行うかを議論すると話がスムーズに進むのではないかと思います。

テストピラミッドの実際の運用

ここではWeb開発を行う際に、上記テストピラミッドに従ってテストをどのように計画・実行していくかの実例を紹介します。

ユニットテスト

自分はWeb開発においてRubyやTypeScriptを利用することが多いので、rspecやjestなどを利用してテストを記述します。テストを記述するタイミングは開発のタイミングで行い、できればテスト駆動開発を行いたいです。テストコードの粒度としては以下のような例になります。

  • フォーム入力パラメータのバリデーション
  • 入力されたデータのフォーマット変換
  • 特定のボタンが押されたときに正しいイベントが発火されているか
  • etc.

ユニットテストだとテストしづらい項目がいくつかあります。例えばフロントエンドではUIに関するテストがそれにあたります。見た目の部分やHTMLのイベントなどブラウザ固有の処理をコード上で表現することはコストが高いので、ユニットテストではそのあたりのテストをあまり対象にすることはありません。バックエンドでは外部のHTTP通信やデータベース等のミドルウェアとのやりとりがそれにあたります。こちらに関しては外部ミドルウェアの条件を一定に保つことが困難なためです。

そのためにはテストしづらい項目をユニットテストのコードから切り離すためにモックを利用します。モックでは実際の処理の代わりに何もしない関数と差し替え、適切なパラメータで適切な回数呼び出されたかをテストします。

ユニットテストでは、システムが想定するすべてのパターンを網羅するようにテストを記述することが理想的です。例えば「Aには1~10文字までの文字列のみ許容する」というバリデーションをテストする場合、最低でも以下のパターンでテストを行うべきです。

  • 0文字
  • 1文字
  • 10文字
  • 11文字

上記のように条件が変わる数値の前後のことを境界値と呼びます。

開発時から常にテストコードが書かれていることは理想的ですが、実際のプロジェクトにおいてテストが書かれていないケースもよくあります。そういう場合に途中からでもテストは書くべきですが、おすすめは不具合修正のタイミングでテストを書くことです。

こちらについては以下のプロセスで不具合を修正します。

  • 期待と異なる挙動を見つける
  • 本来期待すべき挙動に沿ったユニットテストを記述・実行し、現状のコードでそのテストが失敗することを確認する
  • コードを修正し、期待する挙動になるようにする
  • 再度テストを実行し、テストコードがパスすることを確認する

テストがないプロジェクトについては、このようにコードを修正するタイミングで都度テストコードを書いていくところをファーストステップとして、徐々にテストコードを増やしていくといいと思います。

まとめると、ユニットテストでは以下のようなテストを書きます。

  • 他のコンポーネントに依存しない、単一のユニットのテスト
  • 外部システムのテストに関してはモックを利用し、依存しないようにする
  • 開発プロセスにおいてテストの記述を行う

インテグレーションテスト

ここではユニットテストよりもう少し粒度の大きいテストを行います。フロントエンドであればHTML上のボタン等のクリックによって特定の処理が行われることをテストする、バックエンドであればAPIを呼び出した際に期待したレスポンスが返ってきて、データベース上に反映されていることをテストするなどが該当します。この場合、ユニットテストでは避けてきた外部システムのふるまいも含めてテストを行うため、いくつかのツールサポートが必要になってきます。

例えばバックエンドのデータベースに関するテストですと、テスト実行時に実際にデータベースサーバーを起動し、テスト用の事前データを入れた後、実際にデータベースの情報にアクセスして実行するようにします。Rubyですとfactory_botを利用して事前データの準備をテストコードで表現することができます。また、CI環境による自動テスト環境構築時においても、テスト実行時にデータベースサーバーを準備する必要があるため、CI環境構築の難易度も上がります(最近の主要なCIサービスはだいたいDockerが使えるため、比較的難易度は下がってきてはいます)。

テスト項目としては、ここでもすべての仕様を網羅するようなテストが書けていることが理想なのですが、ユニットテストに比べるとテスト記述・実行のコストが高くなっているため、費用対効果を考えるとむやみやたらとテストを書くことは得策ではありません。

以下のテストをバランスよく記述することを意識するといいと思います。

  • 正常系: システムが正常に動作するかどうかのテスト
  • 準正常系: システムが正常に動作したうえで、ユーザー側に責任のあるエラーのテスト
  • 異常系: システムに異常があり、ユーザー側に責任のないエラーのテスト

特にシステムの連携時における異常発生についてはここでテストすることが望ましく、他のテストで行うことは難しいです。

まとめると、インテグレーションテストでは以下のようなテストを書きます。

  • 他のコンポーネントに依存する、複数のシステムが正しく連携して動作するかを確認するテスト
  • 外部システムのテストについては、極力そのシステムと通信を行うようにするが、それらの前提条件はツールによって任意の状態を作るようにする

UIテスト

ここでは実際のUIを通して、開発したシステムが意図通りに動いているかどうかを確認するテストを行います。フロントエンド・バックエンドと一貫してテストを行うため、だれがテストに対して責任を持つか等を考える必要があります。

今まで話してきたテストの中では最も自動化のコストが高く、手動で実行することが多いです。こちらのテストが自動化されることで実際のユースケースに従って自動テストを行うことができるのですが、記述のコストと釣り合うかどうかはテストを書く人のスキルに依存します。

また、テストを記述するコストが高いわりに、ちょっとした改修でテストが正しく動かなくなることが頻発します。そのため、システムの本来の目的が正しく果たせているかどうかを確認する最低限のふるまいのテストだけを書くようにすると、比較的テストが壊れにくくなるかと思います。

ツールとしてはcypress等を使うことでコードでテストを記述することが出来ます。cypressではUIの操作をにコードで記載し、その通りにブラウザ上でテストを行うことができます。こちらについてはまた別の機会で詳しくお話しできればと思います。

UIテストについてはツール等によって楽に書けるなら、実際のふるまいに近いテストが実施できるため、品質保証のために最も信頼できるテストとなります。一方で、費用対効果の面では現状ではあまり効率がいいとは言えず、開発全体のプロセスから考えるとユニットテスト、インテグレーションテストの方を多めに記述する方がよいとされています。

まとめると、UIテストでは以下のようなテストを行います。

  • 実際に動いているシステムを操作し、意図した挙動になっているかどうかを確認するテスト
  • 見た目が正しく意図通りになっているかどうかを確認するテスト

おわりに

テストを記述・実行するためのテスト戦略としてテストピラミッドという考え方を紹介し、それぞれで具体的にどのようなテストを行っているのかについて書きました。テスト書きたいんだけど、どこから書けばいいのかわからないという方の一助になると幸いです。