テスト自動化の戦略



どのテストを自動化すべきか?

  • テストは大きく以下の2つのカテゴリに分類できる。
    • 機能ごとのテスト:ある入力に対してSUTがどのように振る舞うかを検証するもの
    • 機能横断のテスト:機能を横断してシステムの様々な振る舞いを検証するもの

機能ごとのテスト

Customer Tests

  • Customer tests は、ビジネス視点でシステムやアプリケーション全体の振る舞いを検証する。
  • functional tests, acceptance tests, end-user tests などと呼ばれる。
  • テストは開発者によって自動化されることが多いが、エンドユーザが(テストの記述が読めなかったとしても)テストによって特定された振る舞いを仕様として理解できるべき。
  • 様々なツールで自動化すべき。

Unit Tests

  • Unit tests は、開発者視点で1つのクラスやメソッドの振る舞いを検証する。
  • 検証する振る舞いは、必ずしも要求事項と直接関連があるとは限らない。
  • これらのテストは、開発者によって記述され、開発者がコードの振る舞いを把握するのに役立つ。
  • xUnitによって自動化すべき。

Component Tests

  • Component tets は、いくつかのサービスが提供するクラス群からなるコンポーネントの振る舞いをアーキテクチャ視点で検証する。
  • Unit tests と Customer tests の中間に位置し、 integration tests, subsystem tests などと呼ばれる。
  • xUnitによって自動化すべき。

Fault Insertion Tests

  • Fault insertion tests は、欠陥や障害を与えた際の振る舞いを検証する。
  • 上記の3種類のテストであらわれるテストであるが、テスト自動化の視点では、上記の3種類のテストとは別のテストセットとしている。
  • アプリケーションのある部分を置き換えることなく障害を与えるということを自動化することは難しい。

機能横断のテスト

Property Tests

  • パフォーマンステストや、セキュリティテストなどの非機能テストが該当する。
  • 人手のテストが困難であるため自動化すべきだが、xUnit framework は適さないことが多く、それ用の特別なツールを使うべき。
  • アジャイル開発では、ある程度設計が固まった早い段階からこれらのテストを実行できる。プロジェクトを通して、新しい機能が追加されるごとにこれらのテストを継続的に実施できる。

Usability Tests

  • Usability tests は、実際のユーザがソフトウェアを利用できるかを確認することで"fitness for purpose"を検証する。
  • 人間が主観的に評価する必要があるため自動化は非常に難しく人手で確認すべき。

Exploratory Testing

  • Explorattory testing は、プロダクトが首尾一貫したものになっているかを確認する方法。
  • テスターがプラダクトを使い、振る舞いを観測し、仮設を立てて、テストをデザインし、仮設が正しいかを検証する。
  • テスト前にSUTをセットアップする部分は自動化できるが、人間が考えながらテストするため自動化できない。

どのテストをどのツールを使って自動化すべきか?

(一般的な話なので割愛)

どのテストフィクスチャー戦略を使うべきか?

フィクスチャーとは

  • テストフィクスチャー(または フィクスチャー) とは、"テストの事前条件" という意味。
  • テストケースクラスとは、テストメソッドやテストフィクスチャーをセットアップするのに必要なコードを含んだクラスという意味。

Transient Fresh Fixture

  • Transient Fresh Fixture とは、メモリ上にだけ存在しテスト終了後には消えてなくなるフィクスチャ。
  • Set Up Code を必要とする。Teard Down Code や Setupt/Teardown Triggering は、必要となしない。
  • フィクスチャーが1つのテストで独立しているため、他のテストに影響を与えることがない。
  • Shared Fixture や Persistent Fresh Fixture を使うよりはテスト実行に時間がかかることがある。

Persistent Fresh Fixture

  • Persistent Fresh Fixture とは、1つの Test Method の処理を超えて永続しつづけるフィクスチャー。
  • Set Up Code と Tear Down Code を必要とする。Setup/Teardown Triggeringは、必要としない。
  • データベース、ファイルシステムレイテンシーが高いシステムに依存する場合、Slow Tests になることがある。
  • 対処法としては、Minimal Fixuture, Test Double, Immutable Shared Fixutureなどを活用する方法がある。

