尽管商业逻辑类库和数据访问组件之间的绑定是基于接口的,但是,在单元测试中,商业逻辑类库仍旧需要访问数据访问组件的属性和方法,这就产生了单元测试的问题。(更严重一点,既然单元测试伴随开发进行,在准备测试商业逻辑类库时,数据访问组件可能还没有开发出来)。 如果一个“单元测试”和其他很多模块都紧密相关,那它恐怕就不再是一个“单元测试”了。
二。让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。
为了建立一个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 )。下面是一个例子:
注: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()! 先来例子:
很显眼的四个 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 难免有错误,文中的错误还请大家指正。下载示例代码