第6章 MVC基本工具 — 精通MVC 3 框架

来源:互联网 发布:sql 二次查询 编辑:程序博客网 时间:2024/06/06 00:06
 

Essential Tools for MVC
MVC基本工具

 

In this chapter, we are going to look at three tools that should be part of every MVC programmer’s arsenal. We mentioned all three in the previous chapter: a DI container, a unit test framework, and a mocking tool.
本章中,我们打算考察三个工具,它们应该是每个MVC程序员工具库的成员。在上一章中,这三个工具我们提到过:DI(依赖注入)容器、单元测试框架,和模仿工具。

We have picked three specific implementations of these tools for this book, but there are a lot of alternatives for each type of tool. If you can’t get along with the ones we use, don’t worry. There are so many out there that you’re certain to find something that suits the way your mind and workflow operate.
我们为这本书挑选了这些工具的三个专用工具,但是,对每类工具都有很多选择。如果你不适应我们所使用的,不必着急。还有很多,你肯定能从中找到适合你思维方式和工作流程的工具。

As noted in Chapter 5, Ninject is our preferred DI container. It is simple, elegant, and easy to use. There are more sophisticated alternatives, but we like the way that Ninject works with the minimum of configuration. We consider patterns to be starting points, not law, and we have found it easy to tailor our DI with Ninject. If you don’t like Ninject, we recommend trying Unity, which is one of the Microsoft alternatives.
正如第5章所说明的,Ninject是我们比较喜欢的DI容器。它简单、优雅、且易用。还有不少更完善的选择,但我们喜欢Ninject最小配置工作的方式。我们考虑以模式为起点,这并不是定律,而是我们发现用Ninject很容易定制我们的DI。如果你不喜欢Ninject,我们建议你用Unity,这是微软的一个DI工具。

For unit testing, we are going to be using the support that is built in to Visual Studio 2010. We used to use NUnit, which is one of the most popular .NET unit testing frameworks. We like NUnit, but we find that Visual Studio 2010 covers enough of the most important use cases, and the close integration with the rest of the integrated development environment (IDE) is a nice bonus.
对于单元测试,我们打算使用Visual Studio 2010内建的支持。我们习惯于使用NUnit,这是最流行的.NET单元测试框架。我们喜欢NUnit,但我们发现Visual Studio 2010涵盖了足够多的最重要的使用案例,而且与集成开发环境(IDE)的其余部分紧密结合是一个附带的好处。

The third tool we selected is Moq, which is a mocking tool kit. We use Moq to create implementations of interfaces to use in our unit tests. Programmers either love or hate Moq; there’s nothing in the middle. You’ll either find the syntax elegant and expressive, or you’ll be cursing every time you try to use it. If you just can’t get along with it, we suggest looking at Rhino Mocks, which is a nice alternative.
我们所选的第三个工具是Moq,这是一个模仿工具包。我们用Moq来生成单元测试的实现接口。程序员可能喜欢Moq,也可能恨它,再没有中间观点。你可能会发现其语法雅致而富于表现力,也可能每次使用它时都诅咒它。如果你觉得不能适应它,我们建议你考察Rhino Mocks,这是一个很好的选择。

We’ll introduce each of these tools and demonstrate their core features. We don’t provide exhaustive coverage of these tools—each could easily fill a book in its own right—but we’ve given you enough to get started and, critically, to follow the examples in the rest of the book.
我们将分别介绍这些工具,并演示它们的核心特性。我们不会提供这些工具的所有方面 它们每一个都可以写一本书 但我们所给出的,已足以使你起步、对之评价、以及理解本书其余部分的例子。

Using Ninject
使用Ninject

We introduced the idea of DI in Chapter 4. To recap, the idea is to decouple the components in our MVC applications, and we do this with a combination of interfaces and DI. Listing 6-1 shows an interface that expresses functionality for totaling the value of some products, as well as a concrete implementation of that interface.
我们在第4章介绍了DI思想。再次重申,该思想是让我们的MVC应用程序组件实现松耦合,实现的方法是结合接口与DI。清单6-1演示了一个接口,它表示了统计一些产品总价的功能,以及这个接口的具体实现。

Listing 6-1. The Class, the Interface, and Its Implementation

 1 public class Product { 2  3     public int ProductID { get; set; } 4  5     public string Name { get; set; } 6  7     public string Description { get; set; } 8  9     public decimal Price { get; set; }10 11     public string Category { set; get; }12 13 }14 15 public interface IValueCalculator {16 17     decimal ValueProducts(params Product[] products);18 19 }20 21 public class LinqValueCalculator : IValueCalculator {22 23     public decimal ValueProducts(params Product[] products) {24 25         return products.Sum(p => p.Price);26 27     }28 29 }

The Product class is the same one we used in Chapter 5. The IValueCalculator interface defines a method that takes one or more Product objects and returns the cumulative value. We have implemented the interface in the LinqValueCalculator class, which uses the LINQ extension method Sum to neatly generate a total of the Price properties of the Product objects. We now need to create a class that will use the IValueCalculator and that is designed for DI. This class is shown in Listing 6-2.
Product类是我们第5章使用的同一个类。IValueCalculator接口定义了一个方法,它以一个或多个Product对象为参数,并返回累计值。我们在LinqValueCalculator类中实现了这个接口,它使用LINQ扩展方法Sum巧妙地生成了一个对Product对象的Price属性的累计。我们现在需要生成一个使用IValueCalculator的类,而且这是为DI设计的。这个类如清单6-2所示。

Listing 6-2. Consuming the IValueCalculator Interface