Shared Fixuture

  • Shared Fixutre とは、多くのテスト間で故意に再利用して使われるフィクスチャー。
  • Set Up Code, Tear Down Code, Setup/Teardown Triggering をともに必要とする。
  • フィクスチャーのセットアップやテアーダウンの実行コストを抑えられる。
  • テスト間の依存関係が増えるため、Interacting Tests, Unrepeatable Tests, TestRun War といった問題を生む欠点がある。
  • Shared Fixture Setup の手法として以下がある。
    • Prebuild Fixture
    • Lazy Setup
    • Detup Decorator
    • Suite Fixture Setup
    • Chained Tests

どのようにテスタビリティを保証するか?

Control Points と Observation Points

  • テストはインターフェース(interaction points)を通してソフトウェアとやりとりをする。
  • テストの観点では、このインターフェースには、control points と observation points がある。
  • control points は、テストがソフトウェアに対して何かをするようにするポイントである。
  • control points を通して、テストフィクスチャの設定しソフトウェアをある状態にする。また、control points を通して、SUTを実行する。
  • observation points は、テストの妥当性を検証する際にテストがSUTの振る舞いについて確認するポイントである。
  • obsevation points は、SUTやDOCのテスト後の結果を取得するのに使われる。
  • control points も observation points もSUTの同期メソッド呼び出しとして提供される。このことを "going in the front door" と呼ぶ。いくつかの interaction points では "back door" を通して利用することがあり、このことを Back Door Manipulation と呼ぶ。

Interaction Styles と Testability Patterns

  • ソフトウェアのある部分をテストするときに、基本的なテスト方法として round-trip test と layer-crossing test がある。
  • round-trip test は、SUTのパブリックインターフェース(つまり、front door)を通してやりとりする方法。DOC を Fake Object で置き換える場合もある。この方法は、カプセル化に従っているのでシンプルである。パブリックインターフェースさえ知っていればよく、SUTがどのようにビルドされたかを知る必要性はない。
  • layer-crossing testは、APIを通してSUTとやりとりするだけでなく、Test Spy や Mock Object といった Test Double を使って back door でどんなやりとりがあるかを確認する。これは、強力なテストテクニックではあるが、ソフトウェアの変更にともない Oberspecified Software の結果を招く可能性がある。
  • layer-crossing testを書くときは、SUTが依存するコンポーネントに対して代わりとなる dpendency mechanism でビルドする必要がある。例えば、Dependency Injection, Dependency Lookup (Object Factory / Service Locator)などである。別の手法としては、Test-Specific Subclass を利用する方法や、Test Hook を利用する方法がある。Test Hooks は、Test Logic in Production を引き起こすため注意が必要。
  • 非同期テストの場合は、Humble Executable という手法がある。

//

1 on 1

初回

  • 相手のパーソナリティー

  • 仕事にどんな価値観を持っているのか

  • 今の仕事をする上での悩み、問題

  • 将来の方向性

  • 仕事の役割

  • フィードバック

目標の計画と振り返り

マネージャー -> コントリビューター

  • 君の職務のため、チームのため、会社のために最大の価値を生み出すには、どのOKRにフォーカスすべきだと思う?
  • ここに挙げたOKRのうち、組織の主要な取り組みと方向性が一致しているのはどれだろう?

進捗報告

マネージャー -> コントリビューター

  • 君のOKRの進み具合はどうだい?
  • OKRの達成に欠かせない能力はどんなものだろうか?
  • 目標達成を阻害する要因は?
  • 優先事項の変化を受けて、修正、追加、あるいは削除が必要なOKRはあるか?

マネージャ主導のコーチン

マネージャー

  • 私が部下に常に期待する行動や価値観とはどのようなものか。
  • 私が部下に新たに身につけてほしい、あるいはやめてほしい行動や価値観とはどのようなものか。
  • 部下の能力を最大限引き出すために、私はどんなコーチングをすべきか。
  • 対話のなかで、マネージャーはこんな質問をしてもよい。「仕事のうち、一番楽しいと感じるのはどの部分だい?」「君の任務でどこか変えたいことはあるかな?」

