共用方式為


反覆項目 #6 – 使用測試導向的開發 (C#)

Microsoft 提供

下載程式碼

在這第六個反覆項目中,我們會先撰寫單元測試,再針對單元測試撰寫程式碼,藉此將新功能新增至應用程式。 在此反覆項目中,我們會新增連絡人群組。

建置連絡人管理 ASP.NET MVC 應用程式 (C#)

在此系列教學課程中,我們會從頭到尾建置整個連絡人管理應用程式。 Contact Manager 應用程式可讓您儲存連絡人資訊 (姓名、電話號碼和電子郵件地址) 以取得人員名單。

我們會透過多個反覆項目建置應用程式。 每次反覆運算時,我們會逐步改善應用程式。 這種多次反覆運算方法的目標是,讓您能夠瞭解每次變更的原因。

  • 反覆項目 #1 – 建立應用程式 在第一個反覆項目中,我們以最簡單的方式建立 Contact Manager。 我們新增對基本資料庫作業的支援:建立、讀取、更新和刪除 (CRUD)。

  • 反覆項目 #2 – 美化應用程式外觀。 在這個反覆項目中,我們修改預設 ASP.NET MVC 檢視主版頁面和串聯樣式表,以改善應用程式的外觀。

  • 反覆項目 #3 – 新增表單驗證。 在第三個反覆項目中,我們新增基本表單驗證。 我們要防止人員提交未完成表單必填欄位的表單。 我們也驗證電子郵件地址和電話號碼。

  • 反覆項目 #4 – 讓應用程式鬆散耦合。 在這個第四個反覆項目中,我們利用好幾種軟體設計模式,更輕鬆地維護和修改 Contact Manager 應用程式。 例如,我們將應用程式重構為使用存放庫模式和相依性插入模式。

  • 反覆項目 #5 – 建立單元測試。 在第五個反覆項目中,我們會藉由新增單元測試,更容易維護和修改應用程式。 我們會模擬資料模型類別,並為控制器和驗證邏輯建置單元測試。

  • 反覆項目 #6 – 使用測試導向的開發。 在這第六個反覆項目中,我們會先撰寫單元測試,再針對單元測試撰寫程式碼,藉此將新功能新增至應用程式。 在此反覆項目中,我們會新增連絡人群組。

  • 反覆項目 #7 – 新增 Ajax 功能性。 在第七個反覆項目中,我們會藉由新增對 Ajax 的支援來改善應用程式的回應性和效能。

此反覆項目

在 Contact Manager 應用程式的先前反覆項目中,我們建立了單元測試,為程式碼提供安全網。 建立單元測試的動機是讓程式碼更具變更彈性。 有了單元測試,我們可以愉快地對程式碼進行任何變更,並立即知道我們是否中斷了現有的功能。

在此反覆項目中,我們會針對完全不同的用途使用單元測試。 在此反覆項目中,我們會使用單元測試做為應用程式設計理念的一部分,稱為測試導向的開發。 當您練習測試導向的開發時,請先撰寫測試,然後針對測試撰寫程式碼。

更精確地說,在練習測試導向的開發時,您在建立程式碼時完成三個步驟 (Red/Green/Refactor):

  1. 撰寫失敗的單元測試 (紅色)
  2. 撰寫通過單元測試的程式碼 (綠色)
  3. 重構程式碼 (重構)

首先,您要撰寫單元測試。 單元測試應該表達您預期程式碼運作方式的意圖。 當您第一次建立單元測試時,單元測試應該會失敗。 測試應該會失敗,因為您尚未撰寫任何滿足測試的應用程式程式碼。

接下來,您可以撰寫足夠的程式碼,讓單元測試通過。 目標是以最懶散、最草率且最快的方式撰寫程式碼。 您不應該浪費時間思考應用程式的架構。 而是應該專注於撰寫滿足單元測試所表示意圖所需的最少程式碼量。

最後,撰寫足夠的程式碼之後,您可以回溯並考慮應用程式的整體架構。 在此步驟中,您會利用軟體設計模式,例如存放庫模式,來重寫程式碼 (重構),讓您的程式碼更容易維護。 您可以無畏地重寫此步驟中的程式碼,因為單元測試涵蓋您的程式碼。

練習測試導向的開發所產生的許多優點。 首先,測試導向的開發會強制您專注於實際需要撰寫的程式碼。 因為您經常專注於撰寫足夠的程式碼來通過特定測試,所以您無法遊蕩到花邊,並撰寫您永遠不會使用的大量程式碼。

其次,「測試優先」設計方法會強制您從程式碼的使用方式的觀點撰寫程式碼。 換句話說,在練習測試導向的開發時,您會從使用者的觀點持續撰寫測試。 因此,測試導向的開發可能會導致更簡潔且更容易理解的API。

最後,測試導向的開發會強制您在撰寫應用程式的一般程式中撰寫單元測試。 隨著專案期限的臨近,測試通常是出視窗的第一件事。 另一方面,在練習測試導向的開發時,您更有可能對撰寫單元測試有良性,因為測試導向的開發讓單元測試成為建置應用程式程式的核心。

注意

若要深入瞭解測試導向的開發,建議您閱讀 Michael Feathers 書籍 《有效使用舊版程式碼》。

在此反覆項目中,我們會將新功能新增至 Contact Manager 應用程式。 我們新增連絡人群組的支援。 您可以使用連絡人群組將連絡人組織成商務和朋友群組等類別。

我們將遵循測試導向的開發程式,將這項新功能新增至應用程式。 我們會先撰寫單元測試,並針對這些測試撰寫所有程式碼。

經過測試的項目

如先前反覆項目所述,您通常不會撰寫資料存取邏輯或檢視邏輯的單元測試。 您不會撰寫資料存取邏輯的單元測試,因為存取資料庫是相對緩慢的作業。 您不會撰寫檢視邏輯的單元測試,因為存取檢視需要啟動較慢作業的網頁伺服器。 除非可以反覆執行測試,否則您不應該撰寫單元測試

由於測試導向的開發是由單元測試所驅動,因此我們一開始著重於撰寫控制器和商業規則。 我們避免接觸資料庫或檢視。 在本教學課程結束之前,我們不會修改資料庫或建立檢視。 我們從可以測試的項目開始。

建立使用者劇本

在練習測試導向的開發時,您一律從撰寫測試開始。 這會立即提出問題:您如何決定要先撰寫的測試? 若要回答這個問題,您應該撰寫一組使用者劇本

使用者劇本是軟體需求的簡短 (通常是一句話) 描述。 它應該是從使用者觀點撰寫之需求的非技術描述。

以下是描述新連絡人群組功能所需功能的一組使用者劇本:

  1. 使用者可以檢視連絡人群組清單。
  2. 使用者可以建立新的連絡人群組。
  3. 使用者可以刪除現有的連絡人群組。
  4. 使用者可以在建立新連絡人時選取連絡人群組。
  5. 編輯現有的連絡人時,使用者可以選取連絡人群組。
  6. 連絡人群組清單會顯示在 [索引] 檢視中。
  7. 當使用者按一下連絡人群組時,會顯示相符的連絡人清單。

請注意,客戶完全可以理解此使用者案例清單。 沒有提及技術實作詳細資料。

在建置應用程式的過程中,使用者劇本集可能會變得更精細。 您可以將使用者劇本分成多個故事 (需求)。 例如,您可能會決定建立新的連絡人群組應該涉及驗證。 提交沒有名稱的連絡人群組應該會傳回驗證錯誤。

建立使用者劇本清單之後,您就可以撰寫第一個單元測試。 我們將從建立單元測試開始,以檢視連絡人群組清單。

列出連絡人群組

我們的第一個使用者案例是,使用者應該能夠檢視連絡人群組的清單。 我們需要用測試來表達這個故事。

以滑鼠右鍵按一下 ContactManager.Tests 專案中的 Controllers 資料夾,選取 [新增]、[新增測試],然後選取 [單元測試] 範本來建立新的單元測試 (請參閱圖 1)。 將新的單元測試命名為GroupControllerTest.cs,然後按下 [確定] 按鈕。

新增 GroupControllerTest 單元測試

圖 01:新增 GroupControllerTest 單元測試 (按一下以檢視完整大小的圖片)

我們的第一個單元測試包含在清單 1 中。 此測試會驗證群組控制器的 Index() 方法會傳回一組 Groups。 測試會確認檢視資料中傳回群組的集合。

清單 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:成功!(按一下以檢視完整大小的圖片)

建立連絡人群組

現在,我們可以移至第二個使用者劇本。 我們需要能夠建立新的連絡人群組。 我們需要透過測試來表達這個意圖。

清單 4 中的測試會確認使用新的 Group 呼叫 Create() 方法會將 Group 新增至 Index() 方法所傳回的 Groups 清單。 換句話說,如果我建立新的群組,則我應該能夠從 Index() 方法傳回的 Groups 清單中取得新的群組。

清單 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 Controller Create() 方法。 接下來,測試會確認呼叫 Group Controller 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() 巨集指令已修改為傳回 Groups 集合的內容。

我們再次執行了通過單元測試所需的最少工作量。 在對群組控制器進行這些變更之後,我們所有的單元測試都會通過。

新增驗證

此需求未在使用者劇本中明確說明。 不過,要求群組有名稱是合理的。 否則,將連絡人組織到群組並不十分有用。

清單 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() 動作現在同時包含驗證和資料庫邏輯。 目前,群組控制器所使用的資料庫只包含記憶體內部集合。

是時候重構了

Red/Green/Refactor 中的第三個步驟是重構部分。 此時,我們需要暫別程式碼,並考慮如何重構應用程式以改善其設計。 重構階段是我們實作軟體設計原則和模式的最佳方法的階段。

我們可以以任何方式修改程式碼,以便改善程式碼的設計。 我們有一個單元測試的安全網,可防止我們中斷現有的功能。

現在,我們的群組控制器從良好的軟體設計的角度來看是一團糟。 群組控制器包含一團混亂的驗證和資料存取程式碼。 為了避免違反單一責任原則,我們需要將這些考慮分成不同的類別。

重構的群組控制器類別包含在清單 9 中。 控制器已修改為使用 ContactManager 服務層。 這是我們與連絡人控制器一起使用的相同服務層級。

清單 10 包含新增至 ContactManager 服務層的新方法,以支持驗證、列出和建立群組。 IContactManagerService 介面已更新為包含新的方法。

清單 11 包含實作 IContactManagerRepository 介面的新 FakeContactManagerRepository 類別。 與實作 IContactManagerRepository 介面的 EntityContactManagerRepository 類別不同,我們新的 FakeContactManagerRepository 類別不會與資料庫通訊。 FakeContactManagerRepository 類別會使用記憶體內部集合做為資料庫的 Proxy。 我們將在單元測試中使用這個類別做為假存放庫層。

清單 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);
        }
    
    }
}

