ASP.NET MVC

試駕 ASP.NET MVC

Keith Burnell

下載代碼示例

為核心的模型-視圖-控制器 (MVC) 模式是為三個部分 UI 職能的分離。模型表示的資料和行為的您的域。視圖管理模型的顯示,並處理與使用者的交互。控制器協調的視圖和模型之間的相互作用。這本身就很難測試使用者介面邏輯分離業務邏輯使得用極可測試的 MVC 模式實現的應用程式。在這篇文章中我將討論最佳做法和提高 ASP.NET MVC 應用程式,包括如何構建您的解決方案,可測試性技術構建您的代碼以處理依賴關係的注射和 StructureMap 執行依賴注射。

最大可測試性構建您的解決方案

什麼更好的方式來開始我們比每個開發人員從哪裡開始一個新專案的討論: 創建解決方案。我將討論一些鋪設您根據我的經驗發展大型企業的 Visual Studio 解決方案使用測試驅動開發 (TDD) 的 ASP.NET MVC 應用程式的最佳做法。若要開始,我建議使用空專案範本創建 ASP.NET MVC 專案時。其他範本是很好的嘗試,或創建概念驗證,但它們通常包含很多是分心和真實的企業應用程式中不必要的噪音。

每當您創建的任何類型的複雜的應用程式,您應該使用 n 層的方法。ASP.NET MVC 應用程式開發,我建議使用中所示的方法圖 1圖 2,其中包含以下專案:

  • Web 專案包含所有特定于使用者介面的代碼,包括視圖、 查看模型、 腳本、 CSS 和等等。這一層已經能夠訪問只的控制器,服務、 域和共用專案。
  • 該控制器專案包含使用的 ASP.NET MVC 的控制器類。這一層與通訊服務、 域和共用專案。
  • 服務專案包含應用程式的業務邏輯。這一層通信與資料訪問、 域和共用專案。
  • 資料訪問專案包含用於檢索和運算元據的磁碟機的應用程式的代碼。這一層相通的域和共用的專案。
  • 域專案包含應用程式所使用的域物件,並禁止溝通的任何專案。
  • 共用專案包含代碼,需要對多個其它圖層,如記錄器、 常量和其他常見的實用程式碼可用。它允許只能與域專案進行通信。

Interaction Among Layers
圖 1 的圖層之間的互動關係

Example Solution Structure
圖 2 示例解決方案結構

我建議將您的控制器放入一個單獨的 Visual Studio 專案。如何這很容易實現的資訊,請參閱發表于 bit.ly/K4mF2B。將您的控制器放在一個單獨的專案中,可以進一步解耦駐留在使用者介面代碼從控制器中的邏輯。其結果是您的 Web 專案僅包含代碼真正相關的使用者介面。

把您測試專案的位置其中您找到您的測試專案,您對它們的命名是重要。當您正在開發複雜時,企業級應用程式,解決方案往往獲得相當大,其中可以使它難以在解決方案資源管理器中查找特定的類或程式碼片段。將向您現有的基本代碼中添加多個測試專案僅添加到解決方案資源管理器中導航的複雜性。我強烈推薦物理分隔在實際的應用程式代碼中的您的測試專案。我建議放置在解決方案級的測試檔案夾中的所有測試專案。明顯在一個單一的解決方案資料夾中查找您的測試專案和測試減少您的預設解決方案資源管理器視圖中的雜訊,並允許您輕鬆地找到您的測試。

下一步你要單獨的測試類型。超過可能的是,您的解決方案將包含各種測試類型 (單位、 整合、 性能、 UI 和等等),並且它是重要的是要隔離和組每個測試類型。不只這不會使其易於查找特定的測試類型,但它還允許您方便地運行特定類型的所有測試。如果您使用的兩種最受歡迎的 Visual Studio 生產力工具套件,ReSharper (jetbrains.com/ReSharper) 或 CodeRush (devexpress.com/CodeRush),你得到一個內容功能表,允許您用滑鼠右鍵按一下任何資料夾、 專案或解決方案資源管理器中的類,並在該專案中運行包含的所有測試。若要分組的測試類型的測試,為每種類型的測試你打算寫測試的解決方案資料夾內創建一個資料夾。

圖 3 顯示包含多個測試類型資料夾的示例測試的解決方案資料夾。

