The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

ASP.NET MVC 單元測試系列 (3):瞭解 Mock 假物件 ( moq )

我們在上一篇已經得知「可測試性」的思考方向,在本篇文章我們將利用 Mocking 技術強化之前修改過的測試程式,讓我們的單元測試程式可以完全脫離「外部資源」的魔掌 (在此範例是指資料庫連線),我們將使用 moq 工具函式庫協助我們的測試程式更容易、更快速的撰寫,至於 moq 的安裝可參考 moq 官網或 ASP.NET MVC 2 開發實戰 一書【12-3-7 利用Mock假物件完成單元測試】章節的說明 ( P.398 )。

所謂的 Mock 有「模仿」的意思,今後你在很多地方都會看到 Mock 或 Mocking 的字眼,因為 Mock 技巧在單元測試這個領域非常常見,但到底什麼是 Mock 物件?要 Mock 些什麼呢?什麼東西可以 Mock?什麼東西不能 Mock?一定不能 Mock 嗎?我來一一回答這些問題:

1. 什麼是 Mock 物件?

Mock 物件 (Mock Object) 所代表的是一個「模仿的物件」,是 假物件 (Fake Object) 的一種,總之不是真的物件就對了,這句解釋感覺像是廢話連篇,為了要瞭解什麼是 Mock 物件,其重點在於為什麼要有這些假物件的存在。

假物件還包括兩種,一種是今天要講的 Mock 物件,另一種是改天才會講的 Stub 物件。這些假物件的主要目的在於協助單元測試程式可以順利執行,並且讓單元測試專案裡的測試方法 (Test Method) 在執行測試時可以將測試我們想測試的部分就好。

我們拿上一篇最後修改過的 Profile 程式碼來看,我們要測試的對象是 Profile() 這個方法,但是這個方法中又跑去執行 MemberRepository 物件 ( _r ) 的 GetMemberByAccount 方法,這時我們該不該在這個方法的單元測試中測試這一行程式碼呢?

public ActionResult Profile(string account)
{
ViewData.Model = _r.GetMemberByAccount(account);

return View();
}

單元測試來說,答案非常明確:不應該測試 GetMemberByAccount() 方法執行的正確性!

但是這個方法就只有兩行而已耶,這行不用測試,難道只需要測試此方法 (Method) 的回傳型別而已嗎?不用測試實際取得的 Model 資料嗎?

這個問題的答案是:兩行都要測試,但不應該測試 GetMemberByAccount() 方法執行的正確性!

咦???這兩句話沒有衝突嗎?如果不應該測試GetMemberByAccount() 方法執行的正確性,那我怎麼知道 ViewData.Model 取得的資料是什麼??那我要測什麼東西??

這個問題的答案是:是的,你還是要去執行 GetMemberByAccount() 方法,因為這個 Action 的需求就是要去執行 GetMemberByAccount() 方法,但至於執行 GetMemberByAccount() 方法所回傳的資料是否正確並不是 Profile() 這個方法的「單元測試」應該測試的部分,因為 單元測試是軟體測試的最小單位,如果測試的範圍輕易的就會擴展到其他類別或同類別的其他方法,那就不再是最小單位,也就不是單元測試了!

那麼我們應該如何去執行 GetMemberByAccount() 方法,又可以不考慮他的結果是否正確呢?我要用什麼方式執行?我的 MemberRepository 類別定義就是這樣啊!程式碼中該有資料庫連線的部分還是要資料庫連線吧!我要怎樣才能「執行這段程式碼又不管他的執行結果」呢?天啊,寫的單元測試越寫問題越多耶!

上述問題的答案是:這就是 Mock 的功用了!請看下一個問題的解答。

 

2. 要 Mock 些什麼呢?

由上述的例子來說,你要 Mock 的物件就是 MemberRepository 物件 ( _r ),換句話說,我們要模仿出 MemberRepository 物件 ( _r ),並傳入 AccountController 控制器,並且在執行 Profile() 動作方法時可以讓程式正確執行到 GetMemberByAccount() 方法,且可以執行完又不會發生錯誤或例外狀況。

 

3. 什麼東西可以 Mock?什麼東西不能 Mock?

OK! 各位對 Mock 物件應該已經有概念了,這時我們延續上一篇的最後一個修改過後的測試程式,透過 moq 工具函式庫的 Mocking 的方式 模仿出(Mocking) MemberRepository 物件,如下圖示:

alt

首先,先解釋上述程式的程式說明:

  1. 先模擬出一個 MemberRepository 物件,此時的 mock 物件型別為 Moq.Mock<MvcApplication3.Models.MemberRepository>
  2. 設定(Setup)模擬此 MemberRepository 物件時,要執行 GetMemberByAccount("test") 這個方法,而且一定要被執行過才算數 ( Verifiable )。
  3. 設定完成後取出被 Mock 過的物件,並傳入 AccountControoler 控制器。
    : 此時 mock.Object 物件就是 MemberRepository 型別,可以傳入 AccountControoler 控制器。