View Code
public class ShoppingCart {    private IValueCalculator calculator;    public ShoppingCart(IValueCalculator calcParam) {        calculator = calcParam;    }    public decimal CalculateStockValue() {        // define the set of products to sum       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}        };         // calculate the total value of the products        decimal totalValue = calculator.ValueProducts(products);        // return the result        return totalValue;    }}

This is a very simple example. The constructor of the ShoppingCart class takes an IValueCalculator implementation as a parameter in preparation for DI. The CalculateStockValue method creates an array of Product objects and then calls the ValueProducts in the IValueCalculator interface to get a total, which is returned as the result. We have successfully decoupled the ShoppingCart and LinqValueCalculator classes, as shown in Figure 6-1, which illustrates the relationships among our four simple types.
这是一个很简单的例子。ShoppingCart类的构造器以一个IValueCalculator实现作为一个准备DI的参数。CalculateStockValue方法生成一个Product对象的数组,然后调用IValueCalculator接口中的ValueProducts来获得总价,以此作为返回结果。我们已经成功地去掉了ShoppingCart类与LinqValueCalculator类的耦合,如图6-1所示,它描述了这四个简单类型之间的关系。

 

Figure 6-1. The relationships among four simple types
图6-1. 四个简单类型之间的关系

The ShoppingCart class and the LinqValueCalculator class both depend on IValueCalculator, but ShoppingCart has no direct relationship with LinqValueCalculator; in fact, it doesn’t even know that LinqValueCalculator exists. We can change the implementation of LinqValueCalculator, or even substitute an entirely new implementation of IValueCalculator, and the ShoppingCart class is none the wiser.
ShoppingCart类和LinqValueCalculator类都依赖于IValueCalculator,但ShoppingCartLinqValueCalculator没有直接关系,事实上,它甚至不知道LinqValueCalculator的存在。我们可以修改LinqValueCalculator的实现,甚或用一个全新IValueCalculator实现来代替,ChoppingCart类依然一无所知。

n Note The Product class has a direct relationship with all three of the other types. We are not worried by this. Product is the equivalent of a domain model type, and we expect such classes to be strongly coupled with the rest of our application. If we weren’t building MVC applications, we might take a different view on this and decouple Product as well.
注:Product类与所有其它三个类型都有一个直接关系。对此我们不必着急。Product等于域模型类型,而且我们期望这种类与应用程序的其余部分是强耦合的。如果我们不是在建立MVC应用程序,我们也许会对此采取不同的观点,并去掉Product耦合。

Our objective is to be able to create instances of ShoppingCart and inject an implementation of the IValueCalculator class as a constructor parameter. This is the role that Ninject, our preferred DI container, plays for us. But before we can demonstrate Ninject, we need to get set up in Visual Studio.
我们的目标是能够生成ShoppingCart实例,并把IValueCalculator类的一个实现作为构造器参数进行注入。这是Ninject,我们喜欢的DI容器,为我们所起的作用。但在我们能够示范Ninject之前,我们需要在Visual Studio中进行安装。

Creating the Project
生成项目

We are going to start with a simple console application. Create a new project in Visual Studio using the Console Application template, which you can find in the Windows template section. We have called our project NinjectDemo, but the name is not important. Create the interface and classes from Listings 6-1 and 6-2, shown earlier. We have put everything into a single C# code file.
我们打算从一个简单的控制台应用程序开始。在Visual Studio中用控制台模板生成一个新项目,控制台项目可以在Windows模板段找到。我们将此项目称为NinjectDemo,但名字并不重要。生成如前面清单6-16-2所示的接口和类。

Adding Ninject
添加Ninject

To add Ninject to your project, you need the Visual Studio Library Package Manager. Right-click the project in the Solution Explorer window and select Add Package Library Reference from the pop-up menu to open the Add Library Package Reference dialog. Click Online on the left side of the dialog, and then enter Ninject in the search box at the top right. A number of items will appear, as shown in Figure 6-2.
要把Ninject添加到你的项目,你需要Visual Studio库包管理器。在解决方案窗口中右击你的项目,并从弹出菜单中选择“添加包库引用”,以打开“添加库包引用”对话框。在对话框的左侧点击“在线”,然后在右上角的搜索框中输入Ninject。于是会出现一些条目,如图6-2所示。

 

Figure 6-2. Adding Ninject to the Visual Studio project
图6-2. 将Ninject添加到Visual Studio项目

You’ll see several Ninject-related packages, but it should be clear which is the core Ninject library from the name and description—the other items will be extensions for Ninject that integrate it with different development frameworks and tools.
你将看到几个Ninject相关的包,但从名字和描述应该可以看出哪个是核心Ninject 其它条目应该是将Ninject与不同开发框架和工具集成的扩展。

Click the Install button to the right of the entry to add the library to your project. You’ll see the References folder opened in the Solution Explorer window, and the Ninject assembly downloaded and added to your project references.
点击条目右边的“Install”按钮将该库添加到你的项目。你将在解决方案窗口中看到打开的References文件夹,以及下载并被添加到你项目引用中的Ninject程序集。

n Tip If you have problems compiling your project after you have installed the Ninject package, select the Project Properties menu item under the Project menu and change the Target Framework setting from .NET Framework 4 Client Profile to .NET Framework 4. The client profile is a slimmed-down installation that omits a library that Ninject relies on.
提示:在已经安装了Ninject包之后,如果项目编译还有问题,请选择“项目”菜单中的“项目属性”菜单项,将“目标框架”的设置从“.NET Framework 4 Client Profile”改为“.NET Framework 4。客户端轮廓(Client Profile)是一种瘦型安装,它忽略了Ninject所依赖的一个库。

Getting Started with Ninject
Ninject初步

To prepare Ninject for use, we need to create an instance of a Ninject kernel, which is the object we will use to communicate with Ninject. We will do this in the Program class that Visual Studio created as part of the Console Application project template. This is the class that has the Main method. Creating the kernel is demonstrated in Listing 6-3.
为了准备使用Ninject,我们需要生成一个Ninject内核的实例,这是我们用来与Ninject进行通信的对象。我们将在Program类中完成这一工作,Program类是Visual Studio作为控制台应用程序项目模板部件所生成的。这是具有Main方法的类。生成内核如清单6-3所示。

Listing 6-3. Preparing a Ninject Kernel

using Ninject;class Program {    static void Main(string[] args) {        IKernel ninjectKernel = new StandardKernel();    }}

There are two stages to working with Ninject once you’ve created the kernel. The first is to bind the types you want associated with the interfaces you’ve created. In this case, we want to tell Ninject that when it receives a request for an implementation of IValueCalculator, it should create and return an instance of the LinqValueCalculator class. We do this using the Bind and To methods defined in the IKernel interface, as demonstrated in Listing 6-4.
一旦你已经生成了这个内核,与Ninject进行工作有两个阶段。第一阶段是把你想与之进行通信的类型与你已生成的这个接口绑定。这里,我们想告诉Ninject,当它接收到一个实现IValueCalculator的请求时,它应该生成并返回LinqValueCalculator类的一个实例。我们用该IKernel接口中定义的BindTo方法来完成这件事,如清单6-4所示。

Listing 6-4. Binding a Type to Ninject

View Code
class Program {    static void Main(string[] args) {        IKernel ninjectKernel = new StandardKernel();        ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator<();        // 上一条语句有误,语句最后的“<”,应该为“>” — 译者注    }}


The statement in bold binds the IValueCalculator interface to the LinqValueCalculator implementation class. We specify the interface we want to register by using it as the generic type parameter of the Bind method, and pass the type of the concrete implementation we want as the generic type parameter to the To method. The second stage is to use the Ninject Get method to create an object that implements the interface and pass it to the constructor of the ShoppingCart class, as shown in Listing 6-5.
黑体语句把IValueCalculator接口绑定到LinqValueCalculator实现类。通过用我们想要注册的接口作为Bind方法的一般类型参数,我们指定了这个接口,并且把这个类型的具体实现作为一般类型参数传递给To方法。第二阶段是用NinjectGet方法来生成一个实现这个接口的对象,并把它传递给ShoppingCart类的构造器,如清单6-5所示。

Listing 6-5. Instantiating an Interface Implementation via Ninject

view sourceprint?
...
ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
  
// get the interface implementation
IValueCalculator calcImpl = ninjectKernel.Get<IValueCalculator>();
  
// create the instance of ShoppingCart and inject the dependency
ShoppingCart cart = newShoppingCart(calcImpl);
  
// perform the calculation and write out the result
Console.WriteLine("Total: {0:c}", cart.CalculateStockValue());
...

We specify the interface we want an implementation for as the generic type parameter of the Get method. Ninject looks through the bindings we have defined, sees that we have bound the IValueCalculator to the LinqValueCalculator, and creates a new instance for us. We then inject the implementation into the constructor of the ShoppingCart class and call the CalculateStockValue method which in turn invokes a method defined in the interface. The result we get from this code is as follows:
我们把想要实现的接口指定为Get方法的一般类型参数。Ninject通过我们已经定义的绑定进行查找,明白我们已经把IValueCalculator绑定到LinqValueCalculator,于是为我们生成了一个新实例。然后我们把这个实现注入到ShoppingCart类的构造器中,并调用CalculateStockValue方法,它又反过来调用在这个接口中定义的方法。这段代码所得到的结果如下:

Total: $79,843.45

It may seem odd to have gone to the trouble of installing and using Ninject when we could have simply created the instance of LinqValueCalculator ourselves, like this:
当我们可以简单地自己生成LinqValueCalculator实例时,安装和使用Ninject似乎是多余的,像这样:

view sourceprint?
ShoppingCart cart = newShoppingCart(newLinqValueCalculator());

For a simple example like this one, it looks like more effort to use Ninject, but as we start to add complexity to our application, Ninject quickly becomes the low-effort option. In the next few sections, we’ll build up the complexity of the example and demonstrate some different features of Ninject.
对于像这样的一个简单的例子,使用Ninject看上去需要更多的功夫,但当我们开始把复杂性添加到我们的应用程序时,Ninject很快会变得方便。在以下几小节中,我们将建立复杂的例子,并演示Ninject的一些不同特性。

Creating Chains of Dependency
生成依赖链

When you ask Ninject to create a type, it examines the couplings between that type and other types. If there are additional dependencies, Ninject resolves them and creates instances of all of the classes that are required. To demonstrate this feature, we have created a new interface and a class that implements it, as shown in Listing 6-6.
当我们要求Ninject生成一个类型时,它检查这个类型与其它类型之间的耦合。如果有附加的依赖性,Ninject会解析这些依赖性,并生成所需要的所有类的实例。为了演示这一特性,我们生成一个新的接口和实现这个接口的类,如清单6-6所示。

Listing 6-6. Defining a New Interface and Implementation

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

The IDiscounHelper defines the ApplyDiscount method, which will apply a discount to a decimal value. The DefaultDiscounterHelper class implements the interface and applies a fixed 10 percent discount. We can then add the IDiscountHelper interface as a dependency to the LinqValueCalculator, as shown in Listing 6-7.
IDiscountHelper(原文这里少了字母t 译者注)定义了ApplyDiscount方法,它把一个折扣用于一个十进制值。DefaultDiscounterHelper类实现这个接口,并运用固定的10%折扣。我们随后可以把这个IDiscountHelper接口作为对LinqValueCalculator的一个依赖性,如清单6-7所示。

Listing 6-7. Adding a Dependency in the LinqValueCalculator Class

View Code
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));    }}

The newly added constructor for the class takes an implementation of the IDiscountHelper interface, which is then used in the ValueProducts method to apply a discount to the cumulative value of the Product objects being processed. We bind the IDiscountHelper interface to the implementation class with the Ninject kernel as we did for IValueCalculator, as shown in Listing 6-8.
这个类新添加的构造器以IDiscountHelper接口的一个实现为参数,它然后被用于ValueProducts方法中,以便把一个折扣运用于被处理的Product对象的累计值上。我们把IDiscountHelper接口绑定到带有Ninject内核的实现类上,正如我们对IValueCalculator所做的那样,如清单6-8所示。

Listing 6-8. Binding Another Interface to Its Implementation

view sourceprint?
...
IKernel ninjectKernel = newStandardKernel();
  
ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
  
ninjectKernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>();
  
// get the interface implementation
IValueCalculator calcImpl = ninjectKernel.Get<IValueCalculator>();
ShoppingCart cart = newShoppingCart(calcImpl);
Console.WriteLine("Total: {0:c}", cart.CalculateStockValue());
...

Listing 6-8 also uses the classes we created and the interfaces we bound using Ninject. We didn’t need to make any changes to the code that creates the IValueCalculator implementation.
清单6-8也使用了我们生成的类和我们用Ninject绑定的接口。我们不需要对生成IValueCalculator实现的代码作任何修改。

