zoukankan      html  css  js  c++  java
  • 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);

      效果相同,但谓词方法更为灵活。

  • 相关阅读:
    如何将windows版的vim界面语言(默认为中文)设置成英文(转)
    hdu 1023 Train Problem II 完整高精度模板(以输出大Catalan为例)
    第三届蓝桥杯预赛真题解答
    hdu 1016 Prime Ring Problem (dfs)
    博客搬家
    void main()是错的!
    c,c++产生随机数详解
    高性能网站的十四条黄金法则
    云端
    jQuery Tools:Web开发必备的 jQuery UI 库
  • 原文地址:https://www.cnblogs.com/shangec/p/9854290.html
Copyright © 2011-2022 走看看