Deprecated: The each() function is deprecated. This message will be suppressed on further calls in /home/zhenxiangba/zhenxiangba.com/public_html/phproxy-improved-master/index.php on line 456
テスト駆動開発考察
[go: Go Back, main page]

トップへ

テスト駆動開発考察

テスト駆動開発とは

テスト駆動開発(以下TDD)は以前はテストファーストやテストファーストプログラミングと呼ばれていたものです。テストファーストは単純に実装する前にテストを書くことと誤解されることが多かったため名前をテスト駆動開発に改めました。TDDはテストを書くことによって開発(実装)が進捗します。つまり、テストを成功させるためだけに実装していきますので、テストの無い実装は無いことになります。 テスト駆動開発では思いついた実現方法をいきなりコーディングしてはいけません。それどころかクラス設計を行ってもいけません。あくまでもテストを書き、そのテストを成功させるためだけに注力を注ぐ所がポイントです。そんなことをしたら設計をしないいい加減な作りになってしまわないかと気にする方も多いでしょう。しかし、設計が完全だからといってテストがおろそかな方がよっぽどいい加減では無いでしょうか?実際に動作しない机上の設計よりも確実に動作する項目のリストがテストとして存在する方が信頼性は圧倒的に高くは無いでしょうか? また、TDDでは開発者に対する精神的な作用が多いことも述べられています。

TDDでは次のステップで開発を進めていきます。

  1. これからソフトウェアとして作成するための作業項目(タスクリスト)を作成(更新)する。
  2. タスクリストから1つ項目を選びそのテストコードを書く
  3. テストが失敗することが確認する(たまに成功する場合もある。その場合は2へ)
  4. 失敗したテストを最も簡単に成功させるための実装を行う
  5. テストが成功することを確認する
  6. リファクタリングを行い、テストが成功することを確認する。
この1から6までの項目を繰り返し、1のタスクリストが全て消滅するまで繰り返します。特に気をつけなければならない点はあくまでも1つの項目づつ作業を進めるという点です。途中で新たな必要な作業項目が見つかった場合はその場でその開始せずにタスクリストに加えて今行っていた作業を進めます。もしも新たに発見された作業の方が優先度が高ければ、テストが成功する段階まで作業を元に戻してからにしなければなりません。

タスクリストの書き方

タスクリストには自分が今からソフトウェアで実装する必要のある項目のみを記述します。明らかに自分の実装する範囲外で行われる、例えば他の会社で作られたハードが行う項目はタスクリストには含めません。 1つのタスクに複数の項目を書かないようにします。例えば「口座からお金を引き出す際はパスワードで認証した後に入力された金額を引き出せること」この場合認証と引き出しの2つの項目が含まれています。一見するとこの2つの項目は強く関連しているように思えますが、実は別のアスペクト(関心事)として分けることができます。つまり「パスワードによって認証する」と「入力された金額を引き出す」は別のアスペクトですので別の項目として書きます。このように別のタスクとして分割したほうが良いアスペクトには次のようなものがあります。

自前で実装が必要な項目

ライブラリや他のシステムに依存する項目

タイミングに依存する項目

TDDのタスクではない項目

通信プロトコルや永続化などの指定は機能要求では無い技術的な制約です。しかし実際の業務ではこれらの項目を要求として課せられることもあります。この場合は別の項目として書いておきましょう。UIはテスト駆動が不可能なのでTDDのタスクリストからは除くべきです。重要なことは入力された値がシステムに対してどのように影響するのか?ということです。

TDDのメリット

