ASP.NET MVC 4 – 測試驅動 ASP.NET MVC

來源:互聯網
上載者:User
文章目錄
  • 測試驅動 ASP.NET MVC
測試驅動 ASP.NET MVC

Keith Burnell

下載程式碼範例

模型-視圖-控制器 (MVC) 模式的核心是將 UI 功能劃分成三個組成部分。模型表示您的領域的資料和行為。視圖管理模型的顯示並且處理與使用者的互動。控制器協調視圖和模型之間的互動。通過這樣將本質上就難於測試的 UI 邏輯與商務邏輯分離開來,使得使用 MVC 模式實現的應用程式非常易於測試。在本文中,我將論述用於增強您的 ASP.NET MVC 應用程式的可測試性的最佳做法和技術,包括如何建立您的解決方案的結構、設計代碼架構以便處理依賴關係注入以及使用 StructureMap 實現依賴關係注入。

建立您的解決方案的結構以便實現最高的可測試性

與每個開發人員都開始一個新的項目(即建立解決方案)相比,再沒有更好的方式 來開始我們的討論了。我將基於我在使用測試驅動開發 (TDD) 來開發大企業 ASP.NET MVC 應用程式方面的經驗,論述用於規劃您的 Visual Studio 解決方案的一些最佳做法。首先,我建議在建立 ASP.NET MVC 項目時使用空的項目模板。其他模板很適合於實驗或建立概念證明,但它們通常會包含許多會讓人分神且在真正的公司專屬應用程式程式中不必要的幹擾內容。

在您建立任何類型的複雜應用程式時,都應該使用 n 層方法。對於 ASP.NET MVC 應用程式開發,我建議使用在圖 1圖 2 中闡釋的方法,其中包含以下項目:

  • Web 項目包含所有特定於 UI 的代碼,包括視圖、視圖模型、指令碼和 CSS 等。該層只能訪問 Controllers、Service、Domain 和 Shared 項目。
  • Controllers 項目包含 ASP.NET MVC 使用的控制器類。該層與 Service、Domain 和 Shared 項目通訊。
  • Service 項目包含應用程式的商務邏輯。該層與 DataAccess、Domain 和 Shared 項目通訊。
  • DataAccess 項目包含用於檢索和操作驅動應用程式的資料的代碼。該層與 Domain 和 Shared 項目通訊。
  • Domain 項目包含應用程式使用的域項目,並且禁止與任何項目通訊。
  • Shared 項目包含可用於其他多個層的代碼,例如記錄程式、常量和其他常見工具 + 生產力代碼。僅允許該項目與 Domain 項目通訊。


圖 1 各層之間的互動


圖 2 解決方案結構樣本

我建議將您的控制器放置於一個單獨的 Visual Studio 項目中。有關如何輕鬆實現此建議的資訊,請參見 bit.ly/K4mF2B 上的部落格文章。通過將您的控制器放置於單獨的項目中,您可以進一步將處於控制器中的邏輯與 UI 代碼分離開來。結果就是您的 Web 項目僅包含真正與 UI 相關的代碼。

在哪裡放置您的測試專案 在哪裡放置您的測試專案以及如何對這些項目進行命名十分重要。在您開發複雜的、企業級應用程式時,解決方案往往會變得相當大,因此,很難在方案總管中定位代碼的特定類或部分。將多個測試專案添加到您的現有程式碼程式庫中只會導致在方案總管中進行導航更複雜。我強烈建議您將測試專案與實際的應用程式代碼從物理上分隔開來。我建議將所有測試專案都放置於解決方案層級的 Tests 檔案夾中。在單個解決方案檔案夾中定位您的所有測試專案和測試將會顯著減少預設解決方案資源管理員視圖中的幹擾內容,從而允許您輕鬆地定位您的測試。

接下來,您將要分離測試的類型。您的解決方案很可能將包含多種測試類型(單元、整合、效能、UI 等),因此,對每種測試類型進行隔離和分組十分重要。這不僅可以便於定位特定的測試類型,而且還使您可以輕鬆地運行某個特定類型的所有測試。如果您在使用最流行的 Visual Studio 高效工具套件 ReSharper (jetbrains.com/ReSharper) 或 CodeRush (devexpress.com/CodeRush) 中的一個,則會獲得一個操作功能表,該菜單允許您按右鍵方案總管中的任何檔案夾、項目或類,並且運行在該項中包含的所有測試。若要按測試類型對測試進行分組,請在 Tests 解決方案檔案夾內為您計劃編寫的每種測試類型都建立一個檔案夾。

