繰り返し #6 – テスト駆動型開発を使用する (C#)
提供元: Microsoft
この 6 回目のイテレーションでは、最初に単体テストを記述し、単体テストに対してコードを記述することで、新しい機能をアプリケーションに追加します。 このイテレーションでは、連絡先グループを追加します。
連絡先管理の ASP.NET MVC アプリケーションをビルドする (C#)
この一連のチュートリアルでは、連絡先管理アプリケーション全体を最初から最後までビルドします。 連絡先マネージャー アプリケーションを使用すると、人物の一覧に連絡先情報 (名前、電話番号、電子メール アドレス) を保存できます。
複数のイテレーションを通してアプリケーションをビルドします。 イテレーションのたびに、アプリケーションを徐々に改善します。 複数のイテレーションからなるこのアプローチの目的は、お客様が各変更の理由を理解できるようにすることです。
イテレーション #1 - アプリケーションを作成します。 最初のイテレーションでは、可能な限り簡単な方法で連絡先マネージャーを作成します。 基本的なデータベース操作 (生成、読み取り、更新、削除 (CRUD)) のサポートを追加します。
イテレーション #2 - アプリケーションの外観を良くします。 このイテレーションでは、既定の ASP.NET MVC ビュー マスター ページとカスケード スタイル シートを変更することで、アプリケーションの外観を向上させます。
イテレーション #3 - フォームの検証を追加します。 3 番目のイテレーションでは、基本的なフォームの検証を追加します。 必須のフォーム フィールドを入力しないとユーザーがフォームを送信できないようにします。 また、メール アドレスと電話番号も検証します。
イテレーション #4 - アプリケーションを疎結合します。 この 4 番目のイテレーションでは、いくつかのソフトウェア デザイン パターンを利用して、連絡先マネージャー アプリケーションを簡単に維持し、変更できるようにします。 たとえば、Repository パターンと Dependency Injection パターンを使用するようにアプリケーションをリファクターします。
イテレーション #5 - 単体テストを作成します。 5 番目のイテレーションでは、単体テストを追加することで、アプリケーションを簡単に維持し、変更できるようにします。 データ モデル クラスをモックし、コントローラーと検証ロジックの単体テストをビルドします。
イテレーション #6 - テスト駆動開発を使用します。 この 6 番目のイテレーションでは、最初に単体テストを記述し、この単体テストに対してコードを記述することにより、新しい機能をアプリケーションに追加します。 このイテレーションでは、連絡先グループを追加します。
イテレーション #7 - Ajax 機能を追加します。 7 番目のイテレーションでは、Ajax のサポートを追加することで、アプリケーションの応答性とパフォーマンスを向上させます。
このイテレーション
連絡先マネージャー アプリケーションの前のイテレーションでは、コードのセーフティ ネットを提供する単体テストを作成しました。 単体テストを作成する動機は、コードの変更に対する回復性を高めるものでした。 単体テストを実施することで、コードをいつでも変更し、既存の機能が壊れているかどうかをすぐに確認できます。
このイテレーションでは、まったく異なる目的で単体テストを使用します。 このイテレーションでは、「テスト駆動開発」と呼ばれるアプリケーション設計の理念の一部として単体テストを使用します。 テスト駆動型開発を実践するときは、まずテストを記述してから、テストに対してコードを記述します。
より正確には、テスト駆動型の開発を実践する場合、コードの作成時に実行する 3 つの手順 (赤/緑/リファクタリング) があります。
- 失敗する単体テストを記述する (赤)
- 単体テストに合格するコードを記述する (緑)
- コードをリファクタリングする (リファクタリング)
まず、単体テストを記述します。 単体テストは、想定するコードの動作についての意図を表す必要があります。 単体テストを最初に作成すると、単体テストは失敗するはずです。 テストを満たすアプリケーション コードをまだ記述していないため、テストは失敗します。
次に、単体テストに合格するために最小限必要なコードを記述します。 その目標は、最も手を抜いて、最も大雑把に、最も速い方法でコードを記述することです。 アプリケーションのアーキテクチャについて考えて時間を無駄にしないでください。 その代わりに、単体テストで表される意図を満たすために必要な最小限のコードを記述することに重点を置いてください。
最後に、必要最小限のコードを記述した後に、一歩戻ってアプリケーションの全体的なアーキテクチャを検討できます。 この手順では、ソフトウェア設計パターン (リポジトリ パターンなど) を利用してコードを書き換え (リファクタリング) し、コードの保守性を高めます。 コードは単体テストでカバーされているため、この手順でコードを恐れなく書き直すことができます。
テスト駆動型開発を実践することによって生じる多くの利点があります。 まず、テスト駆動型開発では、実際に記述されるべきコードに重点を置くように仕向けられます。 特定のテストに合格するのに最小限必要なコードを記述することに常に重点を置いているため、方向性に迷って決して使用しない大量のコードを書くのを防ぐことができます。
2 つ目は、"テスト優先" 設計手法によって、コードの使用方法の観点からコードを記述するように仕向けられることです。 つまり、テスト駆動型の開発を実践するときは、ユーザーの観点からテストを常に記述しています。 そのため、テスト駆動型の開発では、よりクリーンでわかりやすい API が得られます。
最後に、テスト駆動型開発では、アプリケーションを記述する通常のプロセスの一部として単体テストを強制的に記述します。 プロジェクトの期限が近づくにつれて、通常はテストはまず忘れられてしまうものです。 一方、テスト駆動型開発を実践する場合は、テスト駆動型開発によって単体テストがアプリケーションの構築プロセスの中心になるため、単体テストの記述について忠実である可能性が高くなります。
Note
テスト駆動型開発の詳細については、Michael Feathers の書籍「レガシ コードの効果的な使用」を参照することをお勧めします。
このイテレーションでは、連絡先マネージャー アプリケーションに新しい機能を追加します。 連絡先グループのサポートが追加されました。 連絡先グループを使用して、連絡先をビジネス グループやフレンド グループなどのカテゴリに整理できます。
テスト駆動型開発のプロセスに従って、この新しい機能をアプリケーションに追加します。 最初に単体テストを記述し、これらのテストに対してすべてのコードを記述します。
テスト対象
前のイテレーションで説明したように、通常、データ アクセス ロジックやビュー ロジックの単体テストは記述しません。 データベースへのアクセスは比較的低速な操作であるため、データ アクセス ロジックの単体テストは記述しません。 ビューにアクセスするには、比較的低速な操作である Web サーバーを起動する必要があるため、ビュー ロジックの単体テストは記述しません。 何度も繰り返し非常に高速に実行できないような単体テストは記述しないでください
テスト駆動型の開発は単体テストによって推進されるため、最初はコントローラーとビジネス ロジックの記述に重点を置きます。 データベースまたはビューに触れることは避けます。 このチュートリアルの最後まで、データベースを変更したり、ビューを作成したりすることはありません。 まず、テストできる内容から始めます。
ユーザー ストーリーの作成
テスト駆動型の開発を実践するときは、常にテストを記述することから始めます。 これはすぐに疑問を提起します。最初にどのテストを書くかを決める方法は何ですか? この質問に答えるためには、一連のユーザー ストーリーを記述する必要があります。
ユーザー ストーリーは、ソフトウェア要件の非常に簡単な (通常は 1 文からなる) 説明です。 これは、ユーザーの観点から記述された、技術的でない要件の説明である必要があります。
新しい連絡先グループ機能に必要な機能を説明するユーザー ストーリーのセットを次に示します。
- ユーザーは連絡先グループの一覧を表示できます。
- ユーザーは新しい連絡先グループを作成できます。
- ユーザーは既存の連絡先グループを削除できます。
- ユーザーは、新しい連絡先を作成するときに連絡先グループを選択できます。
- ユーザーは、既存の連絡先を編集するときに連絡先グループを選択できます。
- 連絡先グループの一覧がインデックス ビューに表示されます。
- ユーザーが連絡先グループをクリックすると、一致する連絡先の一覧が表示されます。
このユーザー ストーリーの一覧は、顧客が完全に理解できるものであることに注意してください。 技術的な実装の詳細については言及されません。
アプリケーションを構築する過程で、この一連のユーザー ストーリーはより洗練される可能性があります。 1 つのユーザー ストーリーを複数のストーリー (要件) に分割する場合があります。 たとえば、新しい連絡先グループを作成するには検証が必要であると判断する場合があります。 名前のない連絡先グループを送信すると、検証エラーが返されるべきです。
ユーザー ストーリーの一覧を作成したら、最初の単体テストを記述する準備が整いました。 まず、連絡先グループの一覧を表示するための単体テストを作成します。
連絡先グループの一覧表示
最初のユーザー ストーリーは、ユーザーが連絡先グループの一覧を表示できる必要があるということです。 このストーリーをテストで表現する必要があります。
ContactManager.Tests プロジェクトの Controllers フォルダーを右クリックし、[追加、新しいテスト] を選択し、[単体テスト] テンプレートを選択して、新しい単体テストを作成します (図 1 を参照)。 新しい単体テストに GroupControllerTest.cs という名前を付け、[OK] ボタンをクリックします。
図 01: GroupControllerTest 単体テストの追加 (クリックするとフルサイズの画像が表示されます)
最初の単体テストは、リスト 1 に含まれています。 このテストでは、グループ コントローラーの Index() メソッドが一連のグループを返していることを確認します。 テストでは、グループのコレクションがビュー データで返されることを確認します。
リスト 1 - Controllers\GroupControllerTest.cs
using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Web.Mvc;
using ContactManager.Models;
namespace ContactManager.Tests.Controllers
{
[TestClass]
public class GroupControllerTest
{
[TestMethod]
public void Index()
{
// Arrange
var controller = new GroupController();
// Act
var result = (ViewResult)controller.Index();
// Assert
Assert.IsInstanceOfType(result.ViewData.Model, typeof(IEnumerable));
}
}
}
Visual Studio でリスト 1 のコードを最初に入力すると、赤い波線が多数表示されます。 GroupController クラスまたは Group クラスを作成していません。
この時点では、アプリケーションをビルドすることもできないため、最初の単体テストを実行できません。 それは良いことです。 これは失敗したテストとしてカウントされます。 そのため、アプリケーション コードの記述を開始することが許されます。 テストを実行するのに最小限必要なコードを記述する必要があります。
リスト 2 のグループ コントローラー クラスには、単体テストに合格するために必要な最小限のコードが含まれています。 Index() アクションは、静的にコード化されたグループのリストを返します (Group クラスはリスト 3 で定義されています)。
リスト 2 - Controllers\GroupController.cs
using System.Collections.Generic;
using System.Web.Mvc;
using ContactManager.Models;
namespace ContactManager.Controllers
{
public class GroupController : Controller
{
public ActionResult Index()
{
var groups = new List();
return View(groups);
}
}
}
リスト 3 - Models\Group.cs
namespace ContactManager.Models
{
public class Group
{
}
}
GroupController クラスと Group クラスをプロジェクトに追加すると、最初の単体テストが正常に完了します (図 2 を参照)。 テストに合格するために必要な最小限の作業を行いました。 お疲れさまです。
図 02: Success! (クリックするとフルサイズの画像が表示されます)
連絡先グループの作成
これで、2 番目のユーザー ストーリーに進むことができます。 新しい連絡先グループを作成できる必要があります。 この意図をテストで表現する必要があります。
リスト 4 のテストでは、新しいグループで Create() メソッドを呼び出すと、Index() メソッドによって返されるグループの一覧にグループが追加されることを確認します。 言い換えると、新しいグループを作成すると、Index() メソッドによって返されるグループの一覧から新しいグループを取得できるようになります。
リスト 4 - Controllers\GroupControllerTest.cs
[TestMethod]
public void Create()
{
// Arrange
var controller = new GroupController();
// Act
var groupToCreate = new Group();
controller.Create(groupToCreate);
// Assert
var result = (ViewResult)controller.Index();
var groups = (IEnumerable<Group>)result.ViewData.Model;
CollectionAssert.Contains(groups.ToList(), groupToCreate);
}
リスト 4 のテストでは、新しい連絡先グループでグループ コントローラー Create() メソッドを呼び出します。 次に、このテストでは、グループ コントローラー Index() メソッドを呼び出すと、ビュー データ内の新しいグループが返されることを確認します。
リスト 5 の変更されたグループ コントローラーには、新しいテストに合格するために必要な最小限の変更が含まれています。
リスト 5 - Controllers\GroupController.cs
using System.Collections.Generic;
using System.Web.Mvc;
using ContactManager.Models;
using System.Collections;
namespace ContactManager.Controllers
{
public class GroupController : Controller
{
private IList<Group> _groups = new List<Group>();
public ActionResult Index()
{
return View(_groups);
}
public ActionResult Create(Group groupToCreate)
{
_groups.Add(groupToCreate);
return RedirectToAction("Index");
}
}
}
リスト 5 のグループ コントローラーに新しい Create() アクションがあります。 このアクションにより、グループがグループのコレクションに追加されます。 Index() アクションが変更され、グループのコレクションの内容が返されていることに注意してください。
ここでも、単体テストに合格するために必要な最小限の作業を実行しました。 これらの変更をグループ コントローラーに加えた後には、すべての単体テストに合格します。
検証の追加
この要件は、ユーザー ストーリーに明示的に記載されていませんでした。 ただし、グループに名前を付ける必要は妥当です。 そうしないと、連絡先をグループに整理することはあまり役に立ちません。
リスト 6 には、この意図を表す新しいテストが含まれています。 このテストでは、名前を指定せずにグループを作成しようとすると、モデルの状態で検証エラー メッセージが表示されることを確認します。
リスト 6 - Controllers\GroupControllerTest.cs
[TestMethod]
public void CreateRequiredName()
{
// Arrange
var controller = new GroupController();
// Act
var groupToCreate = new Group();
groupToCreate.Name = String.Empty;
var result = (ViewResult)controller.Create(groupToCreate);
// Assert
var error = result.ViewData.ModelState["Name"].Errors[0];
Assert.AreEqual("Name is required.", error.ErrorMessage);
}
このテストを満たすためには、Group クラスに Name プロパティを追加する必要があります (リスト 7 を参照)。 さらに、グループ コントローラーの Create() アクションに少しの検証ロジックを追加する必要があります (リスト 8 を参照)。
リスト 7 - Models\Group.cs
namespace ContactManager.Models
{
public class Group
{
public string Name { get; set; }
}
}
リスト 8 - Controllers\GroupController.cs
public ActionResult Create(Group groupToCreate)
{
// Validation logic
if (groupToCreate.Name.Trim().Length == 0)
{
ModelState.AddModelError("Name", "Name is required.");
return View("Create");
}
// Database logic
_groups.Add(groupToCreate);
return RedirectToAction("Index");
}
これで、グループ コントローラーの Create() アクションに検証ロジックとデータベース ロジックの両方が含まれていることに注意してください。 現在、グループ コントローラーによって使用されるデータベースは、メモリ内コレクションだけで構成されています。
リファクタリング
赤/緑/リファクタリングの 3 番目の手順は、リファクタリング部分です。 この時点で、コードから一歩下がって、アプリケーションをリファクタリングしてその設計を改善する方法を検討する必要があります。 リファクタリング ステージは、ソフトウェア設計の原則とパターンを実装する最善の方法について一生懸命考える段階です。
コードの設計を改善するために選択した任意の方法で、コードを自由に変更できます。 単体テストというセーフティ ネットがあるので、既存の機能を壊すのを防ぎます。
今のところ、私たちのグループ コントローラは良いソフトウェア設計の観点から見ると混乱した状態です。 このグループ コントローラーでは、検証のコードとデータ アクセスのコードがごちゃごちゃに混ざっています。 単一責任の原則に反しないようにするには、これらの懸念を異なるクラスに分ける必要があります。
リファクタリングされたグループ コントローラー クラスは、リスト 9 に含まれています。 ContactManager サービス レイヤーを使用するようにコントローラーが変更されました。 これは、Contact コントローラーで使用するのと同じサービス レイヤーです。
リスト 10 には、グループの検証、一覧表示、作成をサポートするために ContactManager サービス レイヤーに追加された新しいメソッドが含まれています。 新しいメソッドを含むように IContactManagerService インターフェイスが更新されました。
リスト 11 には、IContactManagerRepository インターフェイスを実装する新しい FakeContactManagerRepository クラスが含まれています。 IContactManagerRepository インターフェイスも実装する EntityContactManagerRepository クラスとは異なり、新しい FakeContactManagerRepository クラスはデータベースと通信しません。 FakeContactManagerRepository クラスは、データベースのプロキシとしてメモリ内コレクションを使用します。 単体テストでは、このクラスを偽のリポジトリ レイヤーとして使用します。
リスト 9 - Controllers\GroupController.cs
using System.Web.Mvc;
using ContactManager.Models;
namespace ContactManager.Controllers
{
public class GroupController : Controller
{
private IContactManagerService _service;
public GroupController()
{
_service = new ContactManagerService(new ModelStateWrapper(this.ModelState));
}
public GroupController(IContactManagerService service)
{
_service = service;
}
public ActionResult Index()
{
return View(_service.ListGroups());
}
public ActionResult Create(Group groupToCreate)
{
if (_service.CreateGroup(groupToCreate))
return RedirectToAction("Index");
return View("Create");
}
}
}
リスト 10 - Controllers\ContactManagerService.cs
public bool ValidateGroup(Group groupToValidate)
{
if (groupToValidate.Name.Trim().Length == 0)
_validationDictionary.AddError("Name", "Name is required.");
return _validationDictionary.IsValid;
}
public bool CreateGroup(Group groupToCreate)
{
// Validation logic
if (!ValidateGroup(groupToCreate))
return false;
// Database logic
try
{
_repository.CreateGroup(groupToCreate);
}
catch
{
return false;
}
return true;
}
public IEnumerable<Group> ListGroups()
{
return _repository.ListGroups();
}
リスト 11 - Controllers\FakeContactManagerRepository.cs
using System;
using System.Collections.Generic;
using ContactManager.Models;
namespace ContactManager.Tests.Models
{
public class FakeContactManagerRepository : IContactManagerRepository
{
private IList<Group> _groups = new List<Group>();
#region IContactManagerRepository Members
// Group methods
public Group CreateGroup(Group groupToCreate)
{
_groups.Add(groupToCreate);
return groupToCreate;
}
public IEnumerable<Group> ListGroups()
{
return _groups;
}
// Contact methods
public Contact CreateContact(Contact contactToCreate)
{
throw new NotImplementedException();
}
public void DeleteContact(Contact contactToDelete)
{
throw new NotImplementedException();
}
public Contact EditContact(Contact contactToEdit)
{
throw new NotImplementedException();
}
public Contact GetContact(int id)
{
throw new NotImplementedException();
}
public IEnumerable<Contact> ListContacts()
{
throw new NotImplementedException();
}
#endregion
}
}
IContactManagerRepository インターフェイスを変更するには、EntityContactManagerRepository クラスに CreateGroup() メソッドと ListGroups() メソッドを実装する必要があります。 最も簡単で最速の方法は、次のようなスタブ メソッドを追加する方法です。
public Group CreateGroup(Group groupToCreate)
{
throw new NotImplementedException();
}
public IEnumerable<Group> ListGroups()
{
throw new NotImplementedException();
}
最後に、アプリケーションの設計に対するこれらの変更を行うには、単体テストにいくつかの変更を加える必要があります。 単体テストを実行するときに FakeContactManagerRepository を使用する必要があります。 更新された GroupControllerTest クラスは、リスト 12 に含まれています。
リスト 12 - Controllers\GroupControllerTest.cs
using System.Collections.Generic;
using System.Web.Mvc;
using ContactManager.Controllers;
using ContactManager.Models;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections;
using System.Linq;
using System;
using ContactManager.Tests.Models;
namespace ContactManager.Tests.Controllers
{
[TestClass]
public class GroupControllerTest
{
private IContactManagerRepository _repository;
private ModelStateDictionary _modelState;
private IContactManagerService _service;
[TestInitialize]
public void Initialize()
{
_repository = new FakeContactManagerRepository();
_modelState = new ModelStateDictionary();
_service = new ContactManagerService(new ModelStateWrapper(_modelState), _repository);
}
[TestMethod]
public void Index()
{
// Arrange
var controller = new GroupController(_service);
// Act
var result = (ViewResult)controller.Index();
// Assert
Assert.IsInstanceOfType(result.ViewData.Model, typeof(IEnumerable));
}
[TestMethod]
public void Create()
{
// Arrange
var controller = new GroupController(_service);
// Act
var groupToCreate = new Group();
groupToCreate.Name = "Business";
controller.Create(groupToCreate);
// Assert
var result = (ViewResult)controller.Index();
var groups = (IEnumerable)result.ViewData.Model;
CollectionAssert.Contains(groups.ToList(), groupToCreate);
}
[TestMethod]
public void CreateRequiredName()
{
// Arrange
var controller = new GroupController(_service);
// Act
var groupToCreate = new Group();
groupToCreate.Name = String.Empty;
var result = (ViewResult)controller.Create(groupToCreate);
// Assert
var error = _modelState["Name"].Errors[0];
Assert.AreEqual("Name is required.", error.ErrorMessage);
}
}
}
これらの変更をすべて行った後には、再び、すべての単体テストに合格します。 赤/緑/リファクタリングのサイクル全体を完了しました。 最初の 2 つのユーザー ストーリーを実装しました。 これで、ユーザー ストーリーで表される要件の単体テストがサポートされるようになりました。 残りのユーザー ストーリーを実装するには、同じ赤/緑/リファクタリングのサイクルを繰り返す必要があります。
データベースの変更
残念ながら、単体テストで示されたすべての要件を満たしても、作業は完了していません。 データベースを変更する必要があります。
新しいグループ データベース テーブルを作成する必要があります。 次のステップを実行します。
- サーバー エクスプローラーのウィンドウで、[テーブル] フォルダーを右クリックし、メニュー オプション [新しいテーブルの追加] を選択します。
- テーブル デザイナーで以下に説明する 2 つの列を入力します。
- ID 列を主キーと ID 列としてマークします。
- フロッピーのアイコンをクリックして、グループという名前で新しいテーブルを保存します。
列名 | [データ型] | [NULL を許容] |
---|---|---|
Id | int | False |
名前 | Nvarchar (50) | False |
次に、連絡先テーブルからすべてのデータを削除する必要があります (そうしないと、連絡先テーブルとグループ テーブルの間にリレーションシップを作成できません)。 次のステップを実行します。
- 連絡先テーブルを右クリックし、[テーブル データの表示] メニュー オプションを選択します。
- すべての行を削除します。
次に、グループ データベース テーブルと既存の連絡先データベース テーブルの間にリレーションシップを定義する必要があります。 次のステップを実行します。
- サーバー エクスプローラーのウィンドウで連絡先テーブルをダブルクリックして、テーブル デザイナーを開きます。
- GroupId という名前の連絡先テーブルに新しい整数列を追加します。
- [リレーションシップ] ボタンをクリックして、[外部キー リレーションシップ] ダイアログを開きます (図 3 を参照)。
- [追加] をクリックします。
- [テーブルと列の指定] ボタンの横に表示される省略記号ボタンをクリックします。
- [テーブルと列] ダイアログで、主キー テーブルとして [グループ] を選択し、主キー列として [ID] を選択します。 外部キー テーブルとして [連絡先] を選択し、外部キー列として [GroupId] を選択します (図 4 を参照)。 [OK] をクリックします。
- [挿入と更新の指定] で、[ルールの削除] の [カスケード] の値を 選択します。
- [閉じる] ボタンをクリックして、[外部キー リレーションシップ] ダイアログを閉じます。
- [保存] ボタンをクリックして、連絡先テーブルへの変更を保存します。
図 03: データベース テーブル リレーションシップの作成 (クリックするとフルサイズの画像が表示されます)
図 04: テーブルのリレーションシップを指定する (クリックするとフルサイズの画像が表示されます)
データ モデルの更新
次に、新しいデータベース テーブルを表すためにデータ モデルを更新する必要があります。 次のステップを実行します。
- Models フォルダー内の ContactManagerModel.edmx ファイルをダブルクリックして、エンティティ デザイナーを開きます。
- デザイナー画面を右クリックし、メニュー オプション [データベースからモデルを更新] を選択します。
- 更新ウィザードで、グループ テーブルを選択し、[完了] ボタンをクリックします (図 5 を参照)。
- グループ エンティティを右クリックし、メニュー オプションの [名前の変更] を選択します。 Groups エンティティの名前を Group (s なし) に 変更します。
- 連絡先エンティティの下部に表示されるグループ ナビゲーション プロパティを右クリックします。 Groups ナビゲーション プロパティの名前を Group (s なし) に 変更します。
図 05: データベースからの Entity Framework モデルの更新 (クリックするとフルサイズの画像が表示されます)
これらの手順を完了すると、データ モデルは連絡先テーブルとグループ テーブルの両方を表します。 エンティティ デザイナーには、両方のエンティティが表示されます (図 6 を参照)。
図 06: グループと連絡先を表示するエンティティ デザイナー (クリックするとフルサイズの画像が表示されます)
リポジトリ クラスの作成
次に、リポジトリ クラスを実装する必要があります。 このイテレーションの過程では、単体テストを満たすコードを記述しながら、IContactManagerRepository インターフェイスにいくつかの新しいメソッドを追加しました。 IContactManagerRepository インターフェイスの最終バージョンは、リスト 14 に含まれています。
リスト 14 - Models\IContactManagerRepository.cs
using System.Collections.Generic;
namespace ContactManager.Models
{
public interface IContactManagerRepository
{
// Contact methods
Contact CreateContact(int groupId, Contact contactToCreate);
void DeleteContact(Contact contactToDelete);
Contact EditContact(int groupId, Contact contactToEdit);
Contact GetContact(int id);
// Group methods
Group CreateGroup(Group groupToCreate);
IEnumerable<Group> ListGroups();
Group GetGroup(int groupId);
Group GetFirstGroup();
void DeleteGroup(Group groupToDelete);
}
}
実際には、連絡先グループの操作に関連するメソッドを実装していません。 現在、EntityContactManagerRepository クラスには、IContactManagerRepository インターフェイスにリストされている各連絡先グループ メソッドのスタブ メソッドがあります。 たとえば、ListGroups() メソッドは現在次のようになっています。
public IEnumerable<Group> ListGroups()
{
throw new NotImplementedException();
}
スタブ メソッドを使用することで、アプリケーションをコンパイルし、単体テストに合格することができました。 そして、次に、これらのメソッドを実際に実装します。 EntityContactManagerRepository クラスの最終バージョンは、リスト 13 に含まれています。
リスト 13 - Models\EntityContactManagerRepository.cs
using System.Collections.Generic;
using System.Linq;
using System;
namespace ContactManager.Models
{
public class EntityContactManagerRepository : ContactManager.Models.IContactManagerRepository
{
private ContactManagerDBEntities _entities = new ContactManagerDBEntities();
// Contact methods
public Contact GetContact(int id)
{
return (from c in _entities.ContactSet.Include("Group")
where c.Id == id
select c).FirstOrDefault();
}
public Contact CreateContact(int groupId, Contact contactToCreate)
{
// Associate group with contact
contactToCreate.Group = GetGroup(groupId);
// Save new contact
_entities.AddToContactSet(contactToCreate);
_entities.SaveChanges();
return contactToCreate;
}
public Contact EditContact(int groupId, Contact contactToEdit)
{
// Get original contact
var originalContact = GetContact(contactToEdit.Id);
// Update with new group
originalContact.Group = GetGroup(groupId);
// Save changes
_entities.ApplyPropertyChanges(originalContact.EntityKey.EntitySetName, contactToEdit);
_entities.SaveChanges();
return contactToEdit;
}
public void DeleteContact(Contact contactToDelete)
{
var originalContact = GetContact(contactToDelete.Id);
_entities.DeleteObject(originalContact);
_entities.SaveChanges();
}
public Group CreateGroup(Group groupToCreate)
{
_entities.AddToGroupSet(groupToCreate);
_entities.SaveChanges();
return groupToCreate;
}
// Group Methods
public IEnumerable<Group> ListGroups()
{
return _entities.GroupSet.ToList();
}
public Group GetFirstGroup()
{
return _entities.GroupSet.Include("Contacts").FirstOrDefault();
}
public Group GetGroup(int id)
{
return (from g in _entities.GroupSet.Include("Contacts")
where g.Id == id
select g).FirstOrDefault();
}
public void DeleteGroup(Group groupToDelete)
{
var originalGroup = GetGroup(groupToDelete.Id);
_entities.DeleteObject(originalGroup);
_entities.SaveChanges();
}
}
}
ビューの作成
既定の ASP.NET ビュー エンジンを使用する場合の ASP.NET MVC アプリケーション。 そのため、特定の単体テストに応答してはビューを作成しません。 しかし、ビューがないとアプリケーションは役に立たないため、連絡先マネージャー アプリケーションに含まれるビューを作成および変更せずにこのイテレーションを完了することはできません。
連絡先グループを管理するには、次の新しいビューを作成する必要があります (図 7 を参照)。
- Views\Group\Index.aspx - 連絡先グループの一覧を表示します
- Views\Group\Delete.aspx - 連絡先グループを削除するための確認フォームを表示します
図 07: グループ インデックス ビュー (クリックするとフルサイズの画像が表示されます)
連絡先グループが含まれるように、次の既存のビューを変更する必要があります。
- Views\Home\Create.aspx
- Views\Home\Edit.aspx
- Views\Home\Index.aspx
このチュートリアルに付属する Visual Studio アプリケーションを見ると、変更されたビューを確認できます。 たとえば、図 8 は連絡先インデックス ビューを示しています。
図 08: 連絡先インデックス ビュー (クリックするとフルサイズの画像が表示されます)
まとめ
このイテレーションでは、テスト駆動型開発アプリケーションの設計手法に従って、連絡先マネージャー アプリケーションに新機能を追加しました。 一連のユーザー ストーリーを作成することから始めました。 ユーザー ストーリーによって表される要件に対応する一連の単体テストを作成しました。 最後に、単体テストで表される要件を満たすのに最小限必要なコードを記述しました。
単体テストで表される要件を満たすのに最小限必要なコードの記述が完了したら、データベースとビューを更新しました。 データベースに新しいグループ テーブルを追加し、Entity Framework データ モデルを更新しました。 また、ビューのセットを作成および変更しました。
次のイテレーション (最後のイテレーション) では、Ajax を利用するようにアプリケーションを書き直します。 Ajax を利用することで、連絡先マネージャー アプリケーションの応答性とパフォーマンスが向上します。