ASP.NET MVC5(四)MVC基本工具

作者 Zhendong Ho 日期 2021-03-26
ASP.NET MVC5(四)MVC基本工具
  • 依赖项注入(DI)容器
  • 单元测试框架
  • 模仿工具

准备项目

打开VS,创建ASP.NET Web应用程序,项目名称为EssentialTools,选择MVC空模板。

创建模型类

在Models文件夹添加Product类。

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 { get; set; }
}

在Models文件夹添加LinqValueCalculator类,它将计算Product对象集合的总价。

public class LinqValueCalculator
{
public decimal ValueProducts(IEnumerable<Product> products)
{
return products.Sum(p => p.Price);
}
}

在Models文件夹添加ShoppingCart类,它表示了Product对象的集合,并且用LinqValueCalculator来确定总价。

public class ShoppingCart
{
private LinqValueCalculator calc;

public ShoppingCart(LinqValueCalculator calcParam)
{
calc = calcParam;
}

public IEnumerable<Product> products { get; set; }

public decimal CalculateProductTotal()
{
return calc.ValueProducts(products);
}
}

添加控制器

添加HomeController。在Index动作方法创建Product对象数组,并用LinqValueCalculator对象产生总价的值,将其传递给View

public class HomeController : Controller
{
private Product[] products = {
new Product { Name = "Kayak", Category = "Watersports", Price = 275M },
new Product { Name = "Lifejacket", Category = "Watersports", Price = 48.95M },
new Product { Name = "Soccer ball", Category = "Soccer", Price = 19.50M },
new Product { Name = "Corner flag", Category = "Soccer", Price = 34.95M }
};

public ActionResult Index()
{
LinqValueCalculator calc = new LinqValueCalculator();
ShoppingCart cart = new ShoppingCart(calc)
{
products = products
};
decimal totalValue = cart.CalculateProductTotal();
return View(totalValue);
}
}

添加视图

添加Index视图,接收的模型类为decimal

@model decimal
@{
Layout = null;
}

<!DOCTYPE html>

<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Index</title>
</head>
<body>
<div>
Total value is $@Model
</div>
</body>
</html>

启动项目,显示结果为$378.40

使用Ninject

依赖注入(DI)的思想是,对MVC应用程序中的组件进行解耦,这是通过接口DI容器相结合来实现的。DI容器创建了对象实例,这是通过创建对象所依赖的接口并将其注入构造器而实现的。

理解问题

  • ShoppingCart类LinqValueCalculator类是紧耦合的
  • HomeController类ShoppingCart类LinqValueCalculator类都是紧耦合的。

这意味着,如果想替换LinqValueCalculator类,就必须在与它有紧耦合关系的类中找出对它的引用,并进行修改

运用接口

在Models文件夹添加IValueCalculator接口

public interface IValueCalculator
{
decimal ValueProducts(IEnumerable<Product> products);
}

然后在LinqValueCalculator类中实现这一接口。

public class LinqValueCalculator : IValueCalculator
{
public decimal ValueProducts(IEnumerable<Product> products)
{
return products.Sum(p => p.Price);
}
}

ShoppingCart类中运用接口。于是便解除了ShoppingCart类与LinqValueCalculator类之间的紧耦合(两个类不再具有直接联系)。

public class ShoppingCart
{
private IValueCalculator calc; // 用接口替代LinqValueCalculator

public ShoppingCart(IValueCalculator calcParam) // 用接口替代LinqValueCalculator
{
calc = calcParam;
}

public IEnumerable<Product> products { get; set; }

public decimal CalculateProductTotal()
{
return calc.ValueProducts(products);
}
}

但是,在HomeController中,与LinqValueCalculator类还是紧耦合。(仍然需要用new关键字创建LinqValueCalculator对象)

public ActionResult Index()
{
IValueCalculator calc = new LinqValueCalculator(); // 与LinqValueCalculator紧耦合
ShoppingCart cart = new ShoppingCart(calc)
{
products = products
};
decimal totalValue = cart.CalculateProductTotal();
return View(totalValue);
}

因此,通过Ninject可以指定LinqValueCalculator作为IValueCalculator的实现,这样,就不需要在HomeController使用new关键字创建LinqValueCalculator对象(解除HomeController与LinqValueCalculator的紧耦合)。

将Ninject添加到VS项目

输入以下Nuget命令:

  • 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初步

在HomeController中,给Index动作方法添加基本的Ninject功能。