在進行所有這些變更之後,我們所有的單元測試都會通過。 我們已完成整個紅色/綠色/重構週期。 我們已實作前兩個使用者劇本。 我們現在已針對使用者劇本中所表達的需求支援單元測試。 實作使用者劇本的其餘部分牽涉到重複相同週期的 Red/Green/Refactor。

修改我們的資料庫

不幸的是,即使我們已經滿足單元測試所表達的所有需求,我們的工作也不會完成。 我們仍然需要修改資料庫。

我們需要建立一個新的群組資料庫表。 執行下列步驟:

  1. 從 [伺服器總管] 視窗中,對 [資料表] 資料夾按一下滑鼠右鍵,然後選取 [新增新資料表] 功能表選項
  2. 在資料表設計工具中輸入下面所述的兩個資料行。
  3. 將 [識別碼] 資料行標示為主鍵和 [識別] 資料行。
  4. 按一下軟碟的圖示,以名稱 [群組] 儲存新的資料表。

資料行名稱 資料類型 允許 Null
Id int False
名稱 nvarchar(50) False

接下來,我們需要從 [連絡人] 資料表中刪除所有資料 (否則,我們無法建立 [連絡人] 和 [群組] 資料表之間的關聯性)。 執行下列步驟:

  1. 以滑鼠右鍵按一下 Contacts 資料表,然後選取 [顯示資料表資料] 功能表選項。
  2. 刪除所有列。

