迭代 6 – 使用测试驱动开发 (VB)

Microsoft

下载代码

在第六次迭代中,我们通过先编写单元测试,然后针对单元测试编写代码,向应用程序添加新功能。 在此迭代中,我们将添加联系人组。

生成联系人管理 ASP.NET MVC 应用程序 (VB)

在本系列教程中,我们将从头到尾构建整个联系人管理应用程序。 通过 Contact Manager 应用程序,可以存储联系人列表的联系人信息(姓名、电话号码和电子邮件地址)。

我们通过多次迭代生成应用程序。 每次迭代后,我们都会逐步改进应用程序。 此多迭代方法的目标是使你能够了解每次更改的原因。

  • 迭代 #1 - 创建应用程序。 在第一次迭代中,我们将以最简单的方式创建联系人管理器。 添加了对基本数据库操作的支持:创建、读取、更新和删除 (CRUD) 。

  • 迭代 #2 - 使应用程序外观美观。 在此迭代中,我们通过修改默认 ASP.NET MVC 视图母版页和级联样式表来改进应用程序的外观。

  • 迭代 #3 - 添加表单验证。 在第三次迭代中,我们添加了基本表单验证。 我们会阻止用户在未填写必填表单字段的情况下提交表单。 我们还验证电子邮件地址和电话号码。

  • 迭代 #4 - 使应用程序松散耦合。 在第四次迭代中,我们将利用多种软件设计模式,以便更轻松地维护和修改 Contact Manager 应用程序。 例如,我们将应用程序重构为使用存储库模式和依赖关系注入模式。

  • 迭代 #5 - 创建单元测试。 在第五次迭代中,我们通过添加单元测试使应用程序更易于维护和修改。 我们将模拟数据模型类,并为控制器和验证逻辑生成单元测试。

  • 迭代 #6 - 使用测试驱动开发。 在第六次迭代中,我们通过先编写单元测试,然后针对单元测试编写代码,向应用程序添加新功能。 在此迭代中,我们将添加联系人组。

  • 迭代 #7 - 添加 Ajax 功能。 在第七次迭代中,我们通过添加对 Ajax 的支持来提高应用程序的响应能力和性能。

此迭代

在上一次对 Contact Manager 应用程序的迭代中,我们创建了单元测试来为我们的代码提供安全网。 创建单元测试的动机是提高代码对更改的复原能力。 通过单元测试,我们可以愉快地对代码进行任何更改,并立即知道我们是否破坏了现有功能。

在此迭代中,我们将单元测试用于完全不同的目的。 在此迭代中,我们使用单元测试作为称为 测试驱动开发的应用程序设计理念的一部分。 练习测试驱动开发时,先编写测试,然后针对测试编写代码。

