Mock解決non-virtual的方法
開始使用TDD做開發,使用Moq來撰寫Unit Test也一段時間了,正當風平浪靜,順利地開發一段時間後,果然碰上了一個棘手的問題,這個問題是使用WebClient這個物件的時候所遇到的,雖然Moq可以順利模擬Mock WebClient,但要定義DownloadString時卻產生了一個問題,首先我們先來看看程式碼。
[TestMethod] public void TestGetHtmlByUrl() { var mockWebClient = new Mock<webclient>();//產生一個WebClient Mock //設定如果呼叫DownloadString,會傳回ok mockWebClient.Setup(p => p.DownloadString("http://blog.sanc.idv.tw")).Returns("ok"); //產生要測試的物件 BusinessObject bo = new BusinessObject(mockWebClient.Object); //執行測試 string getHtml = bo.GetHtmlByUrl("http://blog.sanc.idv.tw"); Assert.AreEqual("ok", getHtml);//驗證測試 }
理論上這樣就可以了,然後很開心地執行測試…於是產生了錯誤…
這個錯誤是表示,Moq要複寫DownloadString這個方法,但是這個方法沒有設定virtual,所以拋出了這個例外,這是因為CLR裡面沒有內建攔截機制,所以Moq預設的攔截機制是利用自動產生的類別,讓此類別繼承,並覆蓋掉所有成員提供的方法,也因此他們需要virtual。
因為WebClient也不是我寫的,我也沒辦法去改寫原始碼,那應該怎麼辦呢?經過查詢後,找到了幾個解法,但都不是那麼的理想…
第一種作法是自己建立一個interface介面,然後讓自己所寫的類別參照於此interface,而不是參照於WebClient,然後再寫一個Class類別來繼承此interface,並於裡面實作WebClient的方法,我相信講到這也是有聽沒有懂,但後面看程式碼就能理解了;其實這也是一種防腐敗層(anti-corruption layer)的作法,利用Adapter設計模式來將非自己寫的,外來的元件做分層,這種做法的好處就是當外來元件做變動的時候,不會影響到我們原本的原件,因為我們原來的元件只會相依於此反腐敗層,那該如何做呢,我們來看看程式碼。
首先我們需要先定義一個interface,並且實作此interface。
public interface IWebClient { string DownloadString(string url); } public class WebClientWrapper : IWebClient { private WebClient client; public WebClientWrapper() { client = new WebClient(); } public string DownloadString(string url) { return client.DownloadString(url); } }
而後,我們自己寫的物件,會相依於此interface,也就是IWebClient。
public class BusinessObject { IWebClient _webClient; public BusinessObject():this(new WebClientWrapper()) { } public BusinessObject(IWebClient webClient) { _webClient = webClient; } public string GetHtmlByUrl(string strURL) { string result = _webClient.DownloadString(strURL); return result; } }
這樣子的話,測試就可以順利去寫了。
[Test] public void Find_customer_using_search() { var mockWebClient = new Mock<iwebclient>(); //改由模擬此介面。 BusinessObject bo = new BusinessObject(mockWebClient.Object); client.Setup(s => s.DownloadString("http://blog.sanc.idv.tw")) .Returns("ok"); string getHtml = bo.GetHtmlByUrl("http://blog.sanc.idv.tw"); Assert.AreEqual("ok", getHtml); }
這樣就是使用到了反腐敗層的做法,但問題來了,如果我要測試WebClientWrapper的話呢?結果還是一樣不能測試阿!!但至少第一個問題解決了某部分的問題…
第二種作法,畢竟山不轉路轉,如果Moq不能測試的話,那就換一套Mock吧,目前旗下最強大的大概是Typemock了吧,據說它可以模擬任何的東西,既然那麼強大,為什麼大家還是要用Moq呢!?因為他需要Money Moeny…另外一套是JustMock,他是新新的一顆新星(真繞舌),不過他依然是要收費的,我想大家也不太可能使用收費產品吧,如果和老闆說,老闆,我們要買一套軟體,它是用來Mock!?我想老闆也不會答應吧(淚),所以繼續跳過吧,最後是大家常常拿來和Moq做比較的RhinoMocks(犀牛!?),他也是免費軟體,而且也可以輕易地由NuGet取得,ok,我們來看看程式碼。
[TestMethod] public void TestGetHtmlByUrl() { /* var mockWebClient = new Mock<iwebclient>(); //改由模擬此介面。 BusinessObject bo = new BusinessObject(mockWebClient.Object); client.Setup(s => s.DownloadString("http://blog.sanc.idv.tw")) .Returns("ok"); string getHtml = bo.GetHtmlByUrl("http://blog.sanc.idv.tw"); Assert.AreEqual("ok", getHtml); */ MockRepository mockWebClient = new MockRepository(); var wc = mockWebClient.DynamicMock<webclient>(); Expect.Call(wc.DownloadString("http://blog.sanc.idv.tw")).Return("ok"); mockWebClient.ReplayAll(); BusinessObject bo = new BusinessObject(mockWebClient.Object); string getHtml = webSpider.GetHtmlByUrl("http://blog.sanc.idv.tw"); Assert.AreEqual("ok", getHtml); }
我特別將之前的Moq語法給註解起來放在一起,其實也可以發現,大家比較多人使用Moq的原因就是因為Moq的Lambda運算式比較簡潔有力。
這邊也稍微介紹一下RhinoMock,他必須先建立一個Repository,然後利用DynamicMock來創建想要的物件,這邊使用DynamicMock的原因,主要是因為他不會採用"嚴格的方式"來創建,也就是說,某些在Test沒定義到的Expect,RhinoMock會睜一隻眼,閉一隻眼讓他過,例如如果BusinessObject的GetHtmlByUrl方法裡面有一段是要將網頁編碼塞給WebClient的Encoding屬性,如果不是採用DynamicMock的方式,就會出現以下錯誤:
測試方法 TestWebSpider.WebSpiderTest.TestGetHtmlByUrl 擲回例外狀況: Rhino.Mocks.Exceptions.ExpectationViolationException: WebClient.set_Encoding(System.Text.UTF8Encoding); Expected #0, Actual #1.
所以我選擇使用DynamicMock來建立,這樣一些比較不重要的資訊,就可以忽略掉,最後我們執行測試看看。
喔喔,終於順利成功了!但是老實說,這兩種解法也都還不滿意,RhinoMocks雖然可以利用這種方式解決,但實務上還是會碰到一些問題,例如當我要將參數設定到模擬出來的WebClient時,還是會給我報錯,而這部分也還沒有解決,未來如果解決了,會持續更新這篇文章。
總結,如果你是非Moq不可,就使用第一種方法吧,雖然會多建立interface和類別,但某種層度來說也是好的,不過依舊治標不治本,如果你是非要寫Unit Test的人( 老實說,還滿感動的,我想通常都是連測試都不寫QQ ),可以考慮第二種作法,反正也不是非用Moq不可,如果你是有錢人,就去買TypeMock吧!!
最後,如果大家有甚麼更好的做法,或是想法,也歡迎討論喔!!謝謝。
參考資料
- http://blog.brianhartsock.com/2009/09/01/using-extension-methods-to-clean-up-mocks-moq/
- http://viswaug.wordpress.com/2010/05/17/helper-classes-for-mock-testing-webclient/
- http://tiredblogger.wordpress.com/2008/05/06/moq-mocks-use-virtual-method-or-interfaces/
- http://virdust.blogspot.com/2010/10/mock-webclient-c-net.html2. http://blog.csdn.net/camel0564/article/details/3135941