先ほども述べましたがTDDは作業者の精神面での効果が大きなメリットになります。まず、TDDを行わない失敗するプロジェクトの例を見てみましょう。
実装を先に書いてからテストを書く場合テストの完全性を保障することは困難です。そして品質面での不安というストレスが少しずつ増加していきます。ストレスが上昇するとテストが減少していきます。テストが減少すると誤りが増加しストレスはさらに増加していきます。また、このような状況で機能の修正や追加を要求された場合は、修正後に動くコードが動くという保障がなくなります。このことがソフトウェア開発者が要求変更を拒む最大の理由となってきたと考えられます。さらには拒むことが許されなければさらにストレスは上昇し、止めることのできないネガティブサイクルに突入しています。例えリファクタリングによってコードを綺麗にメンテナンス可能なように書き換えようと努力しようとも、この状況ではリファクタリングによるミスがさらにストレスレベルを増加させかねませんし、管理者は進捗が無いことに苛立ちを覚えリファクタリングを認めなくなるなることすらあるでしょう。この状況ではどんな手を尽くしても復帰させるよりもやり直した方が良いでしょう。つまりプロジェクトは破綻していると考えられます。
次にTDDを採用しているプロジェクトは同でしょうか?最初からテストは完全です。この段階では非TDDのプロジェクトよりも進捗が若干遅いかもしれません。しかし常にリファクタリングされ、動作するコードはストレスを感じさせません。むしろ安心感さえあります。するとテストが不足していないかをさらに注意深くメンテナンスしようとしますし、機能の追加や修正も全くストレスにはなりません。むしろ追加しやすいようにリファクタリングしてから作業を開始するため追加作業が以前とは比べ物にならない早くて正確なものになります。コードは常にだれでもリファクタリングが可能です。見やすく管理しやすいコードは予想以上にプロジェクト後半の作業を進捗させていきます。

TDDのデメリット

TDDではTDD自体のルール、リファクタリング、自動化ユニットテスト、デザインパターン(※1)などといった知識の保有が前提になります。そのため教育コストがかかることがあります。また、TDDに前向きではないメンバーの多いプロジェクトでは無理に押し付けると余計なストレスとなりかねません。それでは本末転倒となりますので難しいと考えられます。

TDDではテストが不足していることを発見することが難しいです。せいぜい三角測量を使うことで発見できるかもしれませんが、それも完璧ではありません。そして多くのテスト項目が全てパスする絵をみてしまうと作業が完璧なのだと勘違いしてしまうリスクがあります。このことからテストの実装に漏れが無いかを常にレビューしなければなりません。結局最後は人間のチェックになってしまいます。

※1.GOF本の個々のパターンを全て必要な知識とするわけではなく基本的なパターンとその応用的な考えや言葉などを指しています。

リーン思考とTDD

リーン思考とTDDは非常に相性がよく、リーン思考で作業をするためにはTDDは良いプラクティスとなりえます。リーンの代表的な原則に「ムダを排除する」があります。使われない全ての機能はムダですが、TDDでは不必要な実装はしません。というか必要最低限の実装しかしませんのでムダな実装が成果物に入ることはありません。「決定をできるだけ遅らせる」ことに関しても非常に有効です。常に動作するテストとコードがありますのでプロジェクトの後半での決定により仕様がくつがえったとしても、テストを追加して修正した結果が及ぼす副作用を正確に把握することが可能です。重要な決定が最後になったとしても、実装可能な項目から作ることを十分可能にします。「できるだけ早く提供する」という観点からも常に現状の成果が提供可能です。発注者に対して実際に動作させたりテストコードを見せて間違えが無いかを常に確認してもらうことが可能です。「統一性を作りこむ」ではコードの統一性について言うことができます。リファクタリングでいつでも統一の取れたコードに修正することが可能だからです。
リーンの原則が製品開発に有効な原則だとすると、TDD以上にリーンの原則が満たせる開発手法は私的には見つかりません。このことからもTDDがいかに優れた製品開発手法かということが伺えるでしょう。

チーム開発でのTDD

最後にチームでTDDをやる場合に役立つルールや注意事項について述べます。チームでの開発ではソースコード管理システムを使います。ここで使い方のルールを徹底しておかないとチームでのストレスレベルが上がってしまいます。したがってルールを設定しておくことが重要になります。大前提は綺麗なコードのみコミット(チェックイン)可能であることです。チームでの開発では「テスト駆動開発とは」で述べた作業ステップの後に以下の流れが追加されます。括弧内はVSSでの用語を表します。

  1. 更新(最新版の取得)する。CVSはコンフリクトした場合マージ。
  2. テストを実行して動作することを確認する。
  3. コミット(チェックイン)する。
