最近在學習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.
最后代碼下載地址.