public ActionResult Index()
{
IKernel ninjectKernel = new StandardKernel(); // 创建Ninject内核的实例
ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>(); // 配置Ninject内核
IValueCalculator calc = ninjectKernel.Get<IValueCalculator>(); // 使用Ninject创建对象

ShoppingCart cart = new ShoppingCart(calc)
{
products = products
};
decimal totalValue = cart.CalculateProductTotal();
return View(totalValue);
}
  • 第一步,创建一个Ninject的内核实例。它负责解析依赖项创建新的对象(当需要一个对象时,将使用这个内核而不是使用new关键字)。可以通过创建一个StandardKernel(标准内核)类的实例来完成。
  • 第二步,配置Ninject内核。指定每个接口所希望使用的实现对象Bind方法指定要使用的接口To方法指定希望实例化的实现类
  • 第三步,使用Ninject创建对象。调用内核的Get方法,返回指定的实现类型的一个实例。

建立MVC的依赖项注入

创建依赖项解析器

首先要创建一个自定义的依赖项解析器。MVC框架需要使用依赖项解析器来创建类的实例,包括控制器实例。

项目右键,新建Infrastructure文件夹,用于放置MVC应用程序中不适合放在其他文件夹的类。新建NinjectDependencyResolver类

public class NinjectDependencyResolver : IDependencyResolver
{
private IKernel kernel;

public NinjectDependencyResolver(IKernel kernelParam)
{
kernel = kernelParam;
AddBindings();
}

public object GetService(Type serviceType)
{
return kernel.TryGet(serviceType);
}

public IEnumerable<object> GetServices(Type serviceType)
{
return kernel.GetAll(serviceType);
}

private void AddBindings()
{
kernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
}
}

注意

  1. NinjectDependencyResolver类实现了IDependencyResolver接口,它属于System.Mvc命名空间,用于MVC框架获取需要的对象
  2. MVC框架在需要类实例以便对一个传入的请求进行服务时,会调用GetServiceGetServices方法(通过调用Ninject的TryGet和GetAll方法)。
  3. AddBindings方法配置了IValueCalculator接口LinqValueCalculator类之间的关系(建立Ninject绑定)。

注册依赖项解析器

使用Nuget添加Ninject包的时候,会在App_Start文件夹创建一个NinjectWebCommon.cs文件。它定义了应用程序启动时会自动调用的一些方法,目的是将它们集成到ASP.NET的请求生命周期之中。

NinjectWebCommon类的RegisterServices方法中,用SetResolver静态方法NinjectDependencyResolver的实例注册为MVC框架的解析器。

/// <summary>
/// Load your modules or register your services here!
/// </summary>
/// <param name="kernel">The kernel.</param>
private static void RegisterServices(IKernel kernel)
{
System.Web.Mvc.DependencyResolver.SetResolver(new EssentialTools.Infrastructure.NinjectDependencyResolver(kernel));
}

重构Home控制器

修改HomeController,对IValueCalculator接口声明一个依赖项

public class HomeController : Controller
{
private IValueCalculator calc;

public HomeController(IValueCalculator calcParam) // 类构造器,用于接收IValueCalculator的实现
{
calc = calcParam;
}

private Product[] products = {
new Product { Name = "Kayak", Category = "Watersports", Price = 275M },
new Product { Name = "Lifejacket", Category = "Watersports", Price = 48.95M },
new Product { Name = "Soccer ball", Category = "Soccer", Price = 19.50M },
new Product { Name = "Corner flag", Category = "Soccer", Price = 34.95M }
};

public ActionResult Index()
{
ShoppingCart cart = new ShoppingCart(calc)
{
products = products
};
decimal totalValue = cart.CalculateProductTotal();
return View(totalValue);
}
}

这称为构造器注入,是依赖项注入的一种形式。这样就能打破HomeControllerLinqValueCalculator之间的紧耦合关系。

创建依赖项链

当要求Ninject创建一个类型时,它会检查是否还声明了自己的依赖项。如果有额外的依赖项,Ninject会自动地解析这些依赖项,并创建所需要的所有类的实例,以这种方式处理依赖项链

在Models文件夹中,添加Discount.cs文件,并定义一个新的接口和其实现类。DefaultDiscountHelper类实现了IDiscountHelper接口,并运用固定的10%折扣。

public interface IDiscountHelper
{
decimal ApplyDiscount(decimal totalParam);
}