このルールを徹底すると常に動作する綺麗なコードが管理されます。リファクタリングのみが目的の場合も7〜9のステップを踏むことで安心して思い切った作業を可能にします。作業するメンバー全員がこの短いサイクルでコードをインテグレーションしていきますので誤りが埋め込まれた時点で発見されますし、その誤りを修正するまでの時間は極限まで短くなります。結果としてインテグレーションにおけるストレスは上がらないため、その効果は関係の良いチーム作りにまで至ることでしょう。より良い人間関係の形成は良質の製品の開発には必要不可欠なのです。

3つのモードを使い分ける

TDDでは3つのモードを使い分ける必要があります。

プログラマは必ずいずれかのモードで作業しなければなりません。というのは同時に複数のモードであってはならないことを意味しています。テストモードでは新たな失敗するテストを追加することだけに集中してください。つまりテストコードのみ手を入れることが許されます。実装を修正することは許されません。実装モードでは逆にテストコードの修正はしてはいけません。テストを成功させる実装を行うことに集中します。リファクタリングモードでは両方のコードを修正することが許されますがテストの結果は常に成功していなければなりません。当たり前のことのようみ聞こえますがこれが案外守り続けるのが難しいです。現在ではこのモードをサポートするツールが無いためうっかり誤って異なるモードの作業をしてしまうことがありますので、常に自分が今何のモードにあるのかを意識しなければなりません。

TDDと要求開発

TDDは要求開発の出力に駆動されるソフトウェアのモデリング技法として非常に有効な手法です。ここまではウォーターフォールやUPでUMLで実装のモデリングをするのとさほど代わり映えはしませんが、要求開発の出力が殆ど無い状態からでも開発を可能にします。当然ウォーターフォールは要求開発の出力を100%必要とします。次にUPでは開発プロセスのを見ると40%から60%以上は必要としています。特にUPでは要求が決まる前にアーキテクチャを推敲フェーズで決定(※2)しなければならなくそこがリスクになります。
ものにもよりますがTDD(XP)は1%の出力から開発が可能です。1%でも何がしたいか要求が出れば開発を進捗させることが可能だと考えます。UPでは開発の後半でアーキテクチャの大幅な変更は発生しないよう推敲フェーズで予想するのがリスクだと述べましたが、TDDではこのようなアーキテクチャの変更であろうと対処可能にします。

※2. 決定しなければならないのではなく、決定するものだという考え方ですが必ずしもそうでないのであえてこのように述べています。

TDDの教育

このような新しい開発手法や方法を浸透させるためには次の項目を満たせなければなりません。

  1. 実務的に成功することが実証されていること。
  2. どうして成功するのかが説明でき、理解させることが可能なこと。

実務的な成功の実証については、実務の例をとって説明し実際の実績を作っていかなければなりません。また、リファクタリング、JUnitの使い方の説明、オブジェクト指向の知識も必須です。デザインパターンは必須ではないと考えますが知ってた方が効率よくコミュニケーションが取れるので良いかと思います。 2番目のTDDが成功するメカニズムについては今までの記述を説明すればよいでしょう。特にメリットデメリットの項目はTDDを使うことでのプロジェクトの成功メカニズムについて述べましたので有効だと思います。

要求の変更及び削除への追従