部下から上司へのフィードバッグ

マネージャー -> コントリビューター

  • 私は何か君の役に立つことをしているかな?
  • 君が能力を発揮するのを、私が妨げている部分はあるかい?
  • 君がもっと能力を発揮できるように、私に何かできることはあるかい?

キャリア形成

マネージャー -> コントリビューター

  • 現在の任務でさらに活躍するため、どんな技能や能力を身につけたい?
  • 君のキャリア目標を達成するために、どの分野を伸ばしたい?
  • 未来の任務に向けて、どんな技能や能力を身につけたい?
  • 学習、成長、発展という視点で、君が目標を達成するうえで私や会社はどんな支援をできるだろう?

パフォーマンス面談への準備

マネージャー -> コントリビューター

  • 対象期間中のコントリビューターの最も重要な目標と責任は何だったのか。
  • コントリビューターのパフォーマンスはどうだったか。
  • コントリビューターのパフォーマンスが期待以下であるなら、どう軌道修正すべきか。
  • コントリビューターのパフォーマンスが良好あるいは期待以上であるなら、どうすれば燃え尽きを防ぎつつ、高いパフォーマンスを維持させられるだろうか。
  • コントリビューターはどんなときに最も意欲的に仕事に打ち込むか。
  • コントリビューターはどんなときに最も意欲を削がれるか。
  • このコントリビューターの職務における最大の強みは何か。
  • どんな学習機会を与えると、コントリビューターにとってプラスなのか。
  • これからの半年間で、コントリビューターは何にフォーカスすべきか。現在の役職の期待水準に達することか。現在の役職で最大限の貢献をすることか。あるいは新たなプロジェクト、責任範囲の拡大、新たな役職など次の機会に備えることか。

コントリビューター

  • 私は順調に目標を達成しつつあるか。
  • 私は新たな機会を見つけただろうか。
  • 私は自分の仕事が会社全体のマイルストーンとどう結びついているかを理解しているか。
  • 私はマネージャーに何をフィードバッグすべきか。

OKR (Objectives and Key Results)

OKRの種類

コミットするOKR

コミットするOKRとは、組織として必ず達成すると決め、確実に達成されるようにスケジュールやリソースを積極的に調整するOKR。コミットするOKRに期待される評点は1.0である。1.0未満であった場合には原因を究明する。

  • チームには期限までに1.0の成果を確実に達成できるように他の優先事項を調整することが求められる。
  • コミットするOKRで1.0を達成できそうにないチームは、速やかに上申しなければならない。問題が生じたのがOKRをめぐる意見の相違のためか、優先順位についての意見の相違のためか、あるいは十分な時間、人員、リソースを配分できていないためにかかわらず、上申すべき。それにより、チームを管理する立場にある人々が、対策を検討し、対立を解決することができる。
  • コミットするOKRで、期日までに1.0を達成できなかったら、必ず事後分析をする。チームを処罰することが目的ではない。OKRの計画段階や執行段階で何が起きたかを理解し、今後チームがコミットするOKRで確実に1.0を達成する能力を身につけるため。

野心的なOKR

野心的なOKRとは、我々が実現したい世界を描くOKR。どうすればそこに到達できるのか、そのOKRを達成するのにどれほどのリソースが必要か、まるでわからなくても構わない。野心的OKRに期待される平均評点は0.7だが、変動幅は大きい。

  • 野心的なOKRは、特定の四半期のチームの業務遂行能力を超えるはず。チームのメンバーはOKRの優先順位を参考に、コミットする目標を達成した後に残された時間を何に使うかを判断する。一般的に優先順位の高いOKRを、優先順位の低いOKRより先に完了させるべき。
  • 野心的OKRと関連する優先事項は、完了するまでチームのOKRリストに残し、必要なれば次の四半期へと引き継いでいく。進捗がないのを理由に、リストから削除するのは間違いである。それは優先順位のつけ方、リソースの配分、あるいは問題や解決策の理解の欠如といった根深い問題にフタをすることになるから。
  • マネージャにはチームの野心的OKRの達成に必要なりソースを評価すること、そして四半期ごとにそれを要求することが求められる。既知のニーズを組織に報告するのはマネージャの義務である。ただその野心的OKRの優先順位が、組織がコミットするOKRに次ぐものでないかぎり、要求したリソースがすべて与えられると期待すべきでない。