Ninject knows that we want the LinqValueCalculator class to be instantiated when an IValueCalculator is requested. It has examined this class and found that it depends on an interface that it is able to resolve. Ninject creates an instance of DefaultDiscountHelper, injects it into the constructor of the LinqValueCalculator class, and returns the result as an IValueCalculator. Ninject checks every class it instantiates for dependencies in this way, no matter how long or complex the chain of dependencies is.
IValueCalculator被请求时,Ninject知道我们是想实例化LinqValueCalculator类。它会考察这个类,并发现它依赖于一个可以解析的接口。Ninject会生成一个DefaultDiscountHelper实例,把它注入到LinqValueCalculator类的构造器中,并以IValuCalculator作为返回结果。Ninject会以这种方式检查它要实例化的每个依赖性类,而不管这个依赖性链有多长或有多复杂。

Specifying Property and Parameter Values
指定属性与参数值

We can configure the classes that Ninject creates by providing details of properties when we bind the interface to its implementation. We have revised the StandardDiscountHelper class so that it exposes a convenient property to specify the size of the discount, as shown in Listing 6-9.
在我们把接口绑定到它的实现时,我们可以提供属性细节来配置Ninject生成的类。我们修改了StandardDiscountHelper类,于是它暴露了一个方便的属性,以便指定折扣的大小,如清单5-9所示。

Listing 6-9. Adding a Property to an Implementation Class

View Code
public class DefaultDiscountHelper : IDiscountHelper {    public decimal DiscountSize { get; set; }    public decimal ApplyDiscount(decimal totalParam) {        return (totalParam - (DiscountSize / 100m * totalParam));    }}

When we bind the concrete class to the type with Ninject, we can use the WithPropertyValue method to set the value for the DiscountSize property in the DefaultDiscountHelper class, as shown in Listing 6-10.
当我们把具体类绑定到带有Ninject的类型时,我们可以用WithPropertyValue方法来设置DefaultDiscountHelper类中的DiscountSize属性,如清单6-10所示。

Listing 6-10. Using the Ninject WithPropertyValue Method

...
IKernel ninjectKernel = new StandardKernel();
ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
ninjectKernel.Bind<IDiscountHelper>()
        .To<DefaultDiscountHelper>().WithPropertyValue("DiscountSize", 50M);
...

Notice that we must supply the name of the property to set as a string value. We don’t need to change any other binding, nor change the way we use the Get method, to obtain an instance of the ShoppingCart method. The property value is set following construction of the DefaultDiscountHelper class, and has the effect of halving the total value of the items. The result from this change is as follows:
注意,我们必须提供一个字符串值作为要设置的属性名。我们不需要修改任何其它绑定,也不需要修改我们使用Get方法的方式来获得ShoppingCart方法的一个实例。该属性值会按照DefaultDiscountHelper的结构进行设置,并起到了半价的效果。根据这个修改所得到的结果如下:

view sourceprint?
Total: $39,921.73

If you have more than one property value you need to set, you can chain calls to the WithPropertyValue method to cover them all. We can do the same thing with constructor parameters. Listing 6-11 shows the DefaultDiscounter class reworked so that the size of the discount is passed as a constructor parameter.
如果你有不止一个属性值需要设置,你可以链接调用WithPropertyValue方法来涵盖所有这些属性。我们可以用构造器参数做同样的事。清单6-11演示了重写的DefaultDiscounter类,以使折扣大小作为构造器参数来进行传递。

Listing 6-11. Using a Constructor Property in an Implementation Class

View Code
public class DefaultDiscountHelper : IDiscountHelper {    private decimal discountRate;    public DefaultDiscountHelper(decimal discountParam) {        discountRate = discountParam;    }    public decimal ApplyDiscount(decimal totalParam) {        return (totalParam - (discountRate/ 100m * totalParam));    }}

To bind this class using Ninject, we specify the value of the constructor parameter using the WithConstructorArgument method, as shown in Listing 6-12.
为了用Ninject绑定这个类,我们用WithConstructorArgument方法来指定构造器参数的值,如清单6-12所示。

Listing 6-12. Binding to a Class that Requires a Constructor Parameter

view sourceprint?
1...
2IKernel ninjectKernel = newStandardKernel();
3ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
4ninjectKernel.Bind<IDiscountHelper>()
5    .To< DefaultDiscountHelper>().WithConstructorArgument("discountParam", 50M);
6...

This technique allows you to inject a value into the constructor. Once again, we can chain these method calls together to supply multiple values and mix and match with dependencies. Ninject will figure out what we need and create it accordingly.
这种技术允许你把一个值注入到构造器中。再一次地,我们可以把这些方法链接在一起来提供多值、混合值,并与依赖性匹配。Ninject会断定我们的需要并因此而生成它。

Using Self-Binding
自身绑定

A useful feature for integrating Ninject into your code fully is self-binding, which is where a concrete class can be requested (and therefore instantiated) from the Ninject kernel. This may seem like an odd thing to do, but it means that we don’t need to perform the initial DI by hand, like this:
Ninject集成到你的代码中的一个有用的特性是自身绑定,自身绑定是在通过Ninject内核请求一个具体类(并因此实例化)的地方进行的。这样做似乎有点古怪,但它意味着我们不需要手工地执行最初的DI,像这样:

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

ShoppingCart cart = new ShoppingCart(calcImpl);

Instead, we can simply request an instance of ShoppingCart and let Ninject sort out the dependency on the IValueCalculator class. Listing 6-13 shows the use of self-binding.
而是,我们可以简单地请求ShoppingCart的一个实例,让Ninject去挑出对IValueCalculator类的依赖。清单6-13演示了自身绑定的使用。

Listing 6-13. Using Ninject Self-Binding

view sourceprint?
1...
2ShoppingCart cart = ninjectKernel.Get<ShoppingCart>();
3...

We don’t need to do any preparation to self-bind a class. Ninject assumes that’s what we want when we request a concrete class for which it doesn’t have a binding.
我们不需要做任何准备来自身绑定一个类。当我们请求一个还没有进行绑定的具体类时,Ninject假设这就是我们所需要的。

Some DI purists don’t like self-binding, but we do. It helps handle the very first DI in an application and it puts everything, including concrete objects, into the Ninject scope. If we do take the time to register a self-binding type, we can use the features available for an interface, like specifying values for constructor parameters and properties. To register a self-binding, we use the ToSelf method, as demonstrated in Listing 6-14.
有些DI纯粹论者不喜欢自身绑定,但我们喜欢。它帮助处理应用程序中最初的DI工作,并把各种事物,包括具体对象,纳入Ninject范围。如果我们花时间去注册一个自身绑定类型,我们可以使用对一个接口有效的特性,就像指定构造器参数和属性值一样。为了注册一个自身绑定,我们使用ToSelf方法,如清单6-14所示。

Listing 6-14. Self-Binding a Concrete Type

View Code
ninjectKernel.Bind<ShoppingCart>().ToSelf().WithParameter("<parameterName>", <paramvalue>);


This example binds the ShoppingCart to itself and then calls the WithParameter method to supply a value for an (imaginary) property. You can self-bind only with concrete classes.
这个例子把ShoppingCart绑定到自身,然后调用WithParameter方法为一个(假想的)属性提供一个值。你也可以只对具体类进行自身绑定。

Binding to a Derived Type
绑定到派生类型

Although we have focused on interfaces (since that is most relevant in MVC applications), we can also use Ninject to bind concrete classes. In the previous section, we showed you how to bind a concrete class to itself, but we can also bind a concrete class to a derived class. Listing 6-15 shows a ShoppingCart class that has been modified to support easy derivation, and a derived class, LimitShoppingCart, which enhances its parent by excluding all items whose value exceeds a specified price limit.
虽然我们关注于接口(因为这是与MVC应用程序最相关的),但我们也可以用Ninject来绑定具体类。在前面的小节中,我们演示了如何把一个具体类绑定到自身,但我们也可以把一个具体类绑定到一个派生类。清单6-15演示了一个ShoppingCart类,它已作了修改以支持方便的派生,和一个派生类,LimitShoppingCart,它通过排除超过指定的限定价的所有条目的方法增强了父类功能。

Listing 6-15. Creating a Derived Shopping Cart Class

