2011年9月18日

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);//驗證測試   
}

理論上這樣就可以了,然後很開心地執行測試…於是產生了錯誤…

image

這個錯誤是表示,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來建立,這樣一些比較不重要的資訊,就可以忽略掉,最後我們執行測試看看。

image

喔喔,終於順利成功了!但是老實說,這兩種解法也都還不滿意,RhinoMocks雖然可以利用這種方式解決,但實務上還是會碰到一些問題,例如當我要將參數設定到模擬出來的WebClient時,還是會給我報錯,而這部分也還沒有解決,未來如果解決了,會持續更新這篇文章。

總結,如果你是非Moq不可,就使用第一種方法吧,雖然會多建立interface和類別,但某種層度來說也是好的,不過依舊治標不治本,如果你是非要寫Unit Test的人( 老實說,還滿感動的,我想通常都是連測試都不寫QQ ),可以考慮第二種作法,反正也不是非用Moq不可,如果你是有錢人,就去買TypeMock吧!!

最後,如果大家有甚麼更好的做法,或是想法,也歡迎討論喔!!謝謝。

參考資料

  1. http://blog.brianhartsock.com/2009/09/01/using-extension-methods-to-clean-up-mocks-moq/
  2. http://viswaug.wordpress.com/2010/05/17/helper-classes-for-mock-testing-webclient/
  3. http://tiredblogger.wordpress.com/2008/05/06/moq-mocks-use-virtual-method-or-interfaces/
  4. http://virdust.blogspot.com/2010/10/mock-webclient-c-net.html
  5. http://blog.csdn.net/camel0564/article/details/3135941

沒有留言:

張貼留言