接下來,我們需要定義 Groups 資料庫資料表與現有 Contacts 資料庫資料表之間的關聯性。 執行下列步驟:

  1. 按兩下 [伺服器總管] 視窗中的 [連絡人] 資料表,以開啟 [資料表設計工具]。
  2. 將新的整數資料行新增至名為 GroupId 的 Contacts 資料表。
  3. 按一下 [關聯性] 按鈕以開啟 [外鍵關聯性] 對話方塊 (請參閱圖 3)。
  4. 按一下 [新增] 按鈕。
  5. 按一下 [資料表和資料行規格] 按鈕旁邊的省略號按鈕。
  6. 在 [資料表和資料行] 對話方塊中,選取 [群組] 做為主鍵資料表,並將 [標識符] 選取為主鍵資料行。 選取 [連絡人] 做為外鍵資料表,並將 GroupId 選取為外鍵資料行 (請參閱圖 4)。 按一下 [確定] 按鈕。
  7. [插入和更新規範] 下,為 [刪除規則] 選取值串聯
  8. 按一下 [關閉] 按鈕以關閉 [外鍵關聯性] 對話方塊。
  9. 按一下 [儲存] 按鈕將變更儲存到 [連絡人] 資料表。

建立資料庫資料表關聯性

圖 03:建立資料庫資料表關聯性 (按一下以檢視完整大小的圖片)

指定資料表關聯性

圖 04:指定資料表關聯性 (按一下以檢視完整大小的圖片)

更新我們的資料模型

接下來,我們需要更新資料模型來代表新的資料庫資料表。 執行下列步驟:

  1. 按兩下 Models 資料夾中的 ContactManagerModel.edmx 檔案,以開啟實體設計工具。
  2. 以滑鼠右鍵按一下 [設計工具] 介面,然後選取 [從資料庫更新模型] 功能表選項
  3. 在 [更新精靈] 中,選取 [群組] 資料表,然後按一下 [完成] 按鈕 (請參閱圖 5)。
  4. 以滑鼠右鍵按一下 [群組] 實體,然後選取功能表選項 [重新命名]。 將 Groups 實體的名稱變更為 Group (單數)。
  5. 以滑鼠右鍵按一下出現在 [連絡人] 實體底部的 [群組] 導覽屬性。 將 [Groups] 導覽屬性的名稱變更為 [Group] (單數)。

從資料庫更新 Entity Framework 模型

圖 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 應用程式。 因此,您不會建立檢視來回應特定的單元測試。 不過,由於應用程式在沒有檢視的情況下是無用的,因此無法完成此反覆項目,而不需要建立和修改 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 應用程式的回應性和效能。