View Code
public class ShoppingCart {    protected IValueCalculator calculator;    protected Product[] products;    public ShoppingCart(IValueCalculator calcParam) {        calculator = calcParam;        // define the set of products to sum        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() {         // calculate the total value of the products        decimal totalValue = calculator.ValueProducts(products);        // return the result        return totalValue;    }}public class LimitShoppingCart : ShoppingCart {    public LimitShoppingCart(IValueCalculator calcParam)                            : base(calcParam) {        // nothing to do here    }    public override decimal CalculateStockValue() {        // filter out any items that are over the limit        var filteredProducts = products                    .Where(e => e.Price < ItemLimit);        // perform the calculation        return calculator.ValueProducts(filteredProducts.ToArray());    }    public decimal ItemLimit { get; set; }}

We can bind the parent class such that when we request an instance of it from Ninject, an instance of the derived class is created, as shown in Listing 6-16.
我们可以绑定父类,例如我们从Ninject请求父类的实例时,生成一个派生类的实例,如清单6-16所示。

Listing 6-16. Binding a Class to a Derived Version

View Code
...ninjectKernel.Bind<ShoppingCart>()    .To<LimitShoppingCart>()    .WithPropertyValue("ItemLimit", 200M);...

This technique works especially well for binding abstract classes to their concrete implementations.
这一技术对于把抽象类绑定到它们的具体实现上会工作得特别好。

Using Conditional Binding
条件绑定

We can bind multiple implementations of the same interface or multiple derivations of the same class with Ninject and provide instructions about which one should be used under different conditions. To demonstrate this feature, we have created a new implementation of the IValueCalculator interface, called IterativeValueCalculator, which is shown in Listing 6-17.
我们可以用Ninject绑定同一个接口的多个实现,或同一个类的多个派生,并提供在不同的条件下应该使用哪一个的指令。为了演示这一特性,我们生成了IValueCalculator接口的一个新实现,叫做IterativeValueCalculator,如清单6-17所示。

Listing 6-17. A New Implementation of the IValueCalculator

View Code
public class IterativeValueCalculator : IValueCalculator {    public decimal ValueProducts(params Product[] products) {        decimal totalValue = 0;        foreach (Product p in products) {            totalValue += p.Price;        }        return totalValue;    }}

Now that we have some choice of implementation, we can create Ninject bindings that can be used selectively. Listing 6-18 contains an example.
现在,我们有了一些选择性的实现,我们可以有选择地生成Ninject绑定。清单6-18演示了一个例子。

Listing 6-18. A Conditional Ninject Binding

View Code
...ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>();ninjectKernel.Bind<IValueCalculator>()    .To<IterativeValueCalculator>()    .WhenInjectedInto<LimitShoppingCart>();...

The new binding specifies that the IterativeValueCalculator class should be instantiated to service requests for the IValueCalculator interface when the object into which the dependency is being injected is an instance of the LimitShoppingCart class. We have left the original binding for IValueCalculator in place. Ninject tries to find the best match for a binding, and if the criteria for a conditional can’t be satisfied, it helps to have a default binding for the same class or interface, so that Ninject has a fallback value. The most useful conditional binding methods are shown in Table 6-1.
新绑定指明,当要被依赖性注入的对象是LimitShoppingCart类的一个实例时,IterativeValueCalculator类应该被实例化,以便对IValueCalculator接口的请求进行服务。我们在适当的位置留下了对IValueCalculator的绑定。Ninject试图对一个绑定找到最佳匹配,如果一个条件的判据不能得到满足时,它有助于对同一个类或接口采用一个默认绑定,因此,Ninject有一个回滚值。最有用的条件绑定方法如表6-1所示。

Table 6-1. Ninject Conditional Binding Methods
6-1. Ninject条件绑定方法

Method
方法

Effect
效果

When(predicate)

Binding is used when the predicate—a lambda expression—evaluates to true.
当谓词 一个lambda表达式 评估为true时进行绑定

WhenClassHas<T>()

Binding is used when the class being injected is annotated with the attribute whose type is specified by T.
当被注入的类以由T指定类型的属性进行说明时绑定

WhenInjectedInto<T>()

Binding is used when the class being injected into is of type T (see the example in Listing 6-18).
当要被注入的类是类型T时绑定(见清单6-18示例)

 Applying Ninject to ASP.NET MVC
Ninject运用于APS.NET MVC

We’ve shown you the core features of Ninject using a standard Windows console application, but integrating Ninject with ASP.NET MVC couldn’t be easier. The first step is to create a class that’s derived from System.Web.Mvc.DefaultControllerFactory. This is the class that MVC relies on by default to create instances of controller classes. (In Chapter 14, we show you how to replace the default controller factory with a custom implementation.) Our implementation is called NinjectControllerFactory and is shown in Listing 6-19.
我们已经用一个标准的Windows控制台应用程序,向你演示了Ninject的核心特性,但把NinjectASP.NET MVC集成并不是很容易的。第一步是要生成一个从System.Web.Mvc.DefaultControllerFactory派生而来的类。这是MVC默认地赖以生成控制器类实例的一个类。(在第14章,我们将向你演示如何用一个自定义的实现来替换这个默认的控制器生成器(控制器工厂))我们的实现叫做NinjectControllerFactory,如清单6-19所示。

Listing 6-19. The NinjectControllerFactory

View Code
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();            AddBindings();        }        protected override IController GetControllerInstance(RequestContext requestContext,                Type controllerType) {            return controllerType == null                    ? null                    : (IController)ninjectKernel.Get(controllerType);        }        private void AddBindings() {            // put additional bindings here            ninjectKernel.Bind<IProductRepository>().To<FakeProductRepository>();        }    }}

This class creates a Ninject kernel and uses it to service requests for controller classes that are made through the GetControllerInstance method, which is called by the MVC Framework when it wants a controller object. We don’t need to explicitly bind controller classes using Ninject. We can rely on the default self-binding feature, since the controllers are concrete classes that are derived from System.Web.Mvc.Controller.
这个类生成了一个Ninject内核,并用它对通过GetControllerInstance方法产生的控制器类的请求进行服务。GetControllerInstance方法是由MVC框架在需要一个控制器对象时调用的。我们不需要用Ninject明确地绑定控制器。我们可以依靠其默认的自身绑定特性,因为控制器是从System.Web.Mvc.Controller派生的具体类。

The AddBindings method allows us to add other Ninject bindings for repositories and other components we want to keep loosely coupled. We can also use this method as an opportunity to bind controller classes that require additional constructor parameters or property values.
AddBinding方法允许我们对存储库和我们希望保持紧耦合的组件添加其它Ninject绑定。我们也可以把这个方法用作为对需要附加的构造器参数或属性值的控制器类进行绑定的机会。

Once we create this class, we must register it with the MVC Framework, which we do in the Application_Start method of the Global.asax class, as shown in Listing 6-20.
一旦我们生成了这个类,我们必须用MVC框架对它进行注册,我们在Global.asax类的Application_Start方法中来完成这件事,如清单6-20所示。

Listing 6-20. Registering the NinjectControllerFactory Class with the MVC Framework

View Code
protected void Application_Start() {    AreaRegistration.RegisterAllAreas();    RegisterGlobalFilters(GlobalFilters.Filters);    RegisterRoutes(RouteTable.Routes);    ControllerBuilder.Current.SetControllerFactory(new NinjectControllerFactory());}

Now the MVC Framework will use our NinjectControllerFactory to obtain instances of controller classes, and Ninject will handle DI into the controller objects automatically.
现在,MVC框架将使用我们的NinjectControllerFactory来获得控制器的实例,而且Ninject将自动地把DI运用到控制器对象中。

You can see that the listings in this example refer to types such as IProductRepository, FakeProductRepository, Product, and so on. We have created a simple MVC application to demonstrate the Ninject integration, and these are the domain model types and repository types required for the demo. We aren’t going to go into the project because you’ll see these classes used properly in the next chapter. But if you are interested in what we created for this example, you can find the project in the source code download that accompanies this book.
你可以看到本例清单引用了IProductRepositoryFakeProductRepositoryProduct等类型。我们已经生成了一个演示Ninject集成的简单的MVC应用程序,这些类型是本演示所需要的域模型类型和存储库类型。我们此刻不打算介入到这个项目中去,因为你将在下一章看到这些类的适当使用。但如果你对我们生成的这个例子感兴趣,你可以在这本书伴随的源代码下载中找到这个项目。

It might seem that we have traveled a long way to get to a simple integration class, but we think it is essential that you fully understand how Ninject works. A good understanding of your DI container can make development and testing simpler and easier.
我们似乎对一个简单的集成类已经介绍得很多了,但我们认为这是你完全理解Ninject如何工作的基础。对DI容器的良好理解可以使开发和测试更简单容易。

Unit Testing with Visual Studio
Visual Studio的单元测试

There are a lot of .NET unit testing packages, many of which are open source and freely available. In this book, we are going to use the built-in unit test support that comes with Visual Studio 2010. This is the first version of Visual Studio that has testing support we feel is credible and useful.
有很多.NET单元测试包,其中许多是开源和免费的。本书中,我们打算使用Visual Studio 2010所提供的内建的单元测试支持。这是Visual Studio具有测试支持的第一个版本,我们感觉它是可信和有用的。

Many other .NET unit test packages are available. The most popular is probably NUnit. All of the packages do much the same thing, and the reason we have selected the Visual Studio support is that we like the integration with the rest of the IDE, which makes it easier to set up and run tests than using an add-on library. In this section, we’ll show you how to create a unit test project and populate it with tests.
有许多其它.NET单元测试包可用。最流行的可能是NUnit。所有测试包都做了大量同样的事情,而我们选择Visual Studio支持的理由是我们喜欢它与IDE其余部分的集成,这使它比使用一个扩展库更容易建立和运行测试。在本小节中,我们将给你演示如何生成单元测试项目,并用测试来组装它。

n Note Microsoft Visual Web Developer Express doesn’t include support for unit testing. This is one of the ways that Microsoft differentiates between the free and commercial Visual Studio editions. If you are using Web Developer Express, we recommend you use NUnit (www.nunit.org), which works in a similar way to the integrated features that we discuss in this chapter.
注:Microsoft Visual Web Developer Express(微软的另一个简装版开发工具 译者注)不包含单元测试支持。这是微软区分Visual Studio免费版和商业版的方式之一。如果你使用的是Web Developer Express,我们建议你使用NUnitwww.nunit.org),它对我们本章讨论的集成特性有类似的工作方式。

Creating the Project
生成项目

We are going to use another console application project to demonstrate unit testing. Create a project using this template. We called our project ProductApp. When you have created the project, define the interfaces and model type as shown in Listing 6-21.
我们打算用另一个控制台应用程序项目来演示单元测试。用这个模板生成一个项目,我们取名为ProductApp。当你已经生成了这个项目后,定义如清单6-21所示的接口和模型类型。

Listing 6-21. The Interfaces and Model Types for the Unit Test Demonstration

View Code
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<Product> GetProducts();    void UpdateProduct(Product product);}public interface IPriceReducer {    void ReducePrices(decimal priceReduction);}