目標 = 「何を」

  • ゴールと意図を表す。
  • 野心的だが、現実的である。
  • 具体的、客観的で、曖昧さがない。合理的なオブザーバーから見て、目標が達成されたか否かが明確でなければならない。
  • 目標の達成は、会社に明確な価値をもたらす。

主要な結果 = 「どのように」

  • 測定可能なマイルストーン。それを達成することが、目標達成につながる。
  • 活動ではなく、成果を書く。
  • 完了のエビデンスを明記する。

よくある間違え

間違え1. コミットするOKRと野心的OKRを区別できない

  • コミットするOKRにすべき項目を、野心的OKRとすることで、未達の可能性が高まる。
  • 逆に、野心的OKRをコミットするOKRにすると、それを達成する方法を見いだせなくなり守りに入る。

間違え2. 通常業務をOKRとする

  • チームが自分たちにとって本当に必要なことや顧客にとって重要なことではなく、現在のやり方を一切変えずに達成できそうなことをOKRにすることも多い。

間違え3. 弱気な野心的OKR

  • 野心的OKRは、「今より少し人手が増えて、少し幸運に恵まれたら何ができるか」ではなく、「もし制約がほとんどなかったら、数年後、われわれ(あるいは顧客)の世界はどう変わっているかだろうか」と考えるべき。
  • リトマステスト:顧客に本当の要望を尋ねるとしたら、野心的目標はそれを叶える、あるいは上回るものだろうか?

間違え4. 力の出し惜しみ

  • チームのコミットするOKRには、入手可能なリソースの大部分を割くべきだが、全部ではない。コミットするOKRと野心的OKRを合わせると、入手可能なものをやや上回るリソースが必要になるはず。(そうでなければ、すべてコミットする目標ということになる。)
  • チームが人員や資金のすべてを費やさずに、すべてのOKRを達成できる場合、リソースを溜め込んでいるか、チームが限界に調整していない、あるいはその両方と見なされる。経営上層部から、人員やリソースをもっと有効活用しそうなグループに再配分すべきというサインになる。

間違え5. 価値の低いOKR

OKRは明確な事業価値を約束するものでなければならない。そうでなければ、そのためにリソースを割く理由がない。価値の低いOKRとは、たとえば1.0の評点で達成されたとしても、誰も気づかない、あるいは気にしないものである。

  • リトマステスト:そのOKRは合理的に考えて、1.0の評点を得ても、エンドユーザーへの直接的価値あるいは経済的恩恵をもたらさない可能性があるか。

間違え6.コミットする目標に対して、「主要な結果」が不十分

  • OKRは望ましい成果(目標:O)と、その成果を達成するのに必要な測定可能なステップ(主要な結果:KR)に分かれる。すべてのKRで1.0の評点が得られれば、目標も1.0の評点がえられるようにKRを作成することが重要である。
  • よくある失敗は、目標達成に対してKRが全体として「必要不十分」であること。この失敗が起こりやすいのは、チームが困難なKRを完了するのに必要なコミットメント(リソース、優先順位、リスク)を避けようとするから。目標達成に必要なりソースの特定を遅らせるだけでなく、目標が予定通りに達成されないリスクの発覚も遅らせるから。
  • リトマステスト:すべてのKRで1.0の評点を得ても、目標の意図が達成されない可能性があるか。その場合はKRを完了すれば目標も確実に達成されるように、KRを追加・修正する。

