Comments
Description
Transcript
テスト駆動開発入門(後編)
テスト駆動開発入門(後編) goyoki おさらい 1. 最初にテストを書いて実行 (RED) RED 2. テストをパスするまでコード を実装(GREEN) 3. コードをきれいにする (REFACTOR) これを繰り返しインクリメンタルに実装を進める GREEN Refactor おさらい • うるう年判定関数(グレゴリオ暦) – 西暦年が4で割り切れる年は閏年 – ただし、西暦年が100で割り切れる年は平年 – ただし、西暦年が400で割り切れる年は閏年 今回の概要 • TDDの周辺分野について – 今回は「コードとテストの資産化」をテーマに考え 方や方法論を紹介します 概要 • • • • • テストコードの運用 テストコード運用上の問題 テストによるコードの資産化 テストコードのドキュメント化 レガシーコードでのTDD TDDの テストコードの運用 テストコードの運用 • TDDでのテストの持続的効果 – 単体テスト容易性の確保 – 実装仕様の確保 – 自動化された回帰テスト環境の構築 • 継続運用の課題 – 単体テストとしての整理(前篇) – 運用環境の構築 – テストコードのメンテナンス TDDの文脈における テストコードの運用フェーズ • 実装作業中、継続的かつこまめに – 常時実行される回帰テスト – 何度も何度も実行できるように自動化を強力に推 進する 運用環境の構築 • 単体テスト運用環境の軸 • 構成管理システムの運用 • 継続的インテグレーション – 環境の分散 単体テスト運用環境の軸 環境の軸 クライアント サーバ 手動 JUnit、スクリプト:手動で実行 CIサーバ、ビルドサーバ:手動コマンド イベント時 (コミット前等) バージョン管理クライアント:コミットに組 込 IDE:ビルドステップ等に機能 CIサーバ:コミット駆動 自動 スケジューラ:時間駆動 ビルドサーバ:デイリービルド タイミングの軸 構成管理システムの運用 • Ex)バージョン管理システム – コード変更/リリースとテスト実行の同期を取る – テスト対象の時系列データを確保する • 単体テストの自動運用の要 – 回帰テストによるフィードバックを軽快に得る 継続的インテグレーション(CI) • 自動化されたインテグレーションを継続的に実行 1. インテグレーションを統合・自動化 • ビルドに単体テストを統合 2. インテグレーションの実行を自動化 • • インテグレーション用サーバを用意(Hudson有名) 統合したインテグレーションを継続的に自動実行する – コミット時等。ナイトビルドよりもっと頻繁に – 継続的ですばやいフィードバックを実現 単体テスト運用環境 ストレステスト 規格バリデーション DB関連テスト 更新・ 自動実行 更新・ 自動実行 更新・ 自動実行 構成管理サーバ 制約のあるテストを分散運用することで SlowTest問題や高コストを回避する コミット 作業用PC 単体テスト運用マトリクス 環境A 環境B 環境C テストセット1 ○ テスト セット2 ○ テストセット3 ○ テストセット4 テストセット5 …. ○ ○ … 実行の分散方針 • TDDのテストは軽快に • 「実行に0.1sもかかる単体テストは、遅い単体テストである」 (レガシーコード改善ガイド) • Slow Test問題:時間のかかるテストのせいでTDDの効率が落ちる • 重い/制約のあるテストはサーバ側、自動化へ – 時間がかかる/特定環境依存/高コスト • TDDでは必要なテストのみ実行すればよい – 全テスト実行はサーバ側へ テストコードの運用まとめ • • • • • TDDのテストの継続的効果 単体テスト運用環境の軸 構成管理システムと単体テスト 継続的インテグレーション テスト負荷の分散 テストコード運用上の問題 テストコード運用上の問題 • TDDのテストは一般的に継続利用されるため: – メンテナンスしないと品質悪化 – 保守性が悪いとレガシー”テスト”コードとして開発 の足を引っ張ってくる Fragile Test • 製品コードの変更に弱いテスト – 些細なコード変更で大量のテストが失敗 – 仕様や機能とは無関係な変更でテストが失敗 • リファクタリングやCover & Modifyのコストを 増大させる(TDDはリファクタリングを推進するはずなのに・・・) • TDDでのエントロピー問題 Fragile Test void test_1() { Hoge hoge = new Hoge(…); …} void test_2() { Hoge hoge = new Hoge(…); …} void test_3() { Hoge hoge = new Hoge(…); …} …. void test_100() { Hoge hoge = new Hoge(…); …} void test_101() { Hoge hoge = new Hoge(…); …} void test_102() { Hoge hoge = new Hoge(…); …} テストメソッドがHogeクラスに過依存 Hogeクラスのコンストラクタが変更されたら大量のテスト失敗が発生 Fragile Test対策 • 3つの方針 – テスト対象への過依存を避ける – テストの変更可能性に基づいてテストを設計する – テストの保守性に基づいてテストコードを設計する テストのテスト対象への 過依存を避ける • テストにおけるテスト対象への4つの過依存 – Interfaceへの過依存 – Behaviorへの過依存 – Dataへの過依存 – Contextへの過依存 テストのテスト対象への 過依存を避ける • テストフェーズへの過依存 • Ex)Four Phase Test – Setup – Exercise – Verify – Teardown Exersice、Verifyはまだしも、SetupやTeardownで 過剰に依存していないか テストのテスト対象への 過依存を避ける • 対策:過剰な依存部を取り除く – 重複するテスト対象呼び出しはないか? →重複を関数やクラスでまとめる – テスト対象の内部に過剰に依存していないか (リフレクション、Mockなどで無理に内部にアクセスしていないか) →上位テストで内包できる下位テストは簡略化 →問題があればモジュール設計を再検討する – 一部の過依存がまわりを巻き込んでいないか →依存部をラッピングする 対策例:Creation Method public void testHoge_first() { Piyo piyo = new Piyo(1, 2, 3); ... } public void testHoge_second() { Piyo piyo = new Piyo(4, 5, 6); ... } Creation Method public void testHoge_first() { Piyo piyo = createUniquePiyo(); ... } public void testHoge_second() { Piyo piyo = createUniquePiyo(); ... } public Piyo createUniquePiyo() { return new Piyo(generateValue(), generateValue(), generateValue()); } 「Customer」という製品コードのへの依存部が削減された テストの変更可能性に 基づいてテストを設計する • 単体テスト設計のアプローチ – 仕様ベース • 仕様分析によって、仕様保障を目的とするテストを設 計する – 構造ベース • 構造分析によって、構造を網羅するようにテストを設 計する – 経験ベース(統計ベース) • エラー推測、経験を元にテストを設計する • 過去の欠陥統計などに基づいてテストを設計する 単体テスト設計の アプローチの扱い • 仕様ベースのテスト設計を重視 不足を構造ベースのテスト設計で補う – 構造はリファクタリングで変化する • 構造への過依存はFragile Testとなり制約 に – 構造ベースで網羅的に設計したテストはナマモノ アプローチの扱い int hoge(int input) { return input * 2; } Int型は仕様か、構造的な制約か 構造の変更可能性 • 構造ベースでも変更可能性に程度がある – 大規模な構造 – 仕様としての構造 暫定実装 大 内部メンバ ライブラリ モジュール等 のインタフェース 構造の変更可能性 規格化された構造 小 構造の変更可能性 • 構造の変更可能性の2軸 – 時間軸方向の変更可能性 – 構造軸方向の変更可能性 構造軸方向の変更可能性 • 構造的に変更されにくいものか? – 変更可能性:大 • 暫定実装、内部メンバなど • 保守性(移植性など)が务悪な構造 – 中期 • クラス、モジュールなど大まかなレベルの構造 • 保守性が作りこまれた構造 – 長期 • 規格として定義される構造、厳格管理された構造 時間軸方向の変更可能性 • 時間が経過しても変更されにくいものか? – 短期(一時的) • 暫定使用、実装過程での仮実装など • 仕様が不定 – 中期 • 機能、各部モジュールなど – 長期 • 公的規格、標準規格、根幹的なアーキテクチャなど 変更可能性への対応 • 時間的・構造的に安定するものから網羅性を 高める – Ex)規格仕様は作りこんでV&V手段として使えるよ うにする • 「SQLiteのテストコードは4567万8000行。本体のコード は6万7000行」 – 不安的なコードに対するテストコードも不安定 暫定実装に対するテストコードも暫定実装 アーキテクチャ設計による 変更可能性の管理 • アーキテクチャ設計段階で変更可能性を大き く制御できる – 外部仕様定義 • 不定な外部要因を局所化・分離する • テスト容易性を阻害するDOCを局所化する – 内部設計 • 保守性を作りこむ • モジュール設計により、仕様としての構造を定義する テストコード運用上の問題 • Fragile Test • 変更可能性への対応 • TDDとアーキテクチャ設計 テストによるコードの資産化 TDDによる実装アプローチの変化 • TDDで書かれたコードは全体にわたって – 単体テストが確保される – 単体テスト容易性が確保される • 自動化された回帰テスト環境が Cover & Modifyのアプローチを実現する Cover & Modify • 手順 – 1 パスする回帰テストを確保する – 2 テストが成功する状態を保ちつつ、コードを変 更する Edit & Pray • 「変更して動かしてみる」 • Cover & Modifyの対比となる実装アプローチ 世の中で一般的 • 手順 – 1 コードを変更する – 2 うまく動くように祈る Cover & Modify • 「テストで保護(Cover)して修正(Modify)する」 • テストで修正・変更の影響範囲を絞り込む – コードの変更作業が安全に – 保守開発で推奨される実装アプローチ Cover & Modify[実演] • Case 4 で1234を返す Cover & Modify例 • リファクタリング – 1 機能を保護するテストを確保する – 2 テストがパスする状態を維持しながら、コード を変更する Cover & Modify例 • TDDによる機能変更 – 1 変更対象の回帰テストを書く – 2 TDDのサイクルへ • 変更機能のテストを書く(Red) • 実装する(Green) Cover & Modify[課題] • うるう年判定(前回の課題) – テストを削除してください • 追加仕様: – 負の値が入力された場合はfalseを返す TDDとCover & Modify • TDDとCover & Modifyは親和性が高い – コードをテストに対して最適化されるため、コード のテスト容易性が高まる • テストで保護しやすくなる – テストコードが確保される – そもそもCover & ModifyがTDDのようなもの コードの資産化 • TDDはコード資産化効果を促進する – TDDによりCover & Modifyを実現 • コードの保守性が大きく改善 • コードの資産化が促進される • 「テストのないコードはレガシーコード」 • 資産化効果はドキュメント・プロセスでなく、 コードそのものに宿る コードの資産化まとめ • Edit & Pray • Cover & Modify – リファクタリング – 機能追加 • TDDのコード資産化効果 テストコードのドキュメント化 テストコードのドキュメント化 • 「テストコード=実装仕様」という設計アプローチ – TDDでのテスト設計で目指される理想の1つ • Not All! • 他の理想と共存する – テストコードを動く実装仕様書として活用する – バグ出し・品質保証ではなく、仕様記述という目的でテス ト設計を行う Example Driven Development • EDD。用例駆動開発。TDDの1種 • EDDでのテスト=テスト対象の用例 • テストファーストが苦手な人のためのプラク ティスとして有効 • 手順: – 最初に実装コードの用例を考える – 用例をテストで表現する – 以後はTDDのサイクルへ Example Driven Development [課題] • 演算器 – 整数を入力できる – 入力した整数の計算結果を出力できる Behavior Driven Development • BDD。ビヘイビア駆動開発。TDDの亜種 • BDDのテスト=テスト対象のふるまい仕様 – “Behavior Verification”とは異なる • 手順 – 実装のふるまいを考える そしてふるまいをテストで表現する – 以後はTDDのサイクルへ BDDフレームワーク • 単体テストフレームワークの一種 • xUnitの設計に基づくものが多いが、命名や 構造がBDDの思想に合わされている • 仕様記述のためのDSLを提供するものもある BDDフレームワーク • JUnit – assertEquals("hoge name", hoge.getName()); – assertEquals(16, hoge.getAge()); • JDave(BDDフレームワーク) – specify(hoge.getName(), must.equal("hoge name")); – specify(hoge.getAge(), must.equal(16)); Behavior Driven Development [課題] • 演算器 – 整数を入力できる – 入力した整数の計算結果を出力できる ドキュメントとしてのテストコード • Characterization test • Test All-at-Onceによるフィーチャ分析 Characterization test(仕様化テスト) • コードのふるまいや用例を、パスするテストと して表現する – 仕様表現、コードの理解が目的 – 満たすべき仕様として回帰テストとして作用する Characterization test(仕様化テスト) • Characterization testによるコード解析 – 1 適宜の入出力で解析対象のテストを書く(最 初は失敗させる) – 2 テストがパスするまで入出力の値を調整する • テスト失敗したら期待値を実行値に置き換える • 例外が発生したら例外テストに置き換える – 目的が達成されるまでこれを繰り返し、テストを 継ぎ足していく Characterization Test[課題] • 前回の課題のCharacterization Testを用意す る Test All-at-Onceによるフィーチャ分析 • 実装対象に求められるフィーチャをテストメ ソッドとしてすべて洗い出す – テストメソッドはスケルトン。かつignore設定 – TDDの中で1つ1つignore指定除去&スケルトン実 装をすすめ、最終的にTDDのテストコードがすべ てのスケルトンを内包するようにする – 最終的に、洗い出したスケルトンセットが整合性 の取れたフィーチャリストとなる Test All-at-Onceによるフィーチャ分析 [実演] • うるう年判定関数で実施 テストコードのドキュメント化まとめ • • • • TDDとテストコードのドキュメント化 EDD/BDD Characterlization Test Test All-at-Onceによるフィーチャ分析 レガシーコードでのTDD レガシーコードでのTDD • 単体テスト容易性が低いコードではTDD実行 前にコードの修正が必要 – 修正では一般的にコードを悪い方に崩す テストの恩恵とのバランスを考慮する必要がある • Cover & Modifyから外れた修正 • コードを汚くしてしまう修正 通常のTDD 1. テストを書く 2. テストをパスするコードを書く 3. リファクタリングする レガシーコードでのTDD 1. (よく考える) 2. 依存性を排除する 1. 変更点を洗い出す 2. テストを書く場所を見つける 3. 依存性を取り除く 3. テストを書く 4. テストをパスするコードを書く 5. リファクタリングする 依存性の排除 • リスクを許容するとしても、リスクを抑える – 低リスクなツール支援が使えるなら活用 – 低リスクな変更手段があるなら活用 • private→protected • finalを削除する – 上位のテストで補完 • リスクにある変更は慎重に考える – “よく考える” – スクラッチリファクタリングなどでイメージを固める スクラッチリファクタリング • テストや制約を一切無視して自由にリファクタ リングする – テストは記述しない – リファクタリング結果は使い捨て – バージョン管理推奨 • 目指している結果が妥当かどうか評価する リスクのある修正を行うときに有効 依存性の排除例 (コンストラクタのパラメータ化) public Hoge { private MissileCtrl missileCtrl; public Hoge() { missileCtrl = new MissileCtrl (): } public void piyo() { …. missileCtrl.発射(); …. missileCtrl.発射2(); …. } …. } 依存性の排除 (コンストラクタのパラメータ化) public Hoge { private MissileCtrl missileCtrl; public Hoge(MissileCtrl missileCtrl) { this.missileCtrl = missileCtrl; } public Hoge() { thils(new MissaileCtlr()); } public void piyo() { missileCtrl.発射(); } …. } Hoge(new FakeMissileCtrl()); レガシーコードでのTDD[課題] • 前回課題のBookList改変版 レガシーコードでのTDDまとめ • レガシーコードでのTDDのステップ • 依存性の排除 – コンストラクタのパラメータ化 • スクラッチリファクタリング 最後のまとめ • 今回とりあえず覚えてもらいたい要点 – 継続的インテグレーション – Fragile Test – Cover & Modify – テストによるコードの資産家 – テストコード=ドキュメントとするテスト設計アプ ローチ – レガシーコードでのTDD