The Product class is just like the one we used in earlier examples. The IProductRepository interface defines a repository through which we will obtain and update Product objects. The IPriceReducer interface specifies a function that will be applied to all Products, reducing their price by the amount specified by the priceReduction parameter.
Product类只不过与我们前面例子所使用的相同。IProductRepository接口定义了一个存储库,通过它我们将获得和更新产品对象。IPriceReducer接口指定了一个功能,它将运用于所有Products,通过由priceReduction参数所指定的量来降低Products的价格。

Our objective in this example is to create an implementation of IPriceReducer that meets the following conditions:
我们本例的目标是生成IPriceReducer的一个实现,它满足以下条件:

l        The price of all Product items in the repository should be reduced.
降低存储库中所有Product条目的价格

l        The total reduction should be the value of the priceReduction parameter multiplied by the number of products.
总降价应该是priceReduction参数乘以产品数目所得的值

l        The repository UpdateProduct method should be called for every Product object.
存储库UpdateProduct方法应该对每个Product对象进行调用

l        No price should be reduced to less than $1.
降价后没有价格小于$1

To aid us in building this implementation, we have created the FakeRepository class, which implements the IProductRepository interface, as shown in Listing 6-22.
为了帮助我们建立这个实现,我们已经生成了FakeRepository类,它实现了IProductRepository接口,如清单6-22所示。

Listing 6-22. The FakeRepository Class

View Code
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<Product> 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);    }}

We’ll come back to this class later. We have also written a skeletal version of the MyPriceReducer class, which will be our implementation of the IPriceReducer class. This is shown in Listing 6-23.
我们之后将回过头来讨论这个类。我们也已经写出了MyPriceReducer类的一个骨架,它将是IPriceReducer类的实现。如清单6-23所示。

Listing 6-23. The Skeletal MyPriceReducer Class

View Code
public class MyPriceReducer : IPriceReducer {    private IProductRepository repository;    public MyPriceReducer(IProductRepository repo) {        repository = repo;    }    public void ReducePrices(decimal priceReduction) {        throw new NotImplementedException();    }}

This class doesn’t yet implement the ReducePrices method, but it does have a constructor that will let us inject an implementation of the IProductRepository interface.
这个类还没有实现ReducePrices方法,但它却已经有了一个构造器,它让我们注入一个IProductRepository接口的实现。

 The last step is to add Ninject as a reference to our project, using either the Library Package Manager or a version you have downloaded from the Ninject web site.
最后一步是用库包管理器或你从Ninjectweb网站上下载的版本,把Ninject添加为我们项目的引用。

Creating Unit Tests
生成单元测试

We are going to following the TDD pattern and write our unit tests before we write the application code. Right-click the MyPriceReducer.ReducePrices method in Visual Studio, and then select Create Unit Tests from the pop-up menu, as shown in Figure 6-3.
我们打算遵照TDD模式(测试驱动开发模式),并在编写应用程序代码之前,先写我们的单元测试。在Visual Studio中右击MyPriceReducer.ReducePrices方法,然后从弹出菜单中选择“生成单元测试”,如图6-3所示。

 

Figure 6-3. Creating unit tests
6-3. 生成单元测试

Visual Studio will display the Create Unit Tests dialog, shown in Figure 6-4. All of the types that are available in the project are displayed, and you can check the ones for which you want tests created. Since we started this process from the ReducePrices method in the MyPriceReducer class, this item is already checked.
Visual Studio将显示“生成单元测试”对话框,如图6-4所示。项目中可用的所有类型都被显示出来,你可以选中一个你想要生成测试的条目。因为我们是从MyPriceReducer类中的ReducePrices方法启动这一过程的,这个条目已经被选中了。

 

Figure 6-4. Creating the first unit test
6-4. 生成第一个单元测试

Unit tests are created in a separate project from the application itself. Since we haven’t yet created such a project, the Output Project option is set to create a new project for us. Click the OK button, and Visual Studio will prompt you for a name for the test project, as shown in Figure 6-5. The convention we follow is to name the project <MainProjectName>.Tests. Since our project is called ProductApp, our test project will be called ProductApp.Tests.
单元测试以这个应用程序的一个独立项目的形式来生成。因为我们还没有生成这种测试项目,“输出项目”选项被设置到“生成一个新项目”。点击OK之后,Visual Studio将提示你对这个测试项目取一个名字,图6-5所示。我们依照的约定是把这个项目命名为<主项目名>.Tests。因为我们的项目叫做ProductApp,故我们的测试项目将被叫做ProductApp.Tests

 

Figure 6-5. Selecting the name for the test project
6-5. 选择测试项目的名字

Click the Create button to create the project and the unit test. Visual Studio will add the project to the existing solution. If you open the References item for the test project in the Solution Explorer window, you’ll see that Visual Studio has automatically added the assembly references we need, including the output from the main project and Ninject.
点击“Create”按钮,以生成这个项目和单元测试。Visual Studio将把这个项目添加到现在的解决方案中。如果你在解决方案浏览器窗口中打开用于测试项目的“引用”条目,你会看到Visual Studio已经自动地添加了我们所需要的程序集引用,包括了主项目和Ninject

 A new code file called MyPriceReducerTest.cs has been created as part of the test project; it contains some properties and methods to get us started. However, we are going to ignore these and start from scratch so we have only the items we care about. Edit the class file so it matches Listing 6-24.
作为测试项目的一部分,已经生成了一个名为MyPriceReducerTest.cs的新代码文件,它包含了一些让我们开始工作的属性和方法。然而,我们打算忽略这些东西并从头开始,这样便只有我们所关心的条目。编辑这个类文件,让它符合清单6-24

Listing 6-24. The Unit Test Class

