一、可以使用Moq模拟哪些内容?
你可以针对接口和现有类来使用Moq创建模拟对象。当应用于类时,需要具备一定的条件:类不能是封闭类型的(sealed);而且,被模拟的方法必须标记为虚拟类型(virtual)的。你无法简单地模拟静态方法(但是你可以使用Adaptor模式来模拟一个静态方法)。其实,上面这些限制条件与你使用另一个模拟对象框架Rhino Mocks时是一致的。
Moq和Rhino Mocks在后台实现上都使用了代理类的技术。而且,更深入一步了解,这两个框架都派生自相同的Castle DynamicProxy代码基类。
二、使用Moq对方法和属性进行模拟
现在,不妨设想一下,你正在编写一个数据库驱动的Web应用程序-例如一个在线商店。在进行任何其他部分的编码之前,你想首先完成这个电子商店软件的业务逻辑部分的设计。特别是,在编写完你的业务逻辑组件之前,你根本不想投入任何精力去编写数据访问有关的组件。
上面这种情形下特别适合于使用一种模拟对象框架。此时,你可以创建一个接口,用于描述你想使你的数据访问组件看上去的样子。然后,你便可以简单地模拟此接口,并且在测试你的业务逻辑时充分地利用模拟对象的优势(即不需要真正地实现被模拟的组件)。因此,借助于这种模拟方案,你可以先不考虑这些组件有关的编码,直到你已经为这部分被模拟组件的编程作好了充分的准备。
列表1中提供的第一个接口名字为IProductRepository,此描述共描述了两个方法。其中,第一个方法Select()负责返回数据库中所有的产品。第二个方法Get()根据给定的特定产品ID返回一个产品。此外,列表1还提供了一个接口,名字为IProduct;此接口用于描述某一种特定的产品。
列表1–IProductRepository.cs
using System;
using System.Collections.Generic;
namespace MoqSamples.Models
{
public interface IProductRepository
{
List<IProduct> Select();
IProduct Get(int id);
}
public interface IProduct
{
int Id {get; set;}
string Name { get; set; }
}
}
让我们首先来考虑模拟IProduct接口。下列代码将创建一个模拟的Product对象,此对象拥有一个Id属性且其属性值为1,还有一个Name属性且其属性值为“Bushmills”:
//模拟一个Product对象
var newProduct = new Mock<IProduct>();
newProduct.ExpectGet(p => p.Id).Returns(1);
newProduct.ExpectGet(p => p.Name).Returns("Bushmills");
上面的第一行代码从接口IProduct创建一个模拟。注意:这里所使用的Mock类是Moq框架所提供的,此类有一个泛型构造器,它能够接受李创建的接口类型。
接下来,建立了Id属性并且使之返回值1,又建立了Name属性并且使之返回值“Bushmills”。注意,此处是如何使用lambda表达式来描述Id和Name两个属性的。使用lambda表达式而不是使用字符串来描述一个属性的好处在于,支持例如像Resharper这样的重构工具能够对属性进行自动重构。
在上面创建完模拟对象newProduct后,接下来,你就可以像真正实现了接口IProduct一样来使用此模拟对象。例如,下面的断言(Assert)将会成功执行:
Assert.AreEqual("Bushmills", newProduct.Object.Name);
【注意】在此,当你引用模拟对象newProduct时必须使用newProduct.Object。这是因为newProduct变量代表的是代理类,而newProduct.Object变量代表的是实际的newProduct类。
接下来,让我们继续模拟IProductRepository接口。下列代码行创建了一个模拟对象IProductRepository,当调用它的Get()方法时它能够返回newProduct:
//模拟ProductRepository接口
var productRepository = new Mock<IProductRepository>();
productRepository
.Expect(p => p.Get(1))
.Returns(newProduct.Object);
上面的第一行代码通过把接口IProductRepository传递给类Mock的泛型构造器创建相应的模拟对象。接下来,第二行代码建立Get()方法以返回模拟对象newProduct。请再次注意,这里也使用了lambda表达式来描述方法Get()。当创建模拟对象IProductRepository完毕,你就可以在你的测试代码中像下面这样来使用它了:
// Act
var productReturned = productRepository.Object.Get(1);
// Assert
Assert.AreEqual("Bushmills", productReturned.Name);
在上面代码中,当你使用值1调用方法Get()时,返回的是newProduct对象。如果你调用除了1以外的其他值来调用Get()方法,那么将返回一个Null值。如果你想使得Get()方法返回newProduct,而不管传递给它是什么参数,那么你可以通过使用下列代码来创建模拟对象的方式实现:
//模拟ProductRepository接口
var productRepository = new Mock<IProductRepository>();
productRepository
.Expect(p => p.Get(It.IsAny<int>()))
.Returns(newProduct.Object);
请注意,这里当建立方法的期望时我们把约束It.IsAny<int>()传递给方法Get()。这个参数可以使模拟的Get()方法访问任何整数值(integer)并返回newProduct对象。
另外,借助于lambda表达式的帮助,你甚至可以指定你所能够想到的任何定制约束。请参考如下代码片断:
//模拟ProductRepository接口
var productRepository = new Mock<IProductRepository>();
productRepository
.Expect(p => p.Get(It.Is<int>(id => id>0 && id<6)))
.Returns(newProduct.Object);
在上面的代码中,仅当传递给Get()方法的参数介于值0和6之间时才返回一个newProduct对象。显然,这里的约束条件是通过把一个lambda表达式传递给方法It.Is()实现的。
三、使用Moq的行为校验支持
Moq框架可用于执行有限的行为校验功能。例如,你可以使用Moq来检测在调用结束另一个方法之后是否至少调用了某一特定的方法一次。
注意:Moq对于行为校验功能的支持也是有限的。不像其他的模拟对象框架,例如Rhino Mocks和Typemock Isolator,你不能够使用Moq来对象之间的测试复杂的交互。Moq并没有实现像Rhino Mocks和Typemock Isolator所提供的对于同一类型的代码记录(record)与回放(replay)功能的支持。
那么,你该在何时使用行为校验呢?为此,Martin Fowler提供了一个内存缓存的例子。他认为,一个缓存的关键特征在于你无法从其状态中判断当前缓存是否在使用中(在未真正实现这部分编码及投入使用之前)。这种情形特别适合于行为校验-即使针对简单情形下的硬编码方案的测试驱动开发者来说也是这个结论。
现在,不妨设想你已经实现了一个类ProductRepository,此类包含一个名字为GetProduct()的方法,此方法用于产品检索。于是,这个方法首先会试图从缓存中取得商品对象。如果失败,那么此方法将继续努力从数据库中检索相应的产品。列表2给出了这个类相应的代码。
列表2–ProductRepository
using System;
using System.Web;
namespace MoqSamples.Models
{
public class ProductRepository : IProductRepository
{
private ProductCache _cache;
public ProductRepository(ProductCache cache)
{
_cache = cache;
}
public virtual IProduct GetProduct(int id)
{
var product = _cache.Get(id);
if (product == null)
{
product = this.Get(id);
_cache.Set(id, product);
}
return product;
}
public virtual IProduct Get(int id)
{
throw new NotImplementedException();
}
public System.Collections.Generic.List<IProduct> Select()
{
throw new NotImplementedException();
}
}
}
在上面的代码中,通过依赖性注入方式,ProductCache对象被传递给ProductRepository类。在此,ProductRepository的GetProduct()方法首先试图从缓存对象中取得一个Product对象。如果获取失败的话,它将调用Get()方法来从数据库中取得此Product对象。注意,在此我没有给出Get()方法的实现。其实,我们所关注的仅是如何模拟它,所以这就足够了。
此外,注意到ProductCache类是一个强类型包装类,它所包装的是ASP.NET中十分重要的System.Web.Caching.Cache对象。列表3提供了ProductCache的相应代码:
列表3–ProductCache.cs
using System;
using System.Web;
namespace MoqSamples.Models
{
public class ProductCache
{
public virtual IProduct Get(int id)
{
return (IProduct)HttpContext.Current.Cache["product_" + id];
}
public virtual void Set(int id, IProduct product)
{
HttpCon