public class DefaultDiscountHelper : IDiscountHelper
{
public decimal ApplyDiscount(decimal totalParam)
{
return (totalParam - (10m / 100m * totalParam));
}
}

修改LinqValueCalculator类,以使它执行计算时,使用IDiscountHelper接口

public class LinqValueCalculator : IValueCalculator
{
private IDiscountHelper discounter;

public LinqValueCalculator(IDiscountHelper discountParam) // 类构造器,用于接收IDiscountHelper的实现
{
discounter = discountParam;
}

public decimal ValueProducts(IEnumerable<Product> products)
{
return discounter.ApplyDiscount(products.Sum(p => p.Price)); // 将一个折扣运用于Product对象的累计值
}
}

在NinjectDependencyResolver类的AddBindings方法中,将IDiscountHelper接口绑定到它的实现

private void AddBindings()
{
kernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
kernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>();
}

注意:上面代码已经创建了一个依赖项链。HomeController依赖于IValueCalculator接口,Ninject用LinqValueCalculator类对该接口进行解析。LinqValueCalculator又依赖于IDiscountHelper接口,Ninject又用DefaultDiscountHelper类对其进行解析。

指定属性和构造器参数值

在将接口与其实现进行绑定时,可以为属性提供一些值方面的细节,以便对Ninject创建的对象进行配置

修改DefaultDiscountHelper类,定义一个DiscountSize属性,用于计算折扣量。

public class DefaultDiscountHelper : IDiscountHelper
{
public decimal DiscountSize { get; set; } // 定义折扣属性

public decimal ApplyDiscount(decimal totalParam)
{
return (totalParam - (DiscountSize / 100m * totalParam)); // 用折扣属性进行计算
}
}

在NinjectDependencyResolver类中,用WithPropertyValue方法为DiscountSize属性设置一个值。

private void AddBindings()
{
kernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
kernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>().WithPropertyValue("DiscountSize", 50M); // 属性名称为字符串形式
}

这样,不需要修改其他绑定,该属性值会按照DefaultDiscountHelper的结构进行设置,起到了半价的效果。

如果需要设置多个属性值,可以链式调用WithPropertyValue方法,也可以用类构造器参数去实现。

修改DefaultDiscountHelper类,以使折扣大小作为构造器参数进行传递。

public class DefaultDiscountHelper : IDiscountHelper
{
public decimal discountSize; // 定义折扣字段

public DefaultDiscountHelper(decimal discountParam) // 构造器参数设置字段
{
discountSize = discountParam;
}

public decimal ApplyDiscount(decimal totalParam)
{
return (totalParam - (discountSize / 100m * totalParam));
}
}

在Addbindings方法中用WithConstructorArgument方法来指定构造器参数的值

private void AddBindings()
{
kernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
kernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>().WithConstructorArgument("discountParam", 50M); // 指定构造器参数的值
}

使用条件绑定

在Models文件夹添加FlexibleDiscountHelper类,根据总额大小运用不同的折扣。

public class FlexibleDiscountHelper : IDiscountHelper
{
public decimal ApplyDiscount(decimal totalParam)
{
decimal discount = totalParam > 100 ? 70 : 25; // 金额大于100时,7折
return (totalParam - (discount / 100m * totalParam));
}
}

在AddBindings方法中,添加绑定。当Ninject内核要创建一个LinqValueCalculator对象时,应该使用FlexibleDiscountHelper类作为IDiscountHelper的实现。

private void AddBindings()
{
kernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
kernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>().WithConstructorArgument("discountParam", 50M);
kernel.Bind<IDiscountHelper>().To<FlexibleDiscountHelper>().WhenInjectedInto<LinqValueCalculator>(); // 条件绑定
}

Ninject条件绑定方法如下表

方法 效果
When(谓词) 当“谓词”(一个lambda表达式)的结果为true时,实施绑定
WhenClassHas() 当被注入的类以注解属性进行注释,而其类型为T时,实施绑定
WhenInjectedInto() 当要被注入的类是类型T时,实施绑定

设置对象作用域

该Ninject特性有助于调整Ninject所建对象的生命周期,以满足应用程序的需求。默认情况下,Ninject会在每次请求一个对象时,为每个依赖项所需的各个对象创建一个新实例。

修改LinqValueCalculator类的构造器,在每次创建一个新实例时,都向VS的输出窗口写一条消息