View Code
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) {        // 上条语句似乎应当为:public void All_Prices_Are_Changed() { — 译者注          // Arrange            FakeRepository repo = new FakeRepository();            decimal reductionAmount = 10;            IEnumerable<decimal> prices = repo.GetProducts().Select(e => e.Price);            decimal[] initialPrices = prices.ToArray();            MyPriceReducer target = new MyPriceReducer(repo);            // Act            target.ReducePrices(reductionAmount);            prices.Zip(initialPrices, (p1, p2) => {                if (p1 == p2) {                    Assert.Fail();                }                return p1;            });        }    }}

Listing 6-24 contains the first of our unit tests and the attributes that Visual Studio looks for when running tests. The TestClass attribute is applied to a class that contains tests, and the TestMethod attribute is applied to any method that contains a unit test. Methods that don’t have this attribute are assumed to be support methods and are ignored by Visual Studio.
清单6-24含有我们的第一个单元测试和Visual Studio在运行测试时所期待的属性。TestClass属性被用于一个含有测试的类,而TestMethod属性被用于含有一个单元测试的方法。没有这个属性的方法被假设为是支持方法,Visual Studio对之是忽略的。

You can see that we have followed the arrange/act/assert (A/A/A) pattern in the unit test method. There are any number of conventions about how to name unit tests, but our guidance is simply that you use names that make it clear what the test is checking. Our unit test method is called All_Prices_Are_Changed, which seems plenty clear to us. But if you don’t like this style, all that really matters is that you (and your team) understand whatever nomenclature you settle on.
你可以看到,我们遵循了单元测试方法中的“布置/动作/断言”(A/A/A)模式。如何命名单元测试有许多约定,但我们的指导是简单,你所采用的名字能清楚地表达它测试什么即可。我们的单元测试方法名为All_Prices_Are_Changed,这似乎对我们来说已经足够清楚了。但如果你不喜欢这种风格,真正要做的事情是你(及你的团队)理解你所决定的命名法。

In the All_Prices_Are_Changed method, we get set up by defining a LINQ query that we then invoke using the ToArray extension method to get the initial prices for the Product items the FakeRepository class contains. Next, we call the target method and then use the LINQ Zip method to ensure that every price has changed. If any element is unchanged, we call the Asset.Fail method, which causes the unit test to fail.
All_Prices_Are_Changed方法中,我们作了一些准备,定义了一个LINQ查询,我们之后用ToArray扩展方法来调用这个查询,以获得FakeRepository类所含有的Product条目的最初价格。下一步我们调用了目标方法,然后用LINQZip方法以确保每一个价格已经有了变化。如果有任一元素未改变,则调用Asser.Fail方法,它引发此单元测试失败。

There are a lot of different ways to build unit tests. A common one is to have a single giant method that tests all of the required conditions for a feature. We prefer to create a lot of little unit tests that each focus on just one aspect of the application. There are two reasons for our preference. The first is that when a small unit test fails, you know exactly which criteria your code doesn’t meet. The second reason is that we tend to end up with scruffy code in multiple-test methods, as we hack around making the tests reasonable. You may be more focused than we are in your code, but we find a clean application of the A/A/A pattern works best for us.
建立单元测试有很多不同的方法。常用的一个方法是用一个单一的大方法,它测试一个特性的所有必要条件。我们更喜欢生成多个小的单元测试,每一个都仅注重于应用程序的一个方面。我们这种爱好有两个原因。第一是当一个小的单元测试失败时,你可以准确地知道你的代码不满足哪个条件。第二是在我们悠闲地构造合理化测试过程中,我们逐步杜绝了杂乱的代码。你也许比我们更关注于你的代码,但我们发现,一个整洁的A/A/A模式的应用程序对我们来说是最好的。

Following the TDD pattern, we have continued to define our unit tests, as shown in Listing 6-25.
遵照TDD模式,我们继续定义了我们的单元测试,如清单6-25所示。

Listing 6-25. The Remaining Unit Tests

View Code
[TestMethod]public void Correct_Total_Reduction_Amount() {    // Arrange    FakeRepository repo = new FakeRepository();    decimal reductionAmount = 10;    decimal initialTotal = repo.GetTotalValue();    MyPriceReducer target = new MyPriceReducer(repo);    // Act    target.ReducePrices(reductionAmount);    // Assert    Assert.AreEqual(repo.GetTotalValue(),         (initialTotal - (repo.GetProducts().Count() * reductionAmount)));}[TestMethod]public void No_Price_Less_Than_One_Dollar() {    // Arrange    FakeRepository repo = new FakeRepository();    decimal reductionAmount = decimal.MaxValue;    MyPriceReducer target = new MyPriceReducer(repo);    // Act    target.ReducePrices(reductionAmount);    // Assert    foreach (Product prod in repo.GetProducts()) {        Assert.IsTrue(prod.Price >= 1);    }}

Each of these methods follows the same pattern. We create a FakeRepository object and manually inject it into the constructor for the MyPriceReducer class. We then call the ReducePrices method and check the results, using the methods of the Assert class. We are not going to go into the individual tests, because they are all pretty simple. Table 6-2 shows the static methods of the Assert class that you can use to check or report the status of a test.这些方法都遵照了同样的模式。我们生成了FakeRepitory对象,并手工地把它注入到MyPriceReducer类的构造器中。然后我们调用ReducePrices方法,并用Assert类的方法检查结果。我们不打算进入个别的测试,因为它们都很简单。表6-2显示了Assert类的静态方法,你可以用它们来检查或报告一个测试的状态。

Table 6-2. Static Assert Methods 6-2. Assert静态方法

Method 方法

Description 描述

AreEqual<T>(T, T)

AreEqual<T>(T, T, string)

Asserts that two objects of type T have the same value. 断言两个类型T的对象有同样的值

AreNotEqual<T>(T, T)

AreNotEqual<T>(T, T, string)

Asserts that two objects of type T do not have the same value 断言两个类型T的对象的值不相等

AreSame<T>(T, T)

AreSame<T>(T, T, string)

Asserts that two variables refer to the same object 断言两个变量指向同样的对象

AreNotSame<T>(T, T)

AreNotSame<T>(T, T, string)

Asserts that two variables refer to different objects. 断言两个对象指向不同的对象

Fail()

Fail(string)

Fails an assertion—no conditions are checked 舍弃一个断言 无检查条件

Inconclusive()

Inconclusive(string)

Indicates that the result of the unit test can’t be definitively established 指明不能最终建立单元测试的结果

IsTrue(bool)

IsTrue(bool, string)

Asserts that a bool value is true—most often used to evaluate an expression that returns a bool result断言一个布尔值为true 最常用于评估一个返回布尔结果的表达式

IsFalse(bool)

IsFalse(bool, string)

Asserts that a bool value is false 断言一个布尔值为false

IsNull(object)

IsNull(object, string)

Asserts that a variable is not assigned an object reference 断言一个变量没有被分配一个对象参考

IsNotNull(object)

IsNotNull(object, string)

Asserts that a variable is assigned an object reference 断言一个变量被分配了一个对象参考

IsInstanceOfType(object, Type)

IsInstanceOfType(object, Type, string)

Asserts that an object is of the specified type or is derived from the specified type断言一个对象是指定类型的对象,或是从指定类型派生的

IsNotInstanceOfType(object, Type)

IsNotInstanceOfType(object, Type, string)

Asserts that an object is not of the specified type 断言一个对象不是指定类型的对象

Each of the static methods in the Assert class allows you to check some aspect of your unit test. An exception is thrown if an assertion fails, and this means that the entire unit test fails. Each unit test is treated separately, so other tests will continue to be performed. Assert类中的每一个静态方法都可以检查你单元测试的某个方面。如果断言失败,将弹出一个异常,这意味着整个单元测试失败。每一个单元测试都被独立地处理,因此其它单元测试将被继续执行。

Each of these methods is overloaded with a version that takes a string parameter. The string is included as the message element of the exception if the assertion fails. The AreEqual and AreNotEqual methods have a number of overloads that cater to comparing specific types. For example, there is a version that allows strings to be compared without taking case into account.上述每一个方法都有一个以字符串为参数的过载。该字符串作为断言失败时的消息元素。AreEqualAreNotEqual方法有几个过载,以迎合特定类型的比较。例如,有一个版本可以比较字符串而不需要考虑其它情况。

One oddity of note is the ExceptionExpected attribute. This is an assertion that succeeds only if the unit test throws an exception of the type specified by the ExceptionType parameter. This is a neat way of ensuring that exceptions are thrown without needing to mess around with try...catch blocks in your unit test.一个古怪的说明是ExceptionExpected属性。只有单元测试弹出由ExceptionType参数指定的类型的异常时,这个断言才是成功的。这是确保弹出异常而不需要在单元测试中用try…catch块来浪费时间的一种灵活的方式。

Running the Unit Tests (and Failing) 运行单元测试(并失败)

Using Table 6-2, you can see what each of our example unit tests are checking for. To run those tests, select Run from the Visual Studio Test menu and then choose All Tests in Solution. Visual Studio will scan through all of the classes in the current solution looking for the TestClass and TestMethod attributes.利用表6-2,你可以看到我们的每一个单元测试例子检查的是什么。要运行这些测试,从Visual Studio的“测试”菜单选择运行,然后在解决方案中选择所有测试。Visual Studio将扫描当前解决方案中的所有类,以寻找TestClassTestMethod属性。

n Tip If you select Run All from the Test Debug menu, Visual Studio will execute the unit tests but break into the debugger when an assertion fails. This is a very handy feature for checking the values of the inputs to your assertions to see what has gone wrong. 提示:如果你从“测试” “调试”菜单中选择“运行所有测试”,Visual Studio将执行这些单元测试,但当一个断言失败时将中断并进入调试状态。这对检查输入到断言的值,以考察发生了什么错误,是一个十分灵活的。

The Test Results window displays progress as each test is performed, and it gives a green or red indicator to show the results. We have yet to implement our functionality, so all four of our unit tests fail, as shown in Figure 6-6. 测试结果窗口在每个测试被执行时,显示测试过程,而且以绿色或红色指示符来显示测试结果。我们还没有实现我们的功能性,因此所有四个单元测试都是失败的,如图6-6所示。

 

Figure 6-6. The initial unit test results6-6. 最初的单元测试结果

n Tip Right-click one of the items in the Test Results window and select View Test Results Details if you want information about why a test failed.提示:如果你想了解测试为什么失败的信息,可在测试结果窗口中右击一个条目,并选择“查看测试结果细节”。

Implementing the Feature 实现特性

We are now at the point where we can implement the feature, safe in the knowledge that we will be able to check the quality of our code when we are finished. For all our preparation, the implementation of the ReducePrices method is pretty simple, as shown in Listing 6-26. 我们现在已经到了实现功能特性的时候了,从安全角度上讲,当我们完成我们的工作时,我们将能够检查我们代码的质量。对于我们的所有准备,ReducePrices方法的实现是相当简单的,如清单6-26所示。

Listing 6-26. Implementing the Feature

View Code
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);        }    }}

Now let’s run our tests again. This time, they all pass, as shown in Figure 6-7.
现在让我们再次运行我们的测试。这次,它们都通过了,如图6-7所示。

 

Figure 6-7. Passing the unit tests
6-7. 单元测试通过

We have given you a very quick introduction to unit testing, and we’ll continue to demonstrate unit tests as we go through the book. Note that Visual Studio has advanced features in case you catch the unit-testing bug. You’ll see some of these features in the next section when we look at mocking.
我们已经向你十分简洁地介绍了单元测试,随着本书的深入,我们还将继续向你演示单元测试。注意,Visual Studio已经提升了捕捉单元测试缺陷的特性。在下一小节当我们考查模仿时,你将看到一些这样的特性。

You can also arrange tests so that they are performed in sequence, group tests by categories and run them together, record the amount of time unit tests take, and much more. We recommend you explore the unit testing documentation on MSDN.
你也可以安排一些测试以使它们依序执行、按类别组合一些测试以让它们一起运行、记录单元测试所花费的时间,以及其它许多事情。我们建议你研究MSDN上的单元测试文档。

Using Moq
使用Moq

In the previous example, we created the FakeRepository class to support our testing. We have not yet explained how to create a real repository implementation, and so we need a substitute. Even when we have a real implementation, we might not want to use it, because it adds complexity to our test environment (or because the operational cost of the repository is high, or for one of a hundred other reasons).
在前一个例子中,我们生成了一个FakeRepository类来支持我们的测试。由于我们还没有解释如何生成一个实际存储库实现,因此我们需要一个替代品。即使当我们已经有一个实际的实现时,我们也许不想用它,因为它给我们的测试环境增加了复杂性(或者因为存储库的操作代价太高,或者因为其它几百个理由之一)。