An Example Tests Solution Folder
圖 3 示例測試解決方案資料夾

命名您的測試專案如何命名您的測試專案是同等重要的位置找到它們。你想要能夠很容易區分哪些應用程式的一部分是在每個測試專案中測試下,哪種類型的測試專案包含。為此,它是一個好主意來命名您使用以下約定的測試專案: [完整名稱的專案下測試] 五月份。 [測試類型]。這允許您,一目了然,確定到底什麼層您的專案是根據測試和正在執行何種類型的測試。您可能會想,將測試專案放在特定類型的資料夾中,包括測試類型的測試專案的名稱是冗余的但請記住,只有在解決方案資源管理器中使用的資料夾和專案檔案的命名空間中不包括的解決方案。所以雖然控制器單元測試專案位於 Tests\Unit 的解決方案資料夾中,namespace—TestDrivingMVC.Controllers.Test.Unit—doesn't 將反映該資料夾結構。添加測試類型,命名該專案時是測試的有必要的以避免出現命名衝突,並確定您在編輯器中工作哪種類型。圖 4 顯示解決方案資源管理器的測試專案。

Test Projects in Solution Explorer
在解決方案資源管理器中的圖 4 測試專案

引進依賴注入到您的體系結構

你不能得到很遠的單元測試的 n 層應用程式之前您遇到您正在測試的代碼中的依賴項。這些依賴項可能其它圖層的應用程式,或者他們可以完全外部的代碼 (如資料庫、 檔案系統或 Web 服務)。當你在寫單元測試時,您需要正確處理這種情況下,當使用測試雙打 (類比考試、 假貨或存根) 您遇到外部相依性。測試雙打的詳細資訊,請參閱"探索連續的測試雙打"(msdn.microsoft.com/magazine/cc163358) 在 2007 年 9 月發行的 MSDN 雜誌。您可以採取的測試雙打提供的靈活性優勢之前,然而,必須對您的代碼進行架構來處理依賴關係的注射。

依賴注入依賴注入是注射一類需要而不是直接具現化該依賴項的類的具體實現的過程。消費階層不知道實際的具體實施,任何依賴項,但只知道的介面的回依賴項 ; 由消費階層或依賴注射框架提供了具體的實現。

依賴注入的目標是創建非常鬆散耦合的代碼。松耦合讓你輕鬆你依賴項的替代測試雙實現時編寫單元測試。

有三種主要方式完成依賴注入:

  • 屬性注射
  • 建構函式注射
  • 使用依賴注入框架/倒置的控制項容器 (從這點作為迪/政府間海洋學委員會框架指)

您的物件,使其依賴項來設置,如中所示的屬性注射,公開公共屬性圖 5。這種方法很簡單,並要求沒有工裝。

圖 5 屬性注射

 

// Employee Service
public class EmployeeService : IEmployeeService {
  private ILoggingService _loggingService;
  public EmployeeService() {}
  public ILoggingService LoggingService { get; set; }
  public decimal CalculateSalary(long employeeId) {
    EnsureDependenciesSatisfied();
    _loggingService.LogDebug(string.Format(
      "Calculating Salary For Employee: {0}", employeeId));
    decimal output = 0;
    /*
    * Complex logic that needs to be performed
    * in order to determine the employee's salary
    */
    return output;
  }
  private void EnsureDependenciesSatisfied() {
    if (_loggingService == null)
      throw new InvalidOperationException(
        "Logging Service dependency must be satisfied!");
    }
  }
}
// Employee Controller (Consumer of Employee Service)
public class EmployeeController : Controller {
  public ActionResult DisplaySalary(long id) {
    EmployeeService employeeService = new EmployeeService();
    employeeService.LoggingService = new LoggingService();
    decimal salary = employeeService.CalculateSalary(id);
    return View(salary);
  }
}

有三個負面影響,這種做法。 首先,它將負責供應依賴項消費。 下一步,它要求您在您的物件,以確保在使用前設置依賴項實施護衛的代碼。 最後,隨著您的物件具有依賴項的數目增加,具現化物件所需的代碼的數量也隨之增加。

實現使用建構函式注射的依賴注入涉及到通過其建構函式的類的依賴關係時提供具現化建構函式,如中所示圖 6。 這種方法也很簡單,但是,與屬性注射不同類的依賴項始終設置放心。

