ASP.NET MVC/Ninject/Moq

  6.1 示例項目

  項目結構:

  

  6.1.1 創建模型類

  Product.cs;計算Product對象集合的總價類LinqValueCalculator.cs;表示Product集合,並使用LinqValueCalculator來確定總價的類ShoppingCart.cs

 1     public class Product
 2     {
 3         public int ProductID { get; set; }
 4         public string Name { get; set; }
 5         public string Description { get; set; }
 6         public decimal Price { get; set; }
 7         public string Category { get; set; }
 8     }
 9 
10     /// <summary>
11     /// 計算Product對象集合總價
12     /// </summary>
13     public class LinqValueCalculator
14     {
15         public decimal ValueProducts(IEnumerable<Product> products)
16         {
17             return products.Sum(p => p.Price);
18         }
19     }
20 
21     /// <summary>
22     /// 表示Product集合,並使用LinqValueCalculator來確定總價
23     /// </summary>
24     public class ShoppingCart
25     {
26         private LinqValueCalculator calc;
27 
28         public ShoppingCart(LinqValueCalculator calcParam)
29         {
30             calc = calcParam;
31         }
32 
33         public IEnumerable<Product> Products { get; set; }
34         public decimal CalculateProductTotal()
35         {
36             return calc.ValueProducts(Products);
37         }
38     }
View Code

  6.1.2 添加控制器

 1     public class HomeController : Controller
 2     {
 3         private Product[] products =
 4         {
 5             new Product {Name = "Kayak", Category = "Watersports", Price = 275M},
 6             new Product{Name = "Lifejacket", Category = "Watersports", Price = 48.95M},
 7             new Product{Name = "Soccer ball", Category = "Soccer", Price = 19.50M},
 8             new Product{Name = "Corner flag", Category = "Soccer", Price = 34.95M}
 9         };
10 
11         // GET: Home
12         public ActionResult Index()
13         {
14             LinqValueCalculator calc=new LinqValueCalculator();
15             ShoppingCart cart=new ShoppingCart(calc){Products = products};
16             decimal totalValue = cart.CalculateProductTotal();
17 
18             return View(totalValue);
19         }
20     }
View Code

  6.1.3  添加視圖

 1 @model decimal
 2 @{
 3     Layout = null;
 4 }
 5 
 6 <!DOCTYPE html>
 7 
 8 <html>
 9 <head>
10     <meta name="viewport" content="width=device-width" />
11     <title>Value</title>
12 </head>
13 <body>
14 <div>
15     Total value is $@Model
16 </div>
17 </body>
18 </html>
View Code

  6.2 使用Ninject

  DI的思想是,對MVC應用程序中的組件進行解耦,這是通過接口與DI容器相結合來實現的。DI容器創建了對象實例,這是通過創建對象所依賴的接口並將其注入構造器而實現 的。

  6.2.1 理解問題

  上述實例中,它含有用DI解決的基本問題:緊耦合類。ShoppingCart類與LinqValueCalculator類是緊耦合的,而HomeController類與ShoppingCart和LinqValueCalculator都是緊耦合的。

  這意味着,如果替換LinqValueCalculator類,就必須在與它有緊耦合關係的累中找出對它的引用,並進行修改。

  運用接口

  通過使用C#接口,從計算器的實現中抽象出其功能定義,我們可以解決部分問題。在Models文件夾中添加一個IValueCalculator.cs類文件。

1     public interface IValueCalculator
2     {
3         decimal ValueProducts(IEnumerable<Product> products);
4     }
View Code

  在LinqValueCalculator類中實現這一接口

 1    /// <summary>
 2     /// 計算Product對象集合總價
 3     /// </summary>
 4     public class LinqValueCalculator: IValueCalculator
 5     {
 6         public decimal ValueProducts(IEnumerable<Product> products)
 7         {
 8             return products.Sum(p => p.Price);
 9         }
10     }
View Code

  該接口可以打斷ShoppingCart與LinqValueCalculator類之間的緊耦合關係

 1     /// <summary>
 2     /// 表示Product集合,並使用LinqValueCalculator來確定總價
 3     /// </summary>
 4     public class ShoppingCart
 5     {
 6         private IValueCalculator calc;
 7 
 8         public ShoppingCart(IValueCalculator calcParam)
 9         {
10             calc = calcParam;
11         }
12 
13         public IEnumerable<Product> Products { get; set; }
14         public decimal CalculateProductTotal()
15         {
16             return calc.ValueProducts(Products);
17         }
18     }
View Code

  上述過程已經解除了ShoppingCart與ValueCalculator之間的耦合,因爲在使用ShoppingCart時,只要爲其構造器傳遞一個IValueCalculator接口對象就行了,於是,ShoppingCart類與IValueCalculator的實現類不再有直接聯繫,但是C#要求在接口實例化時要指定其實現類,這很好理解,因爲它需要指定你想要用的是哪一個實現類。這意味着,Home控制器在創建LinqValueCalculator對象時仍有問題,如下HomeController.cs文件中的Index方法所示:

1         public ActionResult Index()
2         {
3             //LinqValueCalculator calc=new LinqValueCalculator();//1,原始的:緊耦合
4             IValueCalculator calc = new LinqValueCalculator();//2,依賴接口:緊耦合
5             ShoppingCart cart=new ShoppingCart(calc){Products = products};
6             decimal totalValue = cart.CalculateProductTotal();
7 
8             return View(totalValue);
9         }
View Code

  使用Ninject的目的就是解決此問題,用以對IValueCalculator接口的實現進行實例化,但所需要的實現細節不是Home控制器代碼的一部分(意即,通過Ninject,可以去掉上述Index方法中IValueCalculator這一行,這項工作由Ninject完成,這樣便去掉了Home控制器與總價計算器LinqValueCalculator直接的耦合)。

  這意味着告訴Ninject,LinqValueCalculator是你希望它用於IValueCalculator接口的實現,並且要修改HomeController類,以使它能夠通過Ninject而不是new關鍵字來獲取對象。

  6.2.2 將Ninject添加到Visual Studio項目

  使用“Package Manager Console(包管理控制檯)”,輸入以下命令。

  Install-Package Ninject -version 3.0.1.10

  Install-Package Ninject.Web.Common -version 3.0.0.7

  Install-Package Ninject.MVC3 -Version 3.0.0.6

  第一行用於安裝Ninject內核包,其他命令用於安裝內核的擴展包,以使Ninject能更好的與ASP.NET協同工作。

  6.2.3 Ninject初步

  爲了得到Ninject的基本功能,要做的工作分3個階段,下面首先對Home控制器做修改。

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Web;
 5 using System.Web.Mvc;
 6 using EssentialTools.Models;
 7 using Ninject;//引入Ninject命名控件
 8 
 9 namespace EssentialTools.Controllers
10 {
11     public class HomeController : Controller
12     {
13         private Product[] products =
14         {
15             new Product {Name = "Kayak", Category = "Watersports", Price = 275M},
16             new Product{Name = "Lifejacket", Category = "Watersports", Price = 48.95M},
17             new Product{Name = "Soccer ball", Category = "Soccer", Price = 19.50M},
18             new Product{Name = "Corner flag", Category = "Soccer", Price = 34.95M}
19         };
20 
21         // GET: Home
22         public ActionResult Index()
23         {
24             //LinqValueCalculator calc=new LinqValueCalculator();//方式1,原始的:緊耦合
25             //IValueCalculator calc = new LinqValueCalculator();//方式2,依賴接口:緊耦合
26             IKernel ninjectKernel=new StandardKernel();//方式3,使用Ninject
27             ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
28             IValueCalculator calc = ninjectKernel.Get<IValueCalculator>();
29             ShoppingCart cart=new ShoppingCart(calc){Products = products};
30             decimal totalValue = cart.CalculateProductTotal();
31 
32             return View(totalValue);
33         }
34     }
35 }
View Code

  第一個階段是準備使用Ninject。爲此,創建一個Ninject的內核(Kernel)實例,該實例是一個對象(內核對象),它負責解析依賴項並創建新的對象(爲依賴項創建的對象)。當需要一個對象時,將使用這個內核而不是使用new關鍵字。

  IKernel ninjectKernel=new StandardKernel();//創建內核

  我們需要創建一個Ninject.IKernel接口的實現,可通過創建一個StandardKernel類的新實例來完成。對Ninject進行擴展和定製,可以使用不同種類的內核,但本章只需要這個內置的StandardKernel(標準內核)。

  第二階段是配置Ninject內核,使其理解我們用到的每一個接口所希望使用的實現對象。以下是清單中完成這項工作的語句:

  ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>();

  Ninject使用C#的類型參數創建了一種關係:將想要使用的接口設置爲Bind方法的類型參數,並在其返回的結果上調用To方法。將希望實例化的實現類設置爲To方法的類型參數。該語句告訴Ninject,IValueCalculator接口的依賴項應該通過創建LinqValueCalculator類的實例進行解析。最後一個步驟是使用Ninject來創建一個對象,其做法是調用內核的Get方法,如下所示:

  IValueCalculator calc=ninjectKernel.Get<IValueCalculator>();

  Get方法所使用的類型參數告訴Ninject,我們感興趣的是哪一個接口,而該方法的結果是剛纔用To方法指定的實現類型的一個實例。

  6.2.4 建立MVC的依賴項注入

  1,創建依賴項解析器

  MVC框架需要使用依賴項解析器來創建類的實例,以便對請求進行服務。通過創建自定義解析器,便能保證MVC框架在任何時候都能使用Ninject創建一個對象——包括控制器實例。新建Infrastructure文件夾,用於放置MVC應用程序中不適合放在其他文件夾的類。添加類NinjectDependencyResolver.cs,

 1     public class NinjectDependencyResolver:IDependencyResolver
 2     {
 3         private IKernel kernel;
 4 
 5         public NinjectDependencyResolver(IKernel kernelParam)
 6         {
 7             kernel = kernelParam;
 8             AddBindings();
 9         }
10         public object GetService(Type serviceType)
11         {
12             return kernel.TryGet(serviceType);
13         }
14 
15         public IEnumerable<object> GetServices(Type serviceType)
16         {
17             return kernel.GetAll(serviceType);
18         }
19 
20         private void AddBindings()
21         {
22             kernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
23         }
24     }
View Code

  NinjectDependencyResolver類實現了IDependencyResolver接口,它屬於System.Mvc命名空間,也由MVC框架用於獲取其所需的對象。MVC卷國家在需要實例以便對一個傳入的請求進行服務時,會調用GetService或GetServices方法。依賴項解析器要做的便是創建這一實例——這是一項要通過調用Ninject的TryGet和GetAll方法來完成的任務。TryGet方法的工作方式類似與前面所用的Get方法,但當沒有合適的綁定時,它會返回null,而不是拋出一個異常。GetAll方法支持對單一類型的多個綁定,當有多個不同的服務提供器可用時,可以使用它。

  2,註冊依賴項解析器

  使用NuGet添加的Ninject包在App_Start文件夾中創建了一個名稱爲NinjectWebCommon.cs的文件,它定義了應用程序啓動時會自動調用的一些方法,目的是將它們集成到ASP.NET的請求生命週期中。在NinjectWebCommon類的RegisterServices方法中,添加一條語句,用於創建一個NinjectDependencyResolver類的實例,並用System.Web.Mvc.DependencyResolver類定義的SetResolver靜態方法將其註冊爲MVC框架的解析器,該語句的作用是爲了在Ninject和MVC框架之間創建一個支持DI的橋樑。

