テスタビリティ(試験性)を確保するための設計方針

テスタビリティ(試験性、テスト容易性)は「どれだけ容易にテストできるか」「どれだけテストを実現できるか」の度合いを示す品質特性です。
実践ソフトウェア・エンジニアリングの解説から引用すると、テスタビリティは次の特性から構成されます。

  • 実行円滑性(Operability)
    • テストの実行しやすさ。例えば、テスト実施をブロックする要因が少ないか。
  • 観測容易性(Observability)
    • テスト対象の観測のしやすさ。
  • 制御容易性(Controllability)
    • テスト対象の操作のしやすさ。
  • 分解容易性(Decomposability)
    • テスト対象の分離や分割のしやすさ。例えばテストのためにテスト対象を切り出しやすいか。
  • 単純性(Simplicity)
    • テスト対象の単純性。例えば機能がシンプルで必要なテストが少ないか。
  • 安定性(Stability)
    • テストに影響を与える変更の少なさ。
  • 理解容易性(Understandability)
    • テスト対象の理解しやすさ。

※この他にも、文献によってはプロジェクトの制約度合い(e.g.テストに十分なコストを確保しているか)、テストエンジニアのスキルといったテスト対象の外の特性もテスタビリティに含めているものがあります。

今回は、このテスタビリティを確保するための代表的な設計方針を解説したいと思います。

前提:様々なテストそれぞれにとってのテスタビリティがある

本題に入る前の前提の話です。テスタビリティは、あらゆるソフトウェアテストに関わる品質特性です。手動テスト、自動テスト、あるいはGUIを通したテスト、APIを通したテスト、コードレベルのテストなど種類を問わず、各々のテストにとって、それぞれのテスタビリティがあります。

例えば観測容易性を例をとります。テスタビリティの実装は、テストの種類ごとに次のように変化します。

  • 手動のシステムテストでの優れた観測容易性の実装例
    • 必要なエラーや実行情報を得るためのUIが提供されている
    • テストの前提条件(構成管理情報)を確認するためのUIが提供されている
  • APIテストでの優れた観測容易性の実装例
    • 必要な内部情報を得るためのAPIが揃っている
  • ユニットテストでの優れた観測用意性の実装例
    • 間接的な出力がなく、テストコードから必要な出力を直接簡単に参照できる

以降で解説する設計方針についても、具体的な実装は対象のテストの種類によって異なります。

テスタビリティを確保するための設計方針

テスタビリティを低下させる要因の影響範囲を小さくし、分離・置換できるようにする

テスタビリティを低下させる要因として、以下のような存在があります。

テスタビリティ確保の点では、こうしたテストの支障となるコンポーネントへの依存箇所を最小化すると、テスト可能な範囲が広がります。例えば以下のような対策が有効です。

  • 実行円滑性に劣るコンポーネント群は、Facadeパターンで簡易化したインターフェースを通して制御可能にする。
  • 安定性に劣るコンポーネントは、コンポーネントをなるべく局所化した上で、ラッピングして他との依存性を削減する。例えばUIデザインが頻繁に変更されるなら、UI層を最小化・分離するなど。

さらに、上記のテスタビリティを低下させる要素は、後述する接合部を設けて分離・置換できるようにすると、テスタビリティ確保の助けになります。例えば観測容易性に劣るコンポーネントを、観測手段を埋め込んだTest Doubleに置換できるようにするといった工夫です。

結合度を低く、凝集度を高く

一般的な設計の方針として、コンポーネント間の結合度を低くし、凝集度を高くすることが推奨されていますが、この方針はテスタビリティの改善にも繋がります。
結合度を低くするように設計・実装すると、テスタビリティのうち分解容易性が向上します。凝集度を高くするように設計・実装すると、テスタビリティのうちの単純性や理解容易性が高まります。

特に結合度を低く保つ設計については、設計の要所に接合部(Seam)を設けることが重要です。接合部は、特定のコンポーネントを切り離して他に置換できるようにするための仕組みです。依存性の逆転(Dependency Inversion)、Link Seam(ビルドを分割しリンク時に切り替えられるようにする)などがあります。接合部があると、テスト対象を切り離したり、テストの障害となるコンポーネントをTest Doubleに置換したりすることができるようになり、テストにとって十分な結合性の低さを確保できます。

テストにとって十分な観測点、制御点を設ける

観測点(Observation Point)はテストの出力を得るための手段です。その充実度が観測容易性に直結します。制御点(Control Point)はテスト対象を操作するための手段です。その充実度が制御容易性に直結します。

テスタビリティの確保においては、プロジェクトのそれぞれのテストが求める観測容易性、制御容易性の要求を識別して、それを満たす観測点・制御点を設けることが重要になります。
例えばテストに必要な観測・制御ができるようにテスト用インターフェースを設ける、テストで観測すべき情報がすべて盛り込まれるようにログ設計する、テストにとって必要な情報が得られるようにエラーなど情報通知UIを設ける、といった工夫が有効になります。

テスタビリティを拡張可能にする

後からテスタビリティを拡張可能にすると、テスタビリティを確保できなくても、テストを実施するタイミングで必要なテスタビリティを後から確保できるようになります。
また、開発成果物が将来レガシーコード化する備えの点でも、各所にテスタビリティが注入可能なポイントを実装しておくのが有効です。その備えを行っておくと、将来レガシーコード化しても、テスタビリティを注入して必要なリグレッションテストを構築し、そのテスト使って脱レガシーコードのためのリファクタリングを行うといった対策が打てるようになります。

テスタビリティを拡張可能にする手段としては、例えば前述した接合部があります。接合部を設けると、テストが依存するコンポーネントを、観測・制御手段付きに改造したコンポーネントに置換できるようになります。

テストでとり得る条件を制限する

テストの入出力のパラメータや値が少ないと、テスタビリティのうちの単純性が向上するほか、観測や制御が容易になります。
このパラメータや値の削減には、次のような工夫が有効になります。

  • 変数やメソッドなどのスコープを狭め、グローバルな依存関係を削減する
  • 冗長な引数や戻り値をなくす
  • 内部状態をシンプルに保つ
  • 副作用の発生可能性をなくす
  • 型でとり得る値を制限する

また、値の組み合わせなど、テストでとり得る条件を削減すると、同じく単純性が向上します。この実現のためには、前述のパラメータや値の削減のほか、次のような工夫が有効になります。

  • 制御フローをシンプルに保つ。例えばエラーチェックを冒頭で行ってネストを浅くするなど。これによりデシジョンテーブルの圧縮やCFD法の適用などが可能になる。
  • ロックなどの排他処理や割り込み保護を適切に行い、タイミングの組み合わせを削減する
  • 禁則の組み合わせを、実装上の工夫で実現不能にする
  • DRYを推進する

品質特性のバランスを取る

テスタビリティは、一部の品質特性とトレードオフの関係を持ちます。代表例は、性能効率性やセキュリティです。
セキュリティを例に取ると、例えばテスト用に設けたAPIセキュリティホールになるといった問題が、トレードオフの関係例です。

テスタビリティの確保にあたっては、テスタビリティとトレードオフになる品質を特定して、両者を両立する設計を見つけ出す必要があります。
例えばテスタビリティとセキュリティのトレードオフの場合ですと、次のようなアプローチが設計として有効になります。

  • トラストバウンダリを設け、その内部にテストインターフェースを設ける
  • テスト用とデプロイ用を安全に切り替えられるよう、コードやビルドシステムを工夫する

設計やコードをリーダブルに保つ

理解しやすい命名をするといった、リーダブルにするための設計やコードの原則は、単純性や理解容易性を改善し、テスタビリティを底上げします。