テスト自動化の方針



方針:テストを最初に書く (Write the Tests First)

  • テスト駆動開発(TDD)では、以下の主張が語られる。
    • ユニットテストは多くのデバッグの労力を抑えることができる。この労力はテスト自動化のコストを帳消しにする。
    • プロダクションコードを書く前にテストを書くことで、プロダクションコードをテスト可能な設計にすることを強制できる。

方針:テスト可能な設計 (Design for Testability)

  • 上記と重複するが、テスタビリティを考慮していない場合、テスト自動化は困難である。
  • テスタビリティを考慮していないレガシーコードにユニットテストを導入しようとすると、あらゆる困難に直面する。

方針:フロントドアを最初に使う (Use the Front Door First)

  • オブジェクトには外部からアクセス可能な public interface と、そうではない private interface がある。また、オブジェクトには outgoing interface という依存するオブジェクトとやりとりするために利用されるものがある。
  • フィクスチャーのセットアップや期待する結果の検証のために Back Door Manipulation を利用することは Overcoupled Software の結果を招く可能性がある。また、Behavior Verificatino や Mock Objects の使いすぎは、Overspecified Softwareを招く可能性がある。
  • 通常はSUTをテストするのに round-trip tests (往復テスト)を使うべき。つまり、public interface を通してテストし、正しく振る舞っているかどうかを State Verification で検証する。期待する結果の検証が不十分な場合は、layer-crossing tests を行い、SUTがdepended-on components (DOCs) への呼び出しを検証するために Behaviro Verification を使う。実行速度が遅かったり DOC を利用できなくて Test Double に置き換えなければならない場合は、Fake Object を利用することが望ましい。

方針:コミュニケーションを意図する (Communicate Intent)

  • たくさんのコードや Conditional Test Logic を含むテストは、たいてい Obscure Tests である。すべての詳細事項をもとに全体像を想像する必要があるので、理解するのに多くの時間がかかる。
  • コミュニケーションを意図してテストを構築するとこで、理解しやすくメンテしやすくなる。
  • そのために、テストフィクスチャーの設定や結果の検証で 意図がわかりやすい名前の Test Utility Methods を呼び出すようにする方法がある。テストフィクスチャがテストの期待する結果にどのように影響を与えるか、つまり、どのようなインプットがどのような結果をもたらすのかを Test Method 内で明らかにすべき。