這一切都是這麼的美好,我們執行測試看看吧!很可惜的,發生例外了!因為 GetMemberByAccount 不是一個可以被 Mock 的類別成員!

alt

那麼對 moq 來說,什麼才是可以被 Mock 的類別成員呢?

  1. 有被標注 publicvirtual 修飾詞的成員 ( 也就是可被覆寫的公開方法 )
  2. 有被標注 public介面 (Interface) 所有成員都能被 Mock

備註:像是 JustMock 這套賣錢的 Mock 函示庫宣稱什麼都能 Mock,的確是個不錯的產品!

如果以上述規則來說,難道我們把 MemberRepository 物件的 GetMemberByAccount 方法標注為 virtual 就可以 Mock 了嗎?答案是:沒錯!但還是不夠理想!

首先,我們先把 MemberRepository 物件的 GetMemberByAccount 方法定義修改如下:

public virtual Member GetMemberByAccount(string account)
{
var q = from p in db.Member where p.Account == account select p;

return q.FirstOrDefault();
}

這時我們再跑一次單元測試專案中的 Profile_Get_ReturnsView 測試方法看看,得到的結果如下圖:

alt

什麼?取得 mock.Object 的時候發生例外?這是什麼錯誤呢?

因為我們在執行 Mock 任務時,moq 會透過 反映(Reflection) 的方式主動幫我們建立要被 Mock 的型別的物件實體起來,所以在建立物件實體(instance)一定會執行該類別的建構子(constructor),但我們這個類別的建構子是建立一個預設的 MyDataContext 物件阿 (如下程式碼片段),那 Mock 不就失去意義了?因為到最後取得 Mock 物件時 ( mock.Object ) 還是要建立資料庫連線阿!

public MemberRepository()
{
db = new MyDataContext();
}

沒錯,我們又再度發現一個「可測試性」的弱點,這裡「弱點」的意思是指我們寫的 MemberRepository 類別的可測試性不夠,是我們的 MemberRepository 寫的不好,所以導致不容易被 Mock,換言之就是不容易被通過測試 的意思。(注意:不容易通過測試不容易被測試是兩回事喔!

接下來,我就要開始變魔術了,我們改用 介面 (Interface) 的方式代表 MemberRepository 類別,並利用 Visual Studio 2010 的重構機制幫我們 擷取介面 (Extract Interface),以下是設定步驟:

在 MemberRepository 類別上按右鍵,選取 重構 –> 擷取介面 (點圖可放大)

alt

擷取介面使用預設值即可,Visual Studio 2010 會在與 MemberRepository 類別相同的所在目錄建立另一個 IMemberRepository.cs 檔案,裡面會有介面的宣告:

alt

建立好 IMemberRepository 介面之後我們的 MemberRepository 類別會自動被加上實做介面的語法:

alt

而在 IMemberRepository 介面裡,我們必須將 IMemberRepository 介面改成「公開」的介面,好讓測試程式能夠使用這個介面:

alt

然後我們去修改測試方法,把 Mock 的物件型別修改成「介面型別」,這時 mock.Object 的型別也會自動變成 IMemberRepository 型別:

 alt

由於透過測試程式傳入的型別都從 MemberRepository 類別型別改成 IMemberRepository 介面型別了,所以所有相關方法 (Method) 的參數型別也全部都要一併替換掉:

 alt

alt

全部都修改完,且都建置成功後,我們再跑一次測試看看:

alt

太棒了,終於成功了,而且執行速度比之前快好多好多啊!(是有感覺的那種快)

但是你可能會納悶 moq 是如何辦到的?他只需要一個 介面型別 (Interface Type) 就能產生一個物件?沒錯,就是這樣!moq 使用 Castle DynamicProxy 達成這件任務,基本的原理就是它利用 反映(Reflection) 機制的 Emit 功能動態產生一個 空類別 (也就是所有介面的方法都實做,但都沒有任何功能,只是一個程式骨架而已),所以 Mock 的能力就在於可以利用 DynamicProxy 的機制快速產生出一個假物件出來,用以模仿真物件的行為,是不是很神奇呢!

各位親愛的讀者,感受到 Mock 的威力了嗎?只要你的程式擁有良好的可測試性,再加上擁有良好的 Mocking 工具 ( 如 moq ),就可以讓你的單元測試之路越來越順遂,而在日後的系列文章也會有許多使用 Mocking 的技巧,讓你在各種不同的單元測試情境下都能使用 Mocking 技術簡化測試程式的複雜度。

單元測試的道路已經被打開,險峻的路上還有許多未知的事物(或地雷)等待被發現,歡迎各位留言探討問題,如有謬誤之處敬請指教,謝謝。

程式碼下載

相關連結