NMock --- 从零开始
(在博客园搜索了一下,介绍NMock的文章不是很多,比较系统的介绍NMock的就是 NetCobra 翻译的 NMock简介 。 既然董询的文章很精彩,我为什么还要去看MSDN杂志的那篇长长的英文? 因为,董洵文章中的范例代码我并未在NUnit中测试成功,估计是因为我是纯粹一个初学者的原因( NMock网站首页的那个例子我也没有运行成功,郁闷-_-)。 看来对于一个新手,完整的示例代码是非常重要的) 下载本文示例代码
一。实施单元测试的过程中存在的问题:
Unit testing libraries that have a complex set of dependencies。 比如你要测试某个类(比如是商业逻辑层中的一个类)的某个方法,但这个方法要连接到数据库(通过数据访问层,使用ADO.NET……),还要用到打印机,还要用到……,这个类怎么来进行单元测试呢? 如果你的机器上没有打印机怎么办?
尽管商业逻辑类库和数据访问组件之间的绑定是基于接口的,但是,在单元测试中,商业逻辑类库仍旧需要访问数据访问组件的属性和方法,这就产生了单元测试的问题。(更严重一点,既然单元测试伴随开发进行,在准备测试商业逻辑类库时,数据访问组件可能还没有开发出来)。 如果一个“单元测试”和其他很多模块都紧密相关,那它恐怕就不再是一个“单元测试”了。
二。让Mock Objects 来帮你
Mock Object 虚拟了一个实际的对象(比如一个负责访问数据库的类),并且可以模拟实际对象的行为。在单元测试时候,就可以把Mock Object当作实际对象来用。传统的单元测试术语(unit testing terminology) 包括了 driver 和 stub。driver的目的很单纯,就是为了访问类库的属性和方法,来检测类库的功能是否正确;stub的目的同样单纯,就是提供需要和测试类库交互的那些类的实现(A driver is a piece of software that is written with the sole purpose of accessing properties and methods on a library to test the functionality of that library. A stub is a piece of software whose only purpose is to provide the library under test with an implementation of any modules it may need to communicate with to perform its work.) 。简单地说,对于.NET下的开发者,driver就用NUnit,stub就用NMock。
三。NMock初探
1。下载,http://www.nmock.org/,下载、解压,结果是一个dll文件,添加到相应 peoject 的 References 就行了,别忘了在代码中加上 using NMock; 。
2。一个例子(Shopping类库)
以下是相关的类和接口
public interface IShoppingDataAccess
{
string GetProductName(int productID);
int GetUnitPrice(int productID);
BasketItem[] LoadBasketItems(Guid basketID);
void SaveBasketItems(Guid basketID, BasketItem[] basketItems);
}
public class Basket
{
private ArrayList basketItems_; // 篮子里的具体物品
private Guid basketID_;
private IShoppingDataAccess dataAccess_;
public Basket(IShoppingDataAccess dataAccess)
{
Initialize(dataAccess);
}
// 向购物篮中增加一个物品
public void AddItem(BasketItem item)
{
basketItems_.Add(item);
}
// 把购物篮保存起来
public void Save()
{
dataAccess_.SaveBasketItems(basketID_,
(BasketItem[])basketItems_.ToArray
(typeof(BasketItem)));
}
// 计算购物篮中物品总价格
public decimal CalculateSubTotal()
{
decimal subTotal = 0;
foreach(BasketItem item in basketItems_)
{
subTotal += item.GetPrice();
}
return subTotal;
}
private void Initialize(IShoppingDataAccess dataAccess)
{
dataAccess_ = dataAccess;
basketItems_ = new ArrayList();
basketID_ = Guid.NewGuid();
}
}
注意:Basket 类的构造函数的参数实现了 IShoppingDataAccess 接口,通过该参数来进行数据的访问。(这里用到了一些Inversion of Control Containers and the Dependency Injection pattern. 的知识)。 一个 Basket 对应多个 BasketItem(一个列表),BasketItem 包括product ID,一个 BasketItem object 用它自己的内部的数据访问对象 private IShoppingDataAccess dataAccess_来提供单价、物品名等信息(实现代码略,参看下载的代码)
从上述代码能看到,Shopping 类库用了很多访问数据的方法。这就意味着,如果要对这个类库做有效的单元测试(真正反映了要测试的方法的正确性,不被其他类库影响)的话,就需要把这个类库和数据访问层隔离开。
解决方法:用 NMock 来提供一个dynamic mock ,来实现 IShoppingDataAccess 接口。下面是非常简单的在测试时使用 NMock 的代码:
Basket b = new Basket( ( IShoppingDataAccess )dataAccess.MockInstance );
b.Save();
为了建立一个Mock object,先new一个 DynamicMock 对象,参数是它要模拟的接口(注意 typeof 的用法)。Mock object 本身并不实现某个类,它只是包含了它要模拟的类所拥有的行为(方法)的信息。 接下来 new 一个 Basket 类的对象,因为 Basket 类的构造函数的参数是 IShoppingDataAccess(正合我们的意),所以调用 MockInstance这个property,它产生了一个实例,然后再强制转换到 IShoppingDataAccess 这个接口,刚好满足 Basket 类的构造函数。DynamicMock 利用了反射机制,来公布(emit)一个它想模拟的类型的实现,然后通过 MockInstance property 来暴露出来。MockInstance 返回的结果是 System.Object 类型,所以需要强制转换。
让我们再看看那简单的三行代码。当 Save() 被调用时,Basket 对象调用了 SaveBasketItems() 方法,利用了它自己的 IShoppingDataAccess 类型的成员来访问数据。当然,当 SaveBasketItems() 方法被调用时,DynamicMock 对象完全不知道它自己应该做点什么。幸好,在这个例子中,SaveBasketItems() 方法的返回值是 void,所以几行代码只是测试了 Basket 类的构造函数以及 Save() 方法可以在没有异常的情况下被执行。(好像也没测试出什么东西,mock instance 的默认行为是返回null,这对于测试带有返回值的方法显然是不够的。^_^,接着往下看)
3。设置返回值
DynamicMock class 允许我们修改 MockInstance property 的行为,在我们把 mock instance 传递给测试目标之前,可以调用一些 DynamicMock class 的方法,来告诉 mock instance 应该如何做出反应。
1)SetResult()方法:void SetupResult( System.String methodName, System.Object returnVal, params System.Type[] argTypes )。下面是一个例子:
public void Test3()
{
DynamicMock dataAccess = new DynamicMock( typeof( IShoppingDataAccess ) );
dataAccess.SetupResult( "GetUnitPrice", 99, typeof( int ) );
dataAccess.SetupResult( "GetProductName", "The Moon", typeof( int ) );
BasketItem item = new BasketItem( 1, 2, ( IShoppingDataAccess )dataAccess.MockInstance );
Assert.AreEqual( 99, item.UnitPrice );
Assert.AreEqual( "The Moon", item.ProductName );
Assert.AreEqual( 198, item.GetPrice() );
}
注:BasketItem 类的构造函数形如 public BasketItem( int productID, int quantity, IShoppingDataAccess dataAccess ) { 初始化工作; }
上述代码中有两行 SetupResult(...),我解释一下第一行——当 mock instance 收到了一次对 GetUnitPrice method 的调用(带有一个 int 型参数)时,mock instance 会返回 99 。第二行是什么意思大家就该明白了吧 ……。
接下来的三个 Assert.AreEqual(...)是检查两个值是否相等(这是 NUnit 里的东西哦)。我还是解释一下第一个: UnitPrice 是 BasketItem 类的一个 property,返回物品的单价。Assert.AreEqual(99, item.UnitPrice) 就是看看该物品的单价是否是99。这里重点说明一下,在 BasketItem 类的构造函数中,实际已经调用了 BasketItem 类中的用来做数据访问的类成员 IShoppingDataAccess dataAccess_的 GetUnitPrice() 方法(详情看 BasketItem 类的实现代码);而我们在前面已经利用 SetupResult() 把 GetUnitPrice() 方法的返回值设置为了99,因此 UnitPrice property 的 get 就会返回99,所以这个 Assert 是成功的。
第二个 Asert.AreEqual(...) 就比较简单了,对应着上面第二个 SetupResult(...)。 那么, 第三个 Asert.AreEqual(...) 为什么是 198 呢?看看 BasketItem item = new BAsketItem(...) 那一行,第二个参数 2 不就是quantity(数量)么。 所以 GetPrice() 返回的就是 单价*数量 = 99 * 2 = 198 。
4。设置我们自己的期望(Exceptation)
从上面的例子可以看出来,SetupResult() method 有明显的缺点:不管怎么调用,返回值都一样!如果我想测试一下 Basket class 的计算购物篮中物品总和的功能,仅仅 SetupResult() method 是不够的(购物篮中每样物品利用 GetUnitPrice() 返回的单价都是一样的 )。 下面,隆重推荐 DnyamicMock 的另一个重要方法:ExpectAndReturn()! 先来例子:
public void Test4()
{
DynamicMock dataAccess = new DynamicMock(typeof(IShoppingDataAccess));
dataAccess.ExpectAndReturn("GetUnitPrice", 99, 1);
dataAccess.ExpectAndReturn("GetProductName","The Moon", 1);
dataAccess.ExpectAndReturn("GetUnitPrice", 47, 5);
dataAccess.ExpectAndReturn("GetProductName", "Love", 5);
Basket b = new Basket((IShoppingDataAccess)dataAccess.MockInstance);
BasketItem item = new BasketItem(1, 2, (IShoppingDataAccess)dataAccess.MockInstance);
b.AddItem(item);
item = new BasketItem(5, 1, (IShoppingDataAccess)dataAccess.MockInstance);
b.AddItem(item);
b.Save();
decimal subTotal = b.CalculateSubTotal();
Assert.AreEqual(245, subTotal);
}
很显眼的四个 ExpectAndReturn ,解释一下前两个:当调用 GetUnitPrice() 方法并且参数是 1 时,返回 99;当调用 GetProductName() 方法并且参数是 1 时,返回 "The Moon"。这下子第三和第四个的意思也都很清楚了吧。(注意:ExpectAndReturn 和 SetupResult 一样,这两个方法的参数顺序都是:需要制定行为的方法的名字A,A返回的结果,A的参数。别弄错了哦。)接下来生成两个 BasketItem 类的实例并且把这两个物品都加入了购物篮,根据第一个参数(1 和 5)可以看出来,这两个 item 的单价分别是 99 和 47(看看第一个和第三个 ExpectAndReturn )。然后调用 CalculateSubTotal() 来计算总价,总价就是 99 * 2 + 47 * 1 = 245,刚好满足最后一个 Assert.AreEqual(...)。
----------------------------------------------------------
结论
说到这里,大家应该对 NMock 有了初步的了解。如果你要测试一个有很多外在依赖的类,NMock 可能会给你很大的帮助。不仅在单元测试方面,它还能促进你改进你的代码,让你的代码更加的高内聚低耦合。
最后说一下示例代码。代码我已经 build 过了,里面有3个project,解压以后打开 NMockExample.sln 就可以察看代码了;如果要直接用 NUnit 看看结果,打开 UnitTests\bin\Debug\NMockExample.UnitTests.dll 就可以了。
刚接触 NMock 难免有错误,文中的错误还请大家指正。下载示例代码
ps:有个问题,通过Css Class设置的字体颜色无法显示(只能看到默认的黑色),在编辑器的Preview可以看到颜色,但是Post按钮旁边的PREVIEW按钮就看不到颜色了,不知何故,大家见谅。