圖 3 顯示了一個 Tests 解決方案檔案夾的樣本,其中包含多個測試類型檔案夾。


圖 3 Tests 解決方案檔案夾樣本

命名您的測試專案 測試專案的命名方式與測試專案的定位同樣重要。您希望能夠輕鬆地區分每個測試專案中待測試的應用程式部分以及項目包含的測試類型。因此,最好使用以下約定命名您的測試專案: [待測試專案的完整名稱].Test.[測試類型]。這使您可以迅速準確地確定待測試專案所處的層以及要執行的測試的類型。您可能會認為將測試專案放置於特定於類型的檔案夾中並且在測試專案的名稱中包含測試類型是多餘的,但請記住,解決方案檔案夾僅用於方案總管中,而不包含在專案檔的命名空間中。因此,儘管 Controllers 單元測試項目位於 Tests\Unit 解決方案檔案夾中,但命名空間 (TestDrivingMVC.Controllers.Test.Unit) 未反映該檔案夾結構。在命名項目時添加測試類型是很有必要的,可避免命名衝突以及確定您在編輯器內處理的測試類型。圖 4 顯示具有測試專案的方案總管。


圖 4 方案總管中的測試專案

介紹針對您的體繫結構的依賴關係注入

在您的待測試代碼中遇到依賴關係前,對 n 層應用程式進行的單元測試不會前進多遠。這些依賴關係可以是您的應用程式的其他層,或者可以完全處於您的代碼的外部(例如資料庫、檔案系統或 Web 服務)。在您撰寫單元測試時,需要正確處理此情況,並且在遇到外部依賴關係時使用 Test Double(類比、虛設或存根)。有關 Test Double 的詳細資料,請參考《MSDN 雜誌》2007 年 9 月號刊載的“探索 Test Double 的狀態集”(msdn.microsoft.com/magazine/cc163358)。但在您可以利用 Test Double 所提供的靈活性之前,必須對您的代碼進行設計,以便處理依賴關係的注入。

依賴關係注入 依賴關係注入是注入一個類所要求的具體實現(而不是直接執行個體化該依賴關係的類)的過程。使用類並不知道其任何依賴關係的實際具體實現,僅知道支援依賴關係的介面;具體實現由使用類或依賴關係注入架構提供。

依賴關係注入的目標是建立鬆散耦合程度高的代碼。通過鬆散耦合,您在撰寫單元測試時可以輕鬆地替換您的依賴關係的 Test Double 實現。

有三種主要方法可用於實現依賴關係注入:

  • 屬性注入
  • 建構函式注入
  • 使用依賴關係注入架構/控制容器反轉(自此以後稱作 DI/IoC 架構)

使用屬性注入,您公開對象上的公用屬性,以便能夠設定其依賴關係,如圖 5 中所示。此方法簡單明了並且不需要工具。

圖 5 屬性注入


  
  1. // Employee Service
  2. public class EmployeeService : IEmployeeService {
  3. private ILoggingService _loggingService;
  4. public EmployeeService() {}
  5. public ILoggingService LoggingService { get; set; }
  6. public decimal CalculateSalary(long employeeId) {
  7. EnsureDependenciesSatisfied();
  8. _loggingService.LogDebug(string.Format(
  9. "Calculating Salary For Employee: {0}", employeeId));
  10. decimal output = 0;
  11. /*
  12. * Complex logic that needs to be performed
  13. * in order to determine the employee's salary
  14. */
  15. return output;
  16. }
  17. private void EnsureDependenciesSatisfied() {
  18. if (_loggingService == null)
  19. throw new InvalidOperationException(
  20. "Logging Service dependency must be satisfied!");
  21. }
  22. }
  23. }
  24. // Employee Controller (Consumer of Employee Service)
  25. public class EmployeeController : Controller {
  26. public ActionResult DisplaySalary(long id) {
  27. EmployeeService employeeService = new EmployeeService();
  28. employeeService.LoggingService = new LoggingService();
  29. decimal salary = employeeService.CalculateSalary(id);
  30. return View(salary);
  31. }
  32. }

此方法有三個缺點。首先,它讓使用者負責提供依賴關係。其次,它要求您在對象中實現對代碼的保護,以便確保在使用前設定依賴關係。最後,隨著您的對象的依賴關係數目的增加,執行個體化對象所需的代碼量也將增加。

使用建構函式注入實現依賴關係注入涉及在執行個體化建構函式時通過其建構函式向某個類提供依賴關係,如圖 6 中所示。此方法也簡單明了,但與屬性注入不同,您可以確保始終設定該類的依賴關係。

