「テスト駆動開発?」「テストコード?」まったくわからない方にも画像を使いながら丁寧に解説します。このテスト駆動開発を学べば,C#でよりよいアーキテクチャーでのコーディングが可能になり,保守性の高いコードが書けるようになります。
テストコードとは?
テストコードとは,「ユニットテスト」と呼ばれるもので,プログラミングが正しいかどうかを検証するコードの事です。C#の開発環境では最近のVisual Studio2015などでは
標準で使用できます。
プログラムに対してテストコードを記述しておけば,自動的にテストされ,OKかNGかを毎回教えてくれます。
何かの修正や機能追加でプログラムコードを変更したときに,不具合が混入すればすぐに発見することができます。
近年ではテストコードを記述するのは当然であり,テストコードのないプログラムは怖くて触れないコードという扱いになっています。
昔はVBなどで作成していたころは,そんなことは気にせずに,どんどんプログラミングしていましたが,今のスタンダードはできるだけテストコードでカバーされている
プログラムを書くことがよい事とされています。
では実際にテストコードを書いてみましょう。
Visual Studio2017を使用しますが,ユニットテストが書ければバージョンは何でも構いません。
単体テストプロジェクトの作成
「単体テストプロジェクト」を追加します。
ソリューション名は「TDD」,名前は「TDDTest.Tests」としています。
フォルダはお好きなところを選んでください。
次のようなコードが自動生成されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
using System; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace TDDTest.Tests { [TestClass] public class UnitTest1 { [TestMethod] public void TestMethod1() { } } } |
ここにテストコードを書いていくのですが,テストする対象が必要です。
今作成したプロジェクトはテスト用のプロジェクトで実際のアプリケーションを動作させるプログラムか記述しません。実際のアプリケーションのプログラミングをするプロジェクトを作成しましょう。
Windowsフォームプロジェクトの作成
今回はわかりやすくWindowsフォームで解説します。それではWindowsフォームプロジェクトを作成しましょう。
名前は「TDD.Winform」とします。
フォームが作成されますが今は使いません。
Winformプロジェクトを右クリックして「追加」から「新しいフォルダー」を選びます。
作成されたフォルダーの名前を「Objects」とします。
この作成されたObjectsフォルダーに「追加」から「クラス」を選んで
「Calculation」というクラスを作成します。
作成されたクラスに次のようなADDメソッドを追記します。
1 2 3 4 5 6 7 8 9 10 |
namespace TDD.Winform.Objects { public class Calculation { public int Add(int a,int b) { return a + b; } } } |
Addメソッドは引数のaとbの足し算をしてその値を返却します。今からテストコードでこのコードが正しく動作しているかをテストしていきます。
参照の追加
Testsプロジェクトの参照を右クリックして「参照の追加」を選択します。
TDD.Winformにチェックをいれて「OK」を押下。
これでTestsプロジェクトからWinformプロジェクトが参照できるようになりました。
テストコードの記述
それではテストコードを書いていきましょう。
とりあえずデフォルトで作成されているテストファイル「UnitTest1.cs」に書きましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
using System; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace TDDTest.Tests { [TestClass] public class UnitTest1 { [TestMethod] public void TestMethod1() { <strong> var calculation = new TDD.Winform.Objects.Calculation(); int result = calculation.Add(3, 5); Assert.AreEqual(8, result);</strong> } } } |
これでCalculationクラスのAddメソッドに対してテストコードが書けました。
「3プラス5は8になっているはず!」というテストコードになっています。
次の3行のうち,1行目と2行目は普通のクラスの使い方と同じです。
1 2 3 |
var calculation = new TDD.Winform.Objects.Calculation(); int result = calculation.Add(3, 5); Assert.AreEqual(8, result); |
テストコード特有のものは3行目の「Assert.AreEqual(8, result);」の部分です。
「Assert.AreEqual」は第1引数と第2引数がイコールかどうかをチェックし,テストエクスプローラーに成功か失敗かを示してくれます。
それではテストを実行してみましょう。
テストエクスプローラーの表示
ツールメニューの「テスト」から「ウィンドウ」を選択し「テストエクスプローラー」を表示します。
表示出来たら「すべて実行」をクリックします。
緑色のレ点マークが出れば成功です。これでAddメソッドは正しく動作していることが保証されました。念のためNGになるかも見ておきましょう。
あえてテストを失敗させる
テストコードが誤っていればコードが正しくてもNGになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
using System; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace TDDTest.Tests { [TestClass] public class UnitTest1 { [TestMethod] public void TestMethod1() { var calculation = new TDD.Winform.Objects.Calculation(); int result = calculation.Add(3, 5); <strong>Assert.AreEqual(7, result);</strong> } } } |
例えば「Assert.AreEqual(7, result);」など誤ったテストコードを書いている場合はNGになります。もう一度「すべて実行」をクリックします。
赤い×マークになり失敗となっていますね。
下の方には失敗した原因も記されています。
今回はテストコードをあえて間違えて書きましたが,通常はAddメソッド側に不具合がありNGになるケースと,今回のようにテストコードを書き間違えているケースの2種類あります。
また,TestMethod1()の上に[TestMethod]という記述がありますが,これは非常に大事です。これを書き忘れるとテストされません。試しに一度削除してテストを実行してみましょう。
テストエクスプローラーに何も出てこなくなりました。
今回はテストが1件しかないのでおかしいことに気づけますが,数百件などテスト結果が出ているときは気づかない恐れがあります。
========================================
今回は「C#でテスト駆動開発をする方法」の
第2回
「C#で単体テストをする時の観点と
テスト駆動開発の5つの手順を徹底解説」
をお届けします。
さて前回は2つの値の足し算をするという
単純なAddメソッドを作成してテストコードを実装しました。
その時の最後に[TestMethod]を書き忘れないようにする対策や,
データベースを絡めたテストの方法などがあるといいましたが,
今回はそのあたりのテストの書き方を解説します。
前回までのおさらい
前回までのおさらいをしておきましょう。
まずテストされる側のコードとしてCalculationクラスに
Addメソッドが存在します。
【Calculation.cs】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace TDD.Winform.Objects { public class Calculation { public int Add(int a,int b) { return a + b; } } } |
テストコードではそのAddメソッドを呼び出して結果をテストしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
using System; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace TDDTest.Tests { [TestClass] public class UnitTest1 { [TestMethod] public void TestMethod1() { var calculation = new TDD.Winform.Objects.Calculation(); int result = calculation.Add(3, 5); Assert.AreEqual(7, result); } } } |
前回のテストコードでは,
わざとテストが失敗するように3+5の結果を7と記載しましたので,
元に戻しておきましょう。
Assert.AreEqual(7, result);
の「7」の部分を「8」にしておいてください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
using System; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace TDDTest.Tests { [TestClass] public class UnitTest1 { [TestMethod] public void TestMethod1() { var calculation = new TDD.Winform.Objects.Calculation(); int result = calculation.Add(3, 5); <strong> Assert.AreEqual(8, result);</strong> } } } |
念のためテストを実行しておきましょう。
実行はテストエクスプローラーの「すべて実行」を
クリックするのでしたね。
緑のバーが出ればOKです。
俗にこれをグリーンバーと呼んでいます。(そのままですが)
テスト駆動開発の2つの間違い
[TestMethod]の書き忘れ対策にもなるのですが,テストコードには推奨されている書き方があります。
それがテスト駆動開発といわれるものです。
一言でいうと「テストコードを書いてから実装を書く」という事になります。
テスト駆動開発の勘違い1
「テストコードを先に書く」というとよく質問される内容で,
「1つの機能(画面など)の処理に対して,
すべてのテストコードを書いてから実装を書くのですか?」
といわれることがあります。 これは全く違います。
テストコードと実装のサイクルもっと短いサイクルで回します。
やり方はこの後ゆっくり解説するので安心してくださいね。
テスト駆動開発の勘違い2
2つ目に多い質問でこれもよく聞かれますが,
「単体テスト仕様書(ドキュメント)を作成していないので
テスト駆動開発できません」
というものです。
テスト駆動開発と単体テスト仕様書は全く関係ありません。
テストコードを書く際に単体テスト仕様書が出来上がっている必要はありません。
テスト駆動開発の5つの手順
それではテスト駆動開発の手順を解説します。
- 手順1 テストコードを書く
- 手順2 コンパイルエラーを取り除く
- 手順3 テストを失敗させる(レッドーバー)
- 手順4 実装をする(レッドバーを取り除く)
- 手順5 テストを成功させる(グリーンバー)
以上の5つのサイクルをぐるぐると回します。
何となくわかりますか?
実際にやって意味ないと分かりづらいと思うので実際にやってみましょう。
テスト駆動開発での書き方実演
それでは実際にやってみましょう。
手順1 テストコードを書く
なんのテストコードを書きましょうか?
実際にはプログラムの仕様を考えながらテストコードを書いていくことになります。
例えば2つの数値の掛け算をして値を返す
「Multiplication」という関数を作るとしましょう。
テストはどんな感じになるでしょうか?
例えばこんな感じになります。
1 2 3 4 5 6 7 |
[TestMethod] public void 掛け算() { var calculation = new Calculation(); Assert.AreEqual(10, calculation.Multiplication(2, 5)); } ※コードの一番上に「using TDD.Winform.Objects;」と記載してください。 |
まず初めに「public void 掛け算()」の部分ですが,
テストメソッドの名前を日本語にしています。
初めて見る人は違和感があるかもしれませんが,
この部分を日本語にしている人は結構います。
この部分にテストで何をしているかを書いておきます。
テストメソッドは外部から呼び出されることも,
引数を持つこともないため,
分かりやすいアルファベットの名前を持つ必要もなく,
メソッドの最初のXMLコメントを書く必要もありません。
なのでメソッド名を日本語にするのが
私はわかりやすくてベストだと思っています。
実際には「掛け算_OK」でOKパターンのテストにしたリ,
「掛け算_NG」でエラーパターンのテスト名にしたりと工夫できます。
続いて「var calculation = new Calculation();」
の部分は先ほどと同様です。
「TDD.Winform.Objects」の部分が長いのでusingに移しました。
次の「Assert.AreEqual(10, calculation.Multiplication(2, 5));」が
今回のメインの部分となります。
今回の2つの値の掛け算をする処理を作ることでしたから,
「Multiplication」という名前のメソッドを
Calculationクラスに追加することとします。
2つの値の掛け算ですから,intの引数を2つ受け取ることになるので,
「2」と「5」を引数で投げています。
そうすると正しく動作していれば「10」が返却されることになるはずなので
AreEqualの左側は「10」としています。
これでテストを実行しましょう。
「え?」
「コンパイルエラーが出てるって?」
そうですよね。でもOKです。
これこそがテスト駆動開発です。
手順2 コンパイルエラーを取り除く
手順の2ではコンパイルエラーを取り除くことです。
どうやって取り除くかというと,
CalculationクラスにMultiplicationメソッドを作ればいいのですが,
Visual Studioではテスト駆動開発をサポートする機能がついているので,
使ってみましょう。
まずコンパイルエラーが出ている箇所(Multiplicationの部分)にカーソルを当てます。
その状態で「Control」キーを押しながら「.」ピリオドキーを押してみてください。
「メソッドCalculation.Multiplicationを生成します」と表示されます。
その状態でEnterキーを押しましょう。
すると
コンパイルエラーが消えましたね。
消えたという事は...
「Multiplication」の部分で「F12」を押下してCalculationクラスを見てみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace TDD.Winform.Objects { public class Calculation { public int Add(int a,int b) { return a + b; } public int Multiplication(int v1, int v2) { throw new NotImplementedException(); } } } |
「あら不思議!」というか「あら親切に!」
Multiplicationメソッドができています。
ちゃんとintの引数が2つと戻り値がintになっています。
テスト駆動開発での良い点としてはこの実装部分の自動生成機能があります。
いままで(テスト駆動をする前)は実装部分で
Multiplicationメソッドという名前を付けて実装していましたが,
テスト駆動ではテストコードでMultiplicationという名前を付けて,
コンパイルエラーを取り除くためにコードを自動生成し,
実装部分を作成します。
最初はテストコードの記載中にインテリセンスが表示されないため,
違和感があるかもしれませんが,
テストコードを書きながら命名していっていると思うと
非常に便利な機能であることが実感できると思います。
それではまたテストコードに戻りましょう。
1 2 3 4 5 6 |
[TestMethod] public void 掛け算() { var calculation = new Calculation(); Assert.AreEqual(10, calculation.Multiplication(2, 5)); } |
いまはコンパイルエラーがなくなった状態です。
それでは晴れてテストを実行しましょう。
「え?!」
「テストが失敗しているって!」
OKです。それがテスト駆動開発です。
それこそが手順3「テストコードを失敗させる」です。
手順3「テストコードを失敗させる」
なぜ失敗させるかって? テスト駆動開発の考え方は,
「テストコードをパスさせるために実装を書く」という発想です。
だから無駄な実装コードは生まれません。
ようするに,テストが失敗するから,
それをパスさせるためだけに実装コードを書くというのが
一番シンプルで無駄のない実装であるという考え方なのです。
それにもし,テストメソッドに不備があればこの段階で気づくことができます。
例えば最初から言っていた[TestMethod]の書き忘れがもしあった場合は
この段階で気が付きます。
今はMultiplicationメソッドを自動生成しただけで,
その中身は未実装を示す「 NotImplementedException」の
例外が生成されています。
普通に呼び出せば間違いなくテストは失敗するはずです。
それなのにテスト一覧がすべてOKでグリーンバーになったら,
「おや?」 「今作ったテストが動作していないぞ!?」
って気が付くことができます。
この手順がなかったら,
テストが成功していると勘違いして,
実装を繰り返し,バグがあっても検出できないことになります。
だから「まずレッドバーを出す」
そしてレッドバーをグリーンバーに変えるためだけに実装をしていきます。
手順4 実装をする(レッドバーを取り除く)
それでは手順の4です。ここでようやく実装をします。
いまはレッドバーが出ているので,
Multiplicationメソッドを正しく書かないとグリーンバーは手に入りません。
1 2 3 4 |
public int Multiplication(int v1, int v2) { throw new NotImplementedException(); } |
とりあえず未実装を示す例外
「throw new NotImplementedException();」を削除します。
これがあってはいつまでもグリーンバーは手に入りません。
ちなみにこの例外は実装するまでは消さないほうがいいです。
「後で実装しよう」などとしている場合はこの例外のおかげでエラーになり,
実装漏れをする心配なありません。
さて,それでは実装をしましょう。
1 2 3 4 |
public int Multiplication(int a, int b) { return a * b; } |
まず引数の「v1」「v2」はaとbに変更しました。
実際はプロジェクトの命名規約に従って命名しましょう。
自動生成の変数名がそのまま使えることはまずないです。
実装部分の「return a * b;」はaとbの掛け算をして返却しています。
この状態でテストを実行しましょう。 テストが成功しました。
これで 「手順5 テストを成功させる(グリーンバー)」も完了したことになります。
おさらい
もう一度テスト駆動の手順を見てみましょう。
- テストコードを書く
- コンパイルエラーを取り除く
- テストを失敗させる(レッドーバー)
- 実装をする(レッドバーを取り除く)
- テストを成功させる(グリーンバー)
今見れば,内容はすんなり入ってくるのではないでしょうか?
まずテストコードをコンパイルエラーを出しながら,
メソッド名や引数を考えてインテリセンスを使わずに書いていきます。
その後コンパイルエラーを取り除くために
Visual Studioのサポート機能でメソッドを自動生成します。
メソッド以外にもプロパティなども自動生成できます。
メソッド名やプロパティ名はテストをしながら命名していきます。
そしてレッドバーを確認し,グリーンバーにする。
といった手順を,細かい単位で機能を満たすまでやり続けます。
だから最初の勘違いポイントとして,
全部テストコードを書いてから実装するっていうのは全然そんなことなく,
テストを書いてすぐに実装してっていう短いサイクルの繰り返しだし,
単体テスト仕様書がないからテスト駆動できないっていうのも
全然違う話になります。
実装したい機能がわかっていれば,
それを実現したらこんな結果が返ってくるはずって思いながら,
テストケースを考えて,
それをパスするために実装するってことになります。
このやり方を続けると,すべてのコードがテストコードでカバーされたすばらしいプログラムになります。しかしながら,今回は独立したメソッドに対してのテストコードを書いたので,比較的簡単でしたが。実際にはもっと複雑で,テストコードを書けない場所とか,書きづらい場所とかいろいろあったりして,テストコードを書く技術が必要になります。詳しい実装方法はUdemyのC#でテスト駆動開発をする方法を参考にしてください。
=======================================
「ファイル」とか
「データベース」に絡む場所って
「どうやってテストコードを書けばいいのー?」
今回は「C#でテスト駆動開発をする方法」の
第3回
「C#でファイルやデータベース部分を
テストコードでユニットテストする方法」
をお届けします。
「外部に接触するロジックを含む実装」を
どうやってテストコードでカバーするかを解説しています。
前回はテスト駆動開発の5つの手順を中心に解説しました。
何となくテスト駆動開発がわかってきましたか?
最初は何となくテストを書きながら実装するのは
めんどくさい気がするかもしれませんが,
慣れてくると,テストを書きながらやらないと
うまく実装できないというか,テストを先に書くことで,
いつもシンプルでわかりやすい実装ができていると個人的には感じています。
「テストしやすいコード」と「テストしにくいコード」とは?
C#での実装コードには,テストしやすいコードと,
テストしにくいコードというものがあります。
テストしやすいコードとは,メソッドが完結しているものです。
例えば前回の例で出てきた,「Add」メソッドなどは,
intの引数を2つ取得して,2つの値の合計を返却するというものでしたが,
こういった1つのメソッド内ですべてが完結しているメソッドは
テストがしやすいコードといえます。
単純にintの引数を2つ投げて,戻り値をチェックすれば,
メソッドが正しく動作していることがテストコードで確認できます。
逆に,テストコードが書きにくい実装というものがあります。
例えばファイルを開いたり,データベースに接続している部分などです。
なぜテストコードを書きにくいかというと,例えばコード内で
「CドライブのworkフォルダーのTest.txtファイルの内容を読み込んで
改行コードの有無をチェックする」という仕様があった場合のテストを考えてみましょう。
新しいテストクラスの作成
実際に書いたほうがわかりやすいと思うので,書いていきましょう。
新しテストクラスを作成しましょう。
TDDTest.Testsプロジェクトを右クリックして,「追加」→「新しい項目」を選択します。
新しテストクラスが生成されます。
※Visual Studio2015などではファイル名の入力などが必要かもしれません。
その場合はファイル名を「UnitTest2」として作成してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
using System; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace TDDTest.Tests { [TestClass] public class UnitTest2 { [TestMethod] public void TestMethod1() { } } } |
それでは新しくできたテストクラスに
先ほどの例の通りにテストコードを書いていきましょう。
先ほどの例は「CドライブのworkフォルダーのTest.txtファイルの
内容を読み込んで改行コードの有無をチェックする」というものでした。
Test.txtファイルにアクセスするクラスとして
TestFileクラスがあったとして,
NewLineExistsメソッドでファイル内の文字列に,
改行コードが存在するかをboolで返すメソッドがあるとします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
using System; using Microsoft.VisualStudio.TestTools.UnitTesting; using TDD.Winform.Objects; namespace TDDTest.Tests { [TestClass] public class UnitTest2 { [TestMethod] public void 改行コードの有無チェックができる() { var textFile = new TextFile(); Assert.AreEqual(true, textFile.NewLineExists()); } } } |
TestFileクラスはまだ存在しないため,
WinformプロジェクトのObjectsフォルダーにTestFileクラスを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
using System.IO; using System.IO; namespace TDD.Winform.Objects { public class TextFile { public bool NewLineExists() { var fileString = File.ReadAllText(@"C:\work\Test.txt"); return fileString.Contains(System.Environment.NewLine); } } } |
TestFileクラスのアクセス修飾子をpublicに変更し,
NewLineExistsメソッドを作成します。
さてこの状況で先ほどのテストを実行したらどうなるでしょうか?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
using System; using Microsoft.VisualStudio.TestTools.UnitTesting; using TDD.Winform.Objects; namespace TDDTest.Tests { [TestClass] public class UnitTest2 { [TestMethod] public void ファイルを読み込む() { var testFile = new TestFile(); var fileString = testFile.GetData(); Assert.AreEqual("AAA", fileString); } } } |
もちろんテストはNGとなります。
原因はCドライブにworkフォルダーも
Test.txtファイルも存在しないからです。
では実際にその場所にファイルを置いて,
中身を「AAA」の文字プラス改行コードを入れて
保存したらテストはパスするでしょうか?
実際にやってみましょう。 どうでしたか?テストは成功しますね。
これですべてはめでたしめでたし…でしょうか?
残念ながらこのテストには限界があります。
まず,今やったようにCドライブのworkフォルダーに
Test.txtファイルを設置し,
中身に改行コードをいれなければなりません。
あなたのPCではテストに成功しても
他の人のPCで実行するとNGになる可能性があります。
それになりより,テストのバリエーションが書けません。
改行コードありのテストをすると,改行コード無しのテストはできません。
同一フォルダーに同じファイル名のファイルは複数設置できませんから,
当然そうなります。 次のようなテストコードの共存は不可能になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
using System; using Microsoft.VisualStudio.TestTools.UnitTesting; using TDD.Winform.Objects; namespace TDDTest.Tests { [TestClass] public class UnitTest2 { [TestMethod] public void 改行コードの有無チェックができる() { var textFile = new TextFile(); Assert.AreEqual(true, textFile.NewLineExists()); } [TestMethod] public void 改行コードの有無チェックができるFalse() { var textFile = new TextFile(); Assert.AreEqual(false, textFile.NewLineExists()); } } } |
「TestFileクラスで固定でパスを指定せずに,引数でパスをとるようにして,テストコードからは,いろいろなパスで指定できるようにすればいいのでは?」
と思われるかもしれません。
確かにそれだとテストは可能であり,
テスト用のファイルもVisual Studioに登録して,
自動で吐き出すことでテストすることはでいます。
ファイルの例以外にもデータベースに接続する場合も
同様の問題が起こります。
データベースとの接続部分が含まれる処理を通過させるテストは,
実際に接続先を一時的に変更したり,
テーブルの中身をテストの前後で作り変えるなどの工夫が必要です。
Mock(モック)を使う事で
ファイルやデータベースとの接続箇所のテストを実施する
先述した通り,
ファイルやデータベースとの接触が含まれる実装コードを通そうとすると,
通常はエラーになり,テストを成功させることができません。
その場合は接続先をテスト用に変更したり,
その都度データを書き換えることで対応できなくはないのですが,
テスト用のデータベースを使う場合は,
サーバー上にSQLServerを置いたり,
テーブル追加に追従したり,複数人で開発するには,
いろいろとメンテナンスが大変です。
私はそういった外部との接触部分が絡むテストをするときは
Mockを使う事で解決しています。
Mockとはテスト用にダミーの処理をさせて,
後続処理のテストを実施する手法の事を言います。
似た言葉に「スタブ」というものがありますが,
スタブは未実装部分をコンパイルエラーが出ないために
実装している程度の意味で使用しており,
モックは狙ったテストを成功させるために,
意図的なふるまいをさせるダミークラス的な意味で使用しています。
例えば先ほどの例であれば次のように実装します。
WinformプロジェクトのObjectsフォルダに次のインターフェースを追加します。
1 2 3 4 5 6 7 |
namespace TDD.Winform.Objects { public interface ITextFile { string GetData(); } } |
次に同じくObjectsフォルダーに新しくクラス「TextFileAccess」を作成し,
次のように記載します。
1 2 3 4 5 6 7 8 9 10 11 12 |
using System.IO; namespace TDD.Winform.Objects { public class TextFileAccess : ITextFile { public string GetData() { return File.ReadAllText(@"C:\work\Test.txt"); } } } |
次にFileTextクラスを次のように変更します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
using System.IO; namespace TDD.Winform.Objects { public class TextFile { <strong>private ITextFile _textFile;</strong> <strong>public TextFile(ITextFile textFile) { _textFile = textFile; }</strong> public bool NewLineExists() { var fileString = <strong>_textFile.GetData();</strong> return fileString.Contains(System.Environment.NewLine); } } } |
変更1 ITextFile変数の追加
まず「private ITextFile _textFile;」というメンバー変数を宣言します。
これは先ほど作成したITextFileを設定する変数です。
まだここではインスタンスを生成しません。
変更2 コンストラクタの追加
コンストラクタを追加して,引数でITextFileを受け取ります。
受け取ったITextFileはそのままtextFile変数に設定します。
これで外部から自由にITextFileを設定することが可能となりました。
変更3 ファイルアクセスはインターフェース経由で行う
「var fileString = File.ReadAllText(@”C:\work\Test.txt”);」としていた部分を
「var fileString = _textFile.GetData();」に変更しました。
直接ファイルにアクセスしていた部分
「File.ReadAllText(@”C:\work\Test.txt”);」をなくして
_textFile.GetData()に変更しました。
使い方
実際の実装はTextFile クラスのコンストラクタに
TextFileAccessを設定することで,
GetDataメソッドが呼び出されたときに,
先ほどと同様に「File.ReadAllText(@”C:\work\Test.txt”)」が
呼び出されることになります。
テストコードから呼び出すときは,
後述するMockクラスを設定することで,
任意のテストデータがGetDataメソッドから返却されるように
実装をしていきます。
テストコードの実装
それではテストコードに戻りましょう。
今,テストコードを表示するとTextFileクラスの
インスタンスを生成する箇所「var textFile = new TextFile();」の
部分でコンパイルエラーが出ていますね。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
using System; using Microsoft.VisualStudio.TestTools.UnitTesting; using TDD.Winform.Objects; namespace TDDTest.Tests { [TestClass] public class UnitTest2 { [TestMethod] public void 改行コードの有無チェックができる() { var textFile = new TextFile(); Assert.AreEqual(true, textFile.NewLineExists()); } [TestMethod] public void 改行コードの有無チェックができるFalse() { var textFile = new TextFile(); Assert.AreEqual(false, textFile.NewLineExists()); } } } |
これは引数にITextFileを指定するように
コンストラクタを作成したためです。
ここにITextFileを設定する必要があるのですが,
ここに何を入れましょうか?
ここにMock(モック)を設定します。
ITextFileインターフェースは
stringを返却するGetDataメソッドが1つ定義されています。
実際にはここには「File.ReadAllText(@”C:\work\Test.txt”);」の
戻り値を返す想定です。
ただテストコードを記載するときは実際の
ファイルがなくても動作するようにしたいので,
テストコードから呼び出すときは,
ファイルにアクセスしないように実装します。
Mockの作成
それではMockを作成しましょう。
TDDTest.Testsプロジェクトに普通のクラスを追加し,
名前をTextFileMockとしましょう。
作成されたクラスを次のように変更します。
1 2 3 4 5 6 7 8 9 10 11 12 |
using TDD.Winform.Objects; namespace TDDTest.Tests { <strong>internal</strong> class TextFileMock : <strong>ITextFile</strong> { public string GetData() { throw new NotImplementedException(); } } } |
まずMockクラスのアクセス修飾子はinternalとします。
これはTDDTest.Testsプロジェクトからしか参照しないテスト用のクラスのため,
publicではなくinternalが適切です。
ちなみにC#のクラスにアクセス修飾子を記載しなければ,
デフォルトでinternalになります。
ですが,可読性と意図していることを示すために
明示的にinternalと記載しましょう。
次にITextFileを実装し,
ITextFileで定義しているGetDataメソッドを記載します。
「throw new NotImplementedException();」は未実装の例外です。
コードを自動生成すると記載されます。
それではテストコードの改行コードありのテストを
通すために次のようにテストコードを記載しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 |
using TDD.Winform.Objects; namespace TDDTest.Tests { internal class TextFileMock : ITextFile { public string GetData() { return "AAA" + System.Environment.NewLine; } } } |
GetDataメソッドは文字列の「AAA」プラス改行コードを返却するようにしました。「System.Environment.NewLine」はC#で改行コードを表します。
それでは再びテストコードに戻りましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
using System; using Microsoft.VisualStudio.TestTools.UnitTesting; using TDD.Winform.Objects; namespace TDDTest.Tests { [TestClass] public class UnitTest2 { [TestMethod] public void 改行コードの有無チェックができる() { var textFile = new TextFile(); Assert.AreEqual(true, textFile.NewLineExists()); } [TestMethod] public void 改行コードの有無チェックができるFalse() { var textFile = new TextFile(); Assert.AreEqual(false, textFile.NewLineExists()); } } } |
引き続きTextFileをnewしているところでコンパイルエラーになっています。
なのでその部分を今作ったMockクラスを設定してコンパイルエラーをなくしましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
using System; using Microsoft.VisualStudio.TestTools.UnitTesting; using TDD.Winform.Objects; namespace TDDTest.Tests { [TestClass] public class UnitTest2 { [TestMethod] public void 改行コードの有無チェックができる() { var textFile = new TextFile(new TextFileMock()); Assert.AreEqual(true, textFile.NewLineExists()); } [TestMethod] public void 改行コードの有無チェックができるFalse() { var textFile = new TextFile(new TextFileMock()); Assert.AreEqual(false, textFile.NewLineExists()); } } } |
これでコンパイルエラーはなくなりました。
晴れてテストを実行できるようになりました。
テストを実行してみましょう。
「改行コードの有無チェックができる」のテストは成功していますが,
「改行コードの有無チェックができるFalse」のテストは失敗しています。
これはTextFileMockクラスのGetDataメソッドが必ず
「AAA」+改行コードを返しているためです。
「改行コードの有無チェックができるFalse」のテスト用に
別のMockクラスを作成し,
そのGetDataメソッドで改行コードを含まない文字列を返却すればOKなのですが,
それだとテストケースの数だけMockクラスが必要になります。
それだと手間がかかるので,
TextFileMockクラスの返却できる値を動的に変更できるようにすることで対応できます。
TextFileMockクラスを次のように変更します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
using TDD.Winform.Objects; namespace TDDTest.Tests { internal class TextFileMock : ITextFile { internal string Value { get; set; } public string GetData() { return Value; } } } |
internalのValueプロパティを作成し,
GetDataメソッドではそのValueをそのまま返却しています。
そのため,TextFileMockクラスValueを操作することでMockが
返却する値を操作することができます。
テストコードを次のように変更します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
using System; using Microsoft.VisualStudio.TestTools.UnitTesting; using TDD.Winform.Objects; namespace TDDTest.Tests { [TestClass] public class UnitTest2 { [TestMethod] public void 改行コードの有無チェックができる() { <strong>var textFileMock = new TextFileMock(); textFileMock.Value = "AAA" + System.Environment.NewLine;</strong> var textFile = new TextFile(<strong>textFileMock</strong>); Assert.AreEqual(true, textFile.NewLineExists()); } [TestMethod] public void 改行コードの有無チェックができるFalse() { <strong> var textFileMock = new TextFileMock(); textFileMock.Value = "BBB";</strong> var textFile = new TextFile(<strong>textFileMock</strong>); Assert.AreEqual(false, textFile.NewLineExists()); } } } |
TextFileMockクラスを生成した後に任意の値をValueにセットしています。
これで,TextFileクラスがNewLineExistsメソッドを
読んだ時のGetDataメソッドが返す値が任意に変更できるため,
正常系とエラー系など,自由に値を設定することができます。
この状態でテストを実行してみましょう。
これですべてのテストが成功しました。
実際の実装部分
これでテストコードは成功したので,
TextFileクラスのNewLineExistsメソッドが正しく
動作していることが確認できました。
実際の実装はTextFileクラスをnewする時に
TextFileAccessを生成してコンストラクタの引数に渡すことで,
実際のCドライブのパスにアクセスされて動作することになります。
まとめ
少し長くなってしまいましたが,
要するにファイルなど外部に接触する場所は,
テストコードでのテストがやり辛いため,
インターフェースを作成し,実際の実装と,
テストコードで使用するMockとで切り換えれるようにしておく。
Mockが返却する値を外部から設定できるようにすることで,
テストケースにあった値をMockが返却できるようにする。
というのがポイントとなります。
80分の無料動画コースをプレゼント
最後までお読みいただきありがとうございました。
今回はテスト駆動開発のお話をしましたが、
C#でよりよいコードを書いていくには、
それ以外にも、全体の構成を考えるための、
「C#のアーキテクチャー」の知識も必要となります。
下記より、「C#のアーキテクチャー:あなたのコードが複雑な理由」という、
80分の無料動画コースを公開していますので、
よかったら受け取ってください。
【無料】C#のアーキテクチャー:あなたのコードが複雑な理由