追加のリトマステスト

  • そのOKRを書くのに5分もかからなかったら良いものではない。じっくり考えること。
  • 目標が1行に収まっていないなら、十分簡潔とは言えない。
  • KRにチーム内でしか通じない用語が含まれていたらおそらく良くない。
  • 具体的日付を使う。すべてのKRの期日が四半期の最終日となっている場合まともな計画がない証拠。
  • 「主要な結果」は必ず測定可能なものにする。四半期末に客観的に評価つけられなければならない。
  • 指標に曖昧さがないこと。
  • OKRに含まれていないが、チームによって重要な活動があればOKRに追加する。
  • 規模が大きい組織では、OKRを階層式にする。

Transient Fixture Management

Transient Fixture Management



Building Fresh Fixtures

Transient Fresh Fixture や Persistent Fresh Fixture を作成する方法として以下がある。

  • In-line Fixuture Setup
  • Delegated Setup
  • Implicit Setup

In-line Fixture Setup

  • In-line Setup とは、以下のようにTest Method 内ですべてのフィクスチャーセットアップを行う方法。
public void testStatus_initial() {
  // In-line setup
  Airport departureAirport = new Airport("Calgary", "YYC");
  Airport destinationAirport = new Airport("Toronto", "YYZ");
  Flight flight = new Flisht(flightNumber, departureAirport, destinationairport);

  // Exercise SUT and verify outcome
  assertEquals(FlightState.PROPOSED, flight.getStatus());

  // tearDown:
   // Garbage-collected
}
  • 短所は、Test Code Duplication になりやすいこと。理由は、各 TEst Method で毎回SUTの生成が必要だから。その結果、Fragile Tests によって、High Test Maintenance Cost になりやすい。
  • また、フィクスチャの生成方法が複雑な場合、Obscure Tests になりやすい。関連する問題として、Hard-Coded Test Data となる。

Delegated Setup

  • Delegate Setup とは、In-line Setup の処理を抽出して Test Utility Method として切り出すフィクスチャーセットアップを行う方法。
  • 抽出されたメソッドはテストが依存するオブジェクトを生成するロジックを含むため Creation method と呼ばれる。
  • Creation method に何を生成するかという意図がわかる名前をつけることでテストの読み手はどのように生成するかを意識せずに理解できる。
public void testGetStatus_initial() {
  // Setup
  Flight flight = createAnonymousFlight();
  //Exercise SUT and verify outcome
  assertEquals(FlishtState.PROPOSED, glight.getStatus());
  // Teardown
  // Gabage-collected
}
  • テストが特定のオブジェクトについて生成される必要がない場合は、Anonymous Creation Method を使える。これは、Destinct Generated Vaue を使うことでオブジェクト生成時にユニークなキーを生成でき、同じオブジェクトを複数のテストで使い回すことによる Erratic Test(Unrepeatable Test, Interacting Tests, Test Run Wars)の発生を防ぐ効果がある。
  • テストが生成されるオブジェクトの属性を気にする必要がある場合は、Parameterized Anonymous Creation Mehod を使う。このメソッドは、テストがケアすべき何らかの属性を渡される。
  • 「テストメッソド内であらわれるべき何か重要なものがなければ、テストメソッド内でそれがあらわれないのが重要である。」という筆者のモットーがある。
  • One Bad Attributeパターンは、Creation Method を最初に呼び出し、その後にSUTによってリジェクトされるようなある特定の不正な値のみを置き換える方法である。これにより Test Code Duplication の多くを排除できる。

Implicit Setup

  • Implicit Fixuture Setup とは、多くの xUnit が提供している テストメソッド実行前に呼ばれるhookである。JUnitで言うところの @before アノテーションをつけたメソッド。

  • この方法は、Test Method だけを読んだだけではテストの事前条件がわからなくなるためテストを理解するのが難しくなる。また、Implicit Setup 内では、オブジェクトの参照を保持するのにローカル変数が使えないという問題がある。代わりにインスタンス変数を使った場合、その変数はグローバル変数のように振る舞いどのテスト絵ソッドがどの変数に依存するのかが不明確になるため Obscure Tests を引き起こす。

Tearing Down Transient Fresh Fixtures

  • 多くのプログラミング言語では garbsse collection をサポートしているので、Transient Fresh Fixutureを利用した場合は、特に Tear down として処理をする必要がないのが利点である。

//

テスト自動化の方針