圖 6 建構函式注射

// Employee Service
public class EmployeeService : IEmployeeService {
  private ILoggingService _loggingService;
  public EmployeeService(ILoggingService loggingService) {
    _loggingService = loggingService;
  }
  public decimal CalculateSalary(long employeeId) {
    _loggingService.LogDebug(string.Format(
      "Calculating Salary For Employee: {0}", employeeId));
    decimal output = 0;
    /*
    * Complex logic that needs to be performed
    * in order to determine the employee's salary
    */
    return output;
  }
}
// Consumer of Employee Service
public class EmployeeController : Controller {
  public ActionResult DisplaySalary(long employeeId) {
    EmployeeService employeeService =
      new EmployeeService(new LoggingService());
    decimal salary = employeeService.CalculateSalary(employeeId);
    return View(salary);
  }
}

不幸的是,這種方法仍需要消費者供應依賴項。 此外,它是真的只適用于小的應用程式。 較大的應用程式通常有太多的依賴項,以提供它們通過物件的建構函式。

第三種方式來實現依賴注入是使用迪/政府間海洋學委員會的框架。 迪/政府間海洋學委員會框架完全移除供應來自消費者的依賴關係的責任,並允許您在設計時配置您的依賴關係,並讓它們在運行時解析。 許多迪/政府間海洋學委員會框架可用於.net 中,包括統一 (微軟的產品)、 StructureMap、 溫莎城堡、 Ninject 和更多。 所有不同的迪/政府間海洋學委員會框架的基本概念是相同的與通常選擇一個歸結個人的偏好。 為了演示迪/政府間海洋學委員會框架在本文中,我將使用 StructureMap。

將依賴注入到下一級別與 StructureMap

StructureMap (structuremap。 淨) 是一個被廣泛採用的依存框架。 您可以通過 NuGet 與任一套裝軟體管理器主控台 (安裝套裝軟體 StructureMap) 或 NuGet 套裝軟體管理器 GUI 安裝它 (用滑鼠右鍵按一下您的專案引用資料夾並選擇管理 NuGet 包)。

配置依存關係與 StructureMap StructureMap 實施的 ASP.NET MVC 的第一步是配置您的依賴關係,所以 StructureMap 知道如何解決這些問題。 你這兩個方法中的任意 Global.asax Application_Start 方法中。

第一種方法是手動告訴 StructureMap 為抽象的特定實現它應該使用一個特定的具體實現:

ObjectFactory.Initialize(register => {
  register.For<ILoggingService>().Use<LoggingService>();
  register.For<IEmployeeService>().Use<EmployeeService>();
});

這種方法的缺點是您必須手動註冊依賴項的每個應用程式中,並與大型應用程式,這可能成為一種單調乏味。 此外,因為您在您的 ASP.NET MVC 網站 Application_Start 註冊您的依賴關係,您 Web 層必須有直接瞭解您的應用程式依賴項,線上的其它每一層。

您還可以使用 StructureMap 自動註冊和掃描功能來檢查您的程式集和自動線上的依賴關係。 使用此方法時,StructureMap 會掃描您的程式集,並遇到一個介面時它會查找 (基於名為 IFoo 的介面將由公約 》 映射到具體實施美孚的概念) 相關聯的具體實施:

ObjectFactory.Initialize(registry => registry.Scan(x => {
  x.AssembliesFromApplicationBaseDirectory();
  x.WithDefaultConventions();
}));

依賴項解析器 StructureMap 一旦您有您配置的依賴性,您需要能夠從你基本代碼訪問它們。 這被通過創建一個依賴項衝突解決程式和共用專案中查找,(因為它將需要由應用程式所依賴的所有圖層進行訪問):

public static class Resolver {
  public static T GetConcreteInstanceOf<T>() {
    return ObjectFactory.GetInstance<T>();
  }
}

衝突解決程式類 (如我喜歡這樣稱呼它,因為微軟推出 DependencyResolver 類的 ASP.NET MVC 3,我將討論中有點) 是一個簡單的靜態類,其中包含一個函數。該函數接受泛型參數表示的你正在尋找一個具體的實現中,並將返回 T,這是通過在介面的實際實現的介面的 T。