開発者にとって要求変更や削除については以前から頭の痛い問題です。削除については「どうせ後になって必要になるかもしれない」という言い訳のもと無視されることがほとんどでは無いでしょうか?TDDではこれを禁止しています。使わない機能は全て削除しなければなりません。要求以上のものでも以下のものでもいけないとされているからです。その理由として使われない機能の実装のおかげでコードのメンテナンシビリティが低下するからです。仕様書に書かれていない機能が実装中に山ほど登場したらどうでしょうか?将来に渡って管理することはもはや困難です。それでもとりあうえず納品には支障が無いのでこのようなケースは世の中に山ほどあることでしょう。変更については当然無視できないので難問として重くのしかかりますが、コストと納期の増大を代償になんとか乗り切るというのが一般的な結果です。
TDDでは仕様が変更された場合コードを見るのではなくテストをその仕様通りに書き換えます。もしこれで赤くならなければ問題ありません。最もそんなケースは稀だと思います。あとは通常の通り赤くなったテストをグリーンにするだけです。一方で削除の方が手間がかかります。削除の場合は最初にカバレッジツールなどでランタイム例外に相当するcatch文やアサートを除く正常系処理が全て網羅されていることを確認した後にテストを削除します。その後実行されていない箇所を削除します。この方法だと以前は困難だった条件分岐の削除などにも対応できる点が優れています。





テスト駆動開発の実践例

実践の実証が必要ですのでJavaでeclipseを使ったテスト駆動開発の例を示します。今回はeclipse+J2SE5.0の構成で行います。

準備

数当てゲーム

数当てゲームをTDDで作ってみます。数当てゲームとはゲームを開始するとランダムに1つの数字が設定され、その数字を当てると言うものです。答えが間違えていると間違えた答えが正解の数字よりも大きいか小さいかを返してくれます。対戦の場合は回答回数が少ない方が勝ちとなります。この仕様を実装タスクリストに置き換えると次のようになります。

正解として設定した数値を答えると正解を返す

最も簡単そうな項目を1つ選び作業を開始します。テストはassert文から記述していきますのでaeと打ってからCtrl+spaceを押下してください。先ほど設定したテンプレートが現れます。次に上記要求を満たすためにはどのような結果を期待するのかを日本語で書きます。ここでは具体的な値を出して曖昧さのない期待する実行結果を書きます。次にtabキーを押下すると期待値の入力を促してきますので定数HighOrLowGame.RIGHTを書きます。次のtabキーを押してどの値を評価するのかを入力します。

	public void testRightAnswer(){
		HighOrLowGame game = new HighOrLowGame();
		game.setRightAnswer(4);
		assertEquals("答えが4なので4は正解", HighOrLowGame.RIGHT, game.answer(4));
	}
この段階では外部から見て理想なコードを好き勝手に書いてください。実装の都合は一切考えないことがポイントです。次にコンパイルを通すためにコードを書きます。
public class HighOrLowGame {
	public static Object RIGHT;
	public Object answer(int i) {
		return null;
	}
	public void setRightAnswer(int i) {
	}
}
これでコンパイルが通りますので、次は先ほどのテストを成功させるための最も簡単な実装を行います。最も簡単なというところが肝心です。将来の事を考えたりメンテナンス性を考える必要はありません。
public class HighOrLowGame {
	public static Object RIGHT = new Object();
	public Object answer(int i) {
		return RIGHT;
	}
	public void setRightAnswer(int i) {
	}
}
テストを成功させるための最も簡単な実装は上記のようになります。これでテストは成功しますね?テストが不足していないか見てみましょう。この段階でテストを赤にすることが可能なテストが書けますか?game.setRightAnswerに5を設定するテストを書いたところで結果は変わりませんので必要ありません。機能に変更を行わずにテストを失敗できなければ次に進みます。まだリファクタリングの必要性は無さそうなので次の項目を作りましょう。

正解よりも高い数値を答えると、高いを返し、低い数値を答えると低いを返す

同様に作成したテストは次のようになります。先ずは「高い」を返すテストです。

	public void testHighOrLowAnswer(){
		HighOrLowGame game = new HighOrLowGame();
		game.setRightAnswer(4);		
		assertEquals("答えが4なので5は高いを返す", HighOrLowGame.HIGHER, game.answer(5));
	}
上記テストを成功させる最も簡単な実装は次のようになりました。今のところ変数名などは適当ですが、とりあえずテストを成功させることが先決です。
public class HighOrLowGame {

