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

Microsoft

下载代码

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

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

在本系列教程中,我们将从头到尾构建整个联系人管理应用程序。 通过 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.cs,然后单击“ 确定” 按钮。

添加 GroupControllerTest 单元测试

图 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 中的 Group 控制器类包含通过单元测试所需的最少代码。 Index () 操作返回组的静态编码列表, (列表 3) 中定义了 Group 类。

清单 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:成功! (单击以查看全尺寸图像)

创建联系人组

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

清单 4 中的测试验证是否使用新 Group 调用 Create () 方法将 Group 添加到 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 中的测试使用新的联系人组调用 Group 控制器 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);
}

为了满足此测试,我们需要将 Name 属性添加到 Group 类 (请参阅清单 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 () 操作现在包含验证和数据库逻辑。 目前,组控制器使用的数据库只包含内存中集合。

重构时间

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

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

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

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

列表 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);
        }
    
    }
}

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

修改数据库

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

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

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

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

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

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

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

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

创建数据库表关系

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

指定表关系

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

更新数据模型

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

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

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

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

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

显示组和联系人的实体Designer

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

创建存储库类

接下来,我们需要实现存储库类。 在此迭代过程中,我们在编写代码以满足单元测试时,向 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 应用程序。 因此,不要创建视图来响应特定的单元测试。 但是,由于没有视图的应用程序将毫无用,因此如果不创建和修改 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 应用程序添加了新功能。 我们首先创建了一组用户情景。 我们创建了一组单元测试,这些测试对应于用户情景所表达的要求。 最后,我们编写了足够多的代码来满足单元测试所表达的要求。

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

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