方針:テストを最初に書く (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 は、そのような状況で適切な手法である。

//

テストの考え方の違いについて


テストを先に書くか、後に書くか

  • すでに完成したソフトウェアに対して、自動ユニットテストを書くのは大変な作業になる。
  • テストを先に書くほうが、テスト対象のシステム設計がテスタブルになる。また、プロダクションコードがテストをパスするだけの必要最小限の処理に抑えられる。

テストを書くのか、例を書くのか

  • テストを先に書くと言うと「存在しないソフトウェのテストをどのように書くのか?」という質問を受けることがある。その場合、example-driven development (EDD) について説明する。
  • 例としてテストを書くことによって、テストが要求を満たしているかどうか確認できる。

テストを1個づつ書くか、一度にすべて書くか

  • 「テストを少し書いて、プロダクションコードを書き、テストをまた少し書く…」というアプローチはインクリメンタル開発である。この方法は、テストが失敗した際に、どこに問題があるかをデバッガーをあまり使わなくても特定しやすい(Defect Localization)という利点がある。最後に変更を行った部分がテスト失敗の原因であることが多く、最後の変更内容は頭に残っているからである。
  • 一方で、プロダクションコードを書く前に、フィーチャに必要なすべてのテストを特定することを好む開発者もいる。この方法は、クライアントやテスターの視点でテストを考えられる利点がある。
  • 両者の妥協案として、空のテストメソッドをざっくり記載しておき、テストメソッドの中身は、一度に一つだけ記載していくという方法が考えられる。
  • 上記は、ユニットテストにおける議論である。カスタマーテストにおいては、あるストーリーの開発が始まる前に、そのストーリーのテストをすべて用意することが自然である。

外側から内側にテストを書くか(Outside-in)、内側から外側にテストを書くか(Inside-0ut)

状態の妥当性を確認するのか、振る舞いの妥当性を確認するのか

  • Statistは、テスト対象の初期状態を設定し、テスト実行後に期待する状態になっているかを検証する。
  • Behaviroistは、テスト対象の開始と終了の状態だけでなく、テスト対象が依存するコンポーネントをどう呼んでいるかも含めて検証する。そのため、Mock Objects や Test Spies といったものを多用することになり、テストの独立性は高まるが、依存コンポーネントの変更などによるリファクタリングが困難になる側面もある。

フィクスチャを前もって用意するのか、テストごとに用意するのか

  • Standard Fixture とは、1つ以上の Testcase Classes の すべてのTest Methods のために使われる汎用的なフィクスチャ生成方法である。Delegated Setup や Implicit Setup を使って、テストメソッドごとに Fresh Fixture としてセットアップできる。一方で、多くのテストで再利用される Shared Fixture としてセットアップもできる。
  • アジャイルなアプローチでは、Minimal Fixture というものがある。事前に大規模なフィクスチャは用意せずに、各テストごとに必要最小限のフィクスチャを使うという考え方である。

筆者の考え方

  • テストを先に書く!
  • テストは例である!
  • 普通はテストを1個づつ書くが、事前にすべてのテストをスケルトンとしてリストすることがある。
  • 外側からテストすることで、次のレイヤで必要となるテストを明確化する。
  • 通常は State Verification を使うが、コードカバレッジを良くするために Behavior Verificationに頼ることがある。
  • 個々のテストごとにフィクスチャを用意する。

//

テスト自動化のゴールについて

テスト自動化のゴールは、以下の観点がある。

<テストにより生み出される価値の観点>

  • (1) テストは品質向上の手助けになるべき
  • (2) テストはSUT(テスト対象)を理解する手助けになるべき
  • (3) テストは(リスク生み出さず)リスクを減らものになるべき

<テスト自体のあるべき姿の観点>

  • (4) テストは簡単に実行できるべき
  • (5) テストは記述とメンテナンスが簡単であるべき
  • (6) テストはシステムが進化しても最小のメンテナンスコストで対応できるべき


1. テストは品質向上の手助けになるべき

品質には2つの視点がある。

  • ソフトウェアは正しく作られているか?
  • 我々は正しいソフトウェアを作ったか?

ゴール:仕様としてのテスト(Tests as Specification)

  • テスト駆動開発テストファーストを実践している場合、SUTをビルドする前にテストによってそのSUTが何をすべきかを把握できる。
  • 「正しいソフトウェアを作っているか」を確認するために、SUTがどのように利用されるかをテストが反映したものでなければならない。
  • 様々なテストシナリオを詳細に検討することで、要求が不明確であったり矛盾している箇所を特定できるので、結果として仕様の質を向上できる。

ゴール:バグを寄せ付けない (Bug Repellent)

  • テストはバグを見つけるもの。
  • 自動テストはバグが生み出されるのを防ぐもの。
  • コードのチェックイン前にリグレッションテストが自動的に行われるようにしていれば、チェックインしたコードに既存のバグは含まれていないといえる。

ゴール:欠陥箇所の特定 (Defect Localization)

  • ユニットテストでどのテストが失敗したかによってバグの箇所を素早く特定できるべき。
  • カスタマーテストで失敗した場合、カスタマーが期待する振る舞いでないことを示してくれる。そして、失敗したユニットテストはその理由を示してくれる。このことを「欠陥箇所の特定」と呼ぶ。
  • もしカスタマーテストが失敗しているにもかかわらずユニットテストが失敗していないとしたら、ユニットテストの欠如を示している。

2. テストはSUTを理解する手助けになるべき

バグを寄せ付けないことだけがテストの役割ではない。テストは、テストを読む人にコードがどのように動くことを想定されているかを示すことができる。ブラックボックスコンポーネントのテストは、そのコンポーネントの仕様を表している。

ゴール:ドキュメンテーションとしてのテスト (Tests as Documentation)

  • テストの自動化によって、ドキュメンテーションとしてテストを利用できる。テストはどのような動作結果となるかを示してくれる。
  • システムの詳細な動作を把握したい場合は、デバッガーを利用してテストを実行し、コードの各ステップがどのように動作しているか確認できる。

3. テストはリスクを減らすものになるべき

ゴール:セーフティーネットとしてのテスト (Tests as Safety Net)

  • 自動的なリグレッションテストがないようなレガシーコードを扱う場合、ソースコードの変更は非常にリスキーである。変更によってどこかを破壊してしまったかどうか確認するすべがないからである。
  • 一方、自動的なリグレッションテストがある場合、ソースコードの変更がすばやく行える。コードの変更によってどのテストが失敗したかわかり、例えばあるパラメータが何のために必要なのかがわかったりする。
  • つまり、テストの自動化によってとりあえず何か変更したときのセーフティーネットになる。もし不足しているテストがあれば、それはセーフティーネットの穴になりえる。

ゴール:害を与えてはならない (Do No Harm)

  • 「テスト自動化を導入することでどのようなリスクがあるか」という議論がある。テスト自動化によってSUTに新たな問題をもたらしてはいけないように注意すべきである。
  • The Keep Test Logic Out of Production Code という原則は、SUTにテストに特化した仕組みを混入するリスクを避けるようにするためのものである。
  • 実際にはテストできていないのにソースコードがテストされたと思いコードが信頼できると信じてしまうリスクもある。例えば、Test Doublesを多用してしまってSUTの大部分を置き換えてしまったことで、実際には、SUTがテストできていない場合がある。これは、もう一つの原則である Don't Modify the SUTに通じる。つまり、SUTのどの部分をテストできているのか、SUTのどの部分をテスト固有のロジックによって置き換えているかを把握しておかなければならない。

4. テストは簡単に実行できるべき

もっと頻繁にコード変更できるように、テストがセーフティーネットとなるには、テストが簡単に実行できて頻繁に自動テストを走らせられる必要がある。テストを簡単に実行するには次の4つのゴールが必要である。

  • テストは、なんの追加作業もなく実行できるように Fully Automated Tests でなければならない。
  • テストは、手動操作を介在せずエラーを見つけてリポートできるよう Self-Checking Tests でなければならない。
  • テストは、何回実施しても同じ結果となるように Repeatable Tests でなければならない。
  • 理想的には、各テストは単体で実行できるよう Independent Test であるべき。

ゴール:完全に自動化されたテスト (Fully Automated Test)

  • 手動操作の介在(Manual Intervention)を必要としないテストを Fully Automated Tests という。
  • Fully Automated Tests は、他のゴールを達成するための前提条件である。テスト結果を確認せず、1度しか実行できないような Fully Automated Tests を作成することもできる。
  • Fully Aotomated Tests をベースとして、次に述べる Self-Checking Tests や Repeatable Test というゴールを実現する。

ゴール:セルフチェッキングテスト (Self-Checking Test)

  • Self-Checking Test は、テストが期待する結果が正しいことを検証できるテストのことである。
  • テストランナーは、Holly-wood principal (連絡しないでくれ、こちらから連絡するので)の考え方に基づいている。つまり、テストランナーは、テストがパスしなかったときだけ通知する仕組みである。
  • 多くのテストランナーでは、すべてのテスト結果が正しい場合に green bar を表示し、テストのいずれかが失敗して詳細な確認が必要な場合に red bar を表示する。

ゴール:繰り返し可能なテスト (Repeatable Test)

  • Repeatable Test は、手動操作を介在せず、何回連続して実行しても必ず同じテスト結果となるものである。
  • テスト実行前に手動操作を必要とする Unrepeatable Tests や、同じテストで異なっ結果となるような Nondeterministic Tests だと、テストが失敗した原因調査に多くの時間を要してしまう。
  • Unrepeatable Tests の原因としては、Shared Fixtureを使っている場合などが考えられる。そのような場合は、Automated Teardown などを利用してテスト前のごみを事前に排除するような処理が必要になる。

5. テストは記述とメンテナンスが簡単であるべき

テストコードを書く場合、テストコードを書くことよりもむしろテストすることにフォーカスすべきである。つまり、テストはシンプルでなければならない。変更に少ない労力で対応できるように、重複するようなテストは極力最小化しなければならない。テストを複雑にする理由は次の2つ。

  • 1つのテストでたくさんの機能を検証しようとしてしまう。
  • テストスクリプト言語(例えば、Java)と、テストで表現しようとするドメイン知識間での事前/事後関係において「表現上のギャップ」があまりにも大きい。

ゴール:シンプルテスト (Simple Tests)

  • テスト駆動開発では、コードは一度に一つのテストをパスするよう書かれていくので、テストをシンプルに保つことは非常に重要。
  • 分離された Test Method で Verify One Condition per Test であるべき。
  • 例外的な条件としては、アプリケーションのリアルな操作シナリオを表現するようなカスタマーテストを書く場合である。

ゴール:表現的なテスト (Expressive Tests)

  • Test Utility Methods, Creation Methods, Custom Assertion といったドメイン固有の処理を高次元化することで「表現上のギャップ」に対処できる。
  • DRY (Don't Repeat Yourself) の原則は、テストコードに対しても適用されるべき。
  • ただし、テストは、読み手に意図を伝えるようなものであるべきなので、コアとなるテストロジックは Test Method にとどめておいたほうがよい。

ゴール:関心事の分離 (Separation of Concerns)

  • 関心事の分離には、2つの側面がある。
  • テストコードはプロダクションコードから分離する
  • 各テストケースはひとつの関心事に集中する
  • 悪い代表例として、ビジネスロジックとUIを同じ場所でテストすることが挙げられる。どちらかの関心事(例えば、ユーザーインターフェース)が変更すると、すべてのテストを修正しなければならなくなる。

6. テストはシステムが進化しても最小のメンテナンスで対応できるべき

ゴール:強固なテスト (Robust Test)

  • 開発プロジェクトや要求の変更によって、コード変更は避けられない。コード変更に対して、テストコードの変更は、極力小さいものにしなければならない。
  • テストの重複はなるべく最小化し、テスト環境の変更によるテストへの影響も少ないものにしなければならない。つまり、SUTをテスト環境からなるべく分離し、 Robust Tests にすべき。
  • Verify One Condition per Test, Test Utility Methods といった手法は変更の影響を抑える手法である。

//