	public static Object HIGHER = new Object();
	public static Object RIGHT = new Object();

	private int i;
	public Object answer(int i) {
		if( this.i == i){
			return RIGHT;
		}
		return HIGHER;
	}

	public void setRightAnswer(int i) {
		this.i = i;
	}

}
テストが成功したのでリファクタリングしましょう。リファクタリングでは妥当な名称への変更と重複の除去、適切なアクセススコープの設定、クラス構成への書き換えなどを行ってください。リファクタリングが終わったら更にテストを追加していきます。
	public void testHighOrLowAnswer(){
		HighOrLowGame game = new HighOrLowGame();
		game.setRightAnswer(4);		
		assertEquals("答えが4なので5は高いを返す", HighOrLowGame.HIGHER, game.answer(5));
		assertEquals("答えが4なので3は低いを返す", HighOrLowGame.LOWER, game.answer(3));
	}
実装は次のようになりました。
public final class HighOrLowGame {

	public static Object LOWER = new Object();
	public static Object HIGHER = new Object();
	public static Object RIGHT = new Object();
	private int right;
	public Object answer(int answer) {
		if( right == answer){
			return RIGHT;
		}
		if(right > answer){
			return LOWER;
		}
		return HIGHER;
	}
	public void setRightAnswer(int answer) {
		this.right = answer;
	}
}
ここで、もしもsetRightAnswerを呼び出さないでanswerを呼び出したらどうなるのか?という疑問が生じます。(もっと前に気が付いたかもしれませんが)仕様ではデフォルトの回答などは無いという事ですので、コンストラクタで設定できなければなりません。機能の追加修正ではないのでリファクタリングモードで作業しましょう。HighOrLowGameのコンストラクタは正答を受け取るようにしてsetRightAnswerをprivateにします。このようなリファクタリングはわざとコンパイルエラーにして修正するのが効率的です。リファクタリングした結果は次のようになりました。
	public void testRightAnswer(){
		HighOrLowGame game = new HighOrLowGame(4);
		assertEquals("答えが4なので4は正解", HighOrLowGame.RIGHT, game.answer(4));
	}

	public void testHighOrLowAnswer(){
		HighOrLowGame game = new HighOrLowGame(4);
		assertEquals("答えが4なので5は高いを返す", HighOrLowGame.HIGHER, game.answer(5));
		assertEquals("答えが4なので3は低いを返す", HighOrLowGame.LOWER, game.answer(3));
		
	}


public final class HighOrLowGame {

	public static Object LOWER = new Object();
	public static Object HIGHER = new Object();
	public static Object RIGHT = new Object();

	private int right;
	
	HighOrLowGame(int right){
		setRightAnswer(right);
	}
	public Object answer(int answer) {
		if( right == answer){
			return RIGHT;
		}
		if(right > answer){
			return LOWER;
		}
		return HIGHER;
	}

	private void setRightAnswer(int answer) {
		this.right = answer;
	}

}

正解までにかかった回答の回数を得ることができる

正解までにかかった回数を得るということなので次のようなテストになります。 まだ正解していないのに呼び出すとどうなるのか?また、正解後にanswerを呼び出すとどうなるのか?という疑問が生じてきます。 正解までにかかった回答の回数を得るには回答の回数を求める機能があれば実現できそうですので次のようなテストを書きました。

	public void testNumberOfAnswers(){
		HighOrLowGame game = new HighOrLowGame(4);
		game.answer(4);
		assertEquals("1回で正解なので1を返すこと", 1,game.getNumberOfAnswer());
	}
このテストを書きながら、まだ正解していないのにgetNumberOfAnswerを呼び出すとどうなるのか?また、getNumberOfAnswerを呼び出した後にanswerを呼び出すとどうなるのか?という疑問が生じますがとりあえずタスクリストに加えて今の作業を続行します。 とりあえず先ほどのテストがパスする最も簡単な実装は次のようになります。
public final class HighOrLowGame {