public class LinqValueCalculator : IValueCalculator
{
private IDiscountHelper discounter;
private static int counter = 0;

public LinqValueCalculator(IDiscountHelper discountParam)
{
discounter = discountParam;
System.Diagnostics.Debug.WriteLine(string.Format("Instance {0} created", ++counter)); // 输出counter
}

public decimal ValueProducts(IEnumerable<Product> products)
{
return discounter.ApplyDiscount(products.Sum(p => p.Price));
}
}

修改HomeController,在构造器参数中,增加一个IValueCalculator接口的实现。

public HomeController(IValueCalculator calcParam, IValueCalculator calc2)	// 增加calc2参数
{
calc = calcParam;
}

运行项目,观察VS的输出窗口,发现Ninject创建了LinqValueCalculator类的两个实例

对于某些类,如果希望在整个应用程序中共享一个单一的实例。而对于另一些类,希望为接收到的每个HTTP请求,都创建一个新的实例。可以在建立绑定时,通过作用域特性来控制所创建对象的生命周期

在AddBindings方法中,将请求作用域运用于LinqValueCalculator类。

private void AddBindings()
{
kernel.Bind<IValueCalculator>().To<LinqValueCalculator>().InRequestScope(); // 只创建LinqValueCalculator类的单一实例
kernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>().WithConstructorArgument("discountParam", 50M);
kernel.Bind<IDiscountHelper>().To<FlexibleDiscountHelper>().WhenInjectedInto<LinqValueCalculator>();
}

Ninject的作用域方法如下表