1         private static void RegisterServices(IKernel kernel)
2         {
3             System.Web.Mvc.DependencyResolver.SetResolver(new EssentialTools.Infrastructure.NinjectDependencyResolver(kernel));
4         } 
View Code

  3,重構Home控制器  

 1     public class HomeController : Controller
 2     {
 3         private IValueCalculator calc;
 4         private Product[] products =
 5         {
 6             new Product {Name = "Kayak", Category = "Watersports", Price = 275M},
 7             new Product{Name = "Lifejacket", Category = "Watersports", Price = 48.95M},
 8             new Product{Name = "Soccer ball", Category = "Soccer", Price = 19.50M},
 9             new Product{Name = "Corner flag", Category = "Soccer", Price = 34.95M}
10         };
11 
12         public HomeController(IValueCalculator calcParam,IValueCalculator calc2)
13         {
14             calc = calcParam;
15 
16         }
17         // GET: Home
18         public ActionResult Index()
19         {
20             //LinqValueCalculator calc=new LinqValueCalculator();//1,原始的:緊耦合
21             //IValueCalculator calc = new LinqValueCalculator();//2,依賴接口:緊耦合
22             //IKernel ninjectKernel=new StandardKernel();//3,創建內核
23             //ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
24             //IValueCalculator calc = ninjectKernel.Get<IValueCalculator>();
25             ShoppingCart cart=new ShoppingCart(calc){Products = products};
26             decimal totalValue = cart.CalculateProductTotal();
27 
28             return View(totalValue);
29         }
30     }
View Code

  所做的主要修改是添加了一個類構造器,用於接收IValueCalculator接口的實現,即修改HomeController類,使其聲明一個依賴項。Ninject會在創建該控制器實例時,使用建立起來的配置,爲該控制器創建一個實現IValueCalculator接口的對象。

  所做的另一個修改是從控制器中刪除了任何關於Ninject或LinqValueCalculator類的代碼。最終,我們打破了HomeController與LinqValueCalculator類之間的緊耦合。

  以上創建的是一個構造器注入示例,這是依賴項注入的一種形式。以下是運行示例應用程序,且Internet Explorer對應用程序的跟URL發送請求時所發生的情況。

  • 瀏覽器向MVC框架發送一個請求Home的URL,MVC框架推猜出該請求意指Home控制器,於是創建HomeController類示例。
  • MVC框架在創建HomeController類示例過程中會發現其構造器有一個對IValueCalculator接口的依賴項,於是會要求依賴項解析器對此依賴項進行解析,將該接口指定爲依賴項解析器中GetService方法所使用的類型參數。
  • 依賴項解析器會將傳遞過來的類型參數交給TryGet方法,要求Ninject創建一個新的HomeController接口類實例。
  • Ninject會檢測到HomeController構造器與其實現類LinqValueCalculator具有綁定關係,於是爲該接口創建一個LinqValueCalculator類實例,並將其回遞給依賴項解析器。
  • 依賴項解析器將Ninject所返回的LinqValueCalculator類作爲IValueCalculator接口實現類實例回遞給MVC框架。
  • MVC框架利用依賴項解析器返回的接口類實例創建HomeController控制器實例,並使用該控制器實例對請求進行服務。

  這裏所採取的辦法其好處之一是,任何控制器都可以在應用程序中聲明一個解析器,並由MVC框架使用Ninject來實現。

  所得到的最大好處是,在希望用另一個實現來替代LinqValueCalculator時,只需要對依賴項解析器類進行修改。爲了滿足對於IValueCalculator接口的依賴項,這裏是唯一一處需要爲該接口指定實現類的地方。

  6.2.5 創建依賴項鍊

  當要求Ninject創建一個類型時,它會檢查該類型所聲明的依賴項。它也會檢查這些依賴項是否依賴於其他類型,如果有額外的依賴項,Ninject會自動地解析這些依賴項,並創建所需要的所有類的實例,以這種方式處理依賴項鍊,最終便能夠創建所需類型的實例(本段描述了Ninject處理依賴項鍊的工作方式)。

  在Models文件夾中添加一個Discount.cs類,並用它定義了一個新的接口及其實現類,

 1     public interface IDiscountHelper
 2     {
 3         decimal ApplyDiscount(decimal totalParam);
 4     }
 5 
 6     public class DefaultDiscountHelper : IDiscountHelper
 7     {
 8         //public decimal DiscountSize { get; set; }
 9         public decimal discountSize;
10 
11         public DefaultDiscountHelper(decimal discountParam)
12         {
13             discountSize = discountParam;
14         }
15         public decimal ApplyDiscount(decimal totalParam)
16         {
17             return (totalParam - (discountSize / 100m * totalParam));
18         }
19     }
View Code

  我們修改LinqValueCalculator類,以使它執行計算時使用IDiscountHelper接口,

 1     /// <summary>
 2     /// 計算Product對象集合總價
 3     /// </summary>
 4     public class LinqValueCalculator: IValueCalculator
 5     {
 6         private IDiscountHelper discounter;
 7         public LinqValueCalculator(IDiscountHelper discountParam)
 8         {
 9             discounter = discountParam;
10         }
11         public decimal ValueProducts(IEnumerable<Product> products)
12         {
13             //return products.Sum(p => p.Price);
14             return discounter.ApplyDiscount(products.Sum(p => p.Price));
15         }
16     }
View Code

  如同對IValueCalculator所做的那樣,在NinjectDependencyResolver類中用Ninject內核將IDiscountHelper接口與其實現類進行綁定,

