繰り返し #5 – 単体テストを作成する (VB)
提供元: Microsoft
5 番目のイテレーションでは、単体テストを追加することで、アプリケーションを簡単に維持し、変更できるようにします。 データ モデル クラスをモックし、コントローラーと検証ロジックの単体テストをビルドします。
連絡先管理の ASP.NET MVC アプリケーションをビルドする (VB)
この一連のチュートリアルでは、連絡先管理アプリケーション全体を最初から最後までビルドします。 連絡先マネージャー アプリケーションを使用すると、人物の一覧に連絡先情報 (名前、電話番号、電子メール アドレス) を保存できます。
複数のイテレーションを通してアプリケーションをビルドします。 イテレーションのたびに、アプリケーションを徐々に改善します。 複数のイテレーションからなるこのアプローチの目的は、お客様が各変更の理由を理解できるようにすることです。
イテレーション #1 - アプリケーションを作成します。 最初のイテレーションでは、可能な限り簡単な方法で連絡先マネージャーを作成します。 基本的なデータベース操作 (生成、読み取り、更新、削除 (CRUD)) のサポートを追加します。
イテレーション #2 - アプリケーションの外観を良くします。 このイテレーションでは、既定の ASP.NET MVC ビュー マスター ページとカスケード スタイル シートを変更することで、アプリケーションの外観を向上させます。
イテレーション #3 - フォームの検証を追加します。 3 番目のイテレーションでは、基本的なフォームの検証を追加します。 必須のフォーム フィールドを入力しないとユーザーがフォームを送信できないようにします。 また、メール アドレスと電話番号も検証します。
イテレーション #4 - アプリケーションを疎結合します。 この 4 番目のイテレーションでは、いくつかのソフトウェア デザイン パターンを利用して、連絡先マネージャー アプリケーションを簡単に維持し、変更できるようにします。 たとえば、Repository パターンと Dependency Injection パターンを使用するようにアプリケーションをリファクターします。
イテレーション #5 - 単体テストを作成します。 5 番目のイテレーションでは、単体テストを追加することで、アプリケーションを簡単に維持し、変更できるようにします。 データ モデル クラスをモックし、コントローラーと検証ロジックの単体テストをビルドします。
イテレーション #6 - テスト駆動開発を使用します。 この 6 番目のイテレーションでは、最初に単体テストを記述し、この単体テストに対してコードを記述することにより、新しい機能をアプリケーションに追加します。 このイテレーションでは、連絡先グループを追加します。
イテレーション #7 - Ajax 機能を追加します。 7 番目のイテレーションでは、Ajax のサポートを追加することで、アプリケーションの応答性とパフォーマンスを向上させます。
このイテレーション
以前の連絡先マネージャー アプリケーションのイテレーションでは、アプリケーションをより疎結合するようにリファクタリングしました。 アプリケーションをコントローラー、サービス、リポジトリの各レイヤーに分離しました。 各レイヤーはそれぞれインターフェイスを介して、その下のレイヤーと対話します。
アプリケーションをリファクタリングして、アプリケーションの保守と変更を容易にしました。 たとえば、新しいデータ アクセス テクノロジを使用する必要がある場合は、コントローラー レイヤーまたはサービス レイヤーに触れることなく、単にリポジトリ レイヤーを変更できます。 連絡先マネージャーを疎結合にすることで、アプリケーションの変更に対する回復性が向上しました。
しかし、連絡先マネージャー アプリケーションに新しい機能を追加する必要がある場合はどうなりますか? または、バグを修正するとどうなりますか? 悲しいことですが、コードを書くということは、コードを触るたびに新しいバグを引き起こすリスクがあるということです。
たとえば、ある晴れた日に、上司から連絡先マネージャーに新しい機能を追加するように依頼される場合があるとします。 彼女は、連絡先グループのサポートを追加することを望んでいます。 そして、ユーザーが連絡先を「友達」「ビジネス」などのグループに整理できるようにすることを望んでいます。
この新しい機能を実装するには、連絡先マネージャー アプリケーションの 3 つのレイヤーすべてを変更する必要があります。 コントローラー、サービス レイヤー、リポジトリに新しい機能を追加する必要があります。 コードの変更を開始するとすぐに、これまで動作していた機能が壊れるリスクがあります。
前のイテレーションで行ったように、アプリケーションを別々のレイヤーにリファクタリングすることをお勧めします。 こうすることで、アプリケーションの他の部分に触れることなく、レイヤー全体に変更を加えることができるからです。 ただし、レイヤー内のコードの保守および変更を容易にしたい場合は、コードの単体テストを作成する必要があります。
単体テストは、コードの個々の単位をテストするために使用します。 これらのコード単位は、アプリケーション レイヤー全体よりも小さくなります。 通常、単体テストはコード内の特定のメソッドが期待どおりの動作をするかどうかを検証するために使用します。 たとえば、ContactManagerService クラスによって公開される CreateContact() メソッドの単体テストを作成します。
アプリケーションの単体テストは、セーフティ ネットのように機能します。 アプリケーションのコードを修正するたびに、その変更が既存の機能を壊していないかチェックするために、一連の単体テストを実行できます。 単体テストを使用すると、コードを安全に変更できます。 単体テストにより、アプリケーション内のすべてのコードの変更に対する回復性が向上します。
このイテレーションでは、連絡先マネージャー アプリケーションに単体テストを追加します。 こうすることで、次のイテレーションでは、既存の機能を壊す心配をすることなく、アプリケーションに連絡先グループを追加できます。
Note
NUnit、xUnit.net、MbUnit など、さまざまな単体テスト フレームワークがあります。 このチュートリアルでは、Visual Studio に含まれている単体テスト フレームワークを使用します。 ただし、これらの代替フレームワークの 1 つを簡単に使用することもできます。
テスト対象
完璧な世界では、すべてのコードが単体テストでカバーされます。 完璧な世界では、完璧なセーフティネットがあるはずです。 単体テストを実行することで、アプリケーション内のどのコード行を変更しても、その変更によって既存の機能が壊れたかどうかをすぐに把握できます。
しかし、私たちは完璧な世界に住んでいません。 実際には、単体テストを記述するときは、ビジネス ロジック (検証ロジックなど) のテストの記述に専念します。 特に、データ アクセス ロジックやビュー ロジックの単体テストを記述することはしません。
単体テストを有用なものにするためには、単体テストは非常に迅速に実行される必要があります。 数百 (または数千も) のアプリケーションの単体テストを簡単に蓄積できます。 単体テストの実行に時間がかかる場合は、単体テストの実行を回避できます。 言い換えると、長時間実行される単体テストは、日々のコーディングの目的には役に立ちません。
このため、通常、データベースと対話するコードには単体テストを記述しません。 運用中のデータベースに対して数百の単体テストを実行すると、時間がかかりすぎます。 代わりに、データベースをモックし、モック データベースと対話するコードを記述します (データベースのモックについては以下で説明します)。
同様の理由から、通常、ビューには単体テストを記述しません。 ビューをテストするには、Web サーバーを立ち上げる必要があります。 Web サーバーの立ち上げは比較的時間のかかるプロセスであるため、ビューの単体テストを作成することはお勧めしません。
ビューに複雑なロジックが含まれている場合は、ロジックをヘルパー メソッドに移動することを検討する必要があります。 ヘルパー メソッドの単体テストを記述することで、Web サーバーを立ち上げることなく実行できます。
Note
データ アクセス ロジックまたはビュー ロジックのテストを記述することは、単体テストを記述する場合には良い考えではありませんが、機能テストや統合テストを作成するときには、これらのテストはとても有用です。
Note
ASP.NET MVC は、Web Forms ビュー エンジンです。 Web Forms ビュー エンジンは Web サーバーに依存していますが、他のビュー エンジンは依存しない可能性があります。
モック オブジェクト フレームワークの使用
単体テストをビルドするときは、ほとんどの場合、モック オブジェクト フレームワークを使用する必要があります。 モック オブジェクト フレームワークを使用すると、アプリケーション内のクラスのモックとスタブを作成できます。
たとえば、モック オブジェクト フレームワークを使用して、リポジトリ クラスのモック バージョンを生成できます。 そうすることで、単体テストで実際のリポジトリ クラスの代わりにモック リポジトリ クラスを使用できます。 モック リポジトリを使用することで、単体テストの実行時にデータベース コードの実行を回避できます。
Visual Studio にはモック オブジェクト フレームワークは含まれません。 ただし、.NET フレームワークで使用可能な商用およびオープン ソースのモック オブジェクト フレームワークがいくつかあります。
- Moq - このフレームワークは、オープン ソースの BSD ライセンスで使用可能です。 https://code.google.com/p/moq/ から Moq をダウンロードできます。
- Rhino Mocks - このフレームワークは、オープンソースのBSDライセンスで使用可能です。 http://ayende.com/projects/rhino-mocks.aspx から Rhino Mocks をダウンロードできます。
- Typemock Isolator - これは商用フレームワークです。 http://www.typemock.com/ から試用版をダウンロードできます。
このチュートリアルでは、Moq を使用することにしました。 しかし、RhinoMocks や Typemock Isolator を使用して、連絡先マネージャー アプリケーションのモック オブジェクトを簡単に作成することもできます。
Moq を使用する前に、次の手順を完了する必要があります。
- .
- ダウンロードを解凍する前に、ファイルを右クリックし、[ブロック解除] というラベルのボタンをクリックしてください (図 1 を参照)。
- ダウンロードを解凍します。
- メニュー オプションの [プロジェクト]、[参照の追加] の順に選択して、[参照の追加] ダイアログを開いて、Moq アセンブリへの参照をテスト プロジェクトに追加します。 [参照] タブで、Moq を解凍したフォルダーを参照し、Moq.dll アセンブリを選択します。 [OK] ボタンをクリックします (図 2 を参照)。
図 01: Moq のブロック解除 (クリックすると、フルサイズの画像が表示されます)
図 02: Moq を追加した後の参照 (クリックすると、フルサイズの画像が表示されます)
サービス レイヤーの単体テストの作成
まず、連絡先マネージャー アプリケーションのサービス レイヤーに対して一連の単体テストを作成します。 これらのテストを使用して検証ロジックを検証します。
ContactManager.Tests プロジェクトに Models という名前の新しいフォルダーを作成します。 次に、Models フォルダーを右クリックし、[追加]、[新しいテスト] の順に選択します。 図 3 に示す [新しいテストの追加] ダイアログが表示されます。 [単体テスト] テンプレートを選択し、新しいテストに ContactManagerServiceTest.vb と名前を付けます。 [OK] ボタンをクリックして、新しいテストをテスト プロジェクトに追加します。
Note
一般に、テスト プロジェクトのフォルダー構造は、ASP.NET MVC プロジェクトのフォルダー構造と一致させることが望ましいです。 たとえば、コントローラー テストを Controllers フォルダーに配置したり、モデル テストを Models フォルダーに配置したりといった具合です。
図 03: Models\ContactManagerServiceTest.cs(クリックすると、フルサイズの画像が表示されます)
最初に、ContactManagerService クラスによって公開される CreateContact() メソッドをテストします。 次の 5 つのテストを作成します。
- CreateContact() - 有効な Contact がメソッドに渡されたときに、CreateContact() が値 true を返すことをテストします。
- CreateContactRequiredFirstName() - 名前のない連絡先が CreateContact() メソッドに渡されたときに、エラー メッセージがモデルの状態に追加されることをテストします。
- CreateContactRequiredLastName() - 姓のない連絡先が CreateContact() メソッドに渡されたときに、エラー メッセージがモデルの状態に追加されることをテストします。
- CreateContactInvalidPhone() - 無効な電話番号を持つ連絡先が CreateContact() メソッドに渡されたときに、エラー メッセージがモデルの状態に追加されることをテストします。
- CreateContactInvalidEmail() - 無効なメール アドレスを持つ連絡先が CreateContact() メソッドに渡されたときに、エラー メッセージがモデルの状態に追加されることをテストします。
最初のテストでは、有効な連絡先が検証エラーを生成しないことを確認します。 残りのテストでは、各検証規則を確認します。
これらのテストのコードは、リスト 1 に含まれています。
リスト 1 - Models\ContactManagerServiceTest.vb
Imports Microsoft.VisualStudio.TestTools.UnitTesting
Imports Moq
Imports System.Web.Mvc
<TestClass()> _
Public Class ContactManagerServiceTest
Private _mockRepository As Mock(Of IContactManagerRepository)
Private _modelState As ModelStateDictionary
Private _service As IContactManagerService
<TestInitialize()> _
Public Sub Initialize()
_mockRepository = New Mock(Of IContactManagerRepository)()
_modelState = New ModelStateDictionary()
_service = New ContactManagerService(new ModelStateWrapper(_modelState), _mockRepository.Object)
End Sub
<TestMethod()> _
Public Sub CreateContact()
' Arrange
Dim contactToCreate = Contact.CreateContact(-1, "Stephen", "Walther", "555-5555", "steve@somewhere.com")
' Act
Dim result = _service.CreateContact(contactToCreate)
' Assert
Assert.IsTrue(result)
End Sub
<TestMethod()> _
Public Sub CreateContactRequiredFirstName()
' Arrange
Dim contactToCreate = Contact.CreateContact(-1, String.Empty, "Walther", "555-5555", "steve@somewhere.com")
' Act
Dim result = _service.CreateContact(contactToCreate)
' Assert
Assert.IsFalse(result)
Dim [error] = _modelState("FirstName").Errors(0)
Assert.AreEqual("First name is required.", [error].ErrorMessage)
End Sub
<TestMethod()> _
Public Sub CreateContactRequiredLastName()
' Arrange
Dim contactToCreate = Contact.CreateContact(-1, "Stephen", String.Empty, "555-5555", "steve@somewhere.com")
' Act
Dim result = _service.CreateContact(contactToCreate)
' Assert
Assert.IsFalse(result)
Dim [error] = _modelState("LastName").Errors(0)
Assert.AreEqual("Last name is required.", [error].ErrorMessage)
End Sub
<TestMethod()> _
Public Sub CreateContactInvalidPhone()
' Arrange
Dim contactToCreate = Contact.CreateContact(-1, "Stephen", "Walther", "apple", "steve@somewhere.com")
' Act
Dim result = _service.CreateContact(contactToCreate)
' Assert
Assert.IsFalse(result)
Dim [error] = _modelState("Phone").Errors(0)
Assert.AreEqual("Invalid phone number.", [error].ErrorMessage)
End Sub
<TestMethod()> _
Public Sub CreateContactInvalidEmail()
' Arrange
Dim contactToCreate = Contact.CreateContact(-1, "Stephen", "Walther", "555-5555", "apple")
' Act
Dim result = _service.CreateContact(contactToCreate)
' Assert
Assert.IsFalse(result)
Dim [error] = _modelState("Email").Errors(0)
Assert.AreEqual("Invalid email address.", [error].ErrorMessage)
End Sub
End Class
リスト 1 の Contact クラスを使用するため、Microsoft Entity Framework への参照をテスト プロジェクトに追加する必要があります。 System.Data.Entity アセンブリへの参照を追加します。
リスト 1 には、[TestInitialize] 属性で修飾された Initialize() という名前のメソッドが含まれています。 このメソッドは、各単体テストが実行される前に自動的に呼び出されます (各単体テストの直前に 5 回呼び出されます)。 Initialize() メソッドは、次のコード行を使用してモック リポジトリを作成します。
_mockRepository = New Mock(Of IContactManagerRepository)()
このコード行では、Moq フレームワークを使用して、IContactManagerRepository インターフェイスからモック リポジトリを生成します。 実際の EntityContactManagerRepository の代わりにモック リポジトリを使用することで、各単体テスト実行時にデータベースへのアクセスを回避します。 モック リポジトリは IContactManagerRepository インターフェイスのメソッドを実装しますが、メソッドは実際には何もしません。
Note
Moq フレームワークを使用する場合、_mockRepository と _mockRepository.Object は区別されます。 前者は、モック リポジトリの動作方法を指定するためのメソッドを含む Mock (Of IContactManagerRepository) クラスを参照します。 後者は、IContactManagerRepository インターフェイスを実装する実際のモック リポジトリを参照します。
モック リポジトリは、ContactManagerService クラスのインスタンスを作成するときに Initialize() メソッドで使用されます。 個々の単体テストはすべて、ContactManagerService クラスのこのインスタンスを使用します。
リスト 1 には、各単体テストに対応する 5 つのメソッドが含まれています。 これらの各メソッドは、[TestMethod] 属性で修飾されます。 単体テストを実行すると、この属性を持つすべてのメソッドが呼び出されます。 つまり、[TestMethod] 属性で修飾されたメソッドは単体テストとなります。
CreateContact() という名前の最初の単体テストでは、Contact クラスの有効なインスタンスがメソッドに渡されたときに、CreateContact() を呼び出すと値 true が返されることを検証します。 このテストでは Contact クラスのインスタンスを作成し、CreateContact() メソッドを呼び出して、CreateContact() が値 true を返すことを検証します。
残りのテストでは、CreateContact() メソッドが無効な連絡先で呼び出されたときにメソッドが false を返し、期待される検証エラー メッセージがモデルの状態に追加されることを検証します。 たとえば、CreateContactRequiredFirstName() テストでは、FirstName プロパティの空の文字列を使用して Contact クラスのインスタンスを作成します。 次に、無効な連絡先で CreateContact() メソッドが呼び出されます。 最後に、このテストでは、CreateContact() が false を返し、そのモデルの状態に期待される検証エラー メッセージ「名前が必要です」が含まれていることを検証します。
リスト 1 で単体テストを実行するには、メニュー オプション [テスト]、[実行]、[ソリューション内のすべてのテスト] (CTRL + R、A) を選択します。 テストの結果が [テスト結果] ウィンドウに表示されます (図 4 を参照)。
図 04: テスト結果 (クリックするとフルサイズの画像が表示されます)
コントローラーの単体テストの作成
ASP.NET MVC アプリケーションは、ユーザー操作のフローを制御します。 コントローラーをテストするときは、コントローラーが適切なアクションの結果とビュー データを返すかどうかをテストします。 また、コントローラーが期待どおりにモデル クラスと対話するかどうかをテストすることもできます。
たとえば、リスト 2 には連絡先コントローラーの Create() メソッドの単体テストが 2 つ含まれています。 最初の単体テストでは、有効な連絡先が Create() メソッドに渡されると、Create() メソッドが Index アクションにリダイレクトされることを検証します。 つまり、有効な連絡先を渡すと、Create() メソッドは Index アクションを表す RedirectToRouteResult を返す必要があります。
コントローラー レイヤーをテストするときに ContactManager サービス レイヤーをテストする必要はありません。 したがって、Initialize メソッドで次のコードを使用してサービス レイヤーをモックします。
_service = New Mock(Of IContactManagerService)()
CreateValidContact() 単体テストでは、次のコード行を使用してサービス レイヤー CreateContact() メソッドを呼び出す動作をモックします。
_service.Expect( Function(s) s.CreateContact(contactToCreate) ).Returns(True)
このコード行により、その CreateContact() メソッドが呼び出されたときに、ContactManager サービスのモックは値 true を返します。 サービス レイヤーをモックすることで、サービス レイヤーでコードを実行しなくても、コントローラーの動作をテストできます。
2 番目の単体テストでは、無効な連絡先がメソッドに渡されたときに、Create() アクションによって Create ビューが返されることを検証します。 サービス レイヤーの CreateContact() メソッドが値 false を返すように、次のコードを記述します。
_service.Expect( Function(s) s.CreateContact(contactToCreate) ).Returns(False)
Create() メソッドが期待どおりに動作する場合は、サービス レイヤーが値 false を返したときに Create ビューを返します。 こうすることで、コントローラーは検証エラー メッセージを [作成] ビューに表示でき、ユーザーはその無効な Contact プロパティを修正できます。
コントローラーの単体テストをビルドする場合は、コントローラー アクションから明示的なビュー名を返す必要があります。 たとえば、次のようなビューを返さないようにします。
Return View()
代わりに、次のようなビューを返します。
Return View("Create")
ビューを返すときに明示的でない場合、ViewResult.ViewName プロパティは空の文字列を返します。
リスト 2 - Controllers\ContactControllerTest.vb
Imports Microsoft.VisualStudio.TestTools.UnitTesting
Imports Moq
Imports System.Web.Mvc
<TestClass()> _
Public Class ContactControllerTest
Private _service As Mock(Of IContactManagerService)
<TestInitialize()> _
Public Sub Initialize()
_service = New Mock(Of IContactManagerService)()
End Sub
<TestMethod()> _
Public Sub CreateValidContact()
' Arrange
Dim contactToCreate = New Contact()
_service.Expect(Function(s) s.CreateContact(contactToCreate)).Returns(True)
Dim controller = New ContactController(_service.Object)
' Act
Dim result = CType(controller.Create(contactToCreate), RedirectToRouteResult)
' Assert
Assert.AreEqual("Index", result.RouteValues("action"))
End Sub
<TestMethod()> _
Public Sub CreateInvalidContact()
' Arrange
Dim contactToCreate = New Contact()
_service.Expect(Function(s) s.CreateContact(contactToCreate)).Returns(False)
Dim controller = New ContactController(_service.Object)
' Act
Dim result = CType(controller.Create(contactToCreate), ViewResult)
' Assert
Assert.AreEqual("Create", result.ViewName)
End Sub
End Class
まとめ
このイテレーションでは、Contact Manager アプリケーションの単体テストを作成しました。 これらの単体テストはいつでも実行して、アプリケーションが期待どおりに動作することを検証できます。 単体テストは、アプリケーションのセーフティ ネットとして機能し、将来的にアプリケーションを安全に変更できるようにします。
単体テストのセットを 2 つ作成しました。 まず、サービス レイヤーの単体テストを作成して、検証ロジックをテストしました。 次に、コントローラー レイヤーの単体テストを作成して、フロー制御ロジックをテストしました。 サービス レイヤーをテストするときに、リポジトリ レイヤーをモックして、サービス レイヤーのテストをリポジトリ レイヤーから分離しました。 コントローラー レイヤーをテストするときに、サービス レイヤーをモックしてコントローラー レイヤーのテストを分離しました。
次のイテレーションでは、連絡先グループをサポートするように連絡先マネージャー アプリケーションを変更します。 テスト駆動開発と呼ばれるソフトウェア設計プロセスを使用して、この新しい機能をアプリケーションに追加します。