在前文中,我們僅僅對Moq模拟對象架構的特征及曆史等作了簡單介紹。在本文中,我們将結合執行個體對這個架構作更具體的分析。
一、可以使用Moq模拟哪些内容?
你可以針對接口和現有類來使用Moq建立模拟對象。當應用于類時,需要具備一定的條件:類不能是封閉類型的(sealed);而且,被模拟的方法必須标記為虛拟類型(virtual)的。你無法簡單地模拟靜态方法(但是你可以使用Adaptor模式來模拟一個靜态方法)。其實,上面這些限制條件與你使用另一個模拟對象架構Rhino Mocks時是一緻的。
Moq和Rhino Mocks在背景實作上都使用了代理類的技術。而且,更深入一步了解,這兩個架構都派生自相同的Castle DynamicProxy代碼基類。
二、使用Moq對方法和屬性進行模拟
上面這種情形下特别适合于使用一種模拟對象架構。此時,你可以建立一個接口,用于描述你想使你的資料通路元件看上去的樣子。然後,你便可以簡單地模拟此接口,并且在測試你的業務邏輯時充分地利用模拟對象的優勢(即不需要真正地實作被模拟的元件)。是以,借助于這種模拟方案,你可以先不考慮這些元件有關的編碼,直到你已經為這部分被模拟元件的程式設計作好了充分的準備。
清單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,而不管傳遞給它是什麼參數,那麼你可以通過使用下列代碼來建立模拟對象的方式實作:
.Expect(p => p.Get(It.IsAny<int>()))
.Returns(newProduct.Object);
請注意,這裡當建立方法的期望時我們把限制It.IsAny<int>()傳遞給方法Get()。這個參數可以使模拟的Get()方法通路任何整數值(integer)并傳回newProduct對象。
另外,借助于lambda表達式的幫助,你甚至可以指定你所能夠想到的任何定制限制。請參考如下代碼片斷:
.Expect(p => p.Get(It.Is<int>(id => id>0 && id<6)))
在上面的代碼中,僅當傳遞給Get()方法的參數介于值0和6之間時才傳回一個newProduct對象。顯然,這裡的限制條件是通過把一個lambda表達式傳遞給方法It.Is()實作的。
三、使用Moq的行為校驗支援
Moq架構可用于執行有限的行為校驗功能。例如,你可以使用Moq來檢測在調用結束另一個方法之後是否至少調用了某一特定的方法一次。
注意:Moq對于行為校驗功能的支援也是有限的。不像其他的模拟對象架構,例如Rhino Mocks和Typemock Isolator,你不能夠使用Moq來對象之間的測試複雜的互動。Moq并沒有實作像Rhino Mocks和Typemock Isolator所提供的對于同一類型的代碼記錄(record)與回放(replay)功能的支援。
現在,不妨設想你已經實作了一個類ProductRepository,此類包含一個名字為GetProduct()的方法,此方法用于産品檢索。于是,這個方法首先會試圖從緩存中取得商品對象。如果失敗,那麼此方法将繼續努力從資料庫中檢索相應的産品。清單2給出了這個類相應的代碼。
清單2–ProductRepository
using System.Web;
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()
}
}
在上面的代碼中,通過依賴性注入方式,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
{
return (IProduct)HttpContext.Current.Cache["product_" + id];
public virtual void Set(int id, IProduct product)
HttpCon