最近在学习TDD,在测试驱动开发的时候常常会遇到测试的模块依赖到其他模块的时候,就会使用Mock对象,所以将自己最近学习的心得拿出来分享一下,有什么说的不对的地方,还希望大家跟我指出来!
想必大家都知道单元测试,是一个最小的对程序正确性检查的单位。在面向对象的开发中,往往我们是对一个方法进行测试,我们的测试目的是为了验证这个方法是否正确,也就是说如果这个方法错了,我希望的是一定是这个方法错了,而不是它所依赖的方法错了。如果整个单元测试跑下来,有10个错误,我们希望的是确实有10个方法错了。所以单元测试的独立性很重要,但是单元测试往往会依赖于其他方法,就算我们想尽一切办法解耦,为我们所依赖的方法抽象出一个接口,虽然此时方法依赖于抽象,但是我们必须还是要提供实现,这时如果我们能自己能提供一个正确的实现,确保待测的方法所依赖的是一个稳定正确的实现,那我们就能为测试消除一个影响测试正确性的干扰。不过这只是使用Mock对象的场景之一。
往往我们会在这些场景下使用Mock对象:
1.我们所依赖的对象很不稳定。(常常发生变化,那样我们的单元测试就变得很不稳定)
2.依赖的对象很难被创建 (也许这个对象还没有被其他小组的同事开发出来)
3.依赖的对象访问速度很慢(这里也许依赖对象要连接远程数据库,速度很慢。因为大家想必都知道,单元测试还有个基本的原则就是要快速执行,这里就明显违背了)
好了,前面我也提到,我们为所依赖的模块抽象出一个接口,是确实常用的办法。但是难道我们每次都需要自己建一个类,自己去实现这个接口吗?当然不是,有很多Mock对象框架,能帮我动态的创建一个实现。这里我将简单的介绍一个.NET平台下的Moq框架,我的重点是模仿Moq框架的API提供一个简易的实现,不过最重要的是给对Mock对象框架有兴趣的朋友一点思路。
首先我们来个例子看看Moq框架是怎么样使用的:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using NUnit.Framework; using System.Linq.Expressions; using Moq; namespace MoqDemo.Test { [TestFixture] public class TestMoq { [Test] public void TestMoqHelloWorld() { /* 想必这里很好理解我们虽然没有去实现ITestService * 但是我们通过Moq调用它的SetUp和Returns方法和就成功地 * 完成的让mock.Object.HelloWorld()返回HelloWorld的期望 */ Mock<ITestService> mock = new Mock<ITestService>(); mock.Setup(service => service.HelloWorld()).Returns("HelloWorld"); Assert.AreEqual("HelloWorld", mock.Object.HelloWorld()); } public interface ITestService { string HelloWorld(); } } }
我不知道大家是不是跟我一样,当我第一次看到这个Moq框架时,我觉得很神奇它是怎么样办到的?所以,我决定停下使用Moq的脚步,决定自己来实现一个来也能完成上面的功能。
我实现上述这个API用到了一个.NET平台下的开源框架Castle的动态代理,用到了表达式树。
大致的思路是
1.在实例化Mock对象时,Castle动态创建了接口的实现,并赋给了Mock.Object属性,即Mock.Objec实现了接口。
[Test] public void TestObjectWhenMockInitial() { Mock<TestClass> mock = new Mock<TestClass>(); Assert.IsTrue(mock.Object is TestClass); Mock<ITestInterface> mockInterface = new Mock<ITestInterface>(); Assert.IsTrue(mockInterface.Object is ITestInterface); }
2.在调用SetUp函数的时候,这里的参数是一个表达式树。(如果不清楚表达式树的朋友,可以看看这里,也许会跟你们帮助。)这样我们就可以将调用的方法
像数据一样传递,并把HelloWorld()这个函数在反射中的对象MethodInfo中存放在Mock对象的MethodInfoData中。在断言中,你们可以发现MethodInfoData是一个集合,这里真正是一个字典,键是MethodInfo,值是接下来Return方法赋给的返回值。
[Test] public void TestSetUp() { Mock<TestClass> mock = new Mock<TestClass>(); mock.SetUp(tc => tc.HelloWorld()); Assert.IsTrue( mock.MethodInfoData.Contain(typeof(TestClass).GetMethod("HelloWorld"))); }
3. 在Return中传递的参数就是我们的返回值,这里我会将会把这个值,也就是"HelloWorld"参数存放在mock.MethodInfoData这个字典的值中,当然这里的键就是SetUp方法中传递的HelloWorld()方法的MethodInfo。也许你联系第一个断言,你可以更清楚的理解。可能你看到第二个断言的时候,可能还是会感觉到奇怪,只是将方法和返回值作为键值对放到了字典中,为什么第二个断言就能成功?如果你能读到这里,我很高兴,看来我的文字没有白写。接下来就是Castle要出场的时候了!
[Test] public void TestReturn() { Mock<TestClass> mock = new Mock<TestClass>(); mock.SetUp(tc => tc.HelloWorld()).Return("HelloWorld"); Assert.AreEqual(mock.MethodInfoData[typeof(TestClass).GetMethod("HelloWorld")], "HelloWorld"); Assert.AreEqual("HelloWorld",mock.Object.HelloWorld()); }
4.这里我摘了源代码中一个片段出来,这也是实现Mock的核心,Inteceptor---拦截器。前面提到Mock.Object是由Castle创建的,所以Mock.Object的方法会被这个拦截器拦截,换句话中,即Mock.Object当它调用HelloWorld()方法的时候,会执行Intercept方法,注意这个函数的方法参数是invocation,就相当于它拿到了你所调用的方法的一切,你可以为所欲为的控制你的方法的行为通过invocation参数,所以你可以在代码中看到我利用invocation的ReturnValue修改了方法的返回值。这个返回值就是从字典中取出来的。哈哈,所以我们的目的得逞了,客户端Mock.Object.HelloWorld()方法的返回值被我们改成的"HelloWorld".我们成功了。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Castle.DynamicProxy; namespace CodeJacky.Mock { public class ObjectInteceptor<TEntity>:IInterceptor where TEntity:class { private Mock<TEntity> mock; public ObjectInteceptor(Mock<TEntity> mock) { this.mock = mock; } public void Intercept(IInvocation invocation) { if (mock == null) { throw new InvalidOperationException("mock object can't be null!"); } invocation.ReturnValue = mock.MethodInfoData[invocation.Method]; } } }
总结,这个Demo的代码很少,也很多欠妥的地方。第一个原因是,我觉得就是这样很少的代码才能让有兴趣,但是缺乏这方面知识的朋友快速理解且不丧失兴趣,第二点这编分享只是给大家提供一点思路,供大家参考。最后,不得不说Castle是一个不错的开源项目,最为.NET平台下的动态代理框架,很多著名的开源项目中都用到了它,比如NHibernate,Spring.NET,IBATIS ,当然还包括Moq.
最后代码下载地址.