テスト駆動開発入門(後編)

テスト駆動開発入門(後編)
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