テスト駆動(TDD)-実践-

■仕様

コース料理金額計算処理の作成
	コースの種類
		通常(1):1人3000円
		通常+飲み放題あり(2):1人3500円
		スペシャルコース(3):1人5000円
		※()の中の数値はコース番号

	料金体系:
		人数 * コース料金

■前準備

●ToDoリスト作成
	仕様を確認した上で必要になってくる処理は「人数 * 料金」の料金計算です。
	ですので、ToDoは以下の内容とします。
	
		・料金計算
	
●設計
	最初の設計としてクラスと必要なメンバを考えます。
	クラスはコース料理クラスということで「DinnerCourse」、
	メンバ変数は人数「m_Number」、コースの種類「m_Course」を用意します。

	クラス:
		DinnerCourse

	メンバ変数:
		int m_Number
		int m_Course

■テストファースト(通常コース料金計算)

ToDoリストからまずは「料金計算」処理の実装を行います。
実装コードを作成する前にテストコードを作成します。

●テストコード
	#include <cassert>

	// テストクラス
	class Test
	{
	public:
		// 通常料金テスト
		void TestNormalCourse(void);
	};

	/*
		関数名:
			TestNormalCourse
		引数:
			なし
		戻り値:
			なし
		内容:
			通常コースの料金テストを行う
	*/
	void Test::TestNormalCourse(void)
	{
		DinnerCourse dbg_dinner;

		// コース料理クラスの設定
		dbg_dinner.SetCourse(1);
		dbg_dinner.SetNumber(5);

		// 料金の取得
		int charge = dbg_dinner.GetCharge();

		// 3000円 * 5人以外が返ってきたらアウト
		assert(charge == 3000 * 5 && "NormalCourse GetCharge Error");
	}

	void main(void)
	{
		Test test;

		// テスト実行
		test.TestNormalCourse();
	}

	テスト用クラスとしてTestを作成し、DinnerCourseクラスを作成して設定を行い、
	料金取得関数を呼び出してその後Assertでテストを行う内容になっています。
	今回通常コースのテストとして作成していますので、
	飲み放題有り、スペシャルコースのテストも別途作成するということで、
	ToDoリストを変更します。

		変更前:
			・料金計算
		変更後:
			・料金計算(通常)
			・料金計算(通常+飲み放題)
			・料金計算(スペシャルコース)

■実装コード作成(通常コース料金計算)

テストファーストのコードを作成ができましたので実装コードの作成に移ります。

●コンパイルエラー => Red状態
	現状コンパイルエラー複数発生しているのでそちらのエラーをなくしつつ、
	アサートでエラーが発生するRed状態の実装コードを作成します。

	・コード
		// DinnerCourse.h
		#ifndef DINNER_COURSE_H_
		#define DINNER_COURSE_H_

			class DinnerCourse
			{
			private:
				int m_Number;		// 人数
				int m_Course;		// コース
			public:
				DinnerCourse()
				{
				}

				void SetNumber(int number) 
				{
				}

				void SetCourse(int course) 
				{ 
				}

				// 料金計算
				int GetCharge();
			};

		#endif

		// DinnerCourse.cpp
		#include "DinnerCourse.h"

		int DinnerCourse::GetCharge(void)
		{
			return 0;
		}

	テストを失敗させるためのコードなので、DinnerCourseで定義している
	メンバ関数は空の状態にしています。
	これでコンパイルは通るけど、Assertで確実にエラーが発生するコードになりました。
	
