goyokiinfog.0ch.biz/download/jissen_tdd_2_goyoki.pdf•TDDの周辺分野について...
Transcript of goyokiinfog.0ch.biz/download/jissen_tdd_2_goyoki.pdf•TDDの周辺分野について...
テスト駆動開発入門(後編)
goyoki
おさらい
1. 最初にテストを書いて実行(RED)
2. テストをパスするまでコードを実装(GREEN)
3. コードをきれいにする(REFACTOR)
これを繰り返しインクリメンタルに実装を進める
RED
GREEN
Refactor
おさらい
• うるう年判定関数(グレゴリオ暦)
–西暦年が4で割り切れる年は閏年
–ただし、西暦年が100で割り切れる年は平年
–ただし、西暦年が400で割り切れる年は閏年
今回の概要
• TDDの周辺分野について
–今回は「コードとテストの資産化」をテーマに考え方や方法論を紹介します
概要
• テストコードの運用
• テストコード運用上の問題
• テストによるコードの資産化
• テストコードのドキュメント化
• レガシーコードでのTDD
TDDのテストコードの運用
テストコードの運用
• TDDでのテストの持続的効果–単体テスト容易性の確保
–実装仕様の確保
–自動化された回帰テスト環境の構築
• 継続運用の課題–単体テストとしての整理(前篇)
–運用環境の構築
–テストコードのメンテナンス
TDDの文脈におけるテストコードの運用フェーズ
• 実装作業中、継続的かつこまめに
–常時実行される回帰テスト
–何度も何度も実行できるように自動化を強力に推進する
運用環境の構築
• 単体テスト運用環境の軸
• 構成管理システムの運用
• 継続的インテグレーション
–環境の分散
単体テスト運用環境の軸
クライアント サーバ
手動 JUnit、スクリプト:手動で実行 CIサーバ、ビルドサーバ:手動コマンド
イベント時(コミット前等)
バージョン管理クライアント:コミットに組込IDE:ビルドステップ等に機能
CIサーバ:コミット駆動
自動 スケジューラ:時間駆動 ビルドサーバ:デイリービルド
環境の軸
タイミングの軸
構成管理システムの運用
• Ex)バージョン管理システム
– コード変更/リリースとテスト実行の同期を取る
–テスト対象の時系列データを確保する
• 単体テストの自動運用の要
–回帰テストによるフィードバックを軽快に得る
継続的インテグレーション(CI)
• 自動化されたインテグレーションを継続的に実行
1. インテグレーションを統合・自動化• ビルドに単体テストを統合
2. インテグレーションの実行を自動化• インテグレーション用サーバを用意(Hudson有名)
• 統合したインテグレーションを継続的に自動実行する
– コミット時等。ナイトビルドよりもっと頻繁に
– 継続的ですばやいフィードバックを実現
単体テスト運用環境
作業用PC
構成管理サーバ
DB関連テスト
ストレステスト
規格バリデーション
コミット
更新・自動実行
更新・自動実行
更新・自動実行
制約のあるテストを分散運用することでSlowTest問題や高コストを回避する
単体テスト運用マトリクス
環境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