1         private void AddBindings()
2         {
3             kernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
4             kernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>();
5         }
View Code

  上述這一做法已經創建了一個依賴項鍊。此時Home控制器依賴於IValueCalculator接口,我們已經告訴Ninject用LinqValueCalculator類對該接口進行解析。LinqValueCalculator類又依賴於IDiscountHelper接口,我們又告訴Ninject用DefaultDiscountHelper類對其進行解析。

  Ninject能夠平滑地解析這種依賴項鍊,創建所需的對象爲每一個依賴項進行解析,於是最終能夠創建一個HomeController類的實例,從而對一個HTTP請求進行服務。

  6.2.6 指定屬性和構造器參數值

  在將接口與其實現進行綁定時,可以爲屬性提供一些值方面的細節,以便對Ninject創建的對象進行配置。修改DefaultDiscountHelper類,以使它定義一個DiscountSize屬性,將其用於計算折扣量,

 1     public interface IDiscountHelper
 2     {
 3         decimal ApplyDiscount(decimal totalParam);
 4     }
 5 
 6     public class DefaultDiscountHelper : IDiscountHelper
 7     {
 8         public decimal DiscountSize { get; set; }
 9 
10         public decimal ApplyDiscount(decimal totalParam)
11         {
12             return (totalParam - (DiscountSize / 100m * totalParam));
13         }
14     }
View Code

  在告訴Ninject一個接口需要使用的是哪一個類時,可以用WithPropertyValue方法爲DefaultDiscountHelper類中的DiscountSize屬性設置一個值。修改NinjectDependencyResolver類中的AddBindings,需要設置的屬性名稱是以字符串形式提供的。

1         private void AddBindings()
2         {
3             kernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
4             kernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>().WithPropertyValue("DiscountSize",50M);
5         }
View Code

  如果需要設置多個屬性值,可以鏈接調用WithPropertyValue方法,以涵蓋所有這些屬性,也可以用構造器參數做同樣的事情。重寫DefaultDiscountHelper,以使折扣大小作爲構造器參數進行傳遞。

 1     public interface IDiscountHelper
 2     {
 3         decimal ApplyDiscount(decimal totalParam);
 4     }
 5 
 6     public class DefaultDiscountHelper : IDiscountHelper
 7     {
 8         //public decimal DiscountSize { get; set; }
 9         public decimal discountSize;
10 
11         public DefaultDiscountHelper(decimal discountParam)
12         {
13             discountSize = discountParam;
14         }
15         public decimal ApplyDiscount(decimal totalParam)
16         {
17             return (totalParam - (discountSize / 100m * totalParam));
18         }
19     }
View Code

  爲了綁定這個類,可以在AddBindings方法中用WithConstructorArgument方法來指定構造器參數的值。

1         private void AddBindings()
2         {
3             kernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
4             kernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>().WithConstructorArgument("discountParam", 50M);
5         }
View Code

  6.2.7 使用條件綁定

  Ninject支持多個條件的綁定方法,這讓我們能夠指定內核用哪一個類對某一特定的接口進行相應。在Models文件夾中添加FlexibleDiscountHelper.cs,

1     public class FlexibleDiscountHelper : IDiscountHelper
2     {
3         public decimal ApplyDiscount(decimal totalParam)
4         {
5             decimal discount = totalParam > 100 ? 70 : 25;
6             return (totalParam - (discount / 100m * totalParam));
7         }
8     }
View Code

  這個類會根據總額大小運用不同的折扣,於是我們需要對IDiscountHelper接口的實現類進行選擇,這可以修改NinjectDependencyResolver的AddBindings方法,

1         private void AddBindings()
2         {
3             kernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
4             //kernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>().WithPropertyValue("DiscountSize",50M);
5             kernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>().WithConstructorArgument("discountParam", 50M);
6             kernel.Bind<IDiscountHelper>().To<FlexibleDiscountHelper>().WhenInjectedInto<LinqValueCalculator>();
7         }
View Code

  上述新綁定聲明,在Ninject內核要創建一個LinqValueCalculator對象時,應該使用FlexibleDiscountHelper類作爲IDiscountHelper接口的實現。注意,我們在適當的位置留下了對IDiscountHelper的原有綁定。Ninject會嘗試找出具有最佳匹配,而且這有助於對同一個類或接口採用一個默認綁定,以便在條件判據不能滿足時,讓Ninject能夠進行回滾。Ninject有許多不同的條件綁定方法,最有用的一些條件綁定如下,

  6.2.8 設置對象作用域

  最後一個Ninject特性有助於調整Ninject所建對象的生命週期,以滿足應用程序的需求。默認情況下,Ninject會在每次請求一個對象時,爲每個依賴項所需的各個對象創建一個新實例。

  修改LinqValueCalculator類的構造器,以便在每次創建一個新實例時都向Visual Studio的輸出窗口寫一條消息,

 1     public class LinqValueCalculator: IValueCalculator
 2     {
 3         private IDiscountHelper discounter;
 4         private static int counter = 0;
 5         public LinqValueCalculator(IDiscountHelper discountParam)
 6         {
 7             discounter = discountParam;
 8             System.Diagnostics.Debug.WriteLine(string.Format("Instance {0} created",++counter));
 9         }
10         public decimal ValueProducts(IEnumerable<Product> products)
11         {
12             //return products.Sum(p => p.Price);
13             return discounter.ApplyDiscount(products.Sum(p => p.Price));
14         }
15     }
View Code

  System.Diagnostics.Debug類包含了一些用來寫出調試信息的方法,修改Home控制器,它要求從Ninject獲得theIValueCalculator接口的兩個實現。