●Red => Green状態
	次はRed状態のコードをGreen状態に変更しますが、
	この時の実装方法は3種類あると考えられています。

	①.仮実装
		実装が複雑になりそうな場合は仮の値でグリーンにする方法です。
		これはRed状態が長引くのを防ぐための方法として一番簡易なやり方です。
		仮でもいいのでテストが通ることを確認することを第一として考えられています。

	②.明白な実装
		実装処理が単純ですぐに実装できそうな場合はそのまま実装します。

	③.三角測量
		実装が複雑で、様々なケースが想定される場合に
		別のテストを追加して複数ケースでもGreenになるように実装します。
		これはまず①の「仮実装」を行い、Greenになることを確認し、
		別のテスト(Assertまたはテストメソッド)を追加し再びRedにします。
		その後、きちんと実装を行い二つのテストをGreenにします。

	今回のテストでは処理的に簡単なものが多いので、②の「明白な実装」でもいいのですが、
	せっかくなので、①の「仮実装」で作成したいと思います。

	①仮実装によるGreen状態への変更:
		// DinnerCourse.cpp
		int DinnerCourse::GetCharge(void)
		{
			return 15000;
		}

●Green(正式な実装)
	①または③による実装の場合、Greenになった後に仮ではなく本実装を行います。
	ここで本実装を行った際にRed状態に戻ってしまったらコードを見直してください。

	実装箇所:
		①.SetNumber
			セッターとして機能させる

			// DinnerCourse.h
			void SetNumber(int number)
			{
				m_Number = number;
			}
		
		②.SetCourse
			セッターとして機能させる
			// DinnerCourse.h
			void SetCourse(int Course)
			{
				m_Course = Course;
			}

		③.GetCharge
			人数 * コース金額
			※コース金額は通常コースのみ

			// DinnerCourse.cpp
			int DinnerCourse::GetCharge()
			{
				int charge = 0;

				switch(m_Course)
				{
				case 1:
					charge = m_Number * 3000;
					break;
				}

				return charge;
			}

	これでToDoリストの「料金計算(通常コース)」が完了しました。
	本来はこの後リファクタリングを行うことになるのですが、
	残りの「通常+飲み放題コース」「スペシャルコース」の料金計算も
	「人数 * コース料金」の計算ですので、先にこちらの実装を済ませたいと思います。

■テストファースト(通常+飲み放題コース料金計算)

ToDo「料金計算(通常+飲み放題)」のテストファーストを作成します。
ベースとなるTestクラスは作成しているので「TestFreeDrinkCourse」関数を追加します。

コード:
	#include <cassert>
	#include "DinnerCourse.h"

	// テストクラス
	class Test
	{
	public:
		// 通常料金テスト
		void TestNormalCourse(void);
		// 飲み放題料金テスト
		void TestFreeDrinkCourse(void);
	};

	/*
		関数名:
			TestNormalCourse
		引数:
			なし
		戻り値:
			なし
		内容:
			通常コースの料金テストを行う
	*/
	void Test::TestNormalCourse(void)
	{
		DinnerCourse dbg_dinner;

		// コース料理クラスの設定
		dbg_dinner.SetCourse(1);
		dbg_dinner.SetNumber(5);

		// 料金の取得
		int charge = dbg_dinner.GetCharge();

		// 3000円 * 5人以外が返ってきたらアウト
		assert(charge == 3000 * 5 && "NormalCourse GetCharge Error");
	}

	/*
		関数名:
			TestFreeDrinkCourse
		引数:
			なし
		戻り値:
			なし
		内容:
			通常+飲み放題コースの料金テストを行う
	*/
	void Test::TestFreeDrinkCourse(void)
	{
		DinnerCourse dbg_dinner;

		// コース料理クラスの設定
		dbg_dinner.SetCourse(2);
		dbg_dinner.SetNumber(5);

		// 料金の取得
		int charge = dbg_dinner.GetCharge();

		// 3500円 * 5人以外が返ってきたらアウト
		assert(charge == 3500 * 5 && "FreeDrinkCourse GetCharge Error");
	}

	void main(void)
	{
		Test test;

		// テスト実行
		test.TestNormalCourse();
		test.TestFreeDrinkCourse();
	}

■実装コード作成(通常コース+飲み放題料金計算)

●コンパイルエラー => Red状態
	メンバ自体は通常コースの内容で足りており、コンパイルエラーになることもなく、
	Assertエラーが発生すると思いますので、このまま次のGreen状態にするための実装へ進みます。