圖 6 建構函式注入


  
  1. // Employee Service
  2. public class EmployeeService : IEmployeeService {
  3. private ILoggingService _loggingService;
  4. public EmployeeService(ILoggingService loggingService) {
  5. _loggingService = loggingService;
  6. }
  7. public decimal CalculateSalary(long employeeId) {
  8. _loggingService.LogDebug(string.Format(
  9. "Calculating Salary For Employee: {0}", employeeId));
  10. decimal output = 0;
  11. /*
  12. * Complex logic that needs to be performed
  13. * in order to determine the employee's salary
  14. */
  15. return output;
  16. }
  17. }
  18. // Consumer of Employee Service
  19. public class EmployeeController : Controller {
  20. public ActionResult DisplaySalary(long employeeId) {
  21. EmployeeService employeeService =
  22. new EmployeeService(new LoggingService());
  23. decimal salary = employeeService.CalculateSalary(employeeId);
  24. return View(salary);
  25. }
  26. }

遺憾的是,此方法仍要求使用者提供依賴關係。此外,它確實僅適合於小型應用程式。較大的應用程式通常具有過多的依賴關係,以致無法通過對象的建構函式提供它們。

實現依賴關係注入的第三種方法是使用 DI/IoC 架構。DI/IoC 架構完全消除了由使用者提供依賴關係的責任,並且允許您在設計時配置依賴關係、在運行時解析依賴關係。有許多可用於 .NET 的 DI/IoC 架構,包括 Unity(Microsoft 的產品)、StructureMap、Castle Windsor 和 Ninject 等。作為所有不同 DI/IoC 架構的基礎的概念是相同的,而選擇哪一種架構通常由個人偏好決定。為了在本文中示範 DI/IoC 架構,我將使用 StructureMap。

利用 StructureMap 讓依賴關係注入更上一層樓

StructureMap (structuremap.net) 是一種廣泛採用的依賴關係注入架構。您可以使用封裝管理員控制台 (Install-Package StructureMap) 或 NuGet 封裝管理員 GUI(按右鍵您的項目的引用檔案夾,然後選擇“管理 NuGet 程式包”)通過 NuGet 來安裝該架構。

使用 StructureMap 配置依賴關係 在 ASP.NET MVC 中實現 StructureMap 的第一步是配置您的依賴關係,以便 StructureMap 知道如何對它們進行解析。您可以通過以下兩種方法中的一種在 Global.asax 的 Application_Start 方法中配置依賴關係。

第一種方法是手動指示 StructureMap,對於特定的抽象實現,它應該使用特定的具體實現:


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

此方法的缺點是您必須手動註冊您的應用程式中的每個依賴關係,因此,對於大型應用程式而言,工作量可能會很大。此外,因為您在 ASP.NET MVC 網站的 Application_Start 中註冊依賴關係,因此,您的 Web 層必須直接知道綁定有依賴關係的應用程式的其他每個層。

您還可以使用 StructureMap 自動註冊和掃描功能自動檢查您的程式集和繫結相依性。通過此方法,StructureMap 將掃描您的程式集,並且在它遇到某一介面時,會尋找關聯的具體實現(基於一個概念,即依據慣例,名為 IFoo 的方法將映射到具體實現 Foo):


  
  1. ObjectFactory.Initialize(registry => registry.Scan(x => {
  2. x.AssembliesFromApplicationBaseDirectory();
  3. x.WithDefaultConventions();
  4. }));

StructureMap 依賴關係解決程式 在配置了您的依賴關係後,您需要能夠從您的程式碼程式庫訪問這些依賴關係。這是通過建立依賴關係解決程式並將其定位於 Shared 項目中來實現的(因為它將需要由具有依賴關係的所有應用程式層來訪問):


  
  1. public static class Resolver {
  2. public static T GetConcreteInstanceOf<T>() {
  3. return ObjectFactory.GetInstance<T>();
  4. }
  5. }

Resolver 類(我喜歡這麼稱呼它,因為 Microsoft 與 ASP.NET MVC 3 一起引入了 DependencyResolver 類,稍後我將討論它)是包含一個函數的簡單靜態類。該函數接受泛型參數 T,該參數表示為其尋找具體實現的介面;並且返回 T,這是傳入介面的實際實現。