1         public HomeController(IValueCalculator calcParam,IValueCalculator calc2)
2         {
3             calc = calcParam;
4         }
View Code

  我們並未用Ninject提供的對象執行任何有用的任務——重要的是請求了該接口的兩個實現。

  對於某些類,我們會希望在整個應用程序中共享一個單一的實例。而對於另一些類,又會希望ASP.NET平臺多接受到的每個HTTP請求,都創建一個新的實例。Ninject讓我們能夠使用一種叫做“作用域(Scope)”的特性來控制所創建對象的生命週期,這是在建立接口與其實現之間的綁定時,通過方法電泳來表示的。如下代碼可以看出如何將最有用的作用域運用於MVC框架的應用程序:在NinjectDependencyResolver中將“請求作用域(Request Scope)”運用於LinqValueCalculator類(注意,在以下代碼中,對InRequestScope方法的調用就是運用“請求作用域”)。

 1  public class NinjectDependencyResolver:IDependencyResolver
 2     {
 3         private IKernel kernel;
 4 
 5         public NinjectDependencyResolver(IKernel kernelParam)
 6         {
 7             kernel = kernelParam;
 8             AddBindings();
 9         }
10         public object GetService(Type serviceType)
11         {
12             return kernel.TryGet(serviceType);
13         }
14 
15         public IEnumerable<object> GetServices(Type serviceType)
16         {
17             return kernel.GetAll(serviceType);
18         }
19 
20         private void AddBindings()
21         {
22             //kernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
23             kernel.Bind<IValueCalculator>().To<LinqValueCalculator>().InRequestScope();//請求作用域
24             //kernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>().WithPropertyValue("DiscountSize",50M);
25             kernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>().WithConstructorArgument("discountParam", 50M);
26             kernel.Bind<IDiscountHelper>().To<FlexibleDiscountHelper>().WhenInjectedInto<LinqValueCalculator>();
27         }
28     }
View Code

  InRequestScope擴展方法屬於Ninject.Web.Common命名空間,這是告訴Ninject,對於ASP.NET所接收到的每一個請求,應該只創建LinqValueCalculator類的一個實例。每一個請求都會獲取各自獨立的對象,但同一個請求中的多個依賴項將會用這個類的單一實例進行解析。啓動應用程序,會看到Ninject僅創建一個LinqValueCalculator實例。如果刷新瀏覽器窗口但未重啓應用程序,則會看到Ninject創建了第二個對象。Ninject提供了一系列不同的對象作用域,如下:

  6.3 Visual Studio的單元測試

  首先,對實例項目添加一個IDiscountHelper接口的新實現。在Models文件夾中創建一個名爲MinimumDiscountHelper.cs的新聞界,如下,

 