The FakeRepository class is a mock implementation of the IProductRepository interface. We didn’t implement the true functionality that a real repository would need. We just did enough to be able to write our unit tests. And we added features that were not related to repositories at all. For example, one test required us to ensure that the UpdateProduct method was called a certain number of times, which we did by adding a property. Another test led us to add a method so we could calculate the total value of the Product objects.
FakeRepository类是IProductRepository接口的一个模仿实现。我们并不实现一个实际存储库所需的真实功能。我们只要足以使我们能够编写单元测试即可。而且我们添加了与存储库根本无关的特性。例如,一个测试需要我们确保UpdateProduct方法被调用了一定次数,对此我们是通过添加一个属性来实现的。另一个测试是让我们添加一个方法,于是我们能够计算Product对象的总价。

We created the fake implementation and added the extra bits manually, which makes the FakeRepository class a manual mock (we promise we are not making up these terms). The subject of this part of the chapter is Moq, a framework that makes mocking quicker, simpler, and easier.
我们生成模仿实现并手工地添加了附加位,这使FakeRepository类成为一个手工模仿(我们保证并不是在虚构这些术语)。本章这部分的主题是Moq,这是使模仿更快、更简单、和更容易的一个框架。

Adding Moq to the Visual Studio Project
Moq添加到Visual Studio项目

We are going to build on the previous example and replace the FakeRepository class with a mock created using Moq. To prepare the project, we must add the Moq assembly, either by using the Library Package Manager or by downloading the library from http://code.google.com/p/moq. Add Moq.dll as a project reference (using either the download or the Library Package Manager) to the ProductApp.Tests project (to the unit test projection, not the application project).
我们打算在前面例子的基础上,用一个由Moq生成的模仿代替FakeRepository类。为了准备这个项目,我们必须添加Moq程序集,通过库包管理器,或者通过从http://code.google.com/p/moq下载这个库。把Moq.dll作为一个项目的引用(用下载或者用库包管理器)添加到ProductApp.Tests项目(添加到单元测试项目,而不是加到应用程序项目)。

Creating a Mock with Moq
生成一个带有Moq的模仿

The benefit of using a mocking tool is that we can create mocks that are tailored with just enough functionality to help us in our tests. That means we don’t end up with a mock implementation that gets too complicated. In a real project, unlike in these simple examples, you can easily reach the stage where the mock implementation needs its own tests because it contains so much code. We could make a lot of little manual mocks, but to make that effective, we would need to move the recurring code into a base class, and we would be right back to too much complexity again. Unit testing works best when tests are small and focused, and you keep everything else as simple as possible.
使用模仿工具的一个好处是,我们可以生成经过裁剪的模仿,它只有足以帮助我们进行测试的功能。这意味着我们不会因为太复杂的模仿实现而使工作无法进行。在一个实际项目中,不像这些简单的例子这样,你可以很容易地达到模仿实现需要它的自我测试这一阶段,因为有那么多的代码。我们可以制作许多小型模仿,但要使这些生效,我们需要这些循环代码移到一个基类中,这又使我们再次回到太复杂的状况。当测试较小而且比较集中时,单元测试会工作得很好,因此你要尽可能地让事情简单。

There are two stages to creating a mock using Moq. The first is to create a new Mock<T>, where T is the type we want to mock, as shown in Listing 6-27.
Moq生成一个模仿有两个阶段。第一阶段是生成一个新的Mock<T>,这里T是你想要模仿的类型,如清单6-27所示。

Listing 6-27. Creating a Mock

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

The second stage is to set up the behaviors we want our implementation to demonstrate. Moq will automatically implement all the methods and properties of the type we have given it, but it does so using the default values for types. For example, the IProductRepository.GetProducts method returns an empty IEnumerable<Product>. To change the way Moq implements a type member, we must use the Setup method, as shown in Listing 6-28.
第二阶段是建立我们的实现想要演示的行为。Moq将自动地实现我们已经赋给它的类型的所有方法和属性,但它是用类型的默认值来这样做的。要改变Moq实现一个类型成员的方式,我们必须用Setup方法,如清单6-28所示。

Listing 6-28. Setting up Behaviors Using 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}};mock.Setup(m => m.GetProducts()).Returns(products);

There are three elements to consider when setting up a new Moq behavior, as described in the following sections.
当建立一个新的Moq行为时,有三个元素要考虑,如下一小节所描述的那样。

Using the Moq Method Selector
使用Moq的方法选择器

The first element is the method selected. Moq works using LINQ and lambda expressions. When we call the Setup method, Moq passes us the interface that we have asked it to implement. This is cleverly wrapped up in some LINQ magic that we are not going to get into, but it allows us to select the method we want to configure or verify through a lambda expression. So, when we want to define a behavior for the GetProducts method, we do this:
第一个元素是所选择的方法。MoqLINQlambda表达式进行工作。当我们调用Setup方法时,Moq给我们传递已经要求它实现的接口。它封装了一些我们不打算细说的LINQ魔力,它让我们可以选择我们想要通过一个lambda表达式进行配置或检查的方法。因此,当我们想要给GetProducts方法定义一个行为时,我们这样做:

mock.Setup(m => m.GetProducts()).(<...other methods...>);

We are not going to get into how this works—just know that it does and use it accordingly. The GetProducts method is easy to deal with because it has no parameters. If we want to deal with a method that does take parameters, we need to consider the second element: the parameter filter.
我们不打算说明它是如何工作的 只要知道它能做并因此而用它。GetProducts方法很容易处理,因为它没有参数。如果我们想处理一个有参数的方法,我们需要考虑第二个元素:参数过滤器。

Using Moq Parameter Filters
使用Moq的参数过滤器

We can tell Moq to respond differently based on the parameter values passed to a method. The GetProducts method doesn’t take a parameter, so we will use this simple interface to explain:
我们可以告诉Moq基于不同的参数值响应到一个方法。GetProducts方法并不带有参数,因此我们将用以下这个简单的接口来解释:

view sourceprint?
1public interface IMyInterface {
2    stringProcessMessage(stringmessage);
3}

Listing 6-29 shows the code that creates a mock implementation of that interface with different behaviors for different parameter values.
清单6-29演示了对不同参数值具有不同行为的接口的一个模仿实现。

Listing 6-29. Using Moq Parameter Filters

view sourceprint?
1Mock<IMyInterface> mock = newMock<IMyInterface>();
2mock.Setup(m => m.ProcessMessage("hello")).Returns("Hi there");
3mock.Setup(m => m.ProcessMessage("bye")).Returns("See you soon");

Moq interprets these statements as instructions to return Hi there when the parameter to the ProcessMessage method is hello, and to return See you soon when the parameter value is bye. For all other parameter values, Moq will return the default value for the method result type, which will be null in this case, since we are using strings.
Moq把这些语句解释为,当传给ProcessMessage方法的参数是hello时,返回Hi there,而当参数值是bye时,返回See you soon。对所有其它参数值,Moq将返回该方法结果类型的默认值,在本例情况下是null,因为我们在使用字符串。

It can quickly become tedious to set up responses for all of the possible parameter values that can occur. It becomes tedious and difficult when dealing with more complex types, because you need to create objects that represent them all and use them for comparisons. Fortunately, Moq provides the It class, which we can use to represent broad categories of parameter values. Here is an example:
要对所有可能发生的参数值建立响应,这很快就会成为一件乏味的事。当处理更复杂的类型时,这可能会变得乏味和困难,因为你需要生成表示它们全部的对象并用它们进行比较。幸运的是,Moq提供了It类,我们可以用它表示广泛类别的参数值。这里是一个例子:

view sourceprint?
1mock.Setup(m => m.ProcessMessage(It.IsAny<string>())).Returns("Message received");

The It class defines a number of methods that are used with generic type parameters. In this case, we have called the IsAny method using string as the generic type. This tells Moq that when the ProcessMessage method is called with any string value, it should return the response Message Received. Table 6-3 shows the methods that the It class provides, all of which are static.
It类定义了与一般类型参数使用的许多方法。这里,我们用字符串作为一般类型调用了IsAny方法。这告诉Moq,当ProcessMessage方法以任何字符串值被调用时,它应该返回Messgae Received响应。表6-3显示了It类所提供的方法,所有这些都是静态的。

Table 6-3. The Static Methods of the It Class
6-3. It类的静态方法

Method
方法

Description
描述

Is<T>()

Matches based on a specified predicate (see Listing 6-30 for an example)
基于指定的谓词进行匹配(见清单6-30示例)

IsAny<T>()

Matches if the parameter is any instance of the type T
如果参数是类型T的实例,则匹配

IsInRange<T>

Matches if the parameter is between to defined values
如果参数在所定义的值之间,则匹配

IsRegex

Matches a string parameter if it matches the specified regular expression
如果一个字符串参数符合指定的正则表达式,则匹配

The Is<T> method is the most flexible because it lets you supply a predicate that causes a parameter match if it returns true, as shown in Listing 6-30.
Is<T>方法是最灵活的,因为它让你提供一个谓词,如果它为真,则引发一个参数匹配,如清单6-30所示。

Listing 6-30. Using the It Parameter Filter