	// ...省略

	public int getNumberOfAnswer() {
		return 1;
	}
}
定数を返す仮実装をしていますがこれでも上記のテストはパスします。しかしこれでは明らかにテストが不足しています。機能を追加せずに想定可能なパラメータを変更しただけでテスト結果を赤くすることが可能なのでそのようなケースを追加します。
	public void testNumberOfAnswers(){
		HighOrLowGame game = new HighOrLowGame(4);
		game.answer(0);
		assertEquals("1回答えたので1を返すこと", 1,game.getNumberOfAnswer());

		game = new HighOrLowGame(4);
		game.answer(8);
		game.answer(4);
		assertEquals("2回で正解したので2を返すこと", 2,game.getNumberOfAnswer());
	}
ここで追加したパラメータのテストは失敗しますので修正してなんとか緑にする必要があります。追加した結果は次の通りです。
public final class HighOrLowGame {

	public static Object LOWER = new Object();
	public static Object HIGHER = new Object();
	public static Object RIGHT = new Object();

	private int right;
	private int numberOfAnswer;
	HighOrLowGame(int right){
		setRightAnswer(right);
		numberOfAnswer = 0;
	}
	public Object answer(int answer) {
		numberOfAnswer++;
		if( right == answer){
			return RIGHT;
		}
		if(right > answer){
			return LOWER;
		}
		return HIGHER;
	}

	private void setRightAnswer(int answer) {
		this.right = answer;
	}
	public int getNumberOfAnswer() {
		return numberOfAnswer;
	}
}
このように、一度仮実装を行い、その後テストを追加して実装を固めていく方法を三角測量と呼びます。これは2つ以上のパラメータの例が存在し、それぞれが条件の異なる実装を駆動させる場合に行うテストの方法として役に立ちます。通常この程度の実装では「明白な実装」を用いて簡略化しますが、明白化が行き過ぎるとTDDは破綻してしまうので注意が必要です。明白な実装の結果テストが失敗して驚く場合は三角測量で正確に少しづつ開発を進めましょう。

ここでタスクリスト見直して見ましょう。終了した項目には打ち消し線を引きました。また、2つの項目が新しく追加されています。先ほど実装しながら生じた新たな疑問を具体的な仕様として決定しました。

早速新しく追加された状態に関する不正の仕様を1つずつ片付けていきましょう。例外がスローされることを期待するテストはJUnitでは次のように書きます。
	public void testGetNumberOfAnswerNotRightYet(){
		try{
			HighOrLowGame game = new HighOrLowGame(4);
			game.getNumberOfAnswer();
		}catch(IllegalStateException e){
			return;
		}
		fail("まだ正解していないのにgetNumberOfAnswerを呼び出すとランタイム例外IllegalStateExceptionをスローすること");
	}

	public void testAnswerAfterRight() {
		try{
			HighOrLowGame game = new HighOrLowGame(4);
			game.answer(4);
			game.answer(9);
		}catch(IllegalStateException e){
			return;
		}
		fail("正解後にanswerを呼び出すとランタイム例外IllegalStateExceptionをスローすること");
		
	}

public final class HighOrLowGame {
	// 省略

	private boolean hasRight = false;

	HighOrLowGame(int right){
		setRightAnswer(right);
		numberOfAnswer = 0;
	}
	public Object answer(int answer) {
		if(hasRight){
			throw new IllegalStateException();
		}
		numberOfAnswer++;
		if( right == answer){
			hasRight = true;
			return RIGHT;
		}
		if(right > answer){
			return LOWER;
		}
		return HIGHER;
	}

	private void setRightAnswer(int answer) {
		this.right = answer;
	}
	public int getNumberOfAnswer() {
		if(!hasRight){
			throw new IllegalStateException();
		}
		return numberOfAnswer;
	}
}
ここでは2つ同時に示していますがそれぞれTDDのサイクルをまわしています。

ゲームの結果、回答回数が少ない方のプレーヤーを勝者と判定することができる