更确切地说,在练习测试驱动开发时,在创建代码 (红/绿/重构) 时,需要完成三个步骤:

  1. 编写未通过红色) (单元测试
  2. 编写通过单元测试的代码 (Green)
  3. 重构代码 (重构)

首先,编写单元测试。 单元测试应表达你对代码行为的预期。 首次创建单元测试时,单元测试应失败。 测试应失败,因为你尚未编写任何满足测试的应用程序代码。

接下来,编写足够多的代码,以便单元测试通过。 目标是以最懒、最草率和最快的方式编写代码。 不应浪费时间考虑应用程序的体系结构。 相反,应专注于编写满足单元测试所表达意图所需的最少代码量。

最后,在编写足够的代码后,可以退后一步,考虑应用程序的整体体系结构。 在此步骤中,你将利用软件设计模式(如存储库模式)重写) (重构代码,使代码更易于维护。 可以在此步骤中无所畏惧地重写代码,因为单元测试涵盖了代码。

练习测试驱动开发有许多好处。 首先,测试驱动开发强制你专注于实际需要编写的代码。 由于你一直专注于编写足够多的代码来通过特定测试,因此你无法漫无他法地编写大量你永远不会使用的代码。

其次,“测试优先”设计方法强制你从代码的使用方式的角度编写代码。 换句话说,在练习测试驱动开发时,你会不断从用户的角度编写测试。 因此,测试驱动开发可以生成更简洁、更易于理解的 API。

最后,测试驱动开发会强制你编写单元测试,作为编写应用程序的正常过程的一部分。 随着项目截止日期的临近,测试通常是第一件事。 另一方面,在练习测试驱动开发时,你更有可能对编写单元测试持好态度,因为测试驱动开发使单元测试成为生成应用程序的过程的核心。

注意

若要了解有关测试驱动开发的详细信息,我建议你阅读 Michael Feathers 一书 ,有效地使用旧代码

在此迭代中,我们向 Contact Manager 应用程序添加了一项新功能。 添加了对联系人组的支持。 可以使用“联系人组”将联系人组织为“商务”和“好友”组等类别。

我们将遵循测试驱动开发过程,将此新功能添加到应用程序。 我们将首先编写单元测试,然后针对这些测试编写所有代码。

测试的内容

正如我们在上一次迭代中讨论的那样,通常不会为数据访问逻辑或视图逻辑编写单元测试。 不要为数据访问逻辑编写单元测试,因为访问数据库的操作相对较慢。 你不会为视图逻辑编写单元测试,因为访问视图需要启动 Web 服务器,这是一个相对缓慢的操作。 不应编写单元测试,除非测试可以非常快速地一遍又一遍地执行

由于测试驱动开发由单元测试驱动,因此我们最初侧重于编写控制器和业务逻辑。 我们避免接触数据库或视图。 在本教程结束之前,我们不会修改数据库或创建视图。 我们从可以测试的内容开始。

创建用户情景

练习测试驱动开发时,始终从编写测试开始。 这立即引发了一个问题:如何决定首先编写哪个测试? 若要回答此问题,应编写一组 用户情景

用户情景是一个非常简短 (通常是一个句子) 软件要求的说明。 它应该是从用户角度编写的要求的非技术说明。

下面是描述新联系人组功能所需功能的一组用户情景:

  1. 用户可以查看联系人组的列表。
  2. 用户可以创建新的联系人组。
  3. 用户可以删除现有联系人组。
  4. 用户可以在创建新联系人时选择联系人组。
  5. 编辑现有联系人时,用户可以选择联系人组。
  6. 联系人组的列表显示在“索引”视图中。
  7. 当用户单击联系人组时,将显示匹配联系人的列表。

请注意,客户完全可以理解此用户情景列表。 没有提及技术实现详细信息。

在生成应用程序的过程中,用户情景集可以变得更加完善。 可以将用户情景分解为多个情景, (要求) 。 例如,你可能会决定创建新的联系人组应涉及验证。 提交没有姓名的联系人组应返回验证错误。

创建用户情景列表后,即可编写第一个单元测试。 首先,我们将创建用于查看联系人组列表的单元测试。

列出联系人组

我们的第一个用户案例是,用户应该能够查看联系人组列表。 我们需要通过测试来表达这个故事。

通过右键单击 ContactManager.Tests 项目中的 Controllers 文件夹,选择 “添加”、“新建测试”,然后选择单元测试模板,创建新的 单元测试 (请参阅图 1) 。 将新的单元测试命名为 GroupControllerTest.vb,然后单击“ 确定” 按钮。

添加 GroupControllerTest 单元测试

图 01:添加 GroupControllerTest 单元测试 (单击以查看全尺寸图像)

我们的第一个单元测试包含在清单 1 中。 此测试验证组控制器的 Index () 方法是否返回一组组。 该测试验证在视图数据中是否返回了组集合。

列表 1 - Controllers\GroupControllerTest.vb

Imports Microsoft.VisualStudio.TestTools.UnitTesting
Imports System.Web.Mvc

<TestClass()> _
Public Class GroupControllerTest

    <TestMethod()> _
    Public Sub Index()
        ' Arrange
        Dim controller = New GroupController()

        ' Act
        Dim result = CType(controller.Index(), ViewResult)

        ' Assert
        Assert.IsInstanceOfType(result.ViewData.Model, GetType(IEnumerable(Of Group)))
    End Sub
End Class

当你第一次在 Visual Studio 的清单 1 中键入代码时,你会得到很多红色波浪线。 我们尚未创建 GroupController 或 Group 类。

此时,我们甚至无法生成应用程序,因此无法执行第一个单元测试。 这很好。 这算作失败的测试。 因此,我们现在有权开始编写应用程序代码。 我们需要编写足够的代码来执行测试。

清单 2 中的 Group 控制器类包含通过单元测试所需的最少代码。 Index () 操作返回组的静态编码列表, (清单 3) 中定义的 Group 类。

清单 2 - Controllers\GroupController.vb

Public Class GroupController
    Inherits System.Web.Mvc.Controller

    Function Index() As ActionResult
        Dim groups = new List(Of Group)
        Return View(groups)
    End Function

End Class

清单 3 - Models\Group.vb

Public Class Group

End Class

将 GroupController 和 Group 类添加到项目后,第一个单元测试成功完成 (请参阅图 2) 。 我们已完成通过测试所需的最低工作量。 是时候庆祝了。

成功!

图 02:成功! (单击以查看全尺寸图像)

创建联系人组

现在,我们可以转到第二个用户情景。 我们需要能够创建新的联系人组。 我们需要通过测试来表达这种意图。

清单 4 中的测试验证是否使用新 Group 调用 Create () 方法将组添加到 Index () 方法返回的组列表中。 换句话说,如果我创建新组,则应能够从 Index () 方法返回的组列表中获取新的组。

列表 4 - Controllers\GroupControllerTest.vb

<TestMethod> _
Public Sub Create()
    ' Arrange
    Dim controller = New GroupController()

    ' Act
    Dim groupToCreate = New Group()
    controller.Create(groupToCreate)

    ' Assert
    Dim result = CType(controller.Index(), ViewResult)
    Dim groups = CType(result.ViewData.Model, IEnumerable(Of Group))
    CollectionAssert.Contains(groups.ToList(), groupToCreate)
End Sub

清单 4 中的测试使用新的联系人组调用组控制器 Create () 方法。 接下来,测试验证调用组控制器 Index () 方法是否在视图中返回新的 Group。

清单 5 中修改后的组控制器包含通过新测试所需的最少更改。

清单 5 - Controllers\GroupController.vb

Public Class GroupController
Inherits Controller

Private _groups As IList(Of Group) = New List(Of Group)()

Public Function Index() As ActionResult
    Return View(_groups)
End Function

Public Function Create(ByVal groupToCreate As Group) As ActionResult
    _groups.Add(groupToCreate)
    Return RedirectToAction("Index")

End Function
End Class

清单 5 中的组控制器具有新的 Create () 操作。 此操作将组添加到组集合。 请注意,Index () 操作已修改为返回组集合的内容。

我们再次执行了通过单元测试所需的最低工作量。 对组控制器进行这些更改后,所有单元测试都会通过。

添加验证

此要求未在用户情景中明确说明。 但是,要求组具有名称是合理的。 否则,将联系人组织成组将不太有用。

清单 6 包含一个表示此意图的新测试。 此测试验证在未提供名称的情况下尝试创建组会导致模型状态出现验证错误消息。

列表 6 - Controllers\GroupControllerTest.vb

<TestMethod> _
Public Sub CreateRequiredName()
    ' Arrange
    Dim controller = New GroupController()

    ' Act
    Dim groupToCreate As New Group()
    groupToCreate.Name = String.Empty
    Dim result = CType(controller.Create(groupToCreate), ViewResult)

    ' Assert
    Dim [error] = result.ViewData.ModelState("Name").Errors(0)
    Assert.AreEqual("Name is required.", [error].ErrorMessage)
End Sub

为了满足此测试,我们需要将 Name 属性添加到 Group 类 (请参阅清单 7) 。 此外,我们需要将少量验证逻辑添加到组控制器 Create () 操作 (请参阅清单 8) 。

清单 7 - Models\Group.vb

Public Class Group

    Private _name As String

    Public Property Name() As String
    Get
        Return _name
    End Get
    Set(ByVal value As String)
        _name = value
    End Set
End Property

End Class

列表 8 - Controllers\GroupController.vb

Public Function Create(ByVal groupToCreate As Group) As ActionResult
    ' Validation logic
    If groupToCreate.Name.Trim().Length = 0 Then
    ModelState.AddModelError("Name", "Name is required.")
    Return View("Create")
    End If

    ' Database logic
    _groups.Add(groupToCreate)
    Return RedirectToAction("Index")
End Function

请注意,组控制器 Create () 操作现在同时包含验证和数据库逻辑。 目前,组控制器使用的数据库只包含一个内存中集合。

重构时间

红色/绿色/重构中的第三步是重构部分。 此时,我们需要从代码中退步,考虑如何重构应用程序以改进其设计。 重构阶段是我们认真思考实现软件设计原则和模式的最佳方法的阶段。

我们可以自由地以任何方式修改代码,以改进代码的设计。 我们有一个单元测试的安全网,可防止我们破坏现有功能。

现在,从良好的软件设计的角度来看,我们的组控制器是一团糟。 组控制器包含一堆混乱的验证和数据访问代码。 为了避免违反单一责任原则,我们需要将这些关注点分为不同的类。

重构的组控制器类包含在清单 9 中。 控制器已修改为使用 ContactManager 服务层。 这是与联系人控制器一起使用的相同服务层。

清单 10 包含添加到 ContactManager 服务层以支持验证、列出和创建组的新方法。 IContactManagerService 接口已更新为包含新方法。

清单 11 包含实现 IContactManagerRepository 接口的新 FakeContactManagerRepository 类。 与也实现 IContactManagerRepository 接口的 EntityContactManagerRepository 类不同,我们新的 FakeContactManagerRepository 类不与数据库通信。 FakeContactManagerRepository 类使用内存中集合作为数据库的代理。 我们将在单元测试中使用此类作为假存储库层。

清单 9 - Controllers\GroupController.vb

Public Class GroupController
Inherits Controller

Private _service As IContactManagerService

Public Sub New()
    _service = New ContactManagerService(New ModelStateWrapper(Me.ModelState))

End Sub

Public Sub New(ByVal service As IContactManagerService)
    _service = service
End Sub

Public Function Index() As ActionResult
    Return View(_service.ListGroups())
End Function


Public Function Create(ByVal groupToCreate As Group) As ActionResult
    If _service.CreateGroup(groupToCreate) Then
        Return RedirectToAction("Index")
    End If
    Return View("Create")
End Function

End Class

清单 10 - Controllers\ContactManagerService.vb

Public Function ValidateGroup(ByVal groupToValidate As Group) As Boolean
If groupToValidate.Name.Trim().Length = 0 Then
    _validationDictionary.AddError("Name", "Name is required.")
End If
Return _validationDictionary.IsValid
End Function

Public Function CreateGroup(ByVal groupToCreate As Group) As Boolean Implements IContactManagerService.CreateGroup
    ' Validation logic
    If Not ValidateGroup(groupToCreate) Then
        Return False
    End If

    ' Database logic
    Try
        _repository.CreateGroup(groupToCreate)
    Catch
        Return False
    End Try
    Return True
End Function

Public Function ListGroups() As IEnumerable(Of Group) Implements IContactManagerService.ListGroups
    Return _repository.ListGroups()
End Function

列表 11 - Controllers\FakeContactManagerRepository.vb

Public Class FakeContactManagerRepository
Implements IContactManagerRepository

Private _groups As IList(Of Group) = New List(Of Group)()

#Region "IContactManagerRepository Members"

' Group methods

Public Function CreateGroup(ByVal groupToCreate As Group) As Group Implements IContactManagerRepository.CreateGroup
    _groups.Add(groupToCreate)
    Return groupToCreate
End Function

Public Function ListGroups() As IEnumerable(Of Group) Implements IContactManagerRepository.ListGroups
    Return _groups
End Function

' Contact methods

Public Function CreateContact(ByVal contactToCreate As Contact) As Contact Implements IContactManagerRepository.CreateContact
    Throw New NotImplementedException()
End Function

Public Sub DeleteContact(ByVal contactToDelete As Contact) Implements IContactManagerRepository.DeleteContact
    Throw New NotImplementedException()
End Sub

Public Function EditContact(ByVal contactToEdit As Contact) As Contact Implements IContactManagerRepository.EditContact
    Throw New NotImplementedException()
End Function

Public Function GetContact(ByVal id As Integer) As Contact Implements IContactManagerRepository.GetContact
    Throw New NotImplementedException()
End Function

Public Function ListContacts() As IEnumerable(Of Contact) Implements IContactManagerRepository.ListContacts
    Throw New NotImplementedException()
End Function

#End Region
End Class

修改 IContactManagerRepository 接口需要使用 在 EntityContactManagerRepository 类中实现 CreateGroup () 和 ListGroups () 方法。 执行此操作最懒和最快的方法是添加如下所示的存根方法:

Public Function CreateGroup(groupToCreate As Group) As Group Implements IContactManagerRepository.CreateGroup

    throw New NotImplementedException()

End Function 

Public Function ListGroups() As IEnumerable(Of Group) Implements IContactManagerRepository.ListGroups

    throw New NotImplementedException()

End Function

最后,这些对应用程序设计的更改要求我们对单元测试进行一些修改。 我们现在在执行单元测试时需要使用 FakeContactManagerRepository。 更新后的 GroupControllerTest 类包含在清单 12 中。

清单 12 - Controllers\GroupControllerTest.vb

Imports Microsoft.VisualStudio.TestTools.UnitTesting
Imports System.Web.Mvc

<TestClass()> _
Public Class GroupControllerTest

    Private _repository As IContactManagerRepository
    Private _modelState As ModelStateDictionary
    Private _service As IContactManagerService

    <TestInitialize()> _
    Public Sub Initialize()
        _repository = New FakeContactManagerRepository()
        _modelState = New ModelStateDictionary()
        _service = New ContactManagerService(New ModelStateWrapper(_modelState), _repository)
    End Sub

    <TestMethod()> _
    Public Sub Index()
        ' Arrange
        Dim controller = New GroupController(_service)

        ' Act
        Dim result = CType(controller.Index(), ViewResult)

        ' Assert
        Assert.IsInstanceOfType(result.ViewData.Model, GetType(IEnumerable(Of Group)))
    End Sub

    <TestMethod()> _
    Public Sub Create()
        ' Arrange
        Dim controller = New GroupController(_service)

        ' Act
        Dim groupToCreate = New Group()
        groupToCreate.Name = "Business"
        controller.Create(groupToCreate)

        ' Assert
        Dim result = CType(controller.Index(), ViewResult)
        Dim groups = CType(result.ViewData.Model, IEnumerable(Of Group))
        CollectionAssert.Contains(groups.ToList(), groupToCreate)
    End Sub

    <TestMethod()> _
    Public Sub CreateRequiredName()
        ' Arrange
        Dim controller = New GroupController(_service)

        ' Act
        Dim groupToCreate = New Group()
        groupToCreate.Name = String.Empty
        Dim result = CType(controller.Create(groupToCreate), ViewResult)

        ' Assert
        Dim nameError = _modelState("Name").Errors(0)
        Assert.AreEqual("Name is required.", nameError.ErrorMessage)
    End Sub

End Class

完成所有这些更改后,再次通过所有单元测试。 我们已完成红色/绿色/重构的整个周期。 我们实现了前两个用户情景。 现在,我们提供了针对用户情景中表达的要求的支持单元测试。 实现剩余的用户情景涉及重复相同的红色/绿色/重构周期。

修改数据库

遗憾的是,尽管我们已满足单元测试表达的所有要求,但我们的工作尚未完成。 我们仍然需要修改数据库。

我们需要创建新的 Group 数据库表。 按照以下步骤操作:

  1. 在“服务器资源管理器”窗口中,右键单击“表”文件夹,然后选择菜单选项 “添加新表”。
  2. 在表Designer中输入下面所述的两列。
  3. 将“Id”列标记为主键和“标识”列。
  4. 单击软盘的图标,保存名为“组”的新表。

列名称 数据类型 允许 Null 值
ID int False
名称 nvarchar(50) False

接下来,我们需要从“联系人”表中删除所有数据 (否则,将无法) “联系人”表和“组”表之间创建关系。 按照以下步骤操作:

  1. 右键单击“联系人”表,然后选择菜单选项 “显示表数据”。
  2. 删除所有行。

接下来,我们需要定义 Groups 数据库表与现有 Contacts 数据库表之间的关系。 按照以下步骤操作:

  1. 双击“服务器资源管理器”窗口中的“联系人”表,打开“表”Designer。
  2. 向名为 GroupId 的“联系人”表添加新的整数列。
  3. 单击“关系”按钮打开“外键关系”对话框, (请参阅图 3) 。
  4. 单击“添加”按钮。
  5. 单击“表和列规范”按钮旁边显示的省略号按钮。
  6. 在“表和列”对话框中,选择“组”作为主键表,选择“ID”作为主键列。 选择“联系人”作为外键表,选择“GroupId”作为外键列 (请参阅图 4) 。 单击“确定”按钮。
  7. “插入和更新规范”下,选择“删除规则”的值“级联”。
  8. 单击“关闭”按钮关闭“外键关系”对话框。
  9. 单击“保存”按钮,保存对“联系人”表所做的更改。

创建数据库表关系

图 03:创建数据库表关系 (单击以查看全尺寸图像)

指定表关系

图 04:指定表关系 (单击以查看全尺寸图像)

更新数据模型

接下来,我们需要更新数据模型以表示新的数据库表。 按照以下步骤操作:

  1. 双击 Models 文件夹中的 ContactManagerModel.edmx 文件,打开实体Designer。
  2. 右键单击Designer图面,然后选择菜单选项“从数据库更新模型”。
  3. 在更新向导中,选择“组”表并单击“完成”按钮 (请参阅图 5) 。
  4. 右键单击“组”实体,然后选择菜单选项 “重命名”。 将 Groups 实体的名称更改为 Group (单数) 。
  5. 右键单击显示在“联系人”实体底部的“组”导航属性。 将 Groups 导航属性的名称更改为 Group (单数) 。

从数据库更新实体框架模型

图 05:从数据库更新实体框架模型 (单击以查看全尺寸图像)

完成这些步骤后,数据模型将同时表示“联系人”表和“组”表。 实体Designer应显示这两个实体 (请参阅图 6) 。

显示组和联系人的实体Designer

图 06:实体Designer显示组和联系人 (单击以查看全尺寸图像)

创建存储库类

接下来,我们需要实现存储库类。 在此迭代过程中,我们在编写代码以满足单元测试时,向 IContactManagerRepository 接口添加了几个新方法。 IContactManagerRepository 接口的最终版本包含在清单 14 中。

列表 14 - Models\IContactManagerRepository.vb

Public Interface IContactManagerRepository
' Contact methods
Function CreateContact(ByVal groupId As Integer, ByVal contactToCreate As Contact) As Contact
Sub DeleteContact(ByVal contactToDelete As Contact)
Function EditContact(ByVal groupId As Integer, ByVal contactToEdit As Contact) As Contact
Function GetContact(ByVal id As Integer) As Contact

' Group methods
Function CreateGroup(ByVal groupToCreate As Group) As Group
Function ListGroups() As IEnumerable(Of Group)
Function GetGroup(ByVal groupId As Integer) As Group
Function GetFirstGroup() As Group
Sub DeleteGroup(ByVal groupToDelete As Group)

End Interface

实际上,我们没有在实际 EntityContactManagerRepository 类中实现与使用联系人组相关的任何方法。 目前,EntityContactManagerRepository 类具有 IContactManagerRepository 接口中列出的每个联系人组方法的存根方法。 例如,ListGroups () 方法当前如下所示:

Public Function ListGroups() As IEnumerable(Of Group) Implements IContactManagerRepository.ListGroups

    throw New NotImplementedException()

End Function

存根方法使我们能够编译应用程序并通过单元测试。 但是,现在是时候实际实现这些方法了。 EntityContactManagerRepository 类的最终版本包含在清单 13 中。

清单 13 - Models\EntityContactManagerRepository.vb

Public Class EntityContactManagerRepository
Implements IContactManagerRepository

Private _entities As New ContactManagerDBEntities()

' Contact methods

Public Function GetContact(ByVal id As Integer) As Contact Implements IContactManagerRepository.GetContact
    Return (From c In _entities.ContactSet.Include("Group") _
            Where c.Id = id _
            Select c).FirstOrDefault()
End Function

Public Function CreateContact(ByVal groupId As Integer, ByVal contactToCreate As Contact) As Contact Implements IContactManagerRepository.CreateContact
    ' Associate group with contact
    contactToCreate.Group = GetGroup(groupId)

    ' Save new contact
    _entities.AddToContactSet(contactToCreate)
    _entities.SaveChanges()
    Return contactToCreate
End Function

Public Function EditContact(ByVal groupId As Integer, ByVal contactToEdit As Contact) As Contact Implements IContactManagerRepository.EditContact
    ' Get original contact
    Dim originalContact = GetContact(contactToEdit.Id)

    ' Update with new group
    originalContact.Group = GetGroup(groupId)

    ' Save changes
    _entities.ApplyPropertyChanges(originalContact.EntityKey.EntitySetName, contactToEdit)
    _entities.SaveChanges()
    Return contactToEdit
End Function

Public Sub DeleteContact(ByVal contactToDelete As Contact) Implements IContactManagerRepository.DeleteContact 
    Dim originalContact = GetContact(contactToDelete.Id)
    _entities.DeleteObject(originalContact)
    _entities.SaveChanges()
End Sub

    ' Group methods

Public Function CreateGroup(ByVal groupToCreate As Group) As Group Implements IContactManagerRepository.CreateGroup 
    _entities.AddToGroupSet(groupToCreate)
    _entities.SaveChanges()
    Return groupToCreate
End Function

Public Function ListGroups() As IEnumerable(Of Group) Implements IContactManagerRepository.ListGroups
    Return _entities.GroupSet.ToList()
End Function

Public Function GetFirstGroup() As Group Implements IContactManagerRepository.GetFirstGroup
    Return _entities.GroupSet.Include("Contacts").FirstOrDefault()
End Function

Public Function GetGroup(ByVal id As Integer) As Group Implements IContactManagerRepository.GetGroup
    Return (From g In _entities.GroupSet.Include("Contacts") _
            Where g.Id = id _
            Select g).FirstOrDefault()
End Function

Public Sub DeleteGroup(ByVal groupToDelete As Group) Implements IContactManagerRepository.DeleteGroup
    Dim originalGroup = GetGroup(groupToDelete.Id)
    _entities.DeleteObject(originalGroup)
    _entities.SaveChanges()
End Sub

End Class

创建视图

使用默认 ASP.NET 视图引擎时,ASP.NET MVC 应用程序。 因此,不要创建视图来响应特定的单元测试。 但是,由于没有视图的应用程序将无用,因此如果不创建和修改 Contact Manager 应用程序中包含的视图,则无法完成此迭代。

我们需要创建以下用于管理联系人组的新视图 (请参阅图 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:“联系人索引”视图 (单击以查看全尺寸图像)

总结

在此迭代中,我们遵循测试驱动的开发应用程序设计方法,向 Contact Manager 应用程序添加了新功能。 我们首先创建一组用户情景。 我们创建了一组单元测试,这些测试对应于用户情景所表达的要求。 最后,我们编写了足够多的代码来满足单元测试所表达的要求。

完成编写足够的代码以满足单元测试所表达的要求后,我们更新了数据库和视图。 我们向数据库添加了一个新的 Groups 表,并更新了 Entity Framework 数据模型。 我们还创建并修改了一组视图。

在下一次迭代(即最终迭代)中,我们将重写应用程序以利用 Ajax。 利用 Ajax,我们将提高 Contact Manager 应用程序的响应能力和性能。