跳轉到如何在您的代碼中使用的新的衝突解決程式類之前,想要到為什麼我寫我自己而不是創建一個類,實現 IDependencyResolver 介面引入的 ASP.NET MVC 3 的本土依賴項解析器的位址。IDependencyResolver 功能列入是令人敬畏添加到 ASP.NET MVC 和促進正確的軟體做法大跨越。不幸的是,它駐留在 System.Web.MVC DLL 中,並不想要對我的應用程式體系結構的非 Web 層中的特定于 Web 的技術庫的引用。

解決在代碼中的依賴關係現在做的辛勤工作,解決您的代碼中的依賴關係是簡單。您需要做的就是調用的衝突解決程式類的靜態的 GetConcreteInstanceOf 函數,並傳遞它,你正在尋找一個具體的實現,該介面,如中所示圖 7

解決在代碼中的依賴關係的圖 7

 

public class EmployeeService : IEmployeeService {
  private ILoggingService _loggingService;
  public EmployeeService() {
    _loggingService = 
      Resolver.GetConcreteInstanceOf<ILoggingService>();
  }
  public decimal CalculateSalary(long employeeId) {
    _loggingService.LogDebug(string.Format(
      "Calculating Salary For Employee: {0}", employeeId));
    decimal output = 0;
    /*
    * Complex logic that needs to be performed
    * in order to determine the employee's salary
    */
    return output;
  }
}

在單元測試中注入測試雙打以優勢的 StructureMap 現在,代碼架構使您可以注入無需任何干預,消費者從依賴關係讓我們回到原始的正確處理在單元測試中的依賴關係的任務。下面是該方案:

  • 任務是編寫使用生成的薪水從 EmployeeService 的 CalculateSalary 方法返回的值的 TDD 的邏輯。(你會發現中的 EmployeeService 和 CalculateSalary 函數圖 7。)
  • 有一項要求必須記錄所有調用 CalculateSalary 函數。
  • 定義了用於日誌記錄服務的介面,但執行不完整。當前調用日誌記錄服務,則將引發異常。
  • 任務需要在日誌記錄服務的工作計畫開始之前完成。

很多可能您遇到此類情況之前。現在,不過,您有適當的體系結構中,以便能夠通過將一個測試雙放在它的位置切斷這條領帶到該依賴項的地方。我想在一個可以共用之間的所有我測試專案的專案中創建我測試雙打。正如您看到的圖 8,我在我的測試解決方案資料夾中創建一個共用專案。在專案內部我添加了假貨資料夾,因為要完成我的測試,我需要一種假的 ILoggingService 實現。

Project for Shared Test Code and Fakes
圖 8 專案共用的測試代碼和假貨

創建一個假的測井服務是容易的。首先,我在我名為 LoggingServiceFake 的假貨資料夾內創建一個類。LoggingServiceFake 需要滿足的合同,EmployeeService 預期,這意味著它需要執行 ILoggingService 和它的方法。按照定義,一個假的是包含足夠的代碼以滿足介面的替身。通常,這意味著它具有空實現 void 方法和函數實現包含 return 語句返回一個硬編碼值,如下所示:

 

public class LoggingServiceFake : ILoggingService {
  public void LogError(string message, Exception ex) {}
  public void LogDebug(string message) {}
  public bool IsOnline() {
    return true;
  }
}

現在,假實現的我可以寫我的測試。 要開始,就會創建一個測試類 TestDrivingMVC.Service.Test.Unit 單元測試專案中和,前面討論過的命名約定,每我會叫它 EmployeeServiceTest,如中所示圖 9

圖 9 EmployeeServiceTest 測試類

[TestClass]
public class EmployeeServiceTest {
  private ILoggingService _loggingServiceFake;
  private IEmployeeService _employeeService;
  [TestInitialize]
  public void TestSetup() {
    _loggingServiceFake = new LoggingServiceFake();
    ObjectFactory.Initialize(x => 
      x.For<ILoggingService>().Use(_loggingServiceFake));
    _employeeService = new EmployeeService();
  }
  [TestMethod]
  public void CalculateSalary_ShouldReturn_Decimal() {
    // Arrange
    long employeeId = 12345;
    // Act
    var result = 
      _employeeService.CalculateSalary(employeeId);
    // Assert
    result.ShouldBeType<decimal>();
  }
}

在最大程度的測試類代碼十分簡單。 您想要特別是密切注意的行是:

ObjectFactory.Initialize(x =>
    x.For<ILoggingService>().Use(
    _loggingService));

這是指示 StructureMap 的代碼時要使用的 LoggingServiceFake 我們前面創建的衝突解決程式類嘗試解析 ILoggingService。 我把這段代碼放在標有 TestInitialize,它告訴單元測試框架來執行此方法才能運行測試類中的每個測試的一種方法。

使用迪/政府間海洋學委員會和 StructureMap 工具的力量,我能夠徹底斷絕這條領帶到日誌記錄服務。 這樣做使我要完成我的編碼和單元測試不受該日誌記錄服務的狀態,並不依賴于任何依賴項的真實的單元測試的代碼。

作為預設控制器工廠使用 StructureMap ASP.NET MVC 提供了一個允許您在您的應用程式中添加的控制器如何具現化的自訂實現的可擴充性點。 通過創建一個類,從 DefaultControllerFactory 中繼承 (請參見圖 10),您可以控制如何創建控制器。

圖 10 自訂控制器廠

public class ControllerFactory : DefaultControllerFactory {
  private const string ControllerNotFound = 
  "The controller for path '{0}' could not be found or it does not implement IController.";
  private const string NotAController = "Type requested is not a controller: {0}";
  private const string UnableToResolveController = 
    "Unable to resolve controller: {0}";
  public ControllerFactory() {
    Container = ObjectFactory.Container;
  }
  public IContainer Container { get; set; }
  protected override IController GetControllerInstance(
    RequestContext context, Type controllerType) {
    IController controller;
    if (controllerType == null)
      throw new HttpException(404, String.Format(ControllerNotFound,
      context.HttpContext.Request.Path));
    if (!typeof (IController).IsAssignableFrom(controllerType))
      throw new ArgumentException(string.Format(NotAController,
      controllerType.Name), "controllerType");
    try {
      controller = Container.GetInstance(controllerType) 
        as IController;
    }
    catch (Exception ex) {
      throw new InvalidOperationException(
      String.Format(UnableToResolveController, 
        controllerType.Name), ex);
    }
    return controller;
  }
}

在新的控制器廠,有公共 StructureMap 容器設置的屬性,獲取基於對 StructureMap ObjectFactory (這在中配置圖 10 Global.asax 中)。

接下來,我不會某些類型檢查,然後使用 StructureMap 容器來解決基於提供的控制器的類型參數的當前控制器的 GetControllerInstance 方法的重寫。 因為我用的 StructureMap 自動註冊和掃描功能,在最初配置 StructureMap 時,沒有什麼別的東西我不得不做。

創建一個自訂控制器工廠的好處是你已經不再局限于無參數建構函式對您的控制器。 此時您可能會問自己,"如何我會走到控制器的建構函式的參數提供有關嗎?"DefaultControllerFactory 和 StructureMap 的可擴充性,因為你沒有。 當您為您的控制器聲明參數化的建構函式時,控制器在新的控制器廠解決時,將自動解析依賴項。

正如您看到的圖 11,已經加入 HomeController 的建構函式的 IEmployeeService 參數。 控制器解決在新的控制器工廠,自動解析該控制器的建構函式所需的任何參數。 這意味著您不需要添加代碼,以手動解決該控制器的依賴項 — — 但您仍可以使用假貨,正如前面討論過。

圖 11 解決控制器

public class HomeController : Controller {
  private readonly IEmployeeService _employeeService;
  public HomeController(IEmployeeService employeeService) {
    _employeeService = employeeService;
  }
  public ActionResult Index() {
    return View();
  }
  public ActionResult DisplaySalary(long id) {
    decimal salary = _employeeService.CalculateSalary(id);
    return View(salary);
  }
}

 

通過 ASP.NET MVC 應用程式中使用這些做法和技術,您就會自己定位更容易和更清潔的 TDD 過程。

Keith Burnell 是一名高級軟體工程師與天際線技術。他一直在開發軟體超過 10 年,從事大規模的 ASP.NET 和 ASP.NET MVC Web 網站開發。Burnell 是開發人員社區的積極成員,可以發現在他的博客 (dotnetdevdude.com) 和在 Twitter 上 twitter.com/keburnell

由於以下技術專家,檢討這篇文章: John Ptacek and Clark Sell