在我跳轉到如何在您的代碼中使用新的 Resolver 類之前,我想要介紹一下為什麼我編寫了自己開發的依賴關係解決程式,而不是建立實現隨 ASP.NET MVC 3 引入的 IDependencyResolver 介面的類。包含 IDependencyResolver 功能是對 ASP.NET MVC 的很棒的補充,並且在促進正確的軟體行為方面取得了很大的進步。但遺憾的是,它駐留在 System.Web.MVC DLL 中,而我不希望在應用程式體繫結構的非 Web 層中具有對特定於 Web 技術的庫的引用。

解析代碼中的依賴關係 在完成了所有困難工作後,解析代碼中的依賴關係就很簡單了。您需要完成的全部工作就是調用 Resolver 類的靜態 GetConcreteInstanceOf 函數,並且將其傳遞給您在為其尋找具體實現的介面,如圖 7 中所示。

圖 7 解析代碼中的依賴關係


  
  1. public class EmployeeService : IEmployeeService {
  2. private ILoggingService _loggingService;
  3. public EmployeeService() {
  4. _loggingService =
  5. Resolver.GetConcreteInstanceOf<ILoggingService>();
  6. }
  7. public decimal CalculateSalary(long employeeId) {
  8. _loggingService.LogDebug(string.Format(
  9. "Calculating Salary For Employee: {0}", employeeId));
  10. decimal output = 0;
  11. /*
  12. * Complex logic that needs to be performed
  13. * in order to determine the employee's salary
  14. */
  15. return output;
  16. }
  17. }

利用 StructureMap 在單元測試中注入 Test Double 現在已完成了代碼的結構設計,因此,您可以注入依賴關係而無需來自使用者的介入,讓我們回到在單元測試中正確處理依賴關係這個最初的任務中來吧。它的具體情形是這樣的:

  • 該任務是使用 TDD 撰寫邏輯,以便產生要從 EmployeeService 的 CalculateSalary 方法返回的薪金值。(您將會在圖 7 中發現 EmployeeService 和 CalculateSalary 函數。)
  • 有一個要求,即必須記錄對 CalculateSalary 函數的所有調用。
  • 將定義針對日誌記錄服務的介面,但實現不完整。調用日誌記錄服務當前會引發一個異常。
  • 需要在針對日誌記錄服務的工作按計劃開始前完成該任務。

很有可能您在以前遇到過這種類型的情況。但現在,您具有了正確的體繫結構,能夠通過實施 Test Double 擺脫依賴關係的束縛。我喜歡在一個項目中建立可在我的所有測試專案中共用的 Test Double。如圖 8 中所示,我已在 Tests 解決方案檔案夾中建立了一個 Shared 項目。在該項目中,我添加了一個 Fakes 檔案夾,因為為了完成我的測試,我需要 ILoggingService 的虛設實現。


圖 8 用於共用測試代碼和虛設的項目

為日誌記錄服務建立虛設十分簡單。首先,我在 Fakes 檔案夾內建立了一個名為 LoggingServiceFake 的類。LoggingServiceFake 需要滿足 EmployeeService 預期的約定,這意味著它需要實現 ILoggingService 及其方法。按照定義,虛設是一種替代物,包含對滿足介面剛好足夠的代碼。通常,這意味著它具有 void 方法的空實現,並且函數實現包含返回寫入程式碼值的返回語句,如下所示:


  
  1. public class LoggingServiceFake : ILoggingService {
  2. public void LogError(string message, Exception ex) {}
  3. public void LogDebug(string message) {}
  4. public bool IsOnline() {
  5. return true;
  6. }
  7. }

現在已實現了虛設,我可以編寫測試了。開始時,我將在 TestDrivingMVC.Service.Test.Unit 單元測試項目中建立一個測試類別,按照前面所述的命名規範,我將其命名為 EmployeeServiceTest,如圖 9 中所示。

圖 9 EmployeeServiceTest 測試類別


  
  1. [TestClass]
  2. public class EmployeeServiceTest {
  3. private ILoggingService _loggingServiceFake;
  4. private IEmployeeService _employeeService;
  5. [TestInitialize]
  6. public void TestSetup() {
  7. _loggingServiceFake = new LoggingServiceFake();
  8. ObjectFactory.Initialize(x =>
  9. x.For<ILoggingService>().Use(_loggingServiceFake));
  10. _employeeService = new EmployeeService();
  11. }
  12. [TestMethod]
  13. public void CalculateSalary_ShouldReturn_Decimal() {
  14. // Arrange
  15. long employeeId = 12345;
  16. // Act
  17. var result =
  18. _employeeService.CalculateSalary(employeeId);
  19. // Assert
  20. result.ShouldBeType<decimal>();
  21. }
  22. }

大多數情況下,測試類別代碼非常簡單。您要特別注意的程式碼是:


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

