当ウェブサイトはソフトウェアの開発手法として用いられる「テスト駆動開発」を実践するためのチュートリアルサイトです。解説で使用するサンプルソースは github よりダウンロードすることができます。
テスト駆動開発は「テストを行いながら開発を進めていく」スタイルで、一般的には柔軟で変更に強い開発手法であると知られています。プログラマーとして仕事をしていくことを考えるのならば、テスト駆動開発のやり方を身に着けておくことをおススメします。
当サイトはテスト駆動開発のやり方を、簡単なアプリケーションを作りながら学んでいくことができるので、参考にしてもらえたらうれしいです。テスト駆動開発の概要について学んでから、実際に開発手法について解説してきます。
使用するプログラミング言語は C# になるので、Windows と Mac どちらでも開発を行うことは可能ですが、当サイトのサンプル画面などでは Windows 版の Visual Studio を使用して説明していきますことをご了承ください。
テスト駆動開発のメリット・デメリット
まず初めに「テスト駆動開発」の基礎知識や方法を解説していきます。特に最初は「テスト駆動開発のメリットとデメリット」から解説を行っていきます。
テスト駆動開発のメリット
まずはテスト駆動開発を行う上でのメリットを紹介していきます。テスト駆動開発の主なメリットは「安心」という言葉がぴったりかもしれません。常にテストを行いながら開発を進めていくのが大きなメリットになると考えています。
1.バグへの不安が減る
テスト駆動開発はテストファーストな開発手法であるため、バグへの不安が大きく減ることを意味します。プログラマーであればバグのすべてを消し去ることは難しいのが共通認識だと思いますが、それでも極力発生するバグは減らしたいというのが本音です。
その点、テスト駆動開発はテストと実装を短いスパンでこなしていくため、各機能に対するバグへの不安を取り除くことができます。バグのない細かい部品を集めていけば、組み合わせさえ間違えなければ、おのずとバグは減っていきますね。
またバグを潰しながら進めていけるので、不具合を後工程に残さずに先に進めることができます。目の前のテストを攻略することを主眼に開発を行っていく手法ならではのメリットと言えるでしょう。
2.機能をしっかりと捉えられる
テスト駆動開発はテストを先に書くため、要件・機能をしっかりと理解しておかなければなりません。理解が不足していれば正しいテストケースを書くことができないからです。要件や機能を理解しながら進めることができるので、アプリケーションへの理解も深まっていきます。
いくらプログラマーとして働くとはいえ、技術ばかりに気を取られるわけにはいかず、お客さんのビジネスもしっかりと理解しなくてはなりません。そういう点から言えば、機能を理解してテストを書いていくことは、要件や機能に対する理解を深めてから実装するという段階を踏むことになります。
アプリケーション開発をするうえで、構築対象をしっかりと捉えることは長期的な開発を見据える上で必要なことであると言えますよね。テスト駆動開発はそういう点を考慮した開発手法、と言えるでしょう。
3.快適な開発ができる
これは個人的な意見も入ってきてしまうかもしれませんが、テスト駆動開発でアプリケーションを構築していくと、「快適」に実装を進めることができます。その理由は多分、「テストに通る」という心理的な安心があるからだと思います。
自分が想定した結果の通りにコーディングを行い、その結果を即座に評価してリファクタリング・次の実装に移れるというのは「不安要素」を引き継ぐことなく実装を先に進められるからだと思います。
テスト駆動開発を学ぶことでコーディング作業に明確性を持つことができ、自分が行うべき内容を明確にしながら作業ができるのが大きなメリットと言えるでしょう。例え自分が書いたテストコードでも、テストがちゃんと通れば嬉しくなるものです。
またテストは何度でも実施できるので、ソースコードの修正を行ったとしても機能を保証できるのも「快適」に開発が進められる理由なのかもしれません。例えそれがユニットテストであっても「テストに通る」というのは嬉しいものなのです。
テスト駆動開発のデメリット
さて、ここまでテスト駆動開発のメリットを大々的に書いてきました。まるでテスト駆動開発は完璧なように聞こえているかもしれませんが、実はそうでもありません。メリットもあればデメリットもあるのは万物に共通することです。
ということで、次はテスト駆動開発のデメリットについて触れていきたいと思います。テスト駆動開発はテストを行うことで得られるメリットもありますが、テストを行うことで発生するデメリットもあるのです。
1.コード量が約2倍になる
率直にいってしまうと、テスト駆動開発はテストファーストな開発手法なので、テストコードを書かなくては始まりません。要するにテストコードの分量とプロダクトコードの両方のソースを記述する必要があるということです。
あまり厳密ではないと思いますが、実感としてプロダクトコードで記述した分量と同じくらいのテストコードを記述する必要があります。ということはコード量が簡単に考えると約2倍になってしまうというのがデメリットになります。
イテレーティブな開発現場で、しっかりと納期までの期間が確保されている場合はよいのですが、スケジュール的に厳しい現場ではテストコードを書いていく時間は少なくなるのが現実です。
テスト駆動開発は素晴らしいアプローチですが、それはあくまでも「労力に見合うだけの時間がある」ということが前提になると言えるかもしれません。ピンポイントで特定の機能を実装するときだけテスト駆動開発を組み込んでいく、みたいなパターンがあると思います。
2.メンテナンスが大変である
テスト駆動開発ではテストファーストな開発手法でありますが、それは機能・要件が決まったうえで導かれる「結果ありきの開発スタイル」です。ということは、導くべき結果が変わってしまえばテストの期待値も変わっていくことを意味します。
要件や機能が修正されてしまった場合、過去のテストコードは完全とは言えないモノになり、新しい要件や機能に合致した内容に書き換える必要があります。要するにテストコードもメンテナンスしていかなければなりません。
数多くの要望が入り混じるような現場では、テスト駆動開発で開発を進めていくのが難しい場合があるかもしれませんので、やはりテスト駆動開発は万能とも言えないということになります。テストコードも常にメンテナンスしなければならない、というのがデメリットなのです。
テスト駆動開発をどう使うか
テスト駆動開発のメリット・デメリットを見てきました。メリットの側面だけに光を当ててみると、テスト駆動開発は素晴らしい開発手法の一つであることは間違いありません。しかしながら、それ相応のテストコード記述してメンテナンスを続けるのは容易なことではありません。ましてや納期が厳しい案件においては、現実としてテストコードを記述する工数すら取れないような場合もあります。
とはいえ、テストコードのメンテナンスも行いながら開発を続けられる場合は、「テストコード自体が要件・機能を示す優れたドキュメント」になる可能性もあります。適切にメンテナンスが行われているテストコードは、要件・機能の結果を映し出したものになり、機能の修正箇所を特定するのに役に立つ羅針盤にもなり得ます。
テスト駆動開発の基本サイクル
テスト駆動開発を行う上で欠かせない「テスト駆動開発の基本サイクル」を解説します。Kent Beck の著作「テスト駆動開発」にもある通り、テスト駆動開発は一定の行動を短期間で行いながら徐々に実装コードを大きくしていく作業です。その基本となるサイクルを解説します。
テスト駆動開発の3つのサイクル
テスト駆動開発はテストコードを主軸として、テスト・実装・リファクタリングを短期間でイテレーティブに行っていき、徐々に機能の風呂敷を風呂敷を広げながら進んでいく開発手法になります。テスト駆動開発は、大きく分けると3つのサイクルがあります。
- レッド
- グリーン
- リファクタリング
この3つを素早く行っていくことがテスト駆動開発のメイン部分になります。それぞれのパートについて簡単に解説していきます。実際のやり方は連載をもう少し進めた所で実践していきます。今は「レッド→グリーン→リファクタリングなんてあったな」くらいでOKです。
【レッド】:失敗
テスト駆動開発はテストファーストな手法ですが、テストを書く際にまずは「失敗」するようにします。機能の要件を基にしてテストコードを書いていきますが、最初はプロダクトコード側には手を入れずにテストコードだけを書いていきます。
プロダクトコードに記載されていないコードで問題ありません。まずはテストコードを書いていく段階です。この時に重要なのは「自動テストが認識されているか」です。テストコードを書いたとしても自動テストとして認識されていなければ意味がありませんので、ちゃんと「テストとして認識されているか」を確認する上でも失敗するのは重要になります。
この「失敗している状態」をレッドと呼んでいます。その理由はテスト結果を確認する箇所にレッドバー(もしくはレッドのアイコン)が表示されるからです。
Visual Studioでは上記のように失敗しているテストケースは赤の×マークが表示されていますね。要するに「失敗」という状態を「レッド」と呼んでおり、この状態がテスト駆動開発におけるスタート地点となるのです。
【グリーン】:とりあえずの成功
「レッド」にて失敗するテストコードを書いた後は、とりあえずテストが成功するようにコードを書いていきます。ここで重要になるのは「どんな方法でもよい」という点です。効率性や可読性はおいておき、まずはテストが通るプロダクトコードを仕上げます。
「グリーン」も「レッド」と同様にテスト結果を示す箇所が「緑色」になることに由来しています。「レッド」が危険をイメージさせるように、「グリーン」は安心をイメージできるような感じですね。Visual Studioでは以下のようにグリーンが表示されます。
テスト駆動開発においては、このグリーンの状態を保つことが重要となります。というのもグリーンの状態は「テストに通っている安心なコード」を意味するからです。レッドからグリーンに変化させる1回目はとりあえずテストをクリアすることを目指します。
「グリーン」な状態のプロダクトコードを書き上げることで「最低限の成功」が保証されることを意味します。テスト駆動開発では、このグリーンな状態を保ちながら、より良い方法や可読性の高いコードを探っていきます。
【リファクタリング】:昇華
一度テストが「グリーン」の状態になったら、簡潔でより良いコードを目指していきます。テストに通っているので、最低限の処理・動作は保証されていますので、あとはこの「グリーン」の状態を保ちながら、リファクタリングを行っていけばよいのです。
テストが「グリーン」の状態である以上は、修正内容に問題ないことを保証しているのが心強く感じます。リファクタリングで怖いのは「デグレ」ですが、「グリーン」である以上は動作に影響がないことを保証してくれます。
サイクルを回す
テスト駆動開発はここで紹介した「レッド」「グリーン」「リファクタリング」のサイクルをなるべく早く回していく開発手法です。兎にも角にも、まずはテストコードを書くことから始め、そのテストケースを満たすプロダクトコードを記述していきます。そのあと良いコードを探すためのリファクタリングを行っていきます。
この一連の動作をテストケースを作成しながら進んでいきます。最初の方が小さな部品から作っていくことになると思いますが、次第にそれらが集まっていき、徐々にアプリケーションとしての形が見えるようになっていくはずです。
テストコードを書いていくということは機能・要件をきっちりと理解していなければできないことですので、テスト駆動開発は「業務に目を向けた構築方法」ともいえるかもしれません。単純なスキルだけでなく、適切なテストケースを見極める能力が問われていく開発手法ともいえるでしょう。
単体テスト(ユニットテスト)の基本を解説
テスト駆動開発で実施するべきテストについて解説してきたいと思います。テスト駆動開発では主に「ユニットテスト」を中心に、小さな粒度でのテストを行っていくことがメインになります。そのために必要知識を整理していきます。
アプリケーション開発には大きく分けると「結合テスト」と「単体テスト」の2種類があり、結合テストとはよりシナリオに近いテストを実施し、単体テストは機能内でのより小さな単位、場合によってはメソッド単位でのテストになります。テスト駆動開発では「単体テスト」を実施していきます。
単体テストとは
「単体テスト」とはプログラムを構成する比較的小さな単位が個々の機能を正しく果たしているかどうかを検証するテストのことを指しています。小さな単位のことを「ユニット」とも呼ぶため、単体テストは「ユニットテスト」と呼ばれることもあります。
アプリケーション開発においては関数やメソッドが単体テストの単位となることが多くなります。単体テストは実施対象となる単位が数行~数十行程度のコードになることが多いので、開発の早い段階で実施されることが多いのが特徴です。
テスト駆動開発では機能をユースケースといった大きな単位でテストするのではなく、このユニットに着目してテストをすることがメインとなります。構成する部品が正しいことを確認しながら、より大きな機能を組み立てる土台を作っていきます。
単体テストの仕組み
単体テストは小さなユニットでテストを行っていきます。テスト対象は関数・メソッドとなりますので、それらを起動して結果を確認する機能が必要となります。テスト駆動開発では「ドライバ」「スタブ」を上手く活用しながらテストを実施していきます。
テスト項目を実行させるモジュールを「ドライバ」と呼び、テスト対象の下位モジュールが完成していない場合のための代用品として記述するモジュールを「スタブ」と呼んでいます。
テスト対象となるメソッドは様々な処理から呼ばれ、様々な処理を呼ぶ可能性もあります。中間層であるメソッドの場合は何かに依存され、またモジュール自身も何かに依存していることが大半です。それらを疑似的に用意する必要があるということです。
テスト駆動開発では、この「ドライバ」役をテストクラスが担い、「スタブ」役もテストクラスで作成してモジュールに引き渡すような場合もあります。スタブが代替となるモジュールは、例えばデータを返却するAPIなどが主になるかと思います。そのようなモジュールが必要な場合は、とりあえず仮のものを使用して、メソッド自体が正しい振る舞いをするかどうかを判断するようにします。
単体テストの着目点
単体テストではいったい何に着目してテストを実施するべきなのか迷う人も多いかと思います。実際のところ、私も新人の頃はアプリケーションのテストは何に着目すればいいか分かっていませんでした。それくらいふんわりしている項目だと思います。
単体テストで着目するのは「分岐網羅テスト」と「境界値テスト」になります。分岐網羅テストとはテスト対象となるコードの分岐をすべてちゃんと通るかをチェックし、境界値テストは条件となる境界値が正しい値であるかを確認してい来ます。
分岐網羅テスト
分岐網羅で重要になるのは条件をすべて網羅して、条件分岐が正しくされているかを判定することです。例えば会員区分を渡して料金が返ってくるメソッドをテストする場合、すべての会員区分で正しい料金が返却されるかを確認します。
private int GetPriceByMember(int memberKbn)
{
if (memberKbn.Equals(1))
{
return 1000;
}
if (memberKbn.Equals(2))
{
return 1300;
}
if (memberKbn.Equals(3))
{
return 1500;
}
return 0;
}
上記のような分岐があった場合にテストでは引数で渡す値のmemberKbnを1、2、3とそれ以外の4パターンテストを実施します。分岐にすべて入るかが観点となるため、分岐網羅では少なくとも4パターンは必要になるということです。
境界値テスト
その次に確認するべきは「境界値」になります。「境界値テスト」とも呼んだりしています。これはある基準によってOK・NGが分かれる場合に、OK側の境界値とNG側の境界値をチェックして正しい値で判定が分かれているかを確認していくテストのことを指します。
public bool IsMatchCriteria(int input)
{
return input <= 100 ? true : false;
}
例えば上記のような引数と特定の値を比べて真偽を決めるようなメソッドでは、真偽の基準である値に対して境界値が正しいかというテストを実施します。この場合は100で判定されているため、テスト観点となるのは100と101は最低限必要です。
このようなメソッドで70と120みたいな境界値から乖離しているテストを行っても意味がありません。こういうメソッドで重要となるのは「厳密に判定されるか」ですので、境界値周辺は最低限の確認対象となるのです。
曖昧な部分を排除するために、境界値や特定の条件による分岐が存在する場合は、その境となる値をOK側とNG側でチェックします。これを「境界値テスト」と呼び、アプリケーション開発においてテストに含めるべき項目になります。
テスト項目は「分岐」に着目する
テスト項目を洗い出すのは慣れが必要になると思います。まだエンジニアになりたての場合などは、「どのような値がテスト対象となるのか?」を考えるのに苦労するかと思います。自分自身もそうでした。
そういう方のためにテスト項目を洗い出すための着眼点をお伝えしました。重要なのは「分岐」です。コードで表現するメソッドは様々な判定が入りこみますので、それらが「ちゃんと正しく分岐されるか」が重要になるのです。
「正しく分岐されるか」に着目すると行わなければならないテスト項目が浮かび上がってきます。そこで見出したテスト項目に対してドライバとスタブを活用して、単体テストを行いながら開発を進めるのがテスト駆動開発になるのです。
テストプロジェクトを追加する方法
ここまではテスト駆動開発の基礎的な部分を解説してきましたが、ここからはテスト駆動開発を実際に行っていきます。まずはテスト駆動開発を行うための「テストプロジェクト」を追加する方法を見ていきます。
テストプロジェクトを追加する
テスト駆動開発などを行う場合は、通常のプロダクトコードから離れた場所に別のプロジェクトを追加して、テスト用プログラムからプロダクトプロジェクトを参照してテストを実施していくのが一般的です。
テストコードを配置するためのテストプロジェクトを追加して、「レッド」の状態までもっていく方法を解説していきます。テスト駆動開発を実施するための準備に該当します。
今回は「Visual Studio 2019」を使用して、.Net Frameworkのコンソールアプリケーションを使用していきます。.Net FrameworkはWindowsしか動作しませんので、その他のOSでの実践を考える人は.Net Core用のコンソールアプリケーションを選択してください。
1.コンソールアプリを準備する
まずはじめにコンソールアプリケーションを準備しましょう。今回はアプリケーションの名前を「App01」としています。過去の連載でもそうでしたが、連載内で作成するコンソールアプリケーションは連番で作成していきます。
コンソールアプリケーションを作成すると上記のようになっていると思います。画像のように「テストエクスプローラー」が表示されていない場合は、メニューバーの「表示」から「テストエクスプローラー」を選択してください。
ここまで準備ができたら、次はテスト用プロジェクトを追加していきます。
2.テストプロジェクトを追加する
コンソールアプリケーションの準備ができたら、テスト用のプロジェクトを追加していきます。ソリューションエクスプローラーの「ソリューション」を右クリックして「追加」から「新しいプロジェクト」を選択しましょう。
すると以下のように追加するプロジェクトの種類を選択できますので、一覧の中から「単体テストプロジェクト(.Net Framework)」を選択します。
.Net Coreを使用している人は.Net Core用の単体テストプロジェクトを選択して「次へ」ボタンを押下してください。次の画面ではソリューション名を入力できますので、それっぽい名称を入力したら「作成」ボタンを押下します。
するとソリューションエクスプローラーにテスト用プロジェクトが準備されます。以下のようになっていたらテスト用プロジェクトの準備が完了です。
テストエクスプローラーのペインを掘っていくとテストの一覧が表示されます。現在は1ファイルしか存在せず、UnitTest1クラスのTestMethod1メソッドしかありません。
テストメソッドを追加していくとそれに応じてメソッドが追加されていきます。
3.参照を設定する
テストプロジェクトの準備が整ったら、次はテスト用プロジェクトからプロダクト用のプロジェクトが参照できるようにしていきます。TestProjectのペインを開くと「参照」という項目があるので、そこで右クリックをして「参照の追加」を選択します。
するとプロジェクトの一覧が表示されるので「App01」にチェックを入れます。そしたら「OK」ボタンを押下します。今はApp01プロジェクトしかありませんので迷いませんが、複数のプロジェクトが存在する場合はテストしたいプロジェクトを選択してください。
テストプロジェクトに参照が正しく追加されると、先ほどの「参照」を開いたところにApp01が追加されていると思います。
上記のようになってたらテストプロジェクトへの参照の設定は完了です。最後に簡単なテストを実施して「レッド」の状態を確認して見たいと思います。
4.テストを実施する
それでは簡単にテストを実施してみます。プロダクト用プロジェクトであるApp01に対して新規ファイル「Sample.cs」を作成して以下を記載します。
namespace App01
{
public class Sample
{
public bool ReturnTrue()
{
return true;
}
}
}
大したクラスではありません。ReturnTrueメソッドを一つ持ち、単純にtrueを返しているpublicメソッドを書いただけです。それではテストプロジェクトに戻ります。
テストプロジェクトの「UnitTest01.cs」を選択してください。中にデフォルトで作成された「TestMethod1」メソッドがあると思います。個々のメソッド内にテストを書きます。メソッドを以下のようにしてみましょう。
[TestMethod]
public void TestMethod1()
{
var sample = new App01.Sample();
Assert.IsFalse(sample.ReturnTrue());
}
このように記載したらテストを実行します。テストを実行する場合は2つの方法があります。1つ目は、実行したいメソッドの行内で右クリックをして「テストの実行」を選択してテストを実行する方法です。
もう一つの方法はテストエクスプローラーから実行したいテストを選択して、右クリックを押下して「実行」を選択してテストを実行するという方法です。私は1つのテストを実施する場合はメソッド内の右クリックで実行することが多いです。なお、複数のテストを一括して実行させる場合はテストエクスプローラーからしかできませんので注意が必要です。
どちらの方法でも良いので、先ほど更新したTestMethod1のテストを実行させてみてください。すると以下のように「レッド」の状態になると思います。ここまで再現することができれば問題はありません。
今回はテストを準備するところまでを解説しました。テストクラスやテストメソッド、またテストの判定方法については今後に解説していきます。最後にApp01内のSample.csとTestProject内のUnitTest1.csの中身全体を記載しておきますので確認してみてください。
namespace App01
{
public class Sample
{
public bool ReturnTrue()
{
return true;
}
}
}
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
namespace TestProject
{
[TestClass]
public class UnitTest1
{
[TestMethod]
public void TestMethod1()
{
var sample = new App01.Sample();
Assert.IsFalse(sample.ReturnTrue());
}
}
}
github で配布しているサンプルの App01 がここで解説したコードの配布サンプルとなります。必要な方はダウンロードして確認してみてください。テスト駆動開発を行うための準備は以上です。次はテスト駆動開発で結果を判定する方法を解説します。
Assertクラスの基本を解説
テストエクスプローラーにて、テストコードにおいてテストの合否を判定する方法について解説します。
テストメソッド内においてテストの値を判定するにはAssertクラスというものを利用します。ここではAssertクラスの基礎的な部分よく使うメソッドを紹介していきます。Assertクラスの使い方を知ってからテスト駆動開発の実践に移っていきます。
テストを判定するには
まずテストを判定する方法について解説していきます。アプリケーション開発においてテスト駆動開発をする場合は、おもにユニットテストを自動化させます。ユニットテストではメソッドを検証することになりますが、それは少なくとも何かしらの値が「予想通りになっているか」を判定することになります。
その「何かしらの値」を判定する方法が必要になります。その値の判定がユニットテスト、およびテスト駆動開発をするために、Assertクラスを使用しなくてはなりません。
Asserクラスとは
Assertクラスとはオブジェクト動詞を比較するメソッドを集めたクラスであり、テスト駆動開発において最も使用頻度の高くなるクラスになります。
- AreEqualメソッド
- IsTrueメソッド
- IsFalseメソッド
- IsNullメソッド
- IsNotNullメソッド
などが用意されています。コレクションの検証にはCollectionAssertクラス、特殊な文字列にはStringAssertクラスを使用しますが、ここでは基本的なAssertクラスを使います。
Assertクラスの使い方
それでは簡単にAssertクラスを使用する方法を解説していきます。新しく「App02」のコンソールアプリケーションを作成してください。今回はAssertクラスの使い方に特化するため、テストプロジェクトの名前も変更せず「UnitTestProject1」のままにしています。
App02の作成後のソリューション構成は以下のようになっています。
また画像の通りUnitTestProject1内のUnitTest1クラスに属するTestMethod1メソッドは以下の通りになっています。
[TestClass]
public class UnitTest1
{
[TestMethod]
public void TestMethod1()
{
}
}
ここから代表的な機能をいくつか見ていきたいと思います。基本的にメソッドはTestMetod1内に記載していきますので、今回はクラス等の追加はしないようにしていきます。
AreEqualメソッド
まずはAreEqualメソッドについて解説しますが、AreEqualメソッドは二つのオブジェクトを比べて同じであるかを判定します。例えば以下のような感じで使うことができます。
int num1 = 10;
int num2 = 10;
Assert.AreEqual(num1, num2);
string hello1 = "おはよう";
string hello2 = "おはよう";
Assert.AreEqual(hello1, hello2);
数値や文字列判定を行い、中身が同一であるかを判定します。中身が同一である場合はテスト結果は真となり「グリーン」になります。
IsTrueメソッド
IsTrueメソッドは値がTrueである場合にテスト結果をグリーンにしてくれるメソッドになります。ユニットテストの結果をブール値で判定したい場合などに有効ですね。
bool flg = true;
Assert.IsTrue(flg);
int num3 = 5;
Assert.IsTrue(num3 * 2 == 10);
単純にtrue / false になるブール値を渡すのもよいですが、条件式を渡して判定にするのも問題ないようになります。判定したい値がtrueであればグリーンになり、値がfalseであればレッドになります。
IsFalseメソッド
IsTrueメソッドの反対にあたるのがIsFalseメソッドになります。IsTrueは値がtrueであればグリーンでしたが、IsFalseメソッドでは値がfalseであれはグリーンになります。
bool flg2 = false;
Assert.IsFalse(flg2);
int num4 = 4;
Assert.IsFalse(num4 * 3 == 13);
IsTrueメソッドと同様に値で判定を行ってもよいですし、条件式を渡して真偽の判定を行っても良くなります。IsTrueとIsFalseは同じようなものなので、使用する局面に応じて必要な方を使うとよいと思います。
IsNullメソッド
IsTrueやIsFalseとは別にnullかどうかを判定するメソッドも用意されています。IsNullメソッドでは渡した値がNullである場合にグリーンとなり、それ以外ならばレッドになります。
int? num5 = null;
Assert.IsNull(num5);
分かりやすくするためにnullableのint型に対してnullを突っ込んで判定するようにしました。IsNullメソッドでは、たとえばユニットの戻り値がnullを返すような異常パターンの判定などに使えるでしょう。
IsNotNullメソッド
IsNullメソッドの反対にあたるのがIsNotNullメソッドです。このメソッドはIsNullメソッドの真逆にあたるので使用方法も簡単です。
int? num6 = 5;
Assert.IsNotNull(num6);
Nullでないものを拾うので、殆どの場合ではTrueになるんじゃないかと思います。IsNullは使い道がありそうですが、IsNotNullは「何か値が入っていること」程度の確認にしかならないので、あまり出番はないかもしれませんね。
ここで使用したサンプルコードは github の App02 にて配布していますので、必要に応じて活用してください。github にアップグレードしているサンプルにはコメントも適宜加えてあります。
テスト駆動開発の始め方
ここからは実践編ということで、簡単なアプリケーションを構築しながらテスト駆動開発をやっていきます。
テスト駆動開発で作るもの
テスト駆動開発の実践編ということでスタートしていきます。今回は以下のようなアプリケーションを数回にわたって作っていきたいと思います。
簡単な口座アプリを作成します。コンソールアプリケーションから入金と出金ができ、最終的な口座の収支はテキストファイルとして出力しておき、前回の残金から再スタートできるようにする。
このようなアプリケーションを簡単に作りながら、テスト駆動開発の実践をしていく予定です。GUI部分は作りません。テスト駆動開発ではGUIをテストするのは難しく、ソースコードをユニットテストの対象とするので問題ないでしょう。
アプリケーションの準備をする
ではまずはコンソールアプリケーションを作成して、テストプロジェクトを作成していきます。アプリケーション名を「App03」としてください。いつも通りデフォルトテンプレートのアプリケーションの準備ができたら、いったんは大丈夫です。
次はテストプロジェクトを追加していきます。テスト用のプロジェクトの名前は「BankAccountTest」としておきましょう。
ソリューションエクスプローラーには2つのプロジェクトである「App03」と「BankAccountTest」のプロジェクトがあることを確認したら、BankAccountTestプロジェクトにApp03の参照を追加しておきます。
テスト駆動開発を始める前に
さて、テスト駆動開発を始める前に今回作るアプリケーションの必要な内容を固めておきましょう。作りたいアプリケーションは以下のような感じでした。
簡単な口座アプリを作成します。コンソールアプリケーションから入金と出金ができ、最終的な口座の収支はテキストファイルとして出力しておき、前回の残金から再スタートできるようにする。
ここから考えると以下の機能が必要になると想定できますね。
- 画面からお金の入金と出金ができること
- テキストファイルが出力できること
- テキストファイルが読み込めること
お金の入金と出金ができるというのは、そういうメソッドが存在していることと言い換えることが可能になります。またテキストファイルの読み込み処理も必要でした。
ここでは口座をオブジェクト指向で作っていきたいと思います。また「円」のオブジェクトは完全コンストラクタパターンを使用して実装していくことを考えます。
バリューオブジェクトを考える
「お金」の扱い方をテスト駆動開発で考えていきます。お金をテスト駆動開発は「テスト」を行いながら進めていくので、実際にソースコードを下記ながら実装のイメージを膨らませていきましょう。
お金を扱う方法
お金を扱うにはどうすれば良いのでしょうか。一般的に銀行口座のようなシステムであれば小数点以下は発生しませんし、桁数も十分に取れるint型でも十分なように思うかもしれません。
しかしながら果たして本当にそれが便利なのでしょうか。例えばクラスのプロパティに以下のように定義されているだけだとしたら、「お金」は重要な概念にも関わらず、どこかのクラスの属性でしかないような感じがしちゃいますよね。
public int Money { get; set; }
そういう場合にはプロパティで考えるのではなく、お金それ自体をオブジェクトにしてしまうのが良いです。int型ですと「お金」という意味を成さない、単なる「お金」と名のついたint型でしかないからです。
そこで「値」を扱うためのオブジェクトを作っておくと、「お金」をint型の何かではなく「お金」というオブジェクト(重要な概念)でとらえるようになります。こういう「値」のためのオブジェクトを「バリューオブジェクト」と呼んだりします。その時に使えるのが「完全コンストラクタパターン」です。
バリューオブジェクトを考える
ではまず、既存のテストクラスにデフォルトで作成されたTestMethod1メソッドを改変して「CreateMoneyTestメソッド」にして、以下のテストを書いてみましょう。
[TestMethod]
public void CreateMoneyTest()
{
var money = new Money(100);
Assert.AreEqual(money.Value, 100);
}
バリューオブジェクトは単純に値をラップするクラスであることが殆どです。完全コンストラクタパターンを使って初期値を設定できるようなオブジェクトを考えます。
中身に設定した値をValueプロパティで取り出せるようにしたいと思いますので上記のようなテストを書いてみました。とはいえ、今のところはまだテストができません。Moneyクラスが存在しないのでコンパイルエラーが発生するので解消するためにMoneyクラスを作りましょう。
App03に「ValueObjects」フォルダを作成してMoneyクラスを新規作成します。内容は以下の通りにしてみてください。
namespace App03.ValueObjects
{
public class Money
{
public int Value { get; private set; }
public Money(int value)
{
}
}
}
これができたらテストクラス側を修正していきます。テストクラスの参照の部分に「using App03.ValueObjects;」を追加してください。するとエラーは消えるはずです。まずはテストを最初に書いて、想定したいパターンを記述してテストを行います。まずはエラーになることを想定しています。
using App03.ValueObjects;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
namespace BankAccountTest
{
[TestClass]
public class UnitTest1
{
[TestMethod]
public void CreateMoneyTest()
{
var money = new Money(100);
Assert.AreEqual(money.Value, 100);
}
}
}
テストメソッドは上記の通りです。これでテストを実行すると「レッド」になることが確認できると思います。このレッドの状態をグリーンにすることを考えましょう。この問題を修正するのは非常に簡単です。Moneyクラスのコンストラクタで、オブジェクトの値を意味するプロパティに値を設定していないのが問題でした。
Moneyクラスを以下のように書き換えて再度テストを実行してみましょう。
namespace App03.ValueObjects
{
public class Money
{
public int Value { get; private set; }
public Money(int value)
{
this.Value = value;
}
}
}
この状態でテストをすると「グリーン」になったかと思います。このようにテスト駆動開発を実践していけばケアレスミス等も防げるようになっていきます。今回はコンストラクタでの処理なので、あえて設定していないという点もありますが、テストコードを書いて実践していくことで、書き忘れやもったいないミスなども減らせるということです。
これでテスト駆動開発の最初のステップが完了しました。この後はリファクタリングなのですが、今回は単純なバリューオブジェクトなのでリファクタリングは今のところ不要そうです。次項
ではもう少しバリューオブジェクトを深堀りして、値を足したり引いたりすることを考えていきます。もう少しソースコードに変化が現れるようにしていきたいと思います。
バリューオブジェクトのテスト1
前回は単純にお金を表すバリューオブジェクトを作成するところまでを実施しましたが、今回は今後の展開を見据えてMoneyクラスに処理を追加していきたいと思います。
バリューオブジェクトを作る一つの利点として挙げられるのは「処理をまとめることができる」という点になります。
Moneyクラスに対して何かをする処理を、わざわざUtilクラスなどに作成していくと、同じような処理が沢山作成されてしまう可能性があります。今回はここを深堀していきます。
バリューオブジェクトに処理を加える
最終的に作成したいアプリケーションは「口座」のような特性を持ちますが、「口座」からの処理として出金と入金があると伝えていました。これは処理の中身を想像してもすぐに分かる通り「お金」に対して追加したり、削減したりする処理が想像できるはずです。
という訳なので「円」というオブジェクトは「足したり引いたりできる」という特徴を備えていることがわかります。それらの処理は「円」オブジェクトの中に機能を持たせてしまいましょう。
足し算をするメソッドを実装する
テストクラスに移動して、新規のテストメソッドを作成してみます。テストメソッドを作成するには「public void」のメソッドを作成して属性として[TestMethod]という属性を付与することになります。以下を新しくメソッドとして追加してください。
[TestMethod]
public void AddMoneyTest()
{
var money = new Money(100);
int added = money.Add(100);
Assert.AreEqual(added, 200);
}
まずは汚くても通るコードを考えてみます。例えば元々100のお金に対して、さらに100を追加するような場面を考えています。とりあえずAddメソッドを作ると考えて、そこに100を渡すようにします。
100と100を足すと200になりますので、いったんはint型でそのまま200と返すようにします。まだこの段階ではリファクタリングはせず、とりあえず形にしたい概念をダイレクトに表現してみます。当然のごとくコンパイルエラーになりますので、Moneyクラスに処理が通るようにAddメソッドを追加してみましょう。Moneyクラスは以下のようになるはずです。
namespace App03.ValueObjects
{
public class Money
{
public int Value { get; private set; }
public Money(int value)
{
this.Value = value;
}
public int Add(int add)
{
return this.Value + add;
}
}
}
Addメソッドを見てみると追加した引数に内部で保留している値を足し算して返すようにしました。かなり簡単なやり方で、そんなに綺麗とも言えないとパッと見ても思う形です。とりあえず形を実現するための土台としてはOKです。テストを実施してグリーンになれば次に進めます。
洗練されたオブジェクトの足し算にする
先ほどの処理はグリーンにしましたが、そこまできれいな形であったとは言えませんでした。もう少し「意味のある形」でMoneyクラスの足し算を行えるようにリファクタリングしたいですね。
「足し算をする」の前提を考えてみましょう。足し算をするための前提は、足し合わせるオブジェクト同士が同一の形であることが望ましいと言えます。1+1が2になるのも、1が数値同士であるから足し合わせることができるという前提です。
そこを今回の Money に当てはめると、Money というオブジェクトを足し合わせるには Money のオブジェクトである必要があります。今回作成したメソッドは引数に数値を直接的に渡していました。Money オブジェクトの足し算をするのに数値を渡すのは不自然です。なので目的の足し算を少し修正したいと思います。まずはテストクラスの先ほどのテストメソッドを理想的な形式にリファクタリングします。
[TestMethod]
public void AddMoneyTest()
{
var money = new Money(100);
var target = new Money(100);
int added = money.Add(target);
Assert.AreEqual(added, 200);
}
こんな感じに修正してみました。Money クラスの Add メソッドの引数に Money クラスのオブジェクトを要求することで、より正しい「足し算の形」に近づけたのではないかと思います。いったんはこの形式でテストをグリーンな状況に持っていきます。Money クラスを以下のように修正しました。
namespace App03.ValueObjects
{
public class Money
{
public int Value { get; private set; }
public Money(int value)
{
this.Value = value;
}
public int Add(Money target)
{
return this.Value + target.Value;
}
}
}
Addメソッドの引数を見てみてください。Moneyクラスのオブジェクトを要求して、内部で保管するValueと足し算をして返却する様にしています。いったんグリーンになるか確認をして、想定通りグリーンになっていれば、ここまでのリファクタリングは問題ないことが証明できます。
汎用性のあるオブジェクト
さて次に目を付けたのはテストメソッド内の3行目の記述の部分になります。以下の箇所に不自然さを覚えた人も多いかもしれませんね。
int added = money.Add(target);
足し算をしたらint型の値で返ってきているということです。そのあとのAreEqualの処理で返却値が想定通りであることは問題ないのですが、Moneyクラスの足し算をしたらint型の変数が返ってくるのも不自然ですよね。こういう場合は2つの選択肢が予想できるかと思います。
1. void型の変数にしてオブジェクトの値自身を変えてしまう
2. 新しいオブジェクトを生成して返却する
まずは1の案で進んでみたいと思います。void型の変数にしてオブジェクトの値を変えてしまうという戦略です。これを実施するためにテストを以下のように書き換えてみます。
[TestMethod]
public void AddMoneyTest()
{
var money = new Money(100);
var target = new Money(100);
money.Add(target);
Assert.AreEqual(money.Value, 200);
}
どうでしょうか。足し合わされる元になるオブジェクトに対して足し算をすることで、内部の格納値を変更するような記載となっています。いったんはこれをグリーンの状態にするために修正を加えます。Moneyクラスは以下のような感じになると思います。
namespace App03.ValueObjects
{
public class Money
{
public int Value { get; private set; }
public Money(int value)
{
this.Value = value;
}
public void Add(Money target)
{
this.Value += target.Value;
}
}
}
とりあえずテストを実施してグリーンになることを確認します。これでグリーンになっていれば、リファクタリングでコードを壊してないことが証明されます。「この形式で問題ないか?」を考えてみますが、ちょっと汎用性が低いことに気が付きました。連続で足し算ができない、ということです。
money.Add( ... ).Add( ... );
いまの処理の形式では上記のように処理をチェーンして書けないことに気が付きます。「口座」の入出金に対応するだけなら、別にいいような感じがしますが、あくまでもMoneyクラスは「お金」の概念を表すので、連続して足し引きができてもいいような気がします。というわけでテストを書き直してみます。
[TestMethod]
public void AddMoneyTest()
{
var money = new Money(100);
var newMoney = money.Add(new Money(60)).Add(new Money(50));
Assert.AreEqual(newMoney.Value, 210);
}
こんな感じでAddを2回連続でできるようにしました。これを満たすためにはAddの戻り値をMoneyクラスにする必要がありますね。ということでMoneyクラスの中身を以下のように修正します。
namespace App03.ValueObjects
{
public class Money
{
public int Value { get; private set; }
public Money(int value)
{
this.Value = value;
}
public Money Add(Money target)
{
var newValue = this.Value + target.Value;
return new Money(newValue);
}
}
}
元々のオブジェクトを破棄して、新規のMoneyオブジェクトを返却する様にしています。こうすることで元のオブジェクトに対して影響を与えないオブジェクトを生成することができます。それぞれのオブジェクトが互いに影響しない状態であると言えます。これを「イミュータブルなオブジェクト」といいます。
またMoneyクラスのAddメソッドの方針としてオブジェクトが知らぬところで干渉しないようにしたので、Moneyクラス内のプロパティの持ち方も完全コンストラクタパターンに合わせるようにします。要するにコンストラクタでしか値を設定できなくするということです。
このように既存の処理に対して微調整を行う場合は、テストメソッドに変更をすることなく、ソースコードのみを変更してテストを再実施してグリーンな状態にすればOKです。Moneyクラスを以下のように編集しましょう。
namespace App03.ValueObjects
{
public class Money
{
public int Value { get; }
public Money(int value)
{
this.Value = value;
}
public Money Add(Money target)
{
var newValue = this.Value + target.Value;
return new Money(newValue);
}
}
}
ValueプロパティのSetterを削除しました。プロパティに対してgetterのみ存在する場合はコンストラクタでのみ値を設定することが可能です。もちろんテストはグリーンの状態なので元のコードは破壊することなくリファクタリングが完了していることになります。
バリューオブジェクトのテスト2
Money オブジェクトに必要なメソッドとして「足し合わせ」の処理は前項で作成しました。次は、引き算をするメソッドを実装していくことにします。
引き算のテストを作成する
それでは引き算に対応するメソッドを作成するために定義しておきたいテストから書いていきたいと思います。前回から引き続いて「UnitTest1.cs」にテストメソッドを追加しましょう。
[TestMethod]
public void SubtractMoneyTest()
{
}
まずは中身のない空のメソッドで問題ありません。”[ ]”で囲まれた属性はきちんと記述しているでしょうか?この属性がないとテストメソッドとしてみなされないので注意が必要になります。
では、引き算のための「失敗するテスト」を書いていきます。今回は引き算を「Subtract」メソッドにしたいと考えています。Moneyオブジェクトに対して引き算を行います。
[TestMethod]
public void SubtractMoneyTest()
{
var money = new Money(100);
var newMoney = money.Subtract(new Money(50)).Subtract(new Money(10));
Assert.AreEqual(newMoney.Value, 40);
}
上記のように書けたら、いったんはOKとします。この時点では、まだSubtractメソッドは定義されていませんのでコンパイルエラーになるはずです。
メソッドを定義して失敗させる
さて、テストメソッドが記述できたらクラスにメソッドを定義していきます。今回はMoneyクラスにSubtractメソッドを作成していくのでした。Moneyクラスのインスタンスを返すので、戻り値と引数はともにMoneyクラスになりますね。
public object Subtract(Money money)
{
throw new System.NotImplementedException();
}
「money.Subtract」のSubtract部分にカーソルを合わせながら、”ctrl”を押しながら”.”を押下すると上記のようなメソッドを自動生成してくれます。戻り値がobjectなので、これをMoneyに変更して、自動で生成されたメソッドでテストを通して期待通り「レッド」になるかを確認します。
System.NotImplementedException();
「System.NotImplementedException();」は自動生成してくれるメソッド等で必ず埋め込まれる例外ですので、期待通り「レッド」の状態になるはずです。
メソッドを修正して成功させる
ここからテストを「グリーン」にしていきましょう。前回と似たような感じになりますので、ここはサクッと終わらせたいと思います。あまり難しくないので、完成形だけ以下に書いておきたいと思います。
public Money Subtract(Money money)
{
var newValue = this.Value - money.Value;
return new Money(newValue);
}
こんな感じになったでしょうか。それではテストを実行して、先ほど「レッド」だった状態が「グリーン」に代わるかを確認します。テストを実行すると全テストが問題なくグリーンになるかと思います。これで引き算のメソッドの実装が完了したことになります。
失敗のテストも書いてみよう
さて、これまで「足し算」と「引き算」の機能を追加してきたわけですが、2つとも記述してきたテストは「正常パターン」のみでした。少し練習もかねて「失敗するテスト」も書いてみましょう。ここは練習問題として自分の力で書いてみてください。Assertクラスには様々なメソッドがありますが、今回はAreNotEqualメソッドを使用するとよいでしょう。
[TestMethod]
public void AddMoneyTest_NG()
{
var money = new Money(100);
var newMoney = money.Add(new Money(60));
Assert.AreNotEqual(newMoney.Value, 150);
}
上記のようなメソッドと以下のようなメソッドが記述できたでしょうか。あまり意味のないテストではありますが、一応ちゃんと確認しておいても良いかと思います。「正しくないものは正しくない」のも重要な確認事項の一つですからね。
[TestMethod]
public void SubtractMoneyTest_OK()
{
var money = new Money(100);
var newMoney = money.Subtract(new Money(50));
Assert.AreEqual(newMoney.Value, 60);
}
というわけで自力でこの2つのテストが書ければ一旦はOKとしておきましょう。少しづつテスト駆動開発のイメージがついてきたのではないでしょうか。今回の失敗を検証するテストメソッド名は最後に「_NG」としているので、区別するために成功を検証するメソッド名にも「_OK」を付けておきましょう。そしてテストを実行し、すべてがグリーンになっているか確認出来たらOKです。
オブジェクトに等価比較を実装する
オブジェクトの等価比較について、まずは解説していきます。オブジェクトの等価比較とはオブジェクト同士が等価(要するに同じ)であるかを比較するということです。簡単に言ってしまうと数字の1と1は等価ですよね、ということになります。
この概念をMoneyクラスのオブジェクトにも適用してしましょうということになります。
お金という概念を考えてみましょう。Aさんの持っている10円とBさんの持っている10円は、10円玉の本体が違えども「価値」は同じですよね。AさんとBさんが10円玉を交換したとしても、価値は同様なのでどちらかが損をしたり、得をしたりするわけではありません。これを等価というわけです。
Moneyクラスから生成されるオブジェクトも、100の値を持つMoneyと100の値を持つMoneyは値が同じであるべきですし、それらを簡単に比較できた方がアプリケーションを構築していくうえで今後も活用できそうですよね。
Moneyの等価を実装する
では早速、Moneyクラスから生成されるオブジェクト同士の比較を実装していきます。基本的にはObjectに備わっているEqaulsメソッドをオーバーライドして作成したいと考えています。とはいえ、まずはテストコードから書いていきましょう。
[TestMethod]
public void MoneyEquals_OK()
{
var money = new Money(100);
var otherMoney = new Money(100);
Assert.IsTrue(money.Equals(otherMoney));
}
上記のようなテストコードになったでしょうか。今回はEqualsというメソッドの実装を考えたいので、Equalsの戻り値であるbool値を判定したいと思いました。ですので、あえてAssertクラスのIsTrueメソッドを使用してテストを判定しています。
ここまでテストコードを書いたら、一度テストを実行してみましょう。元々実装されているので「グリーンになるかな?」と思うかもしれませんが、このMoneyEquals_OKテストは「レッド」になりますね。
これはオブジェクトの等価の判定は「同じインスタンスであるかどうか」を判定しているからです。オブジェクトの「値が同値であるか」ではなく、「オブジェクトが同じか」を判定しているということになります。moneyとotherMoneyは別のインスタンスですので、結果は当然エラーになってしまうのです。
IEquatableを実装する
さて、この「レッド」の状態を「グリーン」にするための作業を行っていきます。オブジェクトの等価を実装するときはIEquatableというインターフェースを実装して、Equalsメソッドをオーバーライドする必要があります。
public class Money : IEquatable
Moneyクラスの定義を上記のようにしてみてください。するとIEquatableインターフェースに赤い波線が引かれると思います。IEquatableにカーソルを移動させ、ctrlを押しながら”.”を押下して、「インターフェースを実装します」を選択しましょう。すると以下のようにメソッドが自動生成されるはずです。
public bool Equals(Money other)
{
throw new NotImplementedException();
}
ソースコードを変更したので、もう一度テストを実行してみましょう。当然「レッド」のままであることに変わりはありませんが、少し先に進んだらテストを実行して確認しておく、という姿勢は重要です。
ではEqualsメソッドの中身を実装して、テストを「レッド」から「グリーン」にさせるために変更していきます。以下のような感じでEqualsメソッドを実装すればOKになるはずです。
public bool Equals(Money other)
{
if (other is null) { return false; }
if (this.Value.Equals(other.Value))
{
return true;
}
return false;
}
最初に引数であるotherがnullでないことを確認し、そのあとMoneyのValueプロパティとotherのValueプロパティで値の等価判定を実施すればOKです。Moneyクラスの等価は、結局はそれぞれのValueの値が同地であれば等価と判定できるので、比較的簡単な処理といえるでしょう。
コードを変更したのでテストを実施しておきましょう。すると「レッド」の状態から「グリーン」に変わったことが分かると思います。あとはNGパターンのテストコードも実装して試しておきましょう。
[TestMethod]
public void MoneyEquals_NG()
{
var money = new Money(100);
var otherMoney = new Money(110);
Assert.IsFalse(money.Equals(otherMoney));
}
Equalでないことを判定するので、AssertクラスのIsFalseメソッドを使用しています。ここまでくればオブジェクトの等価判定は完了です。
足し算・引き算・等価の処理が作成できたので、Moneyクラスに必要な機能はおおむね完成したと言えるでしょう。次は口座の残高を記録するための処理の実装に移っていきます。DBを使うといった難しいことはせずに、テキストファイルで残高を残す簡単な機能を考えます。
データの読込機能を実装する
口座管理アプリを作成するために重要な Money オブジェクトの実装がほぼ完了しました。次はデータの読み込み機能を作成していきます。
テストコードを記述する
データをテキストファイルとして出力し、テキストファイルを読込で最新の口座残高を記録するという簡単な手法を取り入れます。まずはデータの読込部分を実装していきますが、「レポジトリ」という名称にして作成したいと思います。
レポジトリとは「英語で「貯蔵庫」「収納庫」の意味」ですが、ここでは単にデータにアクセスして取得する・出力する機能の総称とでも理解しておいてください。まずはテストコードから書いていくことにしましょう。
BankAccoutTestのプロジェクトに新規の単体テスト用ファイルを追加して、RepositoryTest.csを作成しておきます。作成したRepositoryTestに対して以下のテストメソッドを追加してみましょう。
[TestMethod]
public void ReadTest()
{
string path = @"C:\temp\balance.txt";
string text = ((int)10000).ToString();
File.WriteAllText(path, text);
var repos = new BankRepository(path);
var money = repos.Read();
Assert.AreEqual(money.Value, 10000);
}
上の3行はテストデータを作成するテストコードで、下3行が機能のためのテストコードになります。今回はBankRepositoryを使用して値の読込・書込みを行って行きます。
とはいえ、現在はまだコンパイルエラーになるので、コンパイルを通るように簡単に実装を行っていきます。まずはBankRepositoryクラスを作成しますが、そのためにディレクトリ等を追加していきます。
App03のプロジェクト直下に「Repository」フォルダを作成し、その中に「BankRepository.cs」を作成しましょう。作成できたらテストコードのコンパイルエラーを取り除くための作業をしていきます。
コンストラクタを追加する
まずは作成したBankRepository.csにコンストラクタを追加します。今回は暫定的にパスの文字列をコンストラクタに引き渡すようにしたいと思います。いったん、今回は文字列のパスに依存したモジュールとして作成します。
namespace App03.Repository
{
public class BankRepository
{
public BankRepository(string path)
{
}
}
}
BankRepositoryクラスにコンストラクタを追加しました。テストクラス側では、まだコンストラクタを認識できていないので、usingの設定を行います。「using App03.Repository;」を先頭に記述することで、コンストラクタの認識ができました。
Readメソッドを追加する
コンストラクタを追加しましたが、まだReadメソッドの部分でコンパイルエラーが発生しているのでこれを取り除きます。このReadメソッドはMoneyオブジェクトを返却するようなメソッドを想定しています。以下のようなメソッドをBankRepositoryクラスに追加します。
public Money Read()
{
return new Money(0);
}
これを記述することでBankRepository.csの全文は以下のようになりました。
using App03.ValueObjects;
namespace App03.Repository
{
public class BankRepository
{
public BankRepository(string path)
{
}
public Money Read()
{
return new Money(0);
}
}
}
まだ難しいことは考えずに値を0として持つMoneyクラスのオブジェクトを作成して返却するようにしています。本来ならテキストファイルを読み込んで、Moneyオブジェクトを生成して返却するのが理想ですが、今はレッドの状態にするための手順ですのでこれでOKになります。完成したらテストを実行して状態が「レッド」であることを確認しましょう。
Readメソッドを実装する
前項まででReadTestのステータスが「レッド」であることを確認したら、次は正しく実装されるように修正を加えていきます。ここで行いたかったのはテキストファイルから読み込んで、それをMoneyオブジェクトに変換して返却することでした。
BankRepositoryクラスのコンストラクタではパスを引数で取得しているため、いったんはこのパスを使うことができそうです。まずはコンストラクタを修正してパスを保持できるように変更します。
private readonly string _path;
public BankRepository(string path)
{
this._path = path;
}
これでパスをクラス内で使用できるようになりました。この_path変数を使用してReadメソッドを仕上げていくことにしましょう。privateメソッドなどを追加して、以下のようにReadメソッドの周辺を実装をしていきます。
public Money Read()
{
var balance = ReadFile();
return new Money(balance);
}
private int ReadFile()
{
if (!CanReadFile()) { return 0; }
var txt = File.ReadAllText(_path);
if (int.TryParse(txt, out var converted))
{
return converted;
}
return 0;
}
private bool CanReadFile()
{
return File.Exists(_path);
}
ここまで出来たら、いったんテストを実行してみます。「レッド」の状態から「グリーン」になったでしょうか?また読み込めなかった場合に0になるテストも作成してみます。以下のようなテストメソッドを作成して実行してみましょう。
[TestMethod]
public void ReadTest_NG()
{
string path = @"C:\temp\balance.txt";
string text = "テスト";
File.WriteAllText(path, text);
var repos = new BankRepository(path);
var money = repos.Read();
Assert.AreEqual(money.Value, 0);
}
テキストファイルに文字列を入力した場合は正しく読み取れなかったとして、値が0になるパターンのテストになります。メソッド名の最後を「_NG」としたので、最初のテストのメソッド名も語尾に「_OK」を付けておきましょう。最後にメソッド名を変更したので、もう一度テストを実行してすべてのテストを「グリーン」にしておきます。
データの書込機能を実装する
データの読み込み機能が完了したので次は書き込み機能を実装していきます。引き続きテストファーストで実施していきましょう。
テストをコーディングする
書込機能を作るので、早速、書込処理用のテストメソッドを記述していきましょう。基本的には読込機能に近いテストになるのですが、今回は書き出し処理を行った後、新ためてファイルを読み込んで値を確認するという手順を取ります。
[TestMethod]
public void WriteTest_OK()
{
string path = @"C:\temp\balance.txt";
var repos = new BankRepository(path);
var money = new Money(10000);
repos.Write(money);
var balance = repos.Read();
Assert.AreEqual(money.Value, balance.Value);
}
上記のようなテストメソッドを記載しました。最初の4行では値を書き込む処理を記載し、後の2行で正しく書き込めたかをチェックしています。現時点ではWriteメソッドの部分に赤線が引かれており、コンパイルエラーになっているので、次はコンパイルエラーを解消していきましょう。
コンパイルエラーを解消する
前までの状態ではBankRepository.csにWriteメソッドがないためコンパイルエラーとなっていました。コンパイルエラーを解消するために、BankRepositoryクラスにWriteメソッドを、以下のように実装します。
public void Write(Money money)
{
}
Writeメソッドはvoid型にするので、中身はない状態でもコンパイルは通るようになります。この状態でいったんテストを実施して「レッド」になることを確認しましょう。
レッドをグリーンにする
「レッド」の状態になる実装を加えて、問題なく「レッド」になることを確認したら、次は「グリーン」になるために実装を加えていきます。Writeメソッドに対して以下の修正をコーディングしてみましょう。
public void Write(Money money)
{
if (!CanWriteFile()) { return; }
var txt = (money.Value).ToString();
WriteToFile(txt);
}
private bool CanWriteFile()
{
var directory = GetParent();
if (!IsExistDirectory(directory))
{
CreateDirectory(directory);
}
return true;
}
private void CreateDirectory(string path)
{
Directory.CreateDirectory(path);
}
private bool IsExistDirectory(string path)
{
return Directory.Exists(path);
}
private string GetParent()
{
return Directory.GetParent(_path).FullName;
}
private void WriteToFile(string txt)
{
File.WriteAllText(_path, txt);
}
記述するために必要な処理を諸々と入れ込みました。例えばディレクトリの存在判定、パスの親を取得する処理、ディレクトリの作成処理等々ですね。これらを入れこんでWriteメソッドが書き上げられればOKとします。
上記の状態までコーディングが完了したらテストを実施してOKになるかを確認しておきましょう。ReadのテストとWriteのテストでファイル操作を行っているで、ディレクトリを削除した状態でテストを実施すると「レッド」になる可能性もありますが2回目で問題なくなるはずですので、いったんはOKとしたいと思います。
読込・書込機能を抽象化する
現在は読み込み機能と書き込み機能はテキストファイルに対してのみ機能するようにしています。しかし、将来的にデータベースなどを使用して管理したくなる場合もあるかもしれません。そういったときは読み込み機能と書き込み機能を抽象化しておくことで、機能の実態を切り離しておくことができます。
具象と抽象についてざっくりと言ってしまうと「コンポジションや継承を用いてより汎用的に使えるようにすること」と言い換えられます。要するに「すっぽりとほかの機能に置き換えられるようにする」ということでもあります。
今回はBankRepositoryクラスの中身を、将来を見据えて変更可能にしたいと考えています。現在はテキスト形式ですが、DBを使用したり、バイナリ形式にしたりする場合に、モジュール毎切り替えられるようにすることです。インターフェースを用いて、上記のやりたいことを実現していきます。
インターフェースを定義する
では早速、インターフェースを定義していきたいと思います。「Repositoryフォルダ」に対して、IRepository.csを作成しておき、 IRepositoryには2つのメソッドを持たせます。
- Read
- Write
の2つのメソッドのみで十分です。これは読込と書込機能のうちでpublicに公開しているものだけを定義する形式にしています。インターフェースでpublicに公開するメソッドだけを定義しておけば、privateの細かい処理は具象クラスで必要に応じて組み立てることができるからです。IRepositoryインターフェースは以下のようにコーディングします。
using App03.ValueObjects;
namespace App03.Repository
{
public interface IRepository
{
Money Read();
void Write(Money money);
}
}
Readメソッドは戻り値としてMoneyオブジェクトを返却し、WriteメソッドはMoneyオブジェクトを受けて外部に出力する仕様とします。このインターフェースを経由することで具象クラスを交換可能なモジュールへと昇華させます。
BankRepositoryクラスを変更する
インターフェースを定義したのでBankRepositoryクラスを修正していきます。定義したインターフェースを実装していきましょう。BankRepositoryクラスのクラス定義の部分を以下のように変更します。
public class BankRepository : IRepository
こうするだけでIRepositoryインターフェースを実装していることになります。すでにBankRepositoryクラスではReadとWriteメソッドを定義通りに実装しているので変更する必要はありません。
とはいえテストクラスは変更しておきたいと思います。テストクラスでの記述はBankRepositoryで処理するようになっているので、これを抽象クラスで実行させるように変更しておきます。テストプロジェクト内のRepositoryTestクラスを以下のように変更します。
[TestMethod]
public void ReadTest_OK()
{
string path = @"C:\temp\balance.txt";
string text = ((int)10000).ToString();
File.WriteAllText(path, text);
IRepository repos = new BankRepository(path);
var money = repos.Read();
Assert.AreEqual(money.Value, 10000);
}
[TestMethod]
public void ReadTest_NG()
{
string path = @"C:\temp\balance.txt";
string text = "テスト";
File.WriteAllText(path, text);
IRepository repos = new BankRepository(path);
var money = repos.Read();
Assert.AreEqual(money.Value, 0);
}
[TestMethod]
public void WriteTest_OK()
{
string path = @"C:\temp\balance.txt";
IRepository repos = new BankRepository(path);
var money = new Money(1000);
repos.Write(money);
var balance = repos.Read();
Assert.AreEqual(money.Value, balance.Value);
}
変更点は「var repos = new BankRepository(path);」と記述していた箇所を「IRepository repos = new BankRepository(path);」に変更したことになります。左側をIRepositoryとしておくことで、右側をIRepositoryを実装しているクラスであることが条件であることが明記できます。
この流れがテスト駆動開発でいうところの「リファクタリング」になると思います。
特に拡張性を設計に盛り込む場合には、インターフェースや抽象クラス等を用いて抽象の実装を想定しておかないと、修正時の工数が非常に大きくなってしまうので、将来的に代替される可能性のあるモジュールを作成する場合は抽象的な構想も考えておく必要があります。
上記、リファクタリングが完了したら最後にテストを実行しておき、すべてのテストで「グリーン」になることを確認しましょう。こういったリファクタリングを挟むときにテスト駆動開発は強みを発揮してくれます。「機能が確約されている」という安心感ですね。
サービスクラスの実装をしよう
ここからはアプリケーションのサービスクラスの実装作業に移っていきます。これまでは、アプリケーションに必要な部品を主に作ってきましたが、ここからは「部品をどう使っていくか?」を考えていきます。
サービスクラスはユースケースに近い形式でアプリケーションの処理を管理してくれるクラスになります。それでは早速始めていきましょう。
ユニットテストを追加する
ではまずはユニットテストのファイルを追加していきます。これまで通りBankAccountTestプロジェクトを右クリックして「追加」から「単体テスト」を選択してください。ファイル名は「BankServiceTest」としたいと思います。
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
namespace BankAccountTest
{
[TestClass]
public class BankServiceTest
{
[TestMethod]
public void TestMethod1()
{
}
}
}
追加したファイルが上記のようになっていればOKです。ソリューションエクスプローラーでファイル名を変更したい場合は、変更したいファイルを選択して右クリックから「名前の変更」を選択して任意の名称に変更してください。
サービスクラスを考える
さて、サービスクラスをコーディングしていきますが、まずはサービスクラスがどんな役割を持っているかを考えたいと思います。先ほどサービスクラスはユースケースの実行を管理するクラスだと説明しました。
今回のアプリケーションではどんなユースケースがあるのかを考えたいと思います。まず作成するべきは「残高を確認できること」になりますね。また「口座から金額を引き落とせること」「口座に金額を追加できること」の3つが考えられます。ユースケースとしては上記の3つで十分そうです。
それらを必要に応じて実行してくれるクラスを作っていくイメージです。今回のクラスは、どちらかというと「ドメインサービス」に近いイメージで作っていきます。満たしたい仕様を外部からの変更を切り離した状態で実現できるモジュールのイメージです。
残高照会機能を作成する
サービスクラスについて少しは理解が進んだかと思いますので、実際に実装を始めていきたいと思います。まずは作成したユニットテストに記述していく段階です。まずはテスト駆動開発の実践編ですのでテストコードから書いていきましょう。
デフォルトで作成されたテストメソッドを変更して、以下のような感じにしておきます。まずは参照機能から作っていきたいと考えています。
[TestMethod]
public void CheckOnBalance_OK()
{
var service = new BankService();
var balance = service.GetBalance();
}
上記のような感じで記述してみました。BankServiceクラスにGetBalanceというメソッドを追加して、このメソッドがコールされたら口座の残高をMoneyオブジェクトとして返却するイメージです。
とはいえ、口座の残高はどこから取得できるのか?という疑問があります。口座の残高をテキストから読み込むには、前回までに作成してきたBankRepositoryが必要になりますね。この時点でBankServiveはRepositoryに依存したモジュールになりそうです。それを踏まえてテストを修正してみます。
[TestMethod]
public void CheckOnBalance_OK()
{
string path = @"C:\temp\balance.txt";
var repository = new BankRepository(path);
var service = new BankService(repository);
var balance = service.GetBalance();
}
上記のようにテストが作成できれば十分かと思います。あとはテストを実行するためのスタブといったデータをそろえてあげて、Assertクラスのメソッドで確認できるように変更します。
[TestMethod]
public void CheckOnBalance_OK()
{
string writePath = @"C:\temp\balance.txt";
string text = ((int)100).ToString();
File.WriteAllText(writePath, text);
string path = @"C:\temp\balance.txt";
var repository = new BankRepository(path);
var service = new BankService(repository);
var balance = service.GetBalance();
Assert.IsTrue(balance.Equals(new Money(100)));
}
いったんこれで実行したいイメージまで持っていくことができました。事前に10000をファイルに書き込んでおき、BankSeriveのGetBalanceメソッドでそのデータを取得して書き込んだ値と正しくなるかを確認するテストになります。
実装をしてレッドを目指す
テストケースがコーディングできたら、これらをレッドにするように実装を加えていきます。現時点ではBankServiceクラスが存在しないためコンパイルエラーになっているので、まずはコンパイルエラーとならないように修正するのが先決になります。
サービスフォルダを作成する
テストメソッドで記述した「BankSericeクラス」の置き場を用意するために、App03の下に「Servicesフォルダ」を作成し、その中に「BankSerice.cs」を作成したいと思います。
App03プロジェクトを右クリックして「追加」から「新しいフォルダー」を選択し、フォルダ名を「Services」にしましょう。フォルダの作成ができたらフォルダを右クリックして「追加」から「新しい項目」を選択し、「クラス」を選択した状態でファイル名を「BankService」として「追加」を押下します。
以下のようなデフォルトクラスが作成されるかと思います。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace App03.Services
{
internal class BankService
{
}
}
ここまで出来たらいったんはOKです。それではコンパイルエラーを直すための下準備は整いました。
コンストラクタを追加する
デフォルトクラスは作成されていますが、クラスとしての記述は殆どありませんよね。ですのでコンパイルエラーの原因ともなっているコンストラクタ部分を修正していきます。以下のように変更を加えましょう。
using App03.Repository;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace App03.Services
{
public class BankService
{
public BankService(IRepository repos)
{
}
}
}
もともとBankRepositoryはIRepositoryのインターフェースを実装していたことを思い出してください。こういう引数などは、抽象化されていれば機能を表すインターフェースにしておくほうが無難です。上記の実装では内部でレポジトリを保持できていないので、内部変数としてレポジトリを保有できるように変更を加えます。
using App03.Repository;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace App03.Services
{
public class BankService
{
readonly IRepository _repos;
public BankService(IRepository repos)
{
_repos = repos;
}
}
}
上記の変更を加えたら、テストクラスに戻ってもてください。まだコンパイルエラーになっていると思います。現在の状況は作成したBankServiceが参照できていない状態です。「using App03.Services;」を加えたらコンパイルエラーは下の行であるGetBalanceメソッドに移動すると思います。
using App03.Repository;
using App03.Services;
using App03.ValueObjects;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.IO;
namespace BankAccountTest
{
[TestClass]
public class BankServiceTest
{
[TestMethod]
public void CheckOnBalance_OK()
{
string writePath = @"C:\temp\balance.txt";
string text = ((int)100).ToString();
File.WriteAllText(writePath, text);
string path = @"C:\temp\balance.txt";
var repository = new BankRepository(path);
var service = new BankService(repository);
var balance = service.GetBalance();
Assert.IsTrue(balance.Equals(new Money(100)));
}
}
}
GetBalanceメソッドを追加する
GetBalanceメソッドの箇所で赤波線が引かれているのを確認したら、次はこのエラーを解消していきます。直接的な原因はBankServiceクラスにGetBalanceメソッドが存在していないことになります。素直にGetBalanceメソッドを追加しましょう。この時の戻り値は後のAssertの内容を考えてMoneyクラスになります。
using App03.Repository;
using App03.ValueObjects;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace App03.Services
{
public class BankService
{
readonly IRepository _repos;
public BankService(IRepository repos)
{
_repos = repos;
}
public Money GetBalance()
{
throw new NotImplementedException();
}
}
}
上記のように修正を加えたらテストメソッドのコンパイルエラーは解消されているはずです。これでレッドの状態になるかを確認してみましょう。「レッド」になっていれば順調です。CheckOnBalance_OKテストの「グリーン」化は次で行います。
「レッド」から「グリーン」にする
では「レッド」の状態から「グリーン」にする修正を加えていきましょう。とはいえ、BankRepositoryで読込の処理は作成済みですので簡単な修正で終わってしまいます。BankServiceクラスのGetBalanceメソッドを以下のようにしてください。
public Money GetBalance()
{
return _repos.Read();
}
BankRepository、ないしはIRepositoryクラスのReadメソッドはテキストファイルを読み込んでMoneyオブジェクトで返却してくれるメソッドでしたね。この状態でテストを実行すれば「レッド」であったテストエクスプローラーも、すべての項目が「グリーン」になるかと思います。これで参照機能の実装は終了となります。
入金機能を考えよう
金額を参照する機能は完了したので次は、入金機能のユースケースを考えていきましょう。
大まかな機能として実現できれば良い内容は「残高に入金された金額を追加してファイルに出力し、最新の口座残高とすること」になりますよね。入金機能として最低限これだけあれば十分に思いますので、早速テストコードから記述していきましょう。
BankServiceTestクラスを開いて作業していきます。
テストコードのコーディングをする
BankServiceTest.csにて新規のテストメソッドを作成して以下のようなテストを記述しておきます。テスト内容としては、元の金額と新規の金額を足し合わせた金額がファイルに出力されていればOKになります。
[TestMethod]
public void AddBalance_OK()
{
//初期金額
string writePath = @"C:\temp\balance.txt";
string text = ((int)100).ToString();
File.WriteAllText(writePath, text);
string path = @"C:\temp\balance.txt";
var repository = new BankRepository(path);
var service = new BankService(repository);
//入金金額
service.AddBalance(new Money(100));
//残高照会
var balance = service.GetBalance();
Assert.IsTrue(balance.Equals(new Money(200)));
}
初期金額を100円として追加入金が100円としたとき、再度残高照会をすると200円になっているはずです。それを上記のテストメソッドとして作成しました。金額は任意ですので好きな金額に変更してもらってもかまいません。
コンパイルエラーをレッドにする
さてテストメソッドが記述できたので、次はコンパイルエラーの状態からレッドを目指します。現状ではテストメソッド内に記述しているAddBalanceメソッドが存在していないためエラーになっています。この状況を解決しましょう。
AddBalaceメソッドは今のところvoid型ですが、AddBalanceした戻り値としてMoneyクラスを返した方が使い勝手が良さそうですね。ファイルに書き込むまでを一連の流れにしますが、書き込んだ内容を返却するようにすれば、最新の残高を再度ファイルアクセスして検索する必要はなくなります。
public Money AddBalance(Money add)
{
return add;
}
BankServiceにいったん仮のAddBalanceメソッドを記載しておき、まずはレッドの状態を目指します。上記のメソッドを追加すれば、コンパイルエラーが解消できるはずですので、この状態でテストを実行してレッドになるかを確認しましょう。
レッドからグリーンにする
レッドの状態になることを確認出来たら、次はグリーンの状態を目指します。入金処理を分解すると以下のようにすることができます。
- 現在の残高を問い合わせる
- 入金金額を足し合わせる
- 最新の残高をファイル出力する
- 最新の残高を返却する
この4段階に分けることができますので、上記の処理を再現できるようにします。処理自体はそこまで難しくはありません。これまでにファイルの読み書き機能、Moneyクラスの足し・引き処理を作成しているのでこれらを組み合わせるだけになります。
public Money AddBalance(Money add)
{
var balance = _repos.Read();
var newBalance = balance.Add(add);
_repos.Write(newBalance);
return newBalance;
}
という訳でコーディングは上記のようになります。GetBalanceメソッドで残高を取得して、現在の残高に入金金額を足し合わせます。MoneyクラスのAddメソッドでは新しいオブジェクトで返却されるように実装しているので、ここで新規オブジェクトを作成する必要はありません。
最後にAddメソッドで返却されてきた内容を呼び出し元に返却するだけでOKになります。現在の実装としては十分でしょう。ここまで実装が出来たらテストを実行してレッドからグリーンになるかを確認します。グリーンになれば完了です。それでは次回は残高引出の処理を実装していきます。
出金機能には何が必要か
入力機能が完了したので次は出金機能を作成していきます。残高から出金するには何が必要かを考えます。基本的は引き出したい金額を残高から差し引いて最新の残高を更新するだけで十分に思いますね。しかしながらそれだけでは足りなさそうです。
例えば100円しかない口座に200円の引き出し指示が来たとき、これを了承して良いのでしょうか。自動的な貸し出し処理等を発生させるような特別仕様な口座ならよいかもしれませんが、ここではそんな複雑なことは考えないようにします。
残高と引き出し指示を比べて残高が0円以上になる場合に引き出しを許可するようにする必要がありそうです。例外処理の扱い方をどうするか考える必要がありますが、いったんは例外として処理しておきます。という訳でテストコードから考えましょう。
正常パターンのテストコード
まずは簡単に正常パターンのテストコードを記載していきましょう。今回は新規メソッドをWithdrawメソッドとしていきます。BankServiceTest.csに以下のテストメソッドを追加してみましょう。
[TestMethod]
public void Withdraw_OK()
{
//初期金額
string writePath = @"C:\temp\balance.txt";
string text = ((int)100).ToString();
File.WriteAllText(writePath, text);
string path = @"C:\temp\balance.txt";
var repository = new BankRepository(path);
//引出指示
var service = new BankService(repository);
service.Withdraw(new Money(40));
//残高確認
var balance = service.GetBalance();
Assert.IsTrue(balance.Equals(new Money(60)));
}
今回も例のごとく初期金額を100円としていますが、任意の金額に変更してください。さて現在の状態ではコンパイルエラーとなりますので、まずはコンパイルエラーを取り除いてレッドになる状態を目指します。
コンパイルエラーをレッドにする
現在のコンパイルエラーの直接的な原因はWithdrawメソッドがBankServiceクラスに存在しないことによるものです。この原因を取り除く必要があります。まずは暫定的に以下のメソッドをBankServiceクラスに追加します。
public Money Withdraw(Money withdraw)
{
return withdraw;
}
これでコンパイルエラーを取り除くことができると思います。テスト駆動開発では、いきなり実装をスタートさせずに、まずは「レッド」の状態を目指してから正しい実装に着手していくのがセオリーです。
上記の実装が完了したらテストを実施して、無事にテストエクスプローラーがレッドになるかどうかを確認しましょう。レッドの状態になったら先に進むことになります。まずはこのサイクルを体になじませましょう。
レッドからグリーンにする
さて無事にレッドになることが確認できたら先に進みます。次はレッドの状態をグリーンにすることです。引出機能としては以下のような段階を踏む必要があります。
- 残高を確認する
- 出金指示が残高を超えないことを確認する
- 問題なければ残高から出金指示分の金額を引く
- 最新の残高として更新する
上記の手順になるのが一般的になると思いますので、これをシンプルに実装していきましょう。BankSerivceテストの中身は以下のようになるかと思います。
public Money Withdraw(Money withdraw)
{
var balance = _repos.Read();
var newBalance = balance.Subtract(withdraw);
if (newBalance.Value < 0)
{
throw new Exception("残高以上の引き出しはできません。");
}
_repos.Write(newBalance);
return newBalance;
}
出金指示が残高を上回っている場合には例外として処理しておきます。ここでの処理は例外でもエラーメッセージとして別プロパティに格納するでもよいかと思います。とはいえ極力シンプルに行きたいので例外としてハンドリングすることにします。
上記の実装まで済んだらいったんテストを実施してグリーンになるかを確認します。グリーンになればいったんはOKとします。もう少しリファクタリングをしたいと思いますが、まずはテストが通るかを確認しましょう。
リファクタリングで洗練させる
さてリファクタリングをして行きたいとおもいますが、そこまで複雑なことをするつもりはありません。先ほどのWithdrawメソッドで一つ気になる実装があります。それは出金指示と残高を比べる箇所です。
var newBalance = balance.Subtract(withdraw);
if (newBalance.Value < 0)
{
throw new Exception("残高以上の引き出しはできません。");
}
ここの処理ですが残高に対して直接的に0より少ないかを確認しています。この状態ですと、将来的にほかの箇所に同様の実装をする場面では処理が分散します。それに現在の仕様では「出金指示は残高を上回らなくてはならない」という強いルールになりますよね。
こういう「ルール」があちこちに散らばってしまうとバグの温床になってしまいます。こういう処理は一つの「ルール」としてメソッドにまとめておく方が拡張性がありますね。という訳でこの部分を切り離します。このルールは「口座」のルールですのでBankService内で問題ないでしょう。まずはそのためのテストメソッド作りから始めます。
ルールのテストメソッドを実装する
BankServiceTest.csに移動してメソッドを3つほど追加します。OKパターンとNGパターンになります。コーディング内容は「出金指示は残高を超えてはならない」というものでした。
[TestMethod]
public void CanWithdraw_OK1()
{
string path = @"C:\temp\balance.txt";
var repository = new BankRepository(path);
var service = new BankService(repository);
var balance = new Money(100);
var ins = new Money(100);
var canWithdraw = service.CanWithdraw(balance, ins);
Assert.IsTrue(canWithdraw);
}
[TestMethod]
public void CanWithdraw_OK2()
{
string path = @"C:\temp\balance.txt";
var repository = new BankRepository(path);
var service = new BankService(repository);
var balance = new Money(100);
var ins = new Money(99);
var canWithdraw = service.CanWithdraw(balance, ins);
Assert.IsTrue(canWithdraw);
}
[TestMethod]
public void CanWithdraw_NG()
{
string path = @"C:\temp\balance.txt";
var repository = new BankRepository(path);
var service = new BankService(repository);
var balance = new Money(100);
var ins = new Money(101);
var canWithdraw = service.CanWithdraw(balance, ins);
Assert.IsFalse(canWithdraw);
}
今回のテストでは境界チェックを実施したかったので以下の3パターンを用意しています。
- 残高が100円で出金指示が99円の場合
- 残高が100円で出金指示も100円の場合
- 残高が100円で出金指示が101円の場合
上記の場合、先の2パターンが正常で最後のパターンが異常になります。「出金指示が残高を超えてはならない」という場合、残高と出金指示が同値であるという場合は引き出せなくてはなりません。それをチェックするためのテストになります。
コンパイルエラーからグリーンにする
という訳で早速、コンパイルエラーからレッドの状態を目指します。BankServiceクラスにCanWithdrawメソッドを作成してコンパイルエラーを解消します。
public bool CanWithdraw(Money balance, Money ins)
{
throw new NotImplementedException();
}
いったんは上記の状態でテスト実行ができるような状態とします。テストを実行してレッドの状態になることを確認して下さい。レッドであればさらに続けて修正を施していきます。CanWithdrawメソッドは以下のように修正できると思います。
public bool CanWithdraw(Money balance, Money ins)
{
return balance.Subtract(ins).Value >= 0;
}
残高から出金指示を引いて0円以上でれば引き出しが可能となります。上記の処理で十分に判定できるようになるはずです。修正できたらもう一度テストを実施してみましょう。さてグリーンになったでしょうか。それでは最後に元のテストを変更します。
public Money Withdraw(Money withdraw)
{
var balance = _repos.Read();
if (!(CanWithdraw(balance, withdraw)))
{
throw new Exception("残高以上の引き出しはできません。");
}
var newBalance = balance.Subtract(withdraw);
_repos.Write(newBalance);
return newBalance;
}
テストを変更したら再度テストを実行して、すべてがグリーンのままであることを確認します。オールグリーンの状態であればリファクタリングは完了です。テスト駆動開発はリファクタリングをするときにも非常に便利ですね。ケースを網羅したテストをあらかじめ作っておけば、ちょっとしたコードの修正にも柔軟に対応できる実装手法です。
異常パターンのテストコード
さて、最後においておいたWithdrawメソッドの異常パターンのテストコードを実装していきます。まずはテストコードの実装から始めていきましょう。
[TestMethod]
public void Withdraw_NG()
{
//初期金額
string writePath = @"C:\temp\balance.txt";
string text = ((int)100).ToString();
File.WriteAllText(writePath, text);
string path = @"C:\temp\balance.txt";
var repository = new BankRepository(path);
//引出指示
try
{
var service = new BankService(repository);
service.Withdraw(new Money(101));
}
catch (Exception ex)
{
Assert.AreEqual(ex.Message, "残高以上の引き出しはできません。");
}
}
上記のような実装になればOKかなと思います。現在ではWithdrawメソッドの中で、残高がマイナスになる場合はエラーとしているので、いったんはtry ~ catch ~ を使ってエラーを拾うようにしておきましょう。より良い実装が見つかった時に変更するかもしれませんが、今はこのままとしておきます。
さて、ここまででBankServiceに必要な参照・引出・入金の3つの重要な核となる機能の実装が完了しました。これでもともと想定していた機能を実装する準備が整ったと言えそうですね。次回から最終局面である、アプリケーション全体の処理を司るアプリケーションサービスクラスの実装を行います。
アプリケーションサービスクラスを作成する
前回までにBankServiceを作成してきました。このBankServiceは「口座」に関連する処理・機能を提供するモジュールとして実装をしてきました。したがってアプリケーションの全体の流れを管理するような設計にはなっておりませんでした。
ここで作成したいのが「アプリケーションサービスクラス」と呼ばれるクラスです。簡単に言ってしまうと「各機能を組み合わせてアプリケーションの全体の流れを作るクラス」ということになります。このファイルを作成していきます。まずはApp03のプロジェクトを右クリックして「追加」から「クラス」を選択して、クラス名を「ApplicationService」としてファイルを作成しましょう。とりあえず出来たらここまでにしておきます。
機能・実装方針を考えよう
さて口座の仕様はBankServiceクラスなどで定義されていますので、これらをどう組み合わせるかを考えることにしていきます。今回作成したいアプリケーションの大きな流れは以下のようになります。
- 残高を表示する
- メニューを表示する
- ユーザーに選択を促す
- メニューで指定された処理を行う(※)
- 続けて入力するかを決める
という感じです。注釈の部分はもう少し細かく決める必要があります。というのも選択したメニューによって挙動が異なるからです。もう少し細かく見ていきます。
- 入金・出金:金額の入力を促して処理結果を表示し、最新残高を表示する
どちらも金額の入力を促して、処理を実行して最新の残高を表示するところは一緒ですが、処理を実行する場合に様々な制限が加わりますよね。たとえば以下のような感じです。
- 数字で入力されているか
- 金額はプラス値であるか
- 出金金額は正常であるか
基本的にはこのようにあげられると思います。あとは上記をどのように実装するかです。まず大きな処理を考えるとすると以下の形式が思い浮かびます。
- 画面の入力値を受け取って、すべての処理を一つのメソッドで管理する
- メニューごとにpublicメソッドを作成して処理を管理する
処理の今回はコンソールアプリケーションですが、この画面がコンソールではなくWPFやWebだった場合を考慮して拡張性のある方針にしたいと思います。なのでApplicationServiceの入り口は1つに絞り、中で処理を分岐させて一つのメソッドで処理を管理する方針とします。
画面の入力項目を考える
それではApplicationServiceクラスの実装に移りたいと考えますが、その前に画面の入力項目を想定しておきたいと思います。ひとつ前に記載した「ApplicationServiceの入り口は1つに絞り、中で処理を分岐させて一つのメソッドで処理を管理する方針」とするためには入り口で想定されるインプット・アウトプットを決めておく必要がありそうです。
インプットとして考えられるのは「選択されたメニュー」「入力された金額」になるかなと思います。逆にアウトプットは「最新残高」と「メッセージ」になるでしょうか。いったんはこの想定で考えてみましょう。ということで早速、テストクラスから実装を開始します。
テストクラスを実装する
今回は新しくApplicationServiceクラスを作成したので、それらをテストするためのクラスを新しく作成します。いつものごとくBankAccountTestプロジェクトを右クリックして「追加」から「単体テスト」を選択してください。名称がUnitTest2になっているので、ソリューションエクスプローラーでファイルを選択して右クリックして「ApplicationServiceTest」と変更しましょう。以下のようになっていればOKです。
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
namespace BankAccountTest
{
[TestClass]
public class ApplicationServiceTest
{
[TestMethod]
public void TestMethod1()
{
}
}
}
初期時点の処理
まずは初期時点のメッセージやメニュー表示はProgram.csで行う想定なので初期時点では最新残高だけ表示できればOKになると思います。という訳でテストメソッドを記載していきます。デフォルトのTestMethod1を削除して新しくメソッドを作成しましょう。
[TestMethod]
public void InitializeTest()
{
string writePath = @"C:\temp\balance.txt";
string text = ((int)100).ToString();
File.WriteAllText(writePath, text);
var app = new ApplicationService();
var args = app.Initialize();
//初期残高の確認
Assert.IsTrue(args.Balance.Equals(new Money(100)));
}
現状ではApplicationServiceは認識されていない状況です。というのも「アプリケーションサービスクラスを作成する」の項目で作成したApplicationServiceクラスの識別子がinternalだからですね。まずは上記の状態をコンパイルエラーからレッドにするように修正してみましょう。ApplicationServiceクラスから変更していきます。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace App03
{
public class ApplicationService
{
public object Initialize()
{
throw new NotImplementedException();
}
}
}
いったんはApplicationServiceクラスを上記のように変更しています。またApplicationServiceTestクラスから各機能を見ることができるようにusingも定義追加してください。現在は以下の3つを追加しています。
using App03;
using App03.ValueObjects;
using System.IO;
この状態でコンパイルエラーが解消されるかと思いきや、初期残高の確認部分でコンパイルエラーが生じていましたので解消していきます。ApplicationServiceクラスにデータを移送するためだけのクラスを定義しておきましょう。ApplicationServiceの全貌は以下のようになります。
using App03.ValueObjects;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace App03
{
public class ApplicationService
{
public object Initialize()
{
throw new NotImplementedException();
}
}
public class BankDto
{
public Money Balance { get; set; }
}
}
現状でもまだコンパイルエラーは解消されません。これはInitializeメソッドの戻り値がobjectだからですね。これを先ほど作成したBankDtoに変更します。こうすることでコンパイルエラーが消えました。いったんレッドになるかを確認します。
レッドからグリーンにする
さてレッドになるところまで確認ができたので、これらをグリーンにするように修正を加えましょう。そこまで難しくないと思いますので頑張ってチャレンジしてみてください。
public BankDto Initialize()
{
string path = @"C:\temp\balance.txt";
IRepository _repos = new BankRepository(path);
var _service = new BankService(_repos);
return new BankDto()
{
Balance = _service.GetBalance(),
};
}
きれいではありませんが、いったん上記のようにしておきます。リファクタリングは後からしますので、まずは目の前のテストをグリーンにするための実装に集中します。初期時点でのテストとしてはグリーンになることが確認できれば十分です。
リファクタリングをする
グリーンになることを確認したら、次はリファクタリングを行います。先ほどのメソッドでは気になる点がたくさんありました。repositoryやserviceクラスがメソッド内の変数として定義されているので、ほかのメソッドで何回も生成しなくてはなりません。コンストラクタもないですし、そのあたりは直したいです。
public class ApplicationService
{
private readonly IRepository _repos;
private readonly BankService _service;
public ApplicationService()
{
string path = @"C:\temp\balance.txt";
this._repos = new BankRepository(path);
this._service = new BankService(_repos);
}
public BankDto Initialize()
{
return new BankDto()
{
Balance = _service.GetBalance(),
};
}
}
ApplicationServiceクラスを上記のようにリファクタリングしました。テストを実行してグリーンであることを確認します。次に気になる点はパスがそのまま埋め込まれていることです。これを外部の定数クラスに切り出します。新規のConstantsファイルを、ApplicationService.csやProgram.csと同列に作成して以下のようにしておきます。今回はstaticの静的エリアに配置するようにして、いつでも呼び出せるようにしておきましょう。
namespace App03
{
public static class Constants
{
public const string DbPath = @"C:\temp\balance.txt";
}
}
上記のようにすることによって、ApplicationServiceクラスの記述内容も変わってきます。コンストラクタを以下のように修正します。できれば設定値情報などはファイルに決め打ちで記述するのではなく、一か所にまとめておくほうがきれいですよね。
public ApplicationService()
{
string path = Constants.DbPath;
this._repos = new BankRepository(path);
this._service = new BankService(_repos);
}
再度テストを実行して無事にリファクタリングが完了しているかを確認します。すべてグリーンであれば問題ありませんので、いったんはここで終了です。
メインルーチンを処理する処理を作成する
初期時点の処理を実装完了したので、次は大きなメインルーチンの処理を管理するメソッドを実装していきます。各機能のアプリケーションサービスを作成します。
テストコードを実装する
では早速、テスト駆動開発のセオリーに従ってテストコードから実装をしていきましょう。今回は預金処理ということで画面から入金金額と処理区分を受け取って、その処理区分に応じた処理を行うような設計としますので、テストコードは以下のようになると思います。
[TestMethod]
public void DepositTest_OK()
{
//初期残高
string writePath = @"C:\temp\balance.txt";
string text = ((int)0).ToString();
File.WriteAllText(writePath, text);
//入金処理
var app = new ApplicationService();
var arg = app.Execute(
new BankDto()
{
ProcessType = Process.Deposit,
Input = new Money(100),
});
//預金の確認
Assert.IsTrue(arg.Balance.Equals(new Money(100)));
}
初期残高は0円として記録しておき、預金で100円の入金を行い、最新の残高が100円になるかを確認するようなテストコードとしています。預金の場合は出金のような制限がないため上記のテストコードで十分かと思います。
またついでにDeposit(預金)のほかにもWithdraw(出金)側のテストコードも実装しておきましょう。預金の場合と同じような感じで実装できるかと思います。OKパターンに加えてNGパターンのテストコードも実装しておきます。
[TestMethod]
public void WithdrawTest_OK()
{
//初期残高
string writePath = @"C:\temp\balance.txt";
string text = ((int)100).ToString();
File.WriteAllText(writePath, text);
//入金処理
var app = new ApplicationService();
var arg = app.Execute(
new BankDto()
{
ProcessType = Process.Withdraw,
Input = new Money(10),
});
//預金の確認
Assert.IsTrue(arg.Balance.Equals(new Money(90)));
}
[TestMethod]
public void WithdrawTest_NG()
{
//初期残高
string writePath = @"C:\temp\balance.txt";
string text = ((int)100).ToString();
File.WriteAllText(writePath, text);
//入金処理
var app = new ApplicationService();
var arg = app.Execute(
new BankDto()
{
ProcessType = Process.Withdraw,
Input = new Money(101),
});
//メッセージと残高の確認
Assert.AreEqual(arg.Message, "残高以上の引き出しはできません。");
Assert.IsTrue(arg.Balance.Equals(new Money(100)));
}
とりあえずは上記のようにテストコードを記述しておきます。100円から10円引いて残高が90円になることに確認と100円から101円を引いて出金できないパターンの2パターンです。最低限のパターンは網羅できていると思います。
コンパイルエラーからレッドにする
テストコードの実装が完了したのでコンパイルエラーを解消するために修正を加えます。基本的にはApplicationSeriviceクラスに実装を加えていきます。
using App03.Repository;
using App03.Services;
using App03.ValueObjects;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace App03
{
public class ApplicationService
{
private readonly IRepository _repos;
private readonly BankService _service;
public ApplicationService()
{
string path = Constants.DbPath;
this._repos = new BankRepository(path);
this._service = new BankService(_repos);
}
public BankDto Initialize()
{
return new BankDto()
{
Balance = _service.GetBalance(),
};
}
public BankDto Execute(BankDto bankDto)
{
throw new NotImplementedException();
}
}
public enum Process
{
Deposit, Withdraw
}
public class BankDto
{
public Money Input { get; set; }
public Process ProcessType { get; set; }
public Money Balance { get; set; }
public string Message { get; set; }
}
}
ApplicationServiceクラスを上記のように修正します。するとコンパイルエラーは解消されると思います。処理内容の正しさはおいてき、まずはコンパイルエラーからレッドになる状態を確認します。実装が出来たらテストを実施して状態を確認しましょう。
レッドからグリーンにする
追加したテストがレッドになることを確認できたので、ここからテストをグリーンにする作業に移っていきます。まずは預金処理の場合をコーディングしていきましょう。
預金処理の実装を行う
預金処理は基本的にBankServiceクラスのAddBalanceメソッドを呼び出すだけになります。とはいえ、処理タイプとしてenumを定義しているので、その処理に応じて分岐させるようにしておきます。
public BankDto Execute(BankDto bankDto)
{
if (bankDto.ProcessType == Process.Deposit)
{
var balance = _service.AddBalance(bankDto.Input);
return new BankDto()
{
Balance = balance
};
}
return new BankDto();
}
Executeメソッドの中を上記のようにしておきましょう。ソースコードが洗練されていなくても気にせず、まずはグリーンにするための処理を記述します。実装できたらテストを実施してDepositTest_OKがグリーンになることを確認します。
出金処理の実装を行う
預金処理のコーディングが完了したので、次は出金処理の実装に移ります。Executeメソッドに処理をまとめているので、実装箇所は預金処理の場合と変わりません。
public BankDto Execute(BankDto bankDto)
{
if (bankDto.ProcessType == Process.Deposit)
{
var balance = _service.AddBalance(bankDto.Input);
return new BankDto()
{
Balance = balance
};
}
else if (bankDto.ProcessType == Process.Withdraw)
{
var balance = _service.Withdraw(bankDto.Input);
return new BankDto()
{
Balance = balance
};
}
return new BankDto();
}
さてOKパターンのテストをグリーンにするために実装を追加してみました。この状態でいったんテストを実施してグリーンになるかを確認します。WithdrawTest_OKは問題なかったでしょうか。
出金処理の異常パターンを実装する
さて、出金処理については正常パターンの実装までが完了したところです。次にやることは出金処理・預金処理のテスト結果を壊さずに異常パターンをグリーンになるように実装することになります。
テスト駆動開発ではすでに実行したテストを行いながらリファクタリングできるのが強みですので、今回もテストを実施しながら、大胆に、かつ柔軟にリファクタリングを行っていきましょう。
public BankDto Execute(BankDto bankDto)
{
try
{
if (bankDto.ProcessType == Process.Deposit)
{
var balance = _service.AddBalance(bankDto.Input);
return new BankDto()
{
Balance = balance
};
}
else if (bankDto.ProcessType == Process.Withdraw)
{
var balance = _service.Withdraw(bankDto.Input);
return new BankDto()
{
Balance = balance
};
}
return new BankDto();
}
catch (Exception ex)
{
return new BankDto()
{
Balance = _service.GetBalance(),
Message = ex.Message,
};
}
}
今回はWithdrawメソッドにて出金指示が残高を上回る場合について、例外を送出するようにしていたので、大胆にtry ~ catch ~で囲うようにしておき、例外が発生した場合は現状の残高を再取得してメッセージと一緒に返却するようにしています。この状態でテストを通せば問題なくなるはずですので、テストを実施してみてください。
GUI部分を実装する
ここまでにApplicationServiceクラスの実装までを行いました。ここで最後のGUI部分を実装していきます。
Program.csを実装する
さて、このGUI部分についてはテストコードを記述しながら作業を行うことはなく、一気にアプリケーションを完成までもっていきたいと考えています。
必要なものを定義する
最初にアプリケーションに必要なモジュールを作成しておきましょう。このアプリケーションで必要となるのはApplicationServiceモジュールでした。Program.csの冒頭で上記のモジュールを生成しておきます。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace App03
{
class Program
{
static void Main(string[] args)
{
var service = new ApplicationService();
}
}
}
メッセージを表示する処理
まずはユーザーに操作を促すメニューを表示する処理を作成していきます。ここは共通で使いまわせそうなのでメソッド化しておきます。
static void ShowMenu()
{
Console.WriteLine("行いたい操作のメニュー番号を入力してください。");
Console.WriteLine("1:入金 2:出金 3.終了");
}
static void ShowInstruction()
{
Console.WriteLine("金額を入力してください。");
}
static void ShowBalance(Money money)
{
Console.WriteLine($"残高:{ money.Value }円");
}
static void ShowClose()
{
Console.WriteLine("終了します。");
}
ルーチンを考える
あらかた必要そうなメッセージを表示させる処理を定義したら、メインのルーチンを記述していきます。基本的には「3:終了」が入力されるまでは無限に続くような感じにします。
static void Main(string[] args)
{
var service = new ApplicationService();
while(true)
{
ShowMenu();
var input = Console.ReadLine();
if (input == "3")
{
ShowClose();
Console.ReadLine();
break;
}
}
}
「3」が入力されるようにしておきます。細かい部分はおいておき、大まかな部分の実装だけを済ませておきます。これに引き続いて「1」なら入金、「2」なら出金するような分岐も作っておきます。そして最後は処理後の残高を表示させるようにします。
static void Main(string[] args)
{
var service = new ApplicationService();
while(true)
{
ShowMenu();
var input = Console.ReadLine();
if (input == "1")
{
var arg = service.Execute(
new BankDto()
{
ProcessType = Process.Deposit,
Input = new Money(0)
});
ShowBalance(arg.Balance);
}
if (input == "2")
{
var arg = service.Execute(
new BankDto()
{
ProcessType = Process.Withdraw,
Input = new Money(0)
});
ShowBalance(arg.Balance);
}
if (input == "3")
{
ShowClose();
Console.ReadLine();
break;
}
Console.WriteLine();
}
}
上記のように実装しておくことで動くようにはなりました。細かい部分の詰めは必要ですが、予定していた処理には近くなったと思います。では実装を続けていきます。
入力を促す処理を作成する
出金・入金ともに必要となるのが「金額の入力」になります。とりあえず金額の入力を促して正しいかどうかを判定するようなメソッドにしておきましょう。
static bool CanConvert(string txt)
{
return int.TryParse(txt, out var num);
}
次は入力値をMoneyオブジェクトに変換する処理を記述しておきます。
static Money ConvertMoney(string txt)
{
return new Money(int.Parse(txt));
}
これらをルーチンに組み込みます。少し長いですがこんな感じになるはずです。
static void Main(string[] args)
{
var service = new ApplicationService();
while(true)
{
ShowMenu();
var input = Console.ReadLine();
if (input == "1")
{
ShowInstruction();
var txt = Console.ReadLine();
if (!CanConvert(txt))
{
continue;
}
var inputMoney = ConvertMoney(txt);
var arg = service.Execute(
new BankDto()
{
ProcessType = Process.Deposit,
Input = inputMoney
});
ShowBalance(arg.Balance);
}
if (input == "2")
{
ShowInstruction();
var txt = Console.ReadLine();
if (!CanConvert(txt))
{
continue;
}
var inputMoney = ConvertMoney(txt);
var arg = service.Execute(
new BankDto()
{
ProcessType = Process.Withdraw,
Input = inputMoney,
});
ShowBalance(arg.Balance);
}
if (input == "3")
{
ShowClose();
Console.ReadLine();
break;
}
Console.WriteLine();
}
}
一応、これで十分に機能しますね。最後に一気にリファクタリングを行って、メソッドに切り出したりするなどコードをきれいにしておきましょう。最終的にはこんな感じでProgram.csの実装を終わりにしておきます。
using App03.ValueObjects;
using System;
namespace App03
{
class Program
{
static ApplicationService _service;
static void Main(string[] args)
{
_service = new ApplicationService();
while(true)
{
Console.WriteLine();
var selectedMenu = GetSelectedMenu();
if (selectedMenu == Menu.Incorrect)
{
Console.WriteLine("正しいメニュー番号を選択してください。");
continue;
}
if (selectedMenu == Menu.Done)
{
ShowClose();
Console.ReadLine();
break;
}
if (!ProcessSelectedMenu(selectedMenu))
{
Console.WriteLine();
continue;
}
}
}
enum Menu { Incorrect = 0, Deposit = 1, Withdraw = 2, Done = 3 }
static Menu GetSelectedMenu()
{
ShowMenu();
var input = Console.ReadLine();
if (int.TryParse(input, out var num))
{
switch(num)
{
case 1:
return Menu.Deposit;
case 2:
return Menu.Withdraw;
case 3:
return Menu.Done;
default:
return Menu.Incorrect;
}
}
return Menu.Incorrect;
}
static bool ProcessSelectedMenu(Menu menu)
{
ShowInstruction();
var txt = Console.ReadLine();
if (!CanConvert(txt)) { return false; }
var inputMoney = ConvertMoney(txt);
BankDto result = null;
if (menu == Menu.Deposit)
{
result = _service.Execute(
new BankDto()
{
ProcessType = Process.Deposit,
Input = inputMoney
});
}
else if (menu == Menu.Withdraw)
{
result = _service.Execute(
new BankDto()
{
ProcessType = Process.Withdraw,
Input = inputMoney,
});
}
if (result != null && string.IsNullOrEmpty(result.Message))
{
ShowBalance(result.Balance);
return true;
}
else
{
Console.WriteLine(result.Message);
return false;
}
}
static bool CanConvert(string txt)
{
if (int.TryParse(txt, out var num))
{
return num >= 0;
}
return false;
}
static Money ConvertMoney(string txt)
{
return new Money(int.Parse(txt));
}
static void ShowMenu()
{
Console.WriteLine("行いたい操作のメニュー番号を入力してください。");
Console.WriteLine("1:入金 2:出金 3:終了");
}
static void ShowInstruction()
{
Console.WriteLine("金額を入力してください。");
}
static void ShowBalance(Money money)
{
Console.WriteLine($"残高:{ money.Value }円");
}
static void ShowClose()
{
Console.WriteLine("終了します。");
}
}
}
Program.csでの反省点
実装はいったん完成しており問題なく動作することがわかっていますが、よりよくするための改善点はたくさんあります。さっとあげても以下のような改善項目はあげられるでしょう。
- Program.csでMoneyオブジェクトを扱う必要がない
- メッセージの出力もApplicationServiceで行うべき
- Moneyオブジェクトは負の数を許容するべきでない
- Moneyオブジェクトの引数を文字列型にしてみる
などなど様々にあげられます。こうした内容も本来ならばテストコードを記述しながらリファクタリングを行っていくべきではありますが、ここより先はあなた自身の課題として取り組んでみてください。
本来であればProgram.csはGUIに位置するファイルなので、値の大きさを比べたり、処理のルーチンを持ち込んだりする必要はありませんでした。MVCの観点から言ってもGUIは値を受け取ってControllerやModelといった下層に渡すのみに徹するべきです。
テスト駆動開発を取り入れてみよう
かなり長いレッスンとなりましたが、テスト駆動開発に必要な基本的なことがらはすべて網羅してきたつもりです。テスト駆動開発は「テストを先に書いて、後に実装を行う」というスタイルであるため、状態の担保がしやすい開発手法であることがわかります。
また、「状態が担保しやすい」ということは、いくら変更を加えても瞬時に「間違えていないか」を確認できることでもあります。
機能を新規で追加する場合、これまでの処理も同様に行える必要があるため、「いつでも、すぐに確認できる」というのは、開発者にとってとても心強いものであり、同時にストレスからも解放されるというメリットでもあります。
プログラマーはストレスとの闘いでもあるため、こうした精神安定剤のような武器は一つでも多く持っておくべきです。テスト駆動開発の良さは当サイトで解説できたと思っていますので、ここで学んだことを活かして、日常の業務に取り入れてもらえたら嬉しく思います。