方針:テスト対象を改変しない (Don't Modify the SUT)

  • indirect inputs, indirect outputs とは、public interface を介して観測できない入力や出力のことである。
  • 実際のテストでは Test Double や Test-Specific Subclassを使って振る舞いの一部をオーバーライドすることでアプリケーションの一部を置き換えることがある。それは、indirect inputsをコントロールする必要があったり、indirect outputs を横取りして Behavior Verification する必要があるからである。
  • Test Hooksを使おうが、Test-Specific Subclassを使おうが、Test DoubleによってDOCを置き換えようが、テスト対象を改変することは危険である。プロダクションでどのように使われるかを意識してソフトウェアをテストする必要がある。そうしないと、テストしていると思いこんでいるテスト対象の一部が実は置き換えられているということが起こり得る。

方針:テストの独立性を保つ (Keep Tests Independent)

  • マニュアルテストでは、1つのテストでSUTの様々な振る舞いを検証するよう長いテストとなる。人間のテスターの場合、長いテストでテスト失敗によってテストの継続が困難となるかや、その失敗によって後に続くテストが重要ではなくなる(無視してもよい)と認識できる。
  • もしテストが相互依存していたり順番依存していると、個々のテストが失敗した場合に、調査に役に立つフィードバックが失われてしまうことがある。
  • Independent Test とは、それ自体で実行可能なテストである。Fresh Fixtureを使ってSUTの状態を設定し、テスト時の振る舞いを検証できる。Fresh fixtureは、Shared Fixtureよりもテストの独立性を高めるが、Shared Fixutureは、Lonely Tests, inderacting Test, Test Run WarsといったErrastic Testsを引き起こす。
  • テストが独立していれば、ユニットテストが失敗した場合に 失敗の原因となった箇所がピンポイントでわかる(Defect Localization)。

方針:テスト対象を分離する (Isolate the SUT)

  • ソフトウェアが他のソフトウェアに依存していて、そのソフトウェアが頻繁に変更されるような場合、テストは失敗するようになる。この問題を Fragile Test の一形態である Context Sensitivity という。また、Untested Code, Untested Requirements といった問題を引き起こす。
  • この問題を避けるには、DOCで起こり得る反応をすべてインジェクトして、テストを完全にコントロールする方法がある。
  • できる限りテスト対象をその他から分離するようにすべきである。それによって Test Concerns Separetely や Keep Tests Independent といった効果が得られる。
  • Dependency Injection や Dependency Lookup, Test-Specific Subclass を駆使してソフトウェアの依存性置き換える設計が可能である。これによって、テストは何度も実行でき、かつ堅牢なものとなる。

方針:テストの重複を最小限にする (Minimize Test Overlap)

  • 大抵のアプリケーションは検証しなければならないたくさんの機能がある。それらの機能が正しく動作するかを確認するために、すべての起こり得る組み合わせで検証することは不可能である。
  • 同じ機能を検証する複数のテストがあると、メンテナンスコストは増大するばかりか、クオリティがあまり上がらなくなる。
  • 1つのテストで各テスト条件をカバーすべきであり、それ以上でもそれ以下でもない。

方針:テスト不可能なコードを最小限にする (Minimize Untestable Code)

  • GUIコンポーネントやマルチスレッドのコードなど Fully Automated Tests ではテストが難しいコードがある。これらは、自動テストから初期化することや相互作業が困難という環境に組み込まれている。したがって、安全にリファクタリングすることは難しく、既存の機能を改変したり新機能を入れることに危険を伴う。
  • このようなテスト不可能なコードは、テストしたいロジックを移動することによってテスト可能にリファクタリングすることができる。具体的には、Humble Executable や Humble Dialog という手法がある。
  • テスト不可能なコードを最小化することによって、全体のコードカバレッジを向上できる。そうすることで、コードに対する自信を高めて、気兼ねなくリファクタリングできるようになる。

方針:テストロジックをプロダクションコード外に保つ (Keep Test Logic Out of Production Code)

  • プロダクションコードがテスタブルな設計になっていない場合、「if testing then ...」 のような "hooks" を使って、テストコード固有のロジックをプロダクションコードに入れ込みたくなる。
  • テストとは本来システムの振る舞いを検証するためのはずなのに、システムがテスト時に異なる振る舞いをしてしまうことは本末転倒である。さらに "hooks" によってプロダクション環境でソフトウェアが予期しない動きを引き起こすリスクもある。

方針:テストごとに1つの条件を検証する (Verify One Condition per Test)

  • あるテスト条件の終了状態を次のテスト条件の開始状態に再利用し、1つの Test Method で2つのテスト条件をあわせて検証する衝動に駆られることがある。このアプローチはおすすめできない。1つのアサーションで失敗し、残りのテストが実行されないことにより、 Defect Localization が難しくなるからである。
  • もし複数のテストで同じテストフィクスチャーが必要な場合、Test Methods を1つの Test Class per Fixture に移動して Implicit Setup や Delegated Setup を使ってフィクスチャーをセットアップする Test Utility Methods を呼び出すようにリファクタリングできる。
  • 各テストでは独立した4つのフェーズ(fixuture setup, exercise SUT, result verification, fixture teardown)があり、それぞれ順番に実行される。
    • 最初のフェーズでは、SUTが期待する振る舞いを実現するために必要だったり、実際の出力結果を観測するのに必要なもの(例えば、Test Double)を設定するテストフィクスチャーを準備する。
    • 次のフェーズでは、検証しようとしている振る舞いを確認するためにSUTとやりとりをする。これは、1つの独立した振る舞いであるべきであって、SUTの複数部分の振る舞いをテストしたい場合に 1つのSingle-Condition Test としてテストを記載すべきでない。
    • 次のフェーズでは、期待する結果が得られたか、またはテストが失敗したかどうかを確認するために必要なことをする。
    • 最後のフェーズでは、テストフィクスチャーを取り壊して初期状態に戻す。

方針:テストの関心事を分ける (Test Concerns Separately)

  • 1つの Test Methodで複数の関心事をテストすることは、テストの関心事が修正されるごとにテストメソッドが破綻するという問題を引き起こす。
  • また、Defect Localization が保てなくなり Manial Debuggingを使って原因を調査する必要があるという問題もある。
  • テストの関心事を分離することで、テスト自体の理解が簡単になる。テストのサブセットが見つかった場合は、そのサブセットを別の Testcase Class に移動し、新しく作成されたクラスを検証することで、テストの関心事を分離できる。

方針:相応の労力と責任を保証する (Ensure Commensurate Effort and Responsibility)

  • テストを書いたり修正する労力は、それに対する機能を実装する労力を超えるべきでない。
  • 例えば、メタデータを使ってテスト対象の振る舞いを設定可能で、そのメタデータが正しく設定されたかを検証するためのテストを書きたくなる場合、そのようなコードを書くべきでない(テストツール側で保証すべきものという意味か)。Data-Driven Test は、そのような状況で適切な手法である。

//