這是在我們之前建立的 Resolver 類嘗試解析 ILoggingService 時指示 StructureMap 使用 LoggingServiceFake 的代碼。我將此代碼放置於用 TestInitialize 標記的方法中,這指示單元測試架構在測試類別中運行每個測試前都執行該方法。

通過使用功能強大的 DI/IoC 和 StructureMap 工具,我能夠完全擺脫日誌記錄服務的束縛。這樣做使我能夠在不受到日誌記錄服務狀態的影響下完成編碼和單元測試,並且編寫不依賴於任何依賴關係的真正的單元測試代碼。

使用 StructureMap 作為預設的控制器工廠 ASP.NET MVC 提供了一個擴充點,使您能夠添加在您的應用程式中執行個體化控制器的方式的自訂實現。通過建立從 DefaultControllerFactory 繼承的類(參見圖 10),您可以控制建立控制器的方式。

圖 10 自訂控制器工廠


  
  1. public class ControllerFactory : DefaultControllerFactory {
  2. private const string ControllerNotFound =
  3. "The controller for path '{0}' could not be found or it does not implement IController.";
  4. private const string NotAController = "Type requested is not a controller: {0}";
  5. private const string UnableToResolveController =
  6. "Unable to resolve controller: {0}";
  7. public ControllerFactory() {
  8. Container = ObjectFactory.Container;
  9. }
  10. public IContainer Container { get; set; }
  11. protected override IController GetControllerInstance(
  12. RequestContext context, Type controllerType) {
  13. IController controller;
  14. if (controllerType == null)
  15. throw new HttpException(404, String.Format(ControllerNotFound,
  16. context.HttpContext.Request.Path));
  17. if (!typeof (IController).IsAssignableFrom(controllerType))
  18. throw new ArgumentException(string.Format(NotAController,
  19. controllerType.Name), "controllerType");
  20. try {
  21. controller = Container.GetInstance(controllerType)
  22. as IController;
  23. }
  24. catch (Exception ex) {
  25. throw new InvalidOperationException(
  26. String.Format(UnableToResolveController,
  27. controllerType.Name), ex);
  28. }
  29. return controller;
  30. }
  31. }

在這個新的控制器工廠中,我具有一個公用的 StructureMap 容器屬性,它基於 StructureMap ObjectFactory 擷取集(在圖 10 的 Global.asax 中配置)。

接下來,我具有執行某種類型檢查的 GetControllerInstance 方法的替代方法,然後使用 StructureMap 容器基於提供的控制器型別參數解析當前控制器。因為我在最初配置 StructureMap 時使用了 StructureMap 自動註冊和掃描功能,所以無需執行任何其他動作。

建立自訂控制器工廠的好處在於,對於您的控制器,不再局限於無參數建構函式。此時您可能會有這樣的疑問:“我如何向控制器的建構函式提供參數呢?”。藉助於 DefaultControllerFactory 和 StructureMap 的可擴充性,您不必提供參數。當您為控制器聲明參數化的建構函式時,將在新的控制器工廠中解析控制器時自動解析依賴關係。

圖 11 中所示,我已將一個 IEmployeeService 參數添加到了 HomeController 的建構函式。在新的控制器工廠中解析控制器時,將自動解析該控制器的建構函式所要求的所有參數。這意味著您無需手動添加代碼來解析控制器的依賴關係 — 但您仍可以按照前述內容來使用虛設。

圖 11 解析控制器


  
  1. public class HomeController : Controller {
  2. private readonly IEmployeeService _employeeService;
  3. public HomeController(IEmployeeService employeeService) {
  4. _employeeService = employeeService;
  5. }
  6. public ActionResult Index() {
  7. return View();
  8. }
  9. public ActionResult DisplaySalary(long id) {
  10. decimal salary = _employeeService.CalculateSalary(id);
  11. return View(salary);
  12. }
  13. }

通過在您的 ASP.NET MVC 應用程式中使用這些實踐和技術,整個 TDD 過程將更加輕鬆和簡明。

Keith Burnell 是 Skyline Technologies 的進階軟體工程師。 他從事軟體開發工作已經 10 多年了,並專門從事大規模的 ASP.NET 和 ASP.NET MVC 網站開發。 Burnell 積极參与開發人員社區,您可以訪問 dotnetdevdude.com 查看他的部落格,或訪問 twitter.com/keburnell 查看他的微博

原文: http://msdn.microsoft.com/zh-cn/magazine/jj190803.aspx

衷心感謝以下技術專家對本文的審閱: John Ptacek 和 Clark Sell

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.