次はゲームの結果判定を行う処理を作ります。そのテストを次のように書きました。

	public void testJudge(){
		HighOrLowGame a = new HighOrLowGame(5);
		a.answer(5);
		HighOrLowGame b = new HighOrLowGame(5);
		b.answer(0);
		b.answer(5);
		assertEquals("回答回数が少ないプレーヤーが勝者なので結果aが得られること",a,HighOrLowGame.judge(a,b));
	}
その実装は次のようになります。
	public static HighOrLowGame judge(HighOrLowGame a, HighOrLowGame b) {
		if( a.getNumberOfAnswer() < b.getNumberOfAnswer() ){
			return a;
		}
		else{
			return b;
		}
	}
次に引き分けの場合のテストを書きます。引き分けをどのように返すかは実装の詳細なので何でも良いのですが、上記のようなインタフェースでは引き分けを返すオブジェクトを返すわけにも行きませんので、とりあえずチェックド例外で返すことにしました。
	public void testEven(){
		HighOrLowGame a = new HighOrLowGame(5);
		a.answer(5);
		HighOrLowGame b = new HighOrLowGame(5);
		b.answer(5);
		try{
			HighOrLowGame.judge(a,b);
		}catch(JudgeEvenException ex){
			return;		
		}
		fail("回答回数が同じ場合はJudgeEvenExceptionがスローされること");	
	}
	public static HighOrLowGame judge(HighOrLowGame a, HighOrLowGame b)throws JudgeEvenException {
		if( a.getNumberOfAnswer() < b.getNumberOfAnswer() ){
			return a;
		}else if(a.getNumberOfAnswer() > b.getNumberOfAnswer()){
			return b;
		}
		throw new JudgeEvenException();
	}

0-100の範囲の乱数で正答が設定されること

最終的には回答は乱数によって設定されます。しかしTDDのテストは実行の結果として期待される値を書かなければなりませんので、乱数に依存したのテストは書くことができません。今までは乱数に依存する箇所をうまくかいくぐりテストを書いてきましたが、TDDにより駆動することが難しい種類の処理には次のようなものがあげられます。

GUIのテストの完全な自動化は無理ですが、そのほかについてはモックオブジェクトを作成するなどして依存箇所と切り離してTDDにより開発を駆動することができますが、それなりのテクニックや経験が必要になります。 このようにTDDでは設計はテストのしやすさを優先させます。テストのしやすい設計はテストのしずらい綺麗な設計に比べて確実に意味があります。そもそも設計の綺麗さとは何でしょうか?いくら綺麗でも動かなければ意味が無いのです。
ではもしも「0-100の範囲の乱数で正答が設定されること」のテストを書くとしたら次のようになります。
	public void testInitGame(){
		HighOrLowGame game = GameFactory.createNewGame();
		int right = game.getRight();
		assertTrue("100を指定しので0-100の範囲の乱数で正答が設定されること",right <= 100 && right >= 0);
	}
しかしこのテストは品質の検査のテストにはなりますが開発を駆動しません。必ず赤にならなければ開発は駆動されませんのでTDDのテストにはなりません。

public class GameFactory {

	private static Rng rng = new MockRng(0);

	public static HighOrLowGame createNewGame() {
		int right = rng.next();
		return new HighOrLowGame(right);
	}

}

public interface Rng {
	int next();
}

public class MockRng implements Rng {
	private int i;
	public MockRng(int i) {
		this.i = i;
	}
	public int next() {
		return i;
	}
}

GameFactoryは乱数生成機を抽象化して開発中はモックの乱数生成機を用いるようにしています。実際の乱数生成機はTDD意外の方法で実装して後に結合することになります。このように実際の開発シーンではテスト不可能な箇所を抽象化するために設計することはあります。

この段階で全ての項目が打ち消されました。

今回はUIに関する仕様がありませんので開発はここまでですが、これにUIをつける場合は表示部分にのみ集中して実装することができます。

参考文献