1     public class MinimumDiscountHelper : IDiscountHelper
2     {
3         public decimal ApplyDiscount(decimal totalParam)
4         {
5              throw new ArgumentOutOfRangeException();
6         }
7     }
View Code

 

  此例的目標是讓該類演示以下行爲:

  • 總額大於$100,折扣爲10%
  • 總額介於(幷包括)$10到$100之間,折扣爲$5
  • 總額小於$10,無折扣
  • 總額爲負值時,拋出一個ArgumentOutOfRangeException異常。

  MinimumDiscountHelper尚未實現上述行爲,我們將遵循測試驅動開發(TDD)單元測試,並隨之編寫實現代碼。

  6.3.1 創建單元測試項目

  

  添加對EssentialTools項目的引用

   6.3.2 添加單元測試

  修改UnitTest1,如下,

 1     [TestClass]
 2     public class UnitTest1
 3     {
 4         private IDiscountHelper getTestObject()
 5         {
 6             return new MinimumDiscountHelper();
 7         }
 8 
 9         [TestMethod]
10         public void Discount_Above_100()
11         {
12             //準備
13             IDiscountHelper target = getTestObject();
14             decimal total = 200;
15 
16             //動作
17             var discountedTotal = target.ApplyDiscount(total);
18 
19             //斷言
20             Assert.AreEqual(total * 0.9m, discountedTotal);
21         }
22     }
View Code

  提示:測試類只是一種規則的C#類,它並不具備關於MVC項目的特別含義。TestClass和TestMethod註解屬性纔是讓項目具備測試魔力的關鍵因素。

  Assert類有一系列可以在測試中使用的靜態方法。如下:

  提示:Microsoft.VisualStudio.TestTools.UnitTesting命名空間中一個值得注意的成員是ExpectedException屬性。這是一個斷言,只當單元測試拋出ExceptionType參數指定類型的異常時,該斷言纔是成功的。這是一種確保單元測試拋出異常的整潔方式,而不需要在單元測試中構造try...catch塊。

  修改UnitTest1.cs,如下,

 1     [TestClass]
 2     public class UnitTest1
 3     {
 4         private IDiscountHelper getTestObject()
 5         {
 6             return new MinimumDiscountHelper();
 7         }
 8 
 9         [TestMethod]
10         public void Discount_Above_100()
11         {
12             //準備
13             IDiscountHelper target = getTestObject();
14             decimal total = 200;
15 
16             //動作
17             var discountedTotal = target.ApplyDiscount(total);
18 
19             //斷言
20             Assert.AreEqual(total * 0.9m, discountedTotal);
21         }
22 
23         [TestMethod]
24         public void Discount_Between_10_And_100()
25         {
26             //準備
27             IDiscountHelper target = getTestObject();
28 
29             //動作
30             decimal TenDollarDiscount = target.ApplyDiscount(10);
31             decimal HandredDollarDiscount = target.ApplyDiscount(100);
32             decimal FiftyDollarDiscount = target.ApplyDiscount(50);
33 
34             //斷言
35             Assert.AreEqual(5, TenDollarDiscount, "$10 discount iswrong");
36             Assert.AreEqual(95, HandredDollarDiscount, "$100 discount iswrong");
37             Assert.AreEqual(45, FiftyDollarDiscount, "$50 discount iswrong");
38         }
39 
40         [TestMethod]
41         public void Discount_Less_Than_10()
42         {
43             //準備
44             IDiscountHelper target = getTestObject();
45 
46             //動作
47             decimal discount5 = target.ApplyDiscount(5);
48             decimal discount0 = target.ApplyDiscount(0);
49 
50             //斷言
51             Assert.AreEqual(5, discount5);
52             Assert.AreEqual(0, discount0);
53         }
54 
55         [TestMethod]
56         [ExpectedException(typeof(ArgumentOutOfRangeException))]
57         public void Discount_Negative_Total()
58         {
59             //準備
60             IDiscountHelper target = getTestObject();
61 
62             //動作
63             target.ApplyDiscount(-1);
64         }
65     }
View Code

  6.3.3 運行單元測試(並失敗)

  6.3.4 實現特性

  實現MinimumDiscountHelper類,如下,

 1     public class MinimumDiscountHelper : IDiscountHelper
 2     {
 3         public decimal ApplyDiscount(decimal totalParam)
 4         {
 5             if (totalParam < 0)
 6             {
 7                 throw new ArgumentOutOfRangeException();
 8             }
 9             else if (totalParam > 100)
10             {
11                 return totalParam * 0.9M;
12             }
13             else if (totalParam >= 10 && totalParam <= 100)
14             {
15                 return totalParam - 5;
16             }
17             else
18             {
19                 return totalParam;
20             }
21         }
22     }
View Code

  6.3.5 測試並修正代碼

  6.4 使用Moq庫

  上述實例是不依賴於其他類而起作用的單一類。實際項目中,往往還需要測試一些不能孤立運行的對象。在這種情況下,需要將注意力集中於感興趣的類或方法上,才能是你不必對依賴類也進行隱式測試。

  一個有用的辦法是使用模仿對象,它能夠以一種特殊而受控的方式來模擬項目中實際對象的功能。模仿對象能夠讓你縮小測試的側重點,以使你只檢查感興趣的功能。

  6.4.1 理解問題

  在開始使用Moq庫之前,演示一個視圖要修正的問題。在本小節中,打算對LinqValueCalculator類進行單元測試,這個類是在實例項目的Models文件夾中定義的。如下:

 1     public class LinqValueCalculator: IValueCalculator
 2     {
 3         private IDiscountHelper discounter;
 4         private static int counter = 0;
 5         public LinqValueCalculator(IDiscountHelper discountParam)
 6         {
 7             discounter = discountParam;
 8             System.Diagnostics.Debug.WriteLine(string.Format("Instance {0} created",++counter));
 9         }
10         public decimal ValueProducts(IEnumerable<Product> products)
11         {
12             //return products.Sum(p => p.Price);
13             return discounter.ApplyDiscount(products.Sum(p => p.Price));
14         }
15     }
View Code

  爲測試該類,添加一個新的單元測試類,UnitTest2.cs。

 1     [TestClass]
 2     public class UnitTest2
 3     {
 4         private Product[] products =
 5         {
 6             new Product {Name = "Kayak", Category = "Watersports", Price = 275M},
 7             new Product {Name = "Lifejacket", Category = "Watersports", Price = 48.95M},
 8             new Product {Name = "Soccer ball", Category = "Soccer", Price = 19.50M},
 9             new Product {Name = "Corner flag", Category = "Soccer", Price = 34.95M}
10         };
11         [TestMethod]
12         public void Sum_Products_Correctly()
13         {
14             //準備
15             var discounter=new MinimumDiscountHelper();
16             var target=new LinqValueCalculator(discounter);
17             var goalTotal = products.Sum(e => e.Price);
18 
19             //動作
20             var result = target.ValueProducts(products);
21 
22             //斷言
23             Assert.AreEqual(goalTotal,result);
24         }
25     }
View Code

  要面臨的問題是,LinqValueCalculator類依賴於IDiscountHelper接口的實現才能進行操作。在此例中,使用了MinimumDiscountHelper類(這是IDiscountHelper接口的實現類,它表現出以下兩個不同的問題。

  一,已經是單元測試變得複雜和脆弱。爲了創建一個能夠進行工作的單元測試,需要考慮IDiscountHelper實現中的折扣邏輯,以便判斷出ValueProducts方法的預期值。脆弱來自這樣一個事實:一旦該實現中的折扣邏輯發生變化,即使LinqValueCalculator類可以很好的正常工作,測試仍會失敗。

  二,也是最令人擔憂的問題,已經延展了這一單元測試的範圍,使它隱式的包含了MinimumDiscountHelper類。當單元測試失敗時,不易知道問題是出在LinqValueCalculator類中,還是MinimumDiscountHelper類中。

  6.4.2 將Moq添加到Visual Studio項目

  打開NuGet控制檯並輸入以下命令:

  Install-Package Moq -version 4.1.1309.1617-projectnameEssentialTools.Tests

  projectname參數告訴NuGet,我們希望Moq包安裝到自己的哪個單元測試項目中,而不是安裝到主應用程序中。

  6.4.3 對單元測試添加模仿對象

  對單元測試添加模仿對象,其目的是告訴Moq,你想使用哪一種對象,對它的行爲進行配置,然後將該對象運用於測試目標。下面演示如何爲LinqValueCalculator的單元測試添加模仿對象,

 

 1     [TestClass]
 2     public class UnitTest2
 3     {
 4         private Product[] products =
 5         {
 6             new Product {Name = "Kayak", Category = "Watersports", Price = 275M},
 7             new Product {Name = "Lifejacket", Category = "Watersports", Price = 48.95M},
 8             new Product {Name = "Soccer ball", Category = "Soccer", Price = 19.50M},
 9             new Product {Name = "Corner flag", Category = "Soccer", Price = 34.95M}
10         };
11         [TestMethod]
12         public void Sum_Products_Correctly()
13         {
14             ////準備
15             //var discounter=new MinimumDiscountHelper();
16             //var target=new LinqValueCalculator(discounter);
17             //var goalTotal = products.Sum(e => e.Price);
18 
19             ////動作
20             //var result = target.ValueProducts(products);
21 
22             ////斷言
23             //Assert.AreEqual(goalTotal,result);
24 
25             //準備
26             Mock<IDiscountHelper> mock = new Mock<IDiscountHelper>();
27             mock.Setup(m => m.ApplyDiscount(It.IsAny<decimal>()))
28                 .Returns<decimal>(total => total);
29             var target=new LinqValueCalculator(mock.Object);
30 
31             //動作
32             var result = target.ValueProducts(products);
33 
34             //斷言
35             Assert.AreEqual(products.Sum(e=>e.Price),result);
36         }
37     }
View Code

 

  1,創建模仿對象

  第一步是告訴Moq,你想使用的是哪種模仿對象,Moq十分依賴於泛型的類型參數,從以下語句可以看到這種參數的使用方式,這是告訴Moq,要模仿的是IDiscountHelper實現。

  Mock<IDiscountHelper> mock=new Mock<IDiscountHelper>();

  我們創建的是一個強類型的Mock<IDiscountHelper>對象,這是告訴Moq庫,它要處理的是哪種類型。這便是用於單元測試的IDiscountHelper接口,但爲了改善單元測試的側重點,這可以是你想要隔離出來的任何類型。

  2,選擇方法

  除了創建強類型的Mock對象外,還需要指定它的行爲方式。這是模仿過程的核心,它讓你建立模範所需的基準行爲,你可以將這種行爲用於對單元測試中目標對象的功能進行測試。

  mock.Setup(m=>m.ApplyDiscount(It.IsAny<decimal>())).Returns<decimal>(total=>total);

  用Setup方法給模仿對象添加一個方法,Moq可以使用LINQ和Lambda表達式進行工作。在調用Setup方法時,Moq會給我們傳遞要求它實現的接口,它巧妙的封裝了一些不打算細說的LINQ魔力,這種魔力讓我們可以選擇想要通過Lambda表達式進行配置或檢查的方法。對於該單元測試,我們希望定義ApplyDiscount方法的行爲,它是IDiscountHelper接口的唯一方法,也是對LinqValueCalculator類進行測試所需要的方法。

  也必須告訴Moq,我們感興趣的參數值是什麼,這是用It類來做的事情,如下,

  mock.Setup(m=>m.ApplyDiscount(It.IsAny<decimal>())).Returns<decimal>(total=>total);

  這個It類定義了許多以類型參數進行使用的方法。在此實例中,用decimal作爲泛型類型調用了IsAny方法。這是告訴Moq,當以任何十進制值爲參數來調用ApplyDiscount方法時,它應該運用我們定義的這一行爲。以下給出了It類提供的方法,所有這些方法都是靜態的。

  3,定義結果

  Returns方法是讓我們指定在調用模仿方法時Moq要返回的結果。其類型參數用以指定結果的類型,而用Lambda表達式來指定結果。

  mock.Setup(m=>m.ApplyDiscount(It.IsAny<decimal>())).Returns<decimal>(total=>total);

   通過調用帶有decimal類型參數的Returns方法(即Returns<decimal>),這是告訴Moq,要返回一個十進制的值。對於Lambda表達式,Moq給我們傳遞了一個在ApplyDiscount方法中接收的類型值。在此例中,創建了一個穿透方法。在該方法中,我們返回了傳遞給模仿ApplyDiscount方法的值,並未對這個值執行任何操作。

  注:請理解上述過程的思想:wield對LinqValueCalculator進行單元測試,如果創建一個IDiscountHelper模仿對象,便可以在單元測試中排除IDiscountHelper接口的實現類MinimumDiscountHelper,從而使單元測試更爲簡單且容易。用Moq創建模仿對象的整個過程包括幾個步驟:(1)用Mock創建模仿對象;(2)用Setup方法建立模仿對象的行爲;(3)用It類設置行爲的參數;(4)用Return方法執行行爲的返回類型;(5)用Lambda表達式在Return方法中建立具體行爲。

  4,使用模仿對象

  最後是在單元測試中使用這個模仿對象,通過讀取Mock<IDiscountHelper>對象的Object屬性值來實現。

  var target=new LinqValueCalculator(mock.Object);

  總結上述實例,Object屬性返回IDiscountHelper接口的實現,該實現中的ApplyDiscount方法返回它傳遞的十進制參數的值。

  這使單元測試很容易執行,因爲我們可以自行求取Product對象的價格總和,並檢查LinqValueCalculator對象,得到相同的值。

  Assert.AreEqual(products.Sum(e=>e.Price),result);

  以這種方式使用Moq的好處是,單元測試只檢查LinqValueCalculator對象的行爲,並不依賴於任何Models文件夾中IDiscountHelper接口的真實實現。這意味着,當測試失敗時,我們便知道問題出在LinqValueCalculator實現中,或是出在建立模仿對象的方式中。

  6.4.4 創建更復雜的模仿對象

  在UnitTest2.cs文件添加一個新的單元測試,它模仿更爲複雜的IDiscountHelper接口實現。事實上,我們已經用Moq模擬了MinimumDiscountHelper類的行爲。

 1     [TestClass]
 2     public class UnitTest2
 3     {
 4         private Product[] products =
 5         {
 6             new Product {Name = "Kayak", Category = "Watersports", Price = 275M},
 7             new Product {Name = "Lifejacket", Category = "Watersports", Price = 48.95M},
 8             new Product {Name = "Soccer ball", Category = "Soccer", Price = 19.50M},
 9             new Product {Name = "Corner flag", Category = "Soccer", Price = 34.95M}
10         };
11         [TestMethod]
12         public void Sum_Products_Correctly()
13         {
14             ////準備
15             //var discounter=new MinimumDiscountHelper();
16             //var target=new LinqValueCalculator(discounter);
17             //var goalTotal = products.Sum(e => e.Price);
18 
19             ////動作
20             //var result = target.ValueProducts(products);
21 
22             ////斷言
23             //Assert.AreEqual(goalTotal,result);
24 
25             //準備
26             Mock<IDiscountHelper> mock = new Mock<IDiscountHelper>();
27             mock.Setup(m => m.ApplyDiscount(It.IsAny<decimal>()))
28                 .Returns<decimal>(total => total);
29             var target=new LinqValueCalculator(mock.Object);
30 
31             //動作
32             var result = target.ValueProducts(products);
33 
34             //斷言
35             Assert.AreEqual(products.Sum(e=>e.Price),result);
36         }
37 
38         private Product[] createProduct(decimal value)
39         {
40             return new[] {new Product() {Price = value}};
41         }
42 
43         [TestMethod]
44         [ExpectedException(typeof(System.ArgumentOutOfRangeException))]
45         public void Pass_Through_Variable_Discounts()
46         {
47             //準備
48             Mock<IDiscountHelper> mock=new Mock<IDiscountHelper>();
49             mock.Setup(m => m.ApplyDiscount(It.IsAny<decimal>())).Returns<decimal>(total => total);
50             mock.Setup(m => m.ApplyDiscount(It.Is<decimal>(v => v == 0))).Throws<System.ArgumentOutOfRangeException>();
51             mock.Setup(m => m.ApplyDiscount(It.Is<decimal>(v => v > 100))).Returns<decimal>(total => (total * 0.9M));
52             mock.Setup(m => m.ApplyDiscount(It.IsInRange<decimal>(10, 100, Range.Inclusive)))
53                 .Returns<decimal>(total => total - 5);
54             var target=new LinqValueCalculator(mock.Object);
55 
56             //動作
57             decimal FiveDollarDiscount=target.ValueProducts(createProduct(5));
58             decimal TenDollarDiscount = target.ValueProducts(createProduct(10));
59             decimal FiftyDollarDiscount = target.ValueProducts(createProduct(50));
60             decimal HundredDollarDiscount = target.ValueProducts(createProduct(100));
61             decimal FiveHundredDollarDiscount = target.ValueProducts(createProduct(500));
62 
63             //斷言
64             Assert.AreEqual(5,FiveDollarDiscount,"$5 Fail");
65             Assert.AreEqual(5, TenDollarDiscount, "$10 Fail");
66             Assert.AreEqual(45, FiftyDollarDiscount, "$50 Fail");
67             Assert.AreEqual(95, HundredDollarDiscount, "$100 Fail");
68             Assert.AreEqual(450,FiveHundredDollarDiscount, "$500 Fail");
69             target.ValueProducts(createProduct(0));
70         }
71     }
View Code

  根據所接收到的參數值,我們定義了ApplyDiscount方法的4個不同行爲。最簡單行爲的是“全匹配”,它直接返回任意的decimal值,像這樣:

  mock.Setup(m=>m.ApplyDiscount(It.IsAny<decimal>())).Returns<decimal>(total=>total);

  這是用於上一示例的同一行爲,把它放在這兒是因爲調用Setup方法的順序會影響模仿對象的行爲。Moq會以相反的順序評估所給定的行爲,因此,它首先會考慮調用最後一個Setup方法。這意味着,你必須按從最一般到最特殊的順序,小心的創建模仿行爲。It.IsAny<decimal>是此例所定義的最一般的條件,因而我們首先運用它。如果顛倒調用Setup的順序,該行爲將能匹配對ApplyDiscount方法的所有調用,並生成錯誤的模仿結果。

  1,模仿特定值(並拋出異常)

  對於Setup方法的第二個調用,使用了It.Is方法:

  mock.Setup(m=>m.ApplyDiscount(It.Is<decimal>(v=>v==0))).Throws<System.ArgumentOutOfRangeException>();

  若傳遞給ApplyDiscount方法的值爲0,則Is方法的謂詞返回true。這裏並未返回一個結果,而是使用了Throws方法,這回讓Moq拋出一個用類型參數指定的異常實例。

  我們還用了Is方法捕捉了大於100的值,像這樣:

  mock.Setup(m=>m.ApplyDiscount(It.Is<decimal>(v=>v>100))).Returns<decimal>(total=>total*0.9M);

  It.Is方法是爲不同參數值建立指定行爲最靈活的方式,因爲你可以使用任何謂詞來返回true或false。

  2,模仿值的範圍

  It對象最後是與IsInRange方法一起使用的,它讓我們能夠捕捉參數值的範圍。

  mock.Setup(m=>m.ApplyDiscount(It.IsInRange<decimal>(10,100,Range.Inclusive))).Returns<decimal>(total=>total-5);

  這裏介紹這一方法是出獄完整性,我們在項目中可以使用Is方法和一個謂詞來做同樣的事情,像這樣:

  mock.Setup(m=>m.ApplyDiscount(It.Is<decimal>(v=>v>=10&&v<=100))).Returns<decimal>(total=>total-5);

  效果相同,但謂詞方法更爲靈活。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章