●Red => Green状態
	飲み放題コースの料金計算もGetChargeで行います。
	既にある程度の実装ができており、追加実装の内容も単純ですので、
	今回は②の明白な実装でGreen状態にしたいと思います。

	・コード:
		int DinnerCourse::GetCharge()
		{
			int charge = 0;

			switch(m_Course)
			{
			// 通常
			case 1:
				charge = m_Number * 3000;
				break;
			// 飲み放題
			case 2:
				charge = m_Number * 3500;
				break;
			}

			return charge;
		}

これでToDo「通常+飲み放題料金」の実装が完了しました。
もし、Assertまたはコンパイルエラーが発生する場合はコードを見直してください。

■テストファースト(スペシャルコース料金計算)

最後のToDo「料金計算(スペシャルコース料金計算)」のテストファーストを作成します。
ベースとなるTestクラスは作成しているので「TestSpecialCourse」関数を追加します。
コード:
	#include <cassert>
	#include "DinnerCourse.h"

	// テストクラス
	class Test
	{
	public:
		// 通常料金テスト
		void TestNormalCourse(void);
		// 飲み放題料金テスト
		void TestFreeDrinkCourse(void);
		// スペシャルコース料金テスト
		void TestSpecialCourse(void);
	};

	/*
		関数名:
			TestNormalCourse
		引数:
			なし
		戻り値:
			なし
		内容:
			通常コースの料金テストを行う
	*/
	void Test::TestNormalCourse(void)
	{
		DinnerCourse dbg_dinner;

		// コース料理クラスの設定
		dbg_dinner.SetCourse(1);
		dbg_dinner.SetNumber(5);

		// 料金の取得
		int charge = dbg_dinner.GetCharge();

		// 3000円 * 5人以外が返ってきたらアウト
		assert(charge == 3000 * 5 && "NormalCourse GetCharge Error");
	}

	/*
		関数名:
			TestFreeDrinkCourse
		引数:
			なし
		戻り値:
			なし
		内容:
			通常+飲み放題コースの料金テストを行う
	*/
	void Test::TestFreeDrinkCourse(void)
	{
		DinnerCourse dbg_dinner;

		// コース料理クラスの設定
		dbg_dinner.SetCourse(2);
		dbg_dinner.SetNumber(5);

		// 料金の取得
		int charge = dbg_dinner.GetCharge();

		// 3500円 * 5人以外が返ってきたらアウト
		assert(charge == 3500 * 5 && "FreeDrinkCourse GetCharge Error");
	}

	/*
		関数名:
			TestSpecialCourse
		引数:
			なし
		戻り値:
			なし
		内容:
			スペシャルコースの料金テストを行う
	*/
	void Test::TestSpecialCourse(void)
	{
		DinnerCourse dbg_dinner;

		// コース料理クラスの設定
		dbg_dinner.SetCourse(3);
		dbg_dinner.SetNumber(5);

		// 料金の取得
		int charge = dbg_dinner.GetCharge();

		// 5000円 * 5人以外が返ってきたらアウト
		assert(charge == 5000 * 5 && "SpecialCourse GetCharge Error");
	}
		
	void main(void)
	{
		Test test;

		// テスト実行
		test.TestNormalCourse();
		test.TestFreeDrinkCourse();
		test.TestSpecialCourse();
	}

■実装コード作成(スペシャルコース料金計算)

●コンパイルエラー => Red状態
	メンバ自体は通常コースの内容で足りており、コンパイルエラーになることもなく、
	Assertエラーが発生すると思いますので、このまま次のGreen状態にするための実装へ進みます。

●Red => Green状態
	スペシャルコースの料金計算もGetChargeで行います。
	既にある程度の実装ができており、追加実装の内容も単純ですので、
	今回は②の明白な実装でGreen状態にしたいと思います。

	・コード:
		int DinnerCourse::GetCharge()
		{
			int charge = 0;

			switch(m_Course)
			{
			// 通常
			case 1:
				charge = m_Number * 3000;
				break;
			// 飲み放題
			case 2:
				charge = m_Number * 3500;
				break;
			// スペシャル
			case 3:
				charge = m_Number * 5000;
			}

			return charge;
		}

