首页资源分类其它科学普及 > 精通ASP.NET MVC 3框架(第三版)【迷你书】

精通ASP.NET MVC 3框架(第三版)【迷你书】

已有 453308个资源

下载专区

上传者其他资源

文档信息举报收藏

标    签: NETMVC

分    享:

文档简介

精通ASP.NET MVC 3框架(第三版)【迷你书】

文档预览

第6章 „„„ MVC 基本工具 本章将要考察三类工具,它们应是每个 MVC 程序员工具库的组成部分。上一章曾提到过这 三类工具:DI 容器、单元测试框架和模仿工具。 我们为本书挑选了这些工具的三个专用实现,但是,对每一类工具都有很多选择。如果你不 适应我们所使用的,不必担心。还有很多选择,你肯定能从中找到适合于自己思维方式和工作流 程的工具。 正如第 5 章所说明的,Ninject 是我们首选的 DI 容器,它简单、优雅且易用。也有一些其他 选择,但我们喜欢 Ninject 最小配置的工作方式。我们考虑以模式为起点,这不是定律,而是因 为发现用 Ninject 很容易对 DI 进行定制。如果你不喜欢 Ninject,我们建议使用 Unity,这是微软 的一个 DI 工具。 对于单元测试,我们打算使用 Visual Studio 2010 内建的支持。我们习惯于使用 NUnit,这是 最流行的.NET 单元测试框架。我们喜欢 Nunit,除了因为发现 Visual Studio 2010 涵盖了足够多的 最重要的使用案例以外,还因为它与集成开发环境(IDE)的其余部分紧密集成在一起。 我们所选择的第三个工具是 Moq,这是一个模仿工具包,可以用它创建单元测试的接口实现。 程序员要么喜欢 Moq,要么恨它,再没有中间观点。你可能会发现其语法雅致而富有表现力,也 可能每次使用它时都诅咒它。如果发觉对之不能适应,建议你考察 Rhino Mocks,这是一个很好 的选择。 本章将分别介绍这些工具,并演示它们的核心特性。我们不会提供这些工具的所有方面—— 它们每一个都可以写一本书——但我们所给出的是足以使你起步并跟随本书其余部分示例所急 需的知识。 6.1 使用 Ninject 第 4 章介绍了 DI 思想。该思想是对 MVC 应用程序中的组件进行解耦,这是通过接口与 DI 相结合来实现的。清单 6-1 演示了一个表示统计一些产品总价功能的接口和这个接口的具体实现。 清单 6-1 类、接口及其实现 public class Product { 96 6.1 使用 Ninject public int ProductID { get; set; } public string Name { get; set; } public string Description { get; set; } public decimal Price { get; set; } public string Category { set; get; } } public interface IValueCalculator { decimal ValueProducts(params Product[] products); } public class LinqValueCalculator : IValueCalculator { public decimal ValueProducts(params Product[] products) { return products.Sum(p => p.Price); } } Product 类是第 5 章使用的同一个类。IValueCalculator 接口定义了一个方法,它以一个或多 个 Product 对象为参数,并返回累计值。LinqValueCalculator 类实现了这个接口,它使用 LINQ 的 扩展方法 Sum,巧妙地生成了一个对 Product 对象的 Price 属性的累计。我们现在需要创建一个 使用这个 IValueCalculator 的类,而且这是为 DI 设计的。这个类如清单 6-2 所示。 清单 6-2 使用 IValueCalculator 接口 public class ShoppingCart { private IValueCalculator calculator; public ShoppingCart(IValueCalculator calcParam) { calculator = calcParam; } public decimal CalculateStockValue() { // 定义需要求和的 products 集合 Product[] products = { new Product() { Name = "Kayak", Price = 275M}, new Product() { Name = "Lifejacket", Price = 48.95M}, new Product() { Name = "Soccer ball", Price = 19.50M}, new Product() { Name = "Stadium", Price = 79500M} }; // 计算 products 的总值 decimal totalValue = calculator.ValueProducts(products); // 返回结果 return totalValue; } } 97 ■ 第 6 章 MVC 基本工具 这是一个很简单的例子。ShoppingCart 类的构造器以一个 IValueCalculator 实现作为一个准备 DI 的参数。CalculateStockValue 方法创建一个 Product 对象的数组,然后调用 IValueCalculator 接 口中的 ValueProducts 来获得总价,以此作为返回结果。我们已经成功地解除了 ShoppingCart 类 与 LinqValueCalculator 类之间的耦合,如图 6-1 所示,它描述了这四个简单类型之间的关系。 图 6-1 四个简单类型之间的关系 ShoppingCart 类和 LinqValueCalculator 类都依赖于 IValueCalculator,但 ShoppingCart 与 LinqValueCalculator 没有直接关系。事实上,它甚至不知道 LinqValueCalculator 的存在。我们可 以修改 LinqValueCalculator 的实现,甚至用一个全新的 IValueCalculator 实现来代替,ChoppingCart 类依然对此一无所知。 ■ 注:Product 类与所有其他三个类型都有直接关系,对此不用担心。Product 等同于域模型类 型,而且我们期望这种类与应用程序的其余部分是强耦合的。如果不是在建立 MVC 应用程序, 也许会对此采取不同的观点,并解除与 Product 的耦合。 我们的目标是能够创建 ShoppingCart 实例,并把 IValueCalculator 类的一个实现作为构造器 参数进行注入。这是 Ninject——我们喜欢的 DI 容器——为我们所发挥的作用。但在能够示范 Ninject 之前,需要在 Visual Studio 中做好准备工作。 6.1.1 创建项目 我们打算从一个简单的控制台应用程序开始。在 Visual Studio 中用“控制台应用程序”模板 创建一个新项目,你可以在 Windows 模板段中找到这个模板。给这个项目取名为 NinjectDemo, 但名称并不重要。创建如清单 6-1 和清单 6-2 所示的接口和类。于是,我们已经把所有事物都放 进了一个单一的 C#文件之中。 添加 Ninject 为了把 Ninject 添加到项目中,你需要使用 Visual Studio 的“库包管理器(Library Package Manager)”。在解决方案窗口中右击项目,并从弹出菜单中选择“添加包库引用(Add Package Library Reference)”,以打开“添加库包引用(Add Library Package Reference)”对话框。在对话 框的左侧点击“在线(Online)”,然后在右上角的搜索框中输入“Ninject”,于是会出现一些条目, 如图 6-2 所示。 你将看到几个同 Ninject 相关的包,但从名字和描述应该可以看出哪个是核心 Ninject 库—— 98 6.1 使用 Ninject 其他条目应该是将 Ninject 与不同开发框架和工具集成的扩展。 点击条目右边的“安装(Install)”按钮,将该库添加到你的项目中。你将在解决方案窗口中 看到打开的“引用(References)”文件夹,以及下载并被添加到项目引用中的 Ninject 程序集。 图 6-2 将 Ninject 添加到 Visual Studio 项目 ■ 提示:在已经安装了 Ninject 包之后,如果项目编译还有问题,请选择“项目”菜单中的“项 目属性”菜单项,将“目标框架”的设置从“.NET Framework 4 Client Profile”改为“.NET Framework 4”。客户端特征(Client Profile)是一种瘦型安装,它忽略了 Ninject 所依赖的一个库。 6.1.2 Ninject 入门 为了使用 Ninject,我们需要创建一个 Ninject 内核的实例,这是用来与 Ninject 进行通信的对 象。我们将在 Program 类中完成这一工作,Program 类是 Visual Studio 作为控制台应用程序项目 模板部件所创建的。这是具有 Main 方法的类。创建的这个内核如清单 6-3 所示。 清单 6-3 准备 Ninject 内核 using Ninject; class Program { static void Main(string[] args) { IKernel ninjectKernel = new StandardKernel(); } } 一旦创建了这个内核,使用 Ninject 便分为两个阶段。第一阶段是把你想与之进行通信的类 型与已经创建的接口绑定。这里,我们想告诉 Ninject,当它接收到一个实现 IValueCalculator 的 请求时,应该创建并返回 LinqValueCalculator 类的一个实例。这是使用 IKernel 接口中定义的 Bind 和 To 方法来完成的,如清单 6-4 所示。 99 ■ 第 6 章 MVC 基本工具 清单 6-4 将类型绑定到 Ninject class Program { static void Main(string[] args) { IKernel ninjectKernel = new StandardKernel(); ninjectKernel.Bind().To(); } } 黑体语句的作用是把 IValueCalculator 接口绑定到 LinqValueCalculator 实现类。通过使用想要 注册的接口作为 Bind 方法的泛型类型参数的办法,我们指定了这个接口,并且把这个类型的具 体实现作为泛型类型参数传递给 To 方法。第二阶段是用 Ninject 的 Get 方法来创建一个实现这个 接口的对象,并把它传递给 ShoppingCart 类的构造器,如清单 6-5 所示。 清单 6-5 通过 Ninject 实例化一个接口实现 ... ninjectKernel.Bind().To(); // 获取接口实现 IValueCalculator calcImpl = ninjectKernel.Get(); // 创建 ShoppingCart 实例,并注入依赖性 ShoppingCart cart = new ShoppingCart(calcImpl); // 执行计算,并写出结果 Console.WriteLine("Total: {0:c}", cart.CalculateStockValue()); ... 清单 6-5 把想要实现的接口指定为 Get 方法的泛型类型参数。Ninject 通过已经定义的绑定进 行查找,明白我们已经把 IValueCalculator 绑定到 LinqValueCalculator,于是为我们创建一个新实 例。然后我们把这个实现注入到 ShoppingCart 类的构造器中,并调用 CalculateStockValue 方法, 它又反过来调用在这个接口中定义的方法。这段代码所得到的结果如下: Total: $79,843.45 当我们可以简单地创建 LinqValueCalculator 实例时,如此自找麻烦地安装和使用 Ninject 似 乎有点古怪,只要像这样就行了: ShoppingCart cart = new ShoppingCart(new LinqValueCalculator()); 对于像这样一种简单的例子,使用 Ninject 看上去需要更多的工夫,但当我们把复杂性添 加到应用程序时,Ninject 很快会变得事半功倍。在以下几小节中,我们将建立更复杂的例子, 并演示 Ninject 的一些不同特性。 6.1.3 创建依赖性链 当要求 Ninject 创建一个类型时,它会检查这个类型与其他类型之间的耦合。如果有额外的 100 6.1 使用 Ninject 依赖性,Ninject 会解析这些依赖性,并创建所需要的所有类的实例。为了演示这一特性,我们 创建一个新的接口和实现这个接口的类,如清单 6-6 所示。 清单 6-6 定义一个新接口及其实现 public interface IDiscountHelper { decimal ApplyDiscount(decimal totalParam); } public class DefaultDiscountHelper : IDiscountHelper { public decimal ApplyDiscount(decimal totalParam) { return (totalParam ‐ (10m / 100m * totalParam)); } } IDiscountHelper 定 义 了 ApplyDiscount 方 法 , 它 把 一 个 折 扣 运 用 于 一 个 十 进 制 值 。 DefaultDiscounterHelper 类实 现 这个 接口 , 并运 用固 定 的 10%折 扣 。我 们随 后 可以 把这 个 IDiscountHelper 接口作为对 LinqValueCalculator 的一个依赖性,如清单 6-7 所示。 清单 6-7 在 LinqValueCalculator 类中添加一个依赖性 public class LinqValueCalculator : IValueCalculator { private IDiscountHelper discounter; public LinqValueCalculator(IDiscountHelper discountParam) { discounter = discountParam; } public decimal ValueProducts(params Product[] products) { return discounter.ApplyDiscount(products.Sum(p => p.Price)); } } 这 个 类 新 添 加 的 构 造 器 以 IDiscountHelper 接 口 的 一 个 实 现 为 参 数 , 它 然 后 被 用 于 ValueProducts 方法中,以便把一个折扣运用于被处理的 Product 对象的累计值上。正如对 IValueCalculator 所做的那样,我们用 Ninject 内核把 IDiscountHelper 接口绑定到其实现类上,如 清单 6-8 所示。 清单 6-8 把另一个接口绑定到它的实现 ... IKernel ninjectKernel = new StandardKernel(); ninjectKernel.Bind().To(); ninjectKernel.Bind().To(); // 获取接口实现 IValueCalculator calcImpl = ninjectKernel.Get(); ShoppingCart cart = new ShoppingCart(calcImpl); Console.WriteLine("Total: {0:c}", cart.CalculateStockValue()); ... 101 ■ 第 6 章 MVC 基本工具 清单 6-8 也使用了前面创建的类和用 Ninject 绑定的接口。我们不需要对创建 IValueCalculator 实现的代码作任何修改。 当 IValueCalculator 被请求时,Ninject 知道要实例化的是 LinqValueCalculator 类。它会进一 步考察这个类,并发现它依赖于一个可以解析的接口。Ninject 会创建 DefaultDiscountHelper 的一 个实例,把它注入到 LinqValueCalculator 类的构造器中,并以 IValueCalculator 作为返回结果。不 管这个依赖性链有多长或有多复杂,Ninject 都会以这种方式检查它要实例化的每个依赖性类。 6.1.4 指定属性与参数值 在 把 接 口 绑 定 到 它 的 实 现 时 , 可 以 提 供 属 性 细 节 来 配 置 Ninject 创 建 的 类 。 修 改 StandardDiscountHelper 类(应当是 DefaultDiscountHelper 类——译者注),于是它暴露了一个便 利属性,以便指定折扣的大小,如清单 6-9 所示。 清单 6-9 将一个属性添加到一个实现类 public class DefaultDiscountHelper : IDiscountHelper { public decimal DiscountSize { get; set; } public decimal ApplyDiscount(decimal totalParam) { return (totalParam ‐ (DiscountSize / 100m * totalParam)); } } 当 用 Ninject 将 具 体 类 绑 定 到 类 型 时 , 我 们 可 以 用 WithPropertyValue 方 法 来 设 置 DefaultDiscountHelper 类中的 DiscountSize 属性,如清单 6-10 所示。 清单 6-10 使用 Ninject 的 WithPropertyValue 方法 ... IKernel ninjectKernel = new StandardKernel(); ninjectKernel.Bind().To(); ninjectKernel.Bind() .To().WithPropertyValue("DiscountSize", 50M); ... 注意,必须提供一个字符串值作为要设置的属性名。我们不需要修改任何其他绑定,也不需 要 修 改 使 用 Get 方 法 的 方 式 来 获 得 ShoppingCart 方 法 的 一 个 实 例 。 该 属 性 值 会 按 照 DefaultDiscountHelper 的结构进行设置,并起到了半价的效果。根据这个修改所得到的结果如下: Total: $39,921.73 如果需要设置多个属性值,可以链接调用 WithPropertyValue 方法来涵盖所有这些属性。也 可以用构造器参数做同样的事情。清单 6-11 演示了重写的 DefaultDiscounter 类,以使折扣大小 作为构造器参数来进行传递。 清单 6-11 在一个实现类中使用构造器属性 public class DefaultDiscountHelper : IDiscountHelper { 102 6.1 使用 Ninject private decimal discountRate; public DefaultDiscountHelper(decimal discountParam) { discountRate = discountParam; } public decimal ApplyDiscount(decimal totalParam) { return (totalParam ‐ (discountRate/ 100m * totalParam)); } } 为了用 Ninject 绑定这个类,可以用 WithConstructorArgument 方法来指定构造器参数的值, 如清单 6-12 所示。 清单 6-12 绑定到一个需要构造器参数的类 ... IKernel ninjectKernel = new StandardKernel(); ninjectKernel.Bind().To(); ninjectKernel.Bind() .To().WithConstructorArgument("discountParam", 50M); ... 这一技术允许你把一个值注入到构造器中。同样,可以把这些方法调用链接在一起,以提供 多值,并与依赖性混合和匹配。Ninject 会判断出我们的需要,并依此来创建它。 6.1.5 使用自身绑定 把 Ninject 集成到代码中的一个有用的特性是自身绑定,自身绑定是在通过 Ninject 内核请求 一个具体类(并因此实例化)的地方进行的。这么做似乎有点古怪,但它意味着我们不需要手工 地执行最初的 DI,像这样: IValueCalculator calcImpl = ninjectKernel.Get(); ShoppingCart cart = new ShoppingCart(calcImpl); 相反,我们可以简单地请求 ShoppingCart 的一个实例,让 Ninject 去挑出对 IValueCalculator 类的依赖性。清单 6-13 演示了自身绑定的使用。 清单 6-13 使用 Ninject 的自身绑定 ... ShoppingCart cart = ninjectKernel.Get(); ... 对一个类进行自身绑定不需要做任何准备。当我们请求一个还没有进行绑定的具体类时, Ninject 假设自身绑定就是我们所需要的。 有些 DI 纯粹论者不喜欢自身绑定,但我们喜欢。它有助于处理应用程序最初的 DI 工作,并 把各种事物,包括具体对象,纳入 Ninject 范围。如果我们花时间去注册一个自身绑定类型,就 103 ■ 第 6 章 MVC 基本工具 可以使用一个接口可用的特性,就像指定构造器参数和属性值一样。为了注册一个自身绑定,可 以使用 ToSelf 方法,如清单 6-14 所示。 清单 6-14 自身绑定一个具体类型 ninjectKernel.Bind().ToSelf().WithParameter("", ); 这个例子把 ShoppingCart 绑定到它自身,然后调用 WithParameter 方法为一个(假想的)属 性提供一个值。注意,只可以对具体类进行自身绑定。 6.1.6 绑定到派生类型 虽然我们关注于接口(因为这是与 MVC 应用程序最相关的),但也可以用 Ninject 来绑定具 体类。前面的小节演示了如何把一个具体类绑定到自身,但也可以把一个具体类绑定到一个派生 类。清单 6-15 演示了一个 ShoppingCart 类(已对它作了修改,以便于进行派生)和一个派生类 LimitShoppingCart(通过排除超过指定限定价的所有条目,这个派生类增强了父类的功能)。 清单 6-15 创建一个派生的购物车类 public class ShoppingCart { protected IValueCalculator calculator; protected Product[] products; public ShoppingCart(IValueCalculator calcParam) { calculator = calcParam; // 定义需要求和的产品集 products = new[] { new Product() { Name = "Kayak", Price = 275M}, new Product() { Name = "Lifejacket", Price = 48.95M}, new Product() { Name = "Soccer ball", Price = 19.50M}, new Product() { Name = "Stadium", Price = 79500M} }; } public virtual decimal CalculateStockValue() { // 计算产品的总值 decimal totalValue = calculator.ValueProducts(products); // 返回结果 return totalValue; } } public class LimitShoppingCart : ShoppingCart { 104 6.1 使用 Ninject public LimitShoppingCart(IValueCalculator calcParam) : base(calcParam) { // 这里什么都不做 } public override decimal CalculateStockValue() { // 过滤出超限的各数据项 var filteredProducts = products .Where(e => e.Price < ItemLimit); // 执行计算 return calculator.ValueProducts(filteredProducts.ToArray()); } public decimal ItemLimit { get; set; } } 我们可以绑定父类,于是,当通过 Ninject 请求父类的实例时,便会创建一个派生类的实例, 如清单 6-16 所示。 清单 6-16 将一个类绑定到一个派生类 ... ninjectKernel.Bind() .To() .WithPropertyValue("ItemLimit", 200M); ... 这一技术可以很好地把抽象类绑定到它们的具体实现上。 6.1.7 使用条件绑定 可以用 Ninject 绑定同一个接口的多个实现,或同一个类的多个派生,并提供在不同的条件 下应该使用哪一个的指令。为了演示这一特性,本节创建了 IValueCalculator 接口的一个新实现, 叫做“IterativeValueCalculator”,如清单 6-17 所示。 清单 6-17 IValueCalculator 的一个新实现 public class IterativeValueCalculator : IValueCalculator { public decimal ValueProducts(params Product[] products) { decimal totalValue = 0; foreach (Product p in products) { totalValue += p.Price; } return totalValue; } } 现在,有了一些可选的实现,我们可以有选择地创建 Ninject 绑定。清单 6-18 演示了一个例子。 105 ■ 第 6 章 MVC 基本工具 清单 6-18 Ninject 条件绑定 ... ninjectKernel.Bind().To(); ninjectKernel.Bind() .To() .WhenInjectedInto(); ... 这 个 新 绑 定 指 明 , 当 要 被 依 赖 性 注 入 的 对 象 是 LimitShoppingCart 类 的 一个 实 例 时 , IterativeValueCalculator 类应该被实例化,以便对 IValueCalculator 接口的请求进行服务。我们在 适当的位置留下了对 IValueCalculator 的绑定。Ninject 会试图对一个绑定寻找最佳匹配,因而, 在条件判据不能得到满足时,这有助于对同一个类或接口采用一个默认绑定,因此,Ninject 有 一个回滚值。最有用的条件绑定方法如表 6-1 所示。 表 6-1 方法 When(谓词) WhenClassHas() WhenInjectedInto() Ninject 条件绑定方法 效果 当谓词(一个 lambda 表达式)的结果为 true 时,实施绑定 当被注入的类是由 T 指定的类型的注解属性进行注解时,实施绑定 当要被注入的类是类型 T 时,实施绑定(见清单 6-18 示例) 6.2 将 Ninject 运用于 APS.NET MVC 前面已经用一个标准的 Windows 控制台应用程序演示了 Ninject 的核心特性,但把 Ninject 与 ASP.NET MVC 集 成 并 非 易 事 。 第 一 步 是 要 创 建 一 个 从 System.Web.Mvc. DefaultControllerFactory 派生而来的类。这是 MVC 默认的赖以创建控制器类实例的一个类。(第 14 章 将 演 示 如 何 用 一 个 自 定 义 的 实 现 来 替 换 这 个 默 认 的 控 制 器 工 厂 。) 这 个 实 现 叫 做 “NinjectControllerFactory”,如清单 6-19 所示。 清单 6-19 NinjectControllerFactory(Ninject 控制器工厂) using System; using System.Web.Mvc; using System.Web.Routing; using Ninject; using NinjectDemo.Models.Abstract; using NinjectDemo.Models.Concrete; namespace NinjectDemo.Infrastructure { public class NinjectControllerFactory : DefaultControllerFactory { private IKernel ninjectKernel; public NinjectControllerFactory() { ninjectKernel = new StandardKernel(); 106 6.2 将 Ninject 运用于 APS.NET MVC AddBindings(); } protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType) { return controllerType == null ? null : (IController)ninjectKernel.Get(controllerType); } private void AddBindings() { // 这里放置附加绑定 ninjectKernel.Bind().To(); } } } 这个类创建了一个 Ninject 内核,并用它对控制器类的请求进行服务,这种请求是通过 GetControllerInstance 方法形成的。GetControllerInstance 方法是由 MVC 框架在需要一个控制器对 象时调用的。我们不需要用 Ninject 明确地绑定控制器,可以依靠其默认的自身绑定特性,因为 控制器是从 System.Web.Mvc.Controller 派生而来的具体类。 AddBinding 方法允许我们对存储库和希望保持松耦合的组件添加其他 Ninject 绑定。也可以 把这个方法用于对需要额外的构造器参数或属性值的控制器类进行绑定。 一 旦 创 建 了 这 个 类 , 就 必 须 用 MVC 框 架 对 它 进 行 注 册 , 可 以 在 Global.asax 类 的 Application_Start 方法中来完成注册,如清单 6-20 所示。 清单 6-20 用 MVC 框架注册 NinjectControllerFactory 类 protected void Application_Start() { AreaRegistration.RegisterAllAreas(); RegisterGlobalFilters(GlobalFilters.Filters); RegisterRoutes(RouteTable.Routes); ControllerBuilder.Current.SetControllerFactory(new NinjectControllerFactory()); } 现在,MVC 框架将使用我们的 NinjectControllerFactory 来获得控制器的实例,而且,Ninject 将自动地把 DI 运用到控制器对象中。 可以看到,本例清单引用了 IProductRepository、FakeProductRepository、Product 等类型。 我们已经创建了一个简单的 MVC 应用程序以演示这种 Ninject 集成,而这些类型是本演示所 需要的域模型类型和存储库类型。我们此刻还不打算介入到这个项目中去,因为你将在下一 章看到这些类的适当使用。但如果你对这个例子感兴趣,可以在本书的伴随源代码下载中找 到这个项目。 以上似乎对一个简单的集成类已经介绍得太多了,但我们认为,这对你充分理解 Ninject 如 何工作是至关重要的。对 DI 容器的良好理解可以使开发和测试更简单容易。 107 ■ 第 6 章 MVC 基本工具 6.3 Visual Studio 的单元测试 有很多.NET 单元测试包,其中有许多是开源和免费的。在本书中,我们打算使用 Visual Studio 2010 所提供的内建的单元测试支持,这是 Visual Studio 具有测试支持的第一个版本,我们感觉它 是可信和有用的。 还有许多其他.NET 单元测试包可用,最流行的可能是 NUnit。所有测试包的功能大体相同, 本书选择 Visual Studio 支持的理由是,我们喜欢它与 IDE 其余部分的集成,这使得它比使用一个 扩展库更容易建立和运行测试。本小节将演示如何创建单元测试项目,并用测试来组装它。 ■ 注:Microsoft Visual Web Developer Express(微软的另一个简装版开发工具,简称为开发者 版。——译者注)不包含单元测试支持。这是微软区分 Visual Studio 免费版和商业版的方式之一。 如果你使用的是 Web Developer Express,我们建议你使用 NUnit(www.nunit.org),它与本章讨 论的集成特性有类似的工作方式。 6.3.1 创建项目 本节将用另一个控制台应用程序项目来演示单元测试。用这个模板创建一个项目,取名为 “ProductApp”。创建了这个项目之后,请定义如清单 6-21 所示的接口和模型类型。 清单 6-21 用于单元测试演示的接口与模型 public class Product { public int ProductID { get; set; } public string Name { get; set; } public string Description { get; set; } public decimal Price { get; set; } public string Category { set; get; } } public interface IProductRepository { IEnumerable GetProducts(); void UpdateProduct(Product product); } public interface IPriceReducer { void ReducePrices(decimal priceReduction); } Product 类与前面示例所使用的相同。IProductRepository 接口定义了一个存储库,我们将通 过它获取和更新产品对象。IPriceReducer 接口指定了一个将运用于所有 Products 的功能,通过由 priceReduction 参数所指定的量来降低 Products 的价格。 本例的目标是创建 IPriceReducer 的一个实现,它满足以下几个条件: 108 6.3 Visual Studio 的单元测试 y 降低存储库中所有 Product 条目的价格。 y 总降价应该是 priceReduction 参数乘以产品数目所得的值。 y 存储库 UpdateProduct 方法应该对每个 Product 对象进行调用。 y 降价后的价格不小于$1。 为了帮助建立这个实现,我们创建了一个 FakeRepository 类,它实现了 IProductRepository 接口,如清单 6-22 所示。 清单 6-22 FakeRepository 类 public class FakeRepository : IProductRepository { private Product[] products = { new Product() { Name = "Kayak", Price = 275M}, new Product() { Name = "Lifejacket", Price = 48.95M}, new Product() { Name = "Soccer ball", Price = 19.50M}, new Product() { Name = "Stadium", Price = 79500M} }; public IEnumerable GetProducts() { return products; } public void UpdateProduct(Product productParam) { foreach(Product p in products .Where(e => e.Name == productParam.Name) .Select(e => e)) { p.Price = productParam.Price; } UpdateProductCallCount++; } public int UpdateProductCallCount { get; set; } public decimal GetTotalValue() { return products.Sum(e => e.Price); } } 稍后将回过头来讨论这个类。我们也已经写出了 MyPriceReducer 类的一个骨架,它将是 IPriceReducer 类的实现,如清单 6-23 所示。 清单 6-23 MyPriceReducer 类骨架 public class MyPriceReducer : IPriceReducer { private IProductRepository repository; public MyPriceReducer(IProductRepository repo) { repository = repo; } public void ReducePrices(decimal priceReduction) { throw new NotImplementedException(); 109 ■ 第 6 章 MVC 基本工具 } } 这 个 类 还 没 有 实 现 ReducePrices 方 法 , 但 已 经 有 了 一 个 构 造 器 , 它 让 我 们 注 入 一 个 IProductRepository 接口的实现。 最后一步是使用库包管理器或通过 Ninject 的 Web 网站下载,把 Ninject 引用到项目中。 6.3.2 创建单元测试 我们打算遵照 TDD 模式(测试驱动开发模式),并在编写应用程序代码之前,先编写单元测 试。在 Visual Studio 中右击 MyPriceReducer.ReducePrices 方法,然后从弹出菜单中选择“创建单 元测试”,如图 6-3 所示。 图 6-3 创建单元测试 Visual Studio 将显示“创建单元测试(Create Unit Tests)”对话框,如图 6-4 所示。项目中显 示了可用的所有类型,你可以选中一个想要创建测试的条目。由于我们是从 MyPriceReducer 类 中的 ReducePrices 方法启动这一过程的,这个条目已经被选中了。 图 6-4 创建第一个单元测试 单元测试是以应用程序的一个独立项目的形式来创建的。由于还没有创建这个测试项目,“输 出项目(Output Project)”选项被设置到“创建一个新项目”。点击“确定(OK)”之后,Visual Studio 110 6.3 Visual Studio 的单元测试 将提示为这个测试项目取一个名字,如图 6-5 所示。根据约定,应当把这个项目命名为<主项目 名>.Tests。因为该项目叫做“ProductApp”,故测试项目将被命名为“ProductApp.Tests”。 图 6-5 选择测试项目名称 点击“创建(Create)”按钮,以创建这个项目和单元测试。Visual Studio 将把这个项目添加 到现在的解决方案中。如果在解决方案资源管理器窗口中打开该测试项目的“引用(References)” 条目,会看到 Visual Studio 已经自动添加了所需要的程序集引用,包括对主项目和 Ninject 的 引用。 作为测试项目的一部分,已经创建了一个名为“MyPriceReducerTest.cs”的新代码文件,它 包含了一些让我们开始工作的属性和方法。不过,本书将忽略这些东西并从头开始,这样便只有 我们所关心的内容。编辑这个类文件,让它符合清单 6-24。 清单 6-24 单元测试类 using System.Collections.Generic; using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace ProductApp.Tests { [TestClass] public class MyPriceReducerTest { [TestMethod] public void All_Prices_Are_Changed() { // 布置 FakeRepository repo = new FakeRepository(); decimal reductionAmount = 10; IEnumerable prices = repo.GetProducts().Select(e => e.Price); decimal[] initialPrices = prices.ToArray(); MyPriceReducer target = new MyPriceReducer(repo); // 动作 target.ReducePrices(reductionAmount); prices.Zip(initialPrices, (p1, p2) => { if (p1 == p2) { Assert.Fail(); } return p1; }); 111 ■ 第 6 章 MVC 基本工具 } } } 清单 6-24 含有我们的第一个单元测试和 Visual Studio 在运行测试时所期望的注解属性。 TestClass 注解属性被用于一个包含测试的类,而 TestMethod 注解属性被用于包含单元测试的任 何方法。没有这个注解属性的方法均被假设为是支持方法,Visual Studio 对之是忽略的。 可以看到,我们遵循了单元测试方法的“布置/动作/断言(A/A/A)”模式。如何命名单元测 试有许多约定,遵循的原则是简单,并且所采用的名字能清楚地表达它的测试内容。本例的单元 测试方法名为“All_Prices_Are_Changed”,这个含义已经足够清楚了。但如果你不喜欢这种风格, 真正要做的事情是你(及你的团队)理解后所决定的命名法。 在 All_Prices_Are_Changed 方法中,我们定义了一个 LINQ 查询,随后用 ToArray 扩展方法 来调用这个查询,以获得 FakeRepository 类所含有的 Product 条目的最初价格。下一步调用了目 标方法(指 MyPriceReducer.ReducePrices 方法——译者注),然后用 LINQ 的 Zip 方法以确保每一 个价格已经有了变化。如果有任一元素未改变,则调用 Asser.Fail 方法,这将导致单元测试失败。 建立单元测试有很多不同的方法,常用的办法是用一个单一的大方法来测试一个特性的所有 必要条件。但我们更喜欢创建多个小的单元测试,每一个仅关注应用程序的一个方面。我们这种偏 好有两个原因:第一是当一个小的单元测试失败时,你可以准确地知道代码不满足哪个条件;其次 是在这种悠闲地构造合理化测试的过程中,我们逐步杜绝了杂乱的代码。你也许比我们更关注于你 的代码,但我们发现,这种整洁的 A/A/A 模式的应用程序是最好的。 按照这种 TDD 模式,我们继续定义了单元测试,如清单 6-25 所示。 清单 6-25 剩余部分的单元测试 [TestMethod] public void Correct_Total_Reduction_Amount() { // 布置 FakeRepository repo = new FakeRepository(); decimal reductionAmount = 10; decimal initialTotal = repo.GetTotalValue(); MyPriceReducer target = new MyPriceReducer(repo); // 动作 target.ReducePrices(reductionAmount); // 断言 Assert.AreEqual(repo.GetTotalValue(), (initialTotal ‐ (repo.GetProducts().Count() * reductionAmount))); } [TestMethod] public void No_Price_Less_Than_One_Dollar() { // 布置 FakeRepository repo = new FakeRepository(); decimal reductionAmount = decimal.MaxValue; MyPriceReducer target = new MyPriceReducer(repo); 112 6.3 Visual Studio 的单元测试 // 动作 target.ReducePrices(reductionAmount); // 断言 foreach (Product prod in repo.GetProducts()) { Assert.IsTrue(prod.Price >= 1); } } 这些方法都遵照了同样的模式。清单 6-25 创建了 FakeRepository 对象,并手工地把它注入到 MyPriceReducer 类的构造器中。然后调用 ReducePrices 方法,并用 Assert 类的方法检查结果。我 们不打算细说上述一个个测试方法,因为它们都很简单。表 6-2 给出了 Assert 类的一些静态方法, 你可以用它们来检查或报告一个测试的状态。 表 6-2 方法 AreEqual(T, T) AreEqual(T, T, string) AreNotEqual(T, T) AreNotEqual(T, T, string) AreSame(T, T) AreSame(T, T, string) AreNotSame(T, T) AreNotSame(T, T, string) Fail() Fail(string) Inconclusive() Inconclusive(string) IsTrue(bool) IsTrue(bool, string) IsFalse(bool) IsFalse(bool, string) IsNull(object) IsNull(object, string) IsNotNull(object) IsNotNull(object, string) IsInstanceOfType(object, Type) IsInstanceOfType(object, Type, string) IsNotInstanceOfType(object, Type) IsNotInstanceOfType(object, Type, string) 静态的 Assert 方法 描述 断言两个类型 T 的对象有相同的值 断言两个类型 T 的对象的值不相等 断言两个变量指向相同的对象 断言两个变量指向不同的对象 舍弃一个断言——无检查条件 指明不能最终建立单元测试的结果 断言一个布尔值为 true——最常用于评估一个返回布尔结果的表达式 断言一个布尔值为 false 断言一个变量没有被分配一个对象引用 断言一个变量被分配了一个对象引用 断言一个对象是指定类型的对象,或是从指定类型派生的 断言一个对象不是指定类型的对象 113 ■ 第 6 章 MVC 基本工具 Assert 类中的每一个静态方法都可以检查单元测试的某个方面。如果断言失败,将抛出一个 异常,这意味着整个单元测试失败。每一个单元测试都被独立地处理,因此其他单元测试将被继 续执行。 上述每一个方法都有一个以字符串为参数的重载,该字符串作为断言失败时的消息元素。 AreEqual 和 AreNotEqual 方法有几个重载,以满足特定类型的比较。例如,有一个版本可以比较 字符串而不需要考虑其他情况。 一个值得注意的怪现象是 ExceptionExpected 注解属性。只有单元测试抛出由 ExceptionType 参数指定的类型的异常时,这个断言才是成功的。这是确保抛出异常而不需要在单元测试中用 try…catch 块来浪费时间的一种灵活的方式。 6.3.3 运行单元测试(并失败) 利用表 6-2,可以看到每一个单元测试例子检查的是什么。要运行这些测试,从 Visual Studio 的“测试”菜单中选择“运行”,然后在解决方案中选择“所有测试”。Visual Studio 将扫描当前 解决方案中的所有类,以寻找 TestClass 和 TestMethod 注解属性。 ■ 提示:如果从“测试”→“调试”菜单中选择“运行所有测试”,Visual Studio 将执行这些单 元测试,但当一个断言失败时将中断并进入调试状态。这是一个十分灵活的特性,用于对断言 的输入值进行检查,以考察发生了什么错误。 在每个测试被执行时,测试结果窗口会显示测试过程,并以绿色或红色指示符来显示测试结 果。由于此时还没有实现指定的功能,因此,所有这四个单元测试都是失败的,如图 6-6 所示。 图 6-6 最初的单元测试结果 ■ 提示:如果想查看测试失败原因的信息,可在测试结果窗口中右击一个条目,并选择“查看 测试结果细节”。 6.3.4 实现特性 现在到了实现功能特性的时候了。从保险角度来讲,当完成工作时,我们是能够审核代码质 量的。有了之前的准备工作,ReducePrices 方法的实现是相当简单的,如清单 6-26 所示。 114 清单 6-26 实现特性 public class MyPriceReducer : IPriceReducer { private IProductRepository repository; public MyPriceReducer(IProductRepository repo) { repository = repo; } public void ReducePrices(decimal priceReduction) { foreach (Product p in repository.GetProducts()) { p.Price = Math.Max(p.Price ‐ priceReduction, 1); repository.UpdateProduct(p); } } } 再次运行该测试。这次,它们都通过了,如图 6-7 所示。 6.4 使用 Moq 图 6-7 单元测试通过 前面已经十分简洁地介绍了单元测试,随着本书的深入,还将继续演示单元测试。注意, Visual Studio 已经提升了捕捉单元测试缺陷的特性。在下一小节考察模仿时,你将看到一些这 样的特性。 你也可以排列测试以使它们依序执行,按类别组合一些测试以让它们一起运行,记录单元测 试所花费的时间,以及其他许多事情。我们建议你研究 MSDN 上的单元测试文档。 6.4 使用 Moq 前一个例子创建了一个 FakeRepository 类以支持测试。由于本章目前还没有解释如何创建一 个实际的存储库实现,因此需要一个替代品。即使已经有一个实际的实现,也可能不想用它,因 为它给测试环境增加了复杂性(或者因为存储库的操作代价太高,或者因为其他原因)(这里的 意思是,在测试环境中没必要使用实际存储库——译者注)。 FakeRepository 类是 IProductRepository 接口的一个模仿实现。我们没必要实现一个实际存储 库所需的真实功能,只要足以使我们能够编写单元测试即可。而且我们添加了与存储库根本无关 的特性。例如,一个测试需要我们确保 UpdateProduct 方法被调用了一定次数,对此可以通过添 115 ■ 第 6 章 MVC 基本工具 加一个属性来实现;另一个测试是添加一个方法,以便计算 Product 对象的总价。 我们创建了这个模仿实现,并手工添加了这些附加功能,这使 FakeRepository 类成为一种手 工模仿(我们保证并不是在虚构这些术语)。本章这部分的主题是 Moq,它是使模仿更快、更简 单和更容易的一个框架。 6.4.1 将 Moq 添加到 Visual Studio 项目 我们打算在前面例子的基础上,用一个由 Moq 创建的模仿来代替 FakeRepository 类。为了 准备这个项目,必须添加 Moq 程序集。通过库包管理器或者从 http://code.google.com/p/moq 下载 这个库。把 Moq.dll 作为项目的引用(用下载或者用库包管理器)添加到 ProductApp.Tests 项目 (添加到单元测试项目,而不是加到应用程序项目)。 6.4.2 用 Moq 创建模仿 使用模仿工具的好处是,我们可以创建经过定制的模仿,它只包含足以帮助我们进行测试的 功能。这意味着我们不会因为太过复杂的模仿实现而使工作无法进行。在一个实际项目中,不像 前面这些简单示例这样,可以很容易地达到模仿实现需要其自己的测试这一阶段,因为实际项目 有那么多的代码(这里的意思是,实际项目中有太多的代码,完整的模仿实现会很复杂,很难做 到让一个复杂的模仿实现满足各种测试——译者注)。我们可以制作许多小型模仿,但要使它们 生效,需要把重复代码转移到一个基类中,这又使我们再次回到过于复杂的状况。当测试较小且 焦点集中时,单元测试最好,并且能保持事情尽可能简单(这是一种测试方法学。意即,在实际 开发中,最好的办法总是把应用程序的测试分解成一个个小型且焦点集中的测试,这样便于使用 单元测试,而创建满足单元测试的模仿实现是容易的——译者注)。 用 Moq 创建模仿有两个阶段。第一个阶段是创建一个新的 Mock,这里的 T 是要模仿的 类型,如清单 6-27 所示。 清单 6-27 创建 Mock Mock mock = new Mock(); 第二个阶段是建立我们希望该实现要表现的行为。Moq 将自动地实现已经赋给其类型的所有 方法和属性,但这是用类型的默认值来实现的。例如,IProductRepository.GetProducts 方法会返 回一个空的 IEnumerable。要改变 Moq 实现一个类型成员的方式,必须用 Setup 方法, 如清单 6-28 所示。 清单 6-28 用 Moq 设置行为 Product[] products = new Product[] { new Product() { Name = "Kayak", Price = 275M}, new Product() { Name = "Lifejacket", Price = 48.95M}, new Product() { Name = "Soccer ball", Price = 19.50M}, new Product() { Name = "Stadium", Price = 79500M} }; 116 6.4 使用 Moq mock.Setup(m => m.GetProducts()).Returns(products); 当建立一个新的 Moq 行为时,有三个元素要考虑,参照下一小节。 1.使用 Moq 的方法选择器 第一个元素是所选择的方法。Moq 用 LINQ 和 lambda 表达式进行工作。当调用 Setup 方法 时,Moq 传递的是要求它实现的接口。它封装了一些我们不打算细说的 LINQ 魔力,让我们可以 选择想要通过一个 lambda 表达式进行配置或检查的方法(按译者的理解,这种 LINQ 魔力应当 是指 Moq 在 Setup 方法中封装了一些诸如 GetXXX 之类的方法,可以让我们对模仿对象进行适 当操作——译者注)。因此,当想要给 GetProducts 方法定义一个行为时,可以这样: mock.Setup(m => m.GetProducts()).(<...other methods...>); 我们不打算说明它是如何工作的——只要知道它能做并因此而用它。GetProducts 方法很容易 处理,因为它没有参数。如果想处理一个有参数的方法,需要考虑第二个元素:参数过滤器。 2.使用 Moq 的参数过滤器 我们可以告诉 Moq,要根据传递一个方法的参数值作出不同的响应。由于 GetProducts 方法 没有参数,因此我们将用下面这个简单的接口来解释: public interface IMyInterface { string ProcessMessage(string message); } 清单 6-29 演示了创建该接口模仿实现的代码,它对不同的参数值有不同的行为。 清单 6-29 使用 Moq 参数过滤器 Mock mock = new Mock(); mock.Setup(m => m.ProcessMessage("hello")).Returns("Hi there"); mock.Setup(m => m.ProcessMessage("bye")).Returns("See you soon"); Moq 把这些语句解释为,当传递给 ProcessMessage 方法的参数是 hello 时,返回“Hi there”; 而当参数值是 bye 时,返回“See you soon”。对所有其他参数值,Moq 将返回该方法结果类型的 默认值,因为本例使用的是字符串,所以其默认值为 null。 要对所有可能的参数值建立响应,这很快就会成为一件乏味的事。当处理更复杂的类型时, 就会变得乏味和困难,因为你需要创建表示它们全部的对象并用它们进行比较。幸运的是,Moq 提供了 It 类,可以用它表示广泛类别的参数值。这里是一个例子: mock.Setup(m => m.ProcessMessage(It.IsAny())).Returns("Message received"); 这个 It 类定义了使用泛型类型参数的许多方法。此例中,我们用 string 作为泛型类型调用了 IsAny 方法。这告诉 Moq,当 ProcessMessage 方法以任何字符串值为参数被调用时,它应该返回 “Message Received”响应。表 6-3 给出了 It 类所提供的方法,所有这些方法都是静态的。 117 ■ 第 6 章 MVC 基本工具 表 6-3 方法 Is() IsAny() IsInRange IsRegex It 类的静态方法 描述 基于指定的谓词进行匹配(见清单 6-30 示例) 如果参数是类型 T 的实例,则匹配 如果参数在所定义的值之间,则匹配 如果一个字符串参数符合指定的正则表达式,则匹配 Is方法最灵活,因为它让你提供一个谓词,如果它为真,则引发一个参数匹配,如清单 6-30 所示。 清单 6-30 使用 It 参数过滤器 mock.Setup(m => m.ProcessMessage(It.Is(s => s == "hello" || s == "bye"))) .Returns("Message Received"); 这条语句指示,如果字符串参数是 hello 或 bye,则 Moq 返回“Message Received”。 3.返回结果 在建立行为时会经常做这样的事,即,在调用方法时要定义方法返回的结果。前一个例子已 经把 Returns 方法链接到 Setup 调用,以返回一个特定的值。我们也可以用传递给模仿方法的参 数作为 Returns 方法的一个参数,以导出基于输入的输出。清单 6-31 提供了一个演示。 清单 6-31 返回一个基于参数值的结果 mock.Setup(m => m.ProcessMessage(It.IsAny())) .Returns(s => string.Format("Message received: {0}", s)); 这个例子要做的全部工作是用一个与方法参数匹配的泛型类型参数来调用 Returns 方法。 Moq 把这个方法参数传递给 lambda 表达式,于是可以生成一个动态结果。此例创建了一个格式 化的字符串。 6.4.3 使用 Moq 的单元测试 你可以看到,用 Moq 创建模仿实现是十分容易的,尽管可能需要花一些时间才能习惯于这 种语法。一旦已经建立了你需要的行为,便可以通过 Mock.Object 属性获得模仿实现。清单 6-32 演示了 Moq 对 Correct_Total_Reduction_Amount 单元测试的应用程序。 清单 6-32 在测试方法中使用 Moq [TestMethod] public void Correct_Total_Reduction_Amount() { // 布置 Product[] products = new Product[] { new Product() { Name = "Kayak", Price = 275M}, new Product() { Name = "Lifejacket", Price = 48.95M}, 118 6.4 使用 Moq new Product() { Name = "Soccer ball", Price = 19.50M}, new Product() { Name = "Stadium", Price = 79500M} }; Mock mock = new Mock(); mock.Setup(m => m.GetProducts()).Returns(products); decimal reductionAmount = 10; decimal initialTotal = products.Sum(p => p.Price); MyPriceReducer target = new MyPriceReducer(mock.Object); // 动作 target.ReducePrices(reductionAmount); // 断言 Assert.AreEqual(products.Sum(p => p.Price), (initialTotal ‐ (products.Count() * reductionAmount))); } 你可以看到,清单 6-32 只实现了 IProductRepository 所定义的足够多的功能来执行测试。在 本例中,这意味着实现 GetProducts 接口以使它返回测试数据。 清单 6-32 把所有事情都放到了单元测试方法中,以给出一个 Moq 的快速演示,但可以用某 些 Visual Studio 测试特性让事情更简单。所有测试方法都要使用同样的 Product 测试对象,因此 可以把这些对象创建为测试类部分,如清单 6-33 所示。 清单 6-33 创建通用测试数据对象 ... [TestClass] public class MyPriceReducerTest { private IEnumerable products; [TestInitialize] public void PreTestInitialize() { } ... products = new Product[] { new Product() { Name = "Kayak", Price = 275M}, new Product() { Name = "Lifejacket", Price = 48.95M}, new Product() { Name = "Soccer ball", Price = 19.50M}, new Product() { Name = "Stadium", Price = 79500M} }; 我们希望对每个单元测试都从干净的测试数据开始,因此创建了 products 字段,并用 Visaul Studio 测试特性来初始化数据。Visual Studio 将寻找一个具有 TestInitialize 注解属性的方法。如 果找到,它将在这个类中的每个单元测试之前调用这个方法。在上例中,这意味着产品类变量将 用新鲜的测试数据进行初始化。表 6-4 显示了 Visual Studio 支持的其他单元测试注解属性。 119 ■ 第 6 章 MVC 基本工具 表 6-4 注解属性 ClassInitialize ClassCleanup TestInitialize TestCleanup Visual Studio 单元测试注解属性 描述 在类中的单元测试被执行之前调用,必须被用于静态方法 类中的所有单元测试被执行之后调用,必须被用于静态方法 在每个测试执行之前调用 每个测试被执行之后调用 把这些注解属性运用于什么方法名并不重要,因为 Visual Studio 只寻找这个注解属性。当使 用 TestInitialize 注解属性时,可以用两行代码创建并配置特定测试的模仿实现: Mock mock = new Mock(); mock.Setup(m => m.GetProducts()).Returns(products); 当模仿一个更复杂的对象时,Moq 的好处变得更加显著。在以下小节中将演示一些 Moq 的 其他特性,而且本书的后继部分也将演示不同的单元测试技术。 6.4.4 用 Moq 作校验 上 述 测 试 条 件 之 一 是 对 每 个 被 处 理 的 Product 对 象 都 调 用 UpdateProduct 方 法 。 在 FakeRepository 类中,我们是通过在 UpdteProduct 方法的内部定义一个属性并采用增量加 1 的方 法进行测量的。用 Moq 可以更优雅地取得同样效果,如清单 6-34 所示。 清单 6-34 检验方法调用频率 // 执行 target.ReducePrices(reductionAmount); // 断言 foreach (Product p in products) { mock.Verify(m => m.UpdateProduct(p), Times.Once()); } } 利用参数过滤器,我们能够检验 UpdateProduct 方法对所测试的每个 Product 对象确实只调用 了一次。当然,也可以用一个手工模仿来做这件事,但我们喜欢这种从模仿工具获得的简单性。 6.5 小结 本章考察了从事高效 MVC 开发的三个基本工具——Ninject、Visual Studio 2010 内建的单元 测试支持,以及 Moq。对这三种工具还有许多其它选择,开源的或商业的都有。如果你不适应我 们所喜欢的这些工具,也不乏其他选择。你也许会发现自己基本上不喜欢 TDD 或单元测试,或 者乐于手工执行 DI 和模仿。然而,我们认为在开发周期中使用这三种工具有一些实际的好处。 如果你因为从未用过而在犹豫是否采纳它们,我们鼓励你不用怀疑并让它们一展身手——至少在 阅读本书期间。 120

Top_arrow
回到顶部
EEWORLD下载中心所有资源均来自网友分享,如有侵权,请发送举报邮件到客服邮箱bbs_service@eeworld.com.cn 或通过站内短信息或QQ:273568022联系管理员 高进,我们会尽快处理。