View Code
mock.Setup(m => m.ProcessMessage(It.Is<string>(s => s == "hello" || s == "bye")))        .Returns("Message received");

This statement instructs Moq to return Message Received if the string parameter is either hello or bye.
这条语句指示,如果字符串参数是hellobye,则Moq返回Message Received

Returning a Result
返回结果

When we are setting up a behavior, we are often doing so in order to define the result that the method will return when it is called. The previous examples have all chained the Returns method to the Setup call in order to return a specific value. We can also use the parameter passed to the mocked method as a parameter to the Returns method in order to derive an output that is based on the input. Listing 6-31 provides a demonstration.
当我们建立行为时,我们经常做这样的事,即,当调用方法时,我们要定义方法返回的结果。前一个例子已经把Returns方法链接到Setup调用,以返回一个特定的值。我们也可以用传递给模仿方法的参数作为Returns方法的一个参数,以导出基于输入的输出。清单6-31提供了一个演示。

 Listing 6-31. Returning a Result Based on the Parameter Value

1 mock.Setup(m => m.ProcessMessage(It.IsAny<string>()))2         .Returns<string>(s => string.Format("Message received: {0}", s));

All we do is call the Returns method with a generic type parameter that matches the method parameter. Moq passes the method parameter to our lambda expression, and we can generate a dynamic result—in this case, we create a formatted string.
我们要做的全部工作是用一个与方法参数匹配的一般类型参数来调用Returns方法。Moq把这个方法参数传递给我们的lambda表达式,于是我们可以生成一个动态结果 这里,我们生成了一个格式化的字符串。

Unit Testing with Moq
利用Moq的单元测试

You can see how easy it is to create a mocked implementation with Moq, although you might find that it takes a little time before the syntax becomes second nature. Once you’ve set up the behaviors you require, you can get the mocked implementation through the Mock.Object property. Listing 6-32 shows the application of Moq to our Correct_Total_Reduction_Amount unit test.
你可以看到用Moq生成一个模仿实现是多么容易,虽然你也许发现需要花一些时间才能习惯于这种语法。一旦你已经建立了你需要的行为,你可以通过Mock.Object属性获得模仿实现。清单6-32演示了Moq对我们的Correct_Total_Reduction_Amount单元测试的应用程序。

Listing 6-32. Using Moq in a Test Method

view sourceprint?
01[TestMethod]
02public void Correct_Total_Reduction_Amount() {
03    // Arrange
04    Product[] products =new Product[] {
05       newProduct() { Name = "Kayak", Price = 275M},
06        newProduct() { Name = "Lifejacket", Price = 48.95M},
07        newProduct() { Name = "Soccer ball", Price = 19.50M},
08        newProduct() { Name = "Stadium", Price = 79500M}
09    };
10  
11    Mock<IProductRepository> mock =new Mock<IProductRepository>();
12    mock.Setup(m => m.GetProducts()).Returns(products);
13    decimalreductionAmount = 10;
14    decimalinitialTotal = products.Sum(p => p.Price);
15    MyPriceReducer target =new MyPriceReducer(mock.Object);
16  
17    // Act
18    target.ReducePrices(reductionAmount);
19  
20    // Assert
21    Assert.AreEqual(products.Sum(p => p.Price),
22             (initialTotal - (products.Count() * reductionAmount)));
23}

You can see that we have implemented just enough of the functionality defined by IProductRepository to perform our test. In this case, that means implementing the GetProducts interface so that it returns our test data.
你可以看到,我们只实现IProductRepository所定义的足够的功能来执行我们的测试。在本例中,这意味着实现GetProducts接口以使它返回我们的测试数据。

In Listing 6-32 we put everything in the unit test method to give a quick demonstration of Moq, but we can make things simpler by using some of the Visual Studio test features. We know that all of our test methods are going to use the same test Product objects, so we can create these as part of the test class, as shown in Listing 6-33.
在清单6-32中,我们把所有事情都放到单元测试方法中,以给出一个Moq的快速演示,但我们可以用某些Visual Studio测试特性让事情更简单。我们知道,我们的所有测试方法都要使用同样的Product测试对象,因此我们可以把这些对象生成为测试类部分,如清单6-33所示。

Listing 6-33. Creating the Common Test Data Objects

view sourceprint?
01...
02[TestClass]
03public class MyPriceReducerTest {
04    privateIEnumerable<Product> products;
05    [TestInitialize]
06    publicvoid PreTestInitialize() {
07        products =new Product[] {
08            newProduct() { Name = "Kayak", Price = 275M},
09            newProduct() { Name = "Lifejacket", Price = 48.95M},
10            newProduct() { Name = "Soccer ball", Price = 19.50M},
11            newProduct() { Name = "Stadium", Price = 79500M}
12        };
13    }
14    ...

We want to start with clean test data for each unit test, so we have created the field products and used a Visual Studio test feature to initialize the data. Visual Studio will look for a method that has the TestInitialize attribute. If it finds one, it will call that method before each unit test in the class. In our case, this means that the product class variable will be reinitialized with fresh test data. Table 6-4 shows the other unit test attributes that Visual Studio supports.
我们想对每个单元测试都从干净的测试数据开始,因此我们已经生成了products字段,并用Visaul Studio测试特性来初始化数据。Visual Studio将寻找一个具有TestInitialize属性的方法。如果找到一个,它将在类中的每个单元测试之后调用这个方法。在上例中,这意味着产品类变量将用新鲜的测试数据进行初始化。表6-4显示了Visual Studio支持的其它单元测试属性。

Table 6-4. Visual Studio Unit Test Attributes

Attribute
属性

Description
描述

ClassInitialize

Called before the unit tests in the class are performed; must be applied to a static method
在类中的单元测试被执行之前调用,必须被用于静态方法

ClassCleanup

Called after all of the unit tests in the class have been performed; must be applied to a static method
类中的所有单元测试被执行之后调用,必须被用于静态方法

TestInitialize

Called before each test is performed
在每个测试执行之前调用

TestCleanup

Called after each test is performed
每个测试被执行之后调用

The name of the method you apply these attributes to doesn’t matter, because Visual Studio looks for only the attribute. When we use the TestInitialize attribute, we can create and configure our test-specific mock implementation using two lines of code:
你把这些属性运用到什么方法名并不重要,因为Visual Studio只寻找这个属性。当我们用TestInitailize属性时,我们可以用两行代码生成并配置我们的特定测试的模仿实现:

view sourceprint?
1Mock<IProductRepository> mock = new Mock<IProductRepository>();
2  
3mock.Setup(m => m.GetProducts()).Returns(products);

The benefits of Moq become even more significant when mocking a more complex object. We’ll go through some of the other Moq features in the following sections, and we’ll also show you different unit testing techniques as we continue through the book.
当模仿一个更复杂的对象时,Moq的好处变得更加显著。在以下的小节中,我们将进行一些Moq的其它特性,而且在本书的后继部分,我们也将给你演示不同的单元测试技术。

Verifying with Moq
Moq作检验

One of our test criteria was that the UpdateProduct method be called for each Product object that was processed. In the FakeRepository class, we measured this by defining a property and incrementing from within the UpdateProduct method. We can achieve the same effect much more elegantly using Moq, as demonstrated in Listing 6-34.
我们的测试条件之一是对每个被处理的Product对象都调用了UpdateProduct方法。在FakeRepository类中,我们通过在UpdteProduct方法的内部定义一个属性并增量加1的方法进行测量。我们可以用Moq更优雅得多地取得同样效果,如清单6-34所示。

Listing 6-34. Verifying Method-Call Frequency

view sourceprint?
1    // Act
2    target.ReducePrices(reductionAmount);
3    // Assert
4    foreach(Product p inproducts) {
5        mock.Verify(m => m.UpdateProduct(p), Times.Once());
6    }
7}

Using the parameter filter, we are able to verify that the UpdateProduct method has been called exactly once for each of our test Product objects. Of course, we could have done this using a manual mock, but we like the simplicity we get from a mocking tool.
利用参数过滤器,我们能够检查UpdateProduct方法对我们测试的每个Product对象确切地只调用了一次。当然,我们可以用一个手工模仿来做这件事,但我们喜欢我们从模仿工具获得的简单性。

Summary
概要

In this chapter, we looked at the three tools we find essential for effective MVC development—Ninject, Visual Studio 2010’s built-in support for unit testing, and Moq. There are many alternatives, both open source and commercial, for all three tools. If you don’t get along with the tools we prefer, you won’t lack for other choices. You may find that you don’t like TDD or unit testing in general, or that you are happy performing DI and mocking manually. That, of course, is entirely your choice. However, we think there are some substantial benefits in using all three tools in the development cycle. If you are hesitant to adopt them because you’ve never tried them, we encourage you to suspend disbelief and give them a go—at least for the duration of this book.
本章中,我们考察了从事高效MVC开发的三个基本工具 NinjectVisual Studio 2010内建的单元测试支持,以及Moq。对这三种工具还有许多其它选择,开源的或商业的都有。如果你不适应我们所喜欢的这些工具,你也不乏其它选择。你也许会发现你基本上不喜欢TDD或单元测试,或者你乐于手工地执行DI和模仿。这当然完全是你的自由。然而,我们认为在开发周期中使用这三种工具有一些坚实的好处。如果你在犹豫采纳它们,因为你从没用过,我们鼓励你不用怀疑,并让它们一展身手 至少在阅读本书期间。