これでToDo「スペシャルコース料金計算」の実装が完了しました。
もし、Assertまたはコンパイルエラーが発生する場合はコードを見直してください。

■Refactoring

コース毎の料金計算計算が完了したので、リファクタリングを行いたいと思います。
対象はGetCharge関数です。

●コード:
	int DinnerCourse::GetCharge()
	{
		int charge = 0;

		switch(m_Course)
		{
		// 通常
		case 1:
			charge = m_Number * 3000;
			break;
		// 飲み放題
		case 2:
			charge = m_Number * 3500;
			break;
		// スペシャル
		case 3:
			charge = m_Number * 5000;
		}
		return charge;
	}

	上のコードでリファクタリングした方がいい箇所をToDoリストに追加します。

	①.コースの種類番号の定数化
		コースの種類番号がマジックナンバーになっています。
		1、2、3では他の人がソースを見たときにコメントを見ないと判断できません。
	
	②.コース料金取得と計算式の分離
		料金計算についてはコースごとに割り出していますが、
		全てのcaseで「charge = Number * コース金額」となっています。
		ここはswitch文では料金取得のみを行い、その後「料金 * 人数」の計算を行うことで
		コード内で行っている処理が分離し、可読性があがります。

	③.コース料金取得の関数化
		②の作業でGetChargeのswitch文で行っている処理が
		コース料理の料金取得のみとなりましたので、
		メンバ関数化を行い、それをGetChargeが呼び出すことで	
		GetCharge関数の可読性をあげます。
	
	④.GetCharge関数の引数変更
		現在GetChargeではm_Numberとm_Courseを使用して料金計算を行っていますが、
		GetCharge関数の実装を行った結果、情報を保存しておく必要がないので、
		引数に人数とコースの種類設定できるようにし、メンバ変数は削除します。

	⑤.コメントの見直し
		コメントを見直して間違った内容になっていないか、
		不要な情報が書かれていないかを確認します。

	追加されたToDoリスト:
		①.コースの定数化
		②.GetCharge修正(料金取得と計算式の分離)
		③.コース料金取得関数作成
		④.GetCharge関数の引数変更
		⑤.コメントの見直し

	①.コースの定数化
		まずはコースの定数化から始めたいと思います。
		これはenum宣言を行ってGetChargeやテストで使用している箇所を
		変更していきたいと思います。
		変更箇所は以下の通りです。
			
			列挙型:
				enum
				{
					NORMAL,		// 通常
					FREEDRINK,	// 飲み放題
					SPECIAL,	// スペシャル
				};

			Testクラス:
				TestNormalCourse
				TestFreeDrinkCourse
				TestSpecialCourse
			
			DinnerCourseクラス:
				変数:
					Course m_Couse

				関数:
					SetCourse
					GetCharge

		変更が完了したら「コースの定数化」は終了となりますので、
		テストを実行し、Red状態に戻っていないかを確認して下さい。

	②.GetCharge修正(料金取得と計算式の分離)
		次はGetCharge関数の料金取得部分と計算式を分けたいと思います。
		caseの中で計算までやっているので、関数の中で計算は最後に1回だけにします。
		また、修正の際にchargeのままでは変数名に違和感が生じるので
		charge => priceにします。

		修正前:
			int DinnerCourse::GetCharge()
			{
				int charge = 0;

				switch(m_Course)
				{
				// 通常
				case NORMAL:
					charge = m_Number * 3000;
					break;
				// 飲み放題
				case FREEDRINK:
					charge = m_Number * 3500;
					break;
				// スペシャル
				case SPECIAL:
					charge = m_Number * 5000;
					break;
				}

				return charge;
			}

		修正後:
			int DinnerCourse::GetCharge()
			{
				int price = 0;

				switch(m_Course)
				{
				// 通常
				case NORMAL:
					price = 3000;
					break;
				// 飲み放題
				case FREEDRINK:
					price = 3500;
					break;
				// スペシャル
				case SPECIAL:
					price = 5000;
					break;
				}

				return price * m_Number;
			}

		これで「GetCharge修正(料金取得と計算式の分離)」が完了です。
		コースの定数化と同様にテストを実行し、Redに戻っていないかの確認をして下さい。

	③.コース毎の料金取得関数作成
		最後にコース毎の料金取得関数の作成を行います。
		GetCharge関数のswitch文を関数化することで、
		GetCharge関数をよりシンプルにできます。

		追加するメンバ関数
			int GetCoursePrice(Course course);

			int DinnerCourse::GetCoursePrice(Course course)
			{
				switch(course)
				{
				// 通常
				case NORMAL:
					return 3000;
					break;
				// 飲み放題
				case FREEDRINK:
					return 3500;
					break;
				// スペシャル
				case SPECIAL:
					return 5000;
					break;
				}

				return 0;
			}

		GetCoursePrice呼び出し側:
			int DinnerCourse::GetCharge(void)
			{
				int price = GetCoursePrice(m_Course);
				return price * m_Number;
			}

		これで「コース毎の料金取得関数作成」が完了です。
		コースの定数化と同様にテストを実行し、Redに戻っていないかの確認をして下さい。

	④.GetCharge関数の引数変更
		初期設計では人数とコースの種類はクラスで保存できるように
		メンバとして用意していましたが、GetCharge関数を作成してみて
		「現時点では」保存の必要がないと判断できますので、
		人数とコースの種類をGetCharge関数の引数に追加します。

		4-1.オーバーロード作成
			まずはGreen状態のGetChargeを直接変更せずにオーバーロードとして
			新しいGetChargeを作成します。
			
			コード:
				class DinnerCourse
				{
				private:
					int m_Number;		// 人数
					Course m_Course;	// コース

				public:
					DinnerCourse()
					{
					}

					void SetNumber(int number) 
					{
						m_Number = number;
					}
					void SetCourse(Course course) 
					{ 
						m_Course = course;
					}

					// 料金計算
					int GetCharge();
					int GetCharge(int number, Course course);

					// コース料金取得	
					int GetCoursePrice(Course course);
				};

				int DinnerCourse::GetCharge(int number, Course course)
				{
					int price = GetCoursePrice(course);
					return price * number;
				}

			コードが完成したらRed状態になっていないか確認します。

		4-2.GetCharge使用部分の変更
			GetChargeを使用している箇所を探して引数有りのGetChargeに変更します。

			使用箇所:
				Testクラス
					TestNormalCourse();
					TestFreeDrinkCourse();
					TestSpecialCourse();
			
			変更が完了したらRed状態になっていないかを確認します。

		4-3.引数無しGetCharge、m_Number、m_Courseの削除
			4-2が正常に動作しているので引数無しGetCharge、
			m_Number、m_Courseとそのゲッター、セッターを削除します。
			削除が完了したらRedになっていないかを確認します。

		これで「GetCharge関数の引数変更」が完了です。

	⑤.コメントの見直し
		コメントの見直しを行います。
		GetCoursePrice関数のcase文に対して「通常」などのコメントを書いていますが、
		これは定数化した現在ではなくても意味が通じるので削除しても問題ないと思われます。

		修正前:
			int DinnerCourse::GetCoursePrice(Course course)
			{
				switch(course)
				{
				// 通常
				case NORMAL:
					return 3000;
					break;
				// 飲み放題
				case FREEDRINK:
					return 3500;
					break;
				// スペシャル
				case SPECIAL:
					return 5000;
					break;
				}

				return 0;
			}

		修正後:
			int DinnerCourse::GetCoursePrice(Course course)
			{
				switch(course)
				{
				case NORMAL:
					return 3000;
					break;
				case FREEDRINK:
					return 3500;
					break;
				case SPECIAL:
					return 5000;
					break;
				}

				return 0;
			}