名称 效果
InTransientScope() 与未指定作用域效果相同,为每一个被解析的依赖项创建一个新的对象(每依赖项一实例
InSingletonScope()
ToConstant(object)
创建一个单一实例,使其共享于整个应用程序。如果使用InSingletonScope,或者为Ninject
提供ToConstant方法,Ninject便会创建这种实例(每应用一实例
InThreadScope() 创建一个单一实例,将其用于解析一个线程中各个对象的依赖项(每线程一实例
InRequestScope() 创建一个单一实例,用于解析一个HTTP请求中各个对象的依赖项(每请求一实例

VS的单元测试

可以使用VS附带的内置单元测试,也可以使用.NET单元测试包,如NUnit

在Models文件夹创建MinimumDiscountHelper.cs文件,实现IDiscountHelper接口

public class MinimumDiscountHelper : IDiscountHelper
{
public decimal ApplyDiscount(decimal totalParam) // 暂不实现功能
{
/*
- 总额大于$100,折扣为10%
- 总额介于(并包括)$10到$100之间,折扣为$5
- 总额小于$10,无折扣
- 总额为负值时,抛出一个ArgumentOutOfRangeException异常
*/
throw new NotImplementedException();
}
}

创建单元测试项目

右键解决方案名称,添加新项目,选择单元测试项目,项目名称为EssentialTools.Tests

右键EssentialTools.Tests项目,添加EssentialTools项目的引用

添加单元测试

修改UnitTest1.cs文件,添加测试方法。

[TestClass]
public class UnitTest1
{
private IDiscountHelper getTestObject() // 通过该方法建立测试方法
{
return new MinimumDiscountHelper();
}

[TestMethod]
public void Discount_Above_100()
{
// 准备
IDiscountHelper target = getTestObject();
decimal total = 200;

// 动作
var discountedTotal = target.ApplyDiscount(total);

// 断言
Assert.AreEqual(total * 0.9M, discountedTotal);
}
}

含有测试的TestClass注解属性注释,测试方法TestMethod注解属性注释。

Assert类有一些列可以在测试中使用的静态方法,该类位于Microsoft.VisualStudio.TestTools.UnitTesting命名空间,包含了一些对建立和执行测试有用的其他类。(表格略)

注意Microsoft.VisualStudio.TestTools.UnitTesting命名空间中有一个成员是ExpectedException属性。这是一个断言,只当单元测试抛出ExceptionType参数指定类型的异常时,该断言才是成功的。

接下来对测试项目增加一些测试,以验证前述MinimumDiscountHelper的其他行为。

[TestClass]
public class UnitTest1
{
private IDiscountHelper getTestObject()
{
return new MinimumDiscountHelper();
}

[TestMethod]
public void Discount_Above_100()
{
// 准备
IDiscountHelper target = getTestObject();
decimal total = 200;

// 动作
var discountedTotal = target.ApplyDiscount(total);

// 断言
Assert.AreEqual(total * 0.9M, discountedTotal);
}

[TestMethod]
public void Discount_Between_10_And_100()
{
// 准备
IDiscountHelper target = getTestObject();

// 动作
decimal TenDollarDiscount = target.ApplyDiscount(10);
decimal HundredDollarDiscount = target.ApplyDiscount(100);
decimal FiftyDollarDiscount = target.ApplyDiscount(50);

// 断言
Assert.AreEqual(5, TenDollarDiscount, "$10 discount is wrong");
Assert.AreEqual(95, HundredDollarDiscount, "$100 discount is wrong");
Assert.AreEqual(45, FiftyDollarDiscount, "$50 discount is wrong");
}

[TestMethod]
public void Discount_less_Than_10()
{
// 准备
IDiscountHelper target = getTestObject();

// 动作
decimal discount5 = target.ApplyDiscount(5);
decimal discount0 = target.ApplyDiscount(0);

// 断言
Assert.AreEqual(5, discount5);
Assert.AreEqual(0, discount0);
}

[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void Discount_Negative_Total()
{
// 准备
IDiscountHelper target = getTestObject();

// 动作
target.ApplyDiscount(-1);
}
}

运行单元测试(并失败)

点击VS菜单栏测试,选择测试资源管理器,在窗口左上角点击在视图中运行所有测试

结果都是失败的,因为所测试的这些方法还没有实现

实现特性

实现MinimumDiscountHelper类的功能。

public decimal ApplyDiscount(decimal totalParam)
{
if (totalParam < 0)
{
throw new ArgumentOutOfRangeException();
}
else if (totalParam > 100)
{
return totalParam * .09M;
}
else if (totalParam > 10 && totalParam <= 100)
{
return totalParam - 5;
}
else return totalParam;
}

测试并修正代码

实现完功能后,运行所有测试

结果显示,3个单元测试通过,但是Discount_Between_10_And_100失败。

原来问题出现在10边界值上。只需要在if语句添加=,包含恰好为10的值即可。

再次运行所有测试,全部通过。

使用Moq库

在实际的项目中,往往需要测试一些不能孤立运行的对象

一个有用的办法是使用模仿对象,它能够以一种特殊而受控的方式,来模拟项目中实际对象的功能。

右键EssentialTools.Tests项目,添加新项,选择基本单元测试,文件名为UnitTest2.cs

修改UnitTest2.cs文件,添加一个用于ShoppingCart类的单元测试。

[TestClass]
public class UnitTest2
{

private Product[] products = {
new Product { Name = "Kayak", Category = "Watersports", Price = 275M },
new Product { Name = "Lifejacket", Category = "Watersports", Price = 48.95M },
new Product { Name = "Soccer ball", Category = "Soccer", Price = 19.50M },
new Product { Name = "Corner flag", Category = "Soccer", Price = 34.95M }
};

[TestMethod]
public void Sum_Products_Correctly()
{
// 准备
var discounter = new MinimumDiscountHelper();
var target = new LinqValueCalculator(discounter);
var goalTotal = products.Sum(e => e.Price);

// 动作
var result = target.ValueProducts(products);

// 断言
Assert.AreEqual(goalTotal, result);
}
}

要面临的问题是,LinqValueCalculator类依赖于IDiscountHelper接口的实现才能进行操作。即:

  • 单元测试变得复杂和脆弱。一旦该实现中的折扣逻辑发生变化,即使LinqValueCalculator可以正常工作,测试仍会失败
  • 当单元测试失败时,不易知道问题是出在LinqValueCalculator类中,还是MinimumDiscountHelper类中。

将Moq添加到VS项目中

打开Nuget控制台输入以下命令:Install-Packet Moq -version 4.1.1309.1617 -projectname EssentialTools.Tests

projectname参数告诉Nuget,希望把Moq包安装到单元测试项目中,而不是安装到主应用程序中。

对单元测试添加模仿对象

对单元测试添加模仿对象,即告诉Moq,使用哪一种对象,对它的行为进行配置,然后将该对象运用于测试目标。

UnitTest2.cs的单元测试中,添加测试方法Sum_Products_Correctly,使用Mock对象

[TestMethod]
public void Sum_Products_Correctly()
{
// 准备
Mock<IDiscountHelper> mock = new Mock<IDiscountHelper>(); // 1.创建模仿对象。模仿的是IDiscountHelper的实现
mock.Setup(m => m.ApplyDiscount(It.IsAny<decimal>())).Returns<decimal>(total => total); // 2.模仿行为 + 3.定义结果
var target = new LinqValueCalculator(mock.Object); // 4.使用模仿对象

// 动作
var result = target.ValueProducts(products);

// 断言
Assert.AreEqual(products.Sum(e => e.Price), result);
}

注意

  • 使用Mock<T> mock = new Mock<T>();语句,创建模仿对象,并指定想要测试的接口T。
  • Setup方法,给模仿对象添加一个方法(可用LINQ或lambda表达式),传递要求它实现的接口(ApplyDiscount)。
  • It类定义了许多以泛型类型参数进行使用的方法。本例用decimal调用了IsAny方法,当以任何十进制值为参数来调用ApplyDiscount方法时生效。
  • Returns方法指定在调用模仿方法时Moq要返回的结果类型参数用以指定结果的类型(decimal)lambda表达式指定结果
  • 通过读取Mock<IDiscountHelper>对象的Object属性值来使用模仿对象。即mock.Object

下表展示了It类的静态方法。

方法 描述
Is<T>(predicate) 指定类型T的值,该值使“predicate(谓词)”返回true
IsAny<T>() 指定类型T的值为任意值
IsInRange<T>(min, max, kind) 如果参数介于定义值之间,而且是T类型的,则匹配。最后一个参数(kind)是该范围
的一个枚举值,可以是Inclusive(包括)或Exclusive(排除)
IsRegex(expr) 如果参数匹配指定的正则表达式,则匹配一个字符串参数

用Moq创建模仿对象的过程包括几个步骤:

  1. 用Mock创建模仿对象。
  2. 用Setup方法建立模仿对象的行为。
  3. 用It类设置行为的参数。
  4. 用Returns方法指定行为的返回值。
  5. 用lambda表达式在Returns方法中建立具体行为。

使用Moq的好处是,单元测试只检查LinqValueCalculator对象的行为,并不依赖于任何Models文件夹中的IDiscountHelper接口的实现。即,当测试失败时,可以知道问题出在LinqValueCalculator实现中,或者是建立模仿对象的方式中。

创建更复杂的模仿对象

UnitTest2.cs文件中,添加一个新的测试方法Pass_Through_Variable_Discounts,模仿更加复杂的IDiscountHelper接口实现。

[TestMethod]
[ExpectedException(typeof(System.ArgumentOutOfRangeException))]
public void Pass_Through_Variable_Discounts()
{
// 准备
Mock<IDiscountHelper> mock = new Mock<IDiscountHelper>();
mock.Setup(m => m.ApplyDiscount(It.IsAny<decimal>())).Returns<decimal>(total => total);
mock.Setup(m => m.ApplyDiscount(It.Is<decimal>(v => v == 0))).Throws<System.ArgumentOutOfRangeException>();
mock.Setup(m => m.ApplyDiscount(It.Is<decimal>(v => v > 100))).Returns<decimal>(total => (total * 0.9M));
mock.Setup(m => m.ApplyDiscount(It.IsInRange<decimal>(10, 100, Range.Inclusive))).Returns<decimal>(total => total - 5);
var target = new LinqValueCalculator(mock.Object);

// 动作
decimal FiveDollarDiscount = target.ValueProducts(createProduct(5));
decimal TenDollarDiscount = target.ValueProducts(createProduct(10));
decimal FiftyDollarDiscount = target.ValueProducts(createProduct(50));
decimal HundredDollarDiscount = target.ValueProducts(createProduct(100));
decimal FiveHundredDollarDiscount = target.ValueProducts(createProduct(500));

// 断言
Assert.AreEqual(5, FiveDollarDiscount, "$5 Fail");
Assert.AreEqual(5, TenDollarDiscount, "$10 Fail");
Assert.AreEqual(45, FiftyDollarDiscount, "$50 Fail");
Assert.AreEqual(95, HundredDollarDiscount, "$100 Fail");
Assert.AreEqual(450, FiveHundredDollarDiscount, "$500 Fail");
target.ValueProducts(createProduct(0));
}

注意

  1. 调用Setup方法的顺序影响模仿对象的行为,Moq会以相反的顺序评估给定的行为,必须按照从最一般得到最特殊的顺序。
  2. It.Is方法是为不同参数值建立指定行为最灵活的方式,可以用任何谓词返回true或false。(模仿特定值)
  3. It.IsInRange方法能够捕捉参数值的范围。(模仿值范围)