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

我們在[上一篇](/post/2010/09/14/ASPNET-MVC-Unit-Testing-Part-02-Testability)已經得知「可測試性」的思考方向，在本篇文章我們將利用 Mocking 技術強化之前修改過的測試程式，讓我們的單元測試程式可以完全脫離「外部資源」的魔掌 (在此範例是指**資料庫連線**)，我們將使用 [moq](http://code.google.com/p/moq/) 工具函式庫協助我們的測試程式更容易、更快速的撰寫，至於 [moq](http://code.google.com/p/moq/) 的安裝可參考 [moq](http://code.google.com/p/moq/) 官網或 [ASP.NET MVC 2 開發實戰](http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9789866348624&sid=58261) 一書【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) 在執行測試時可以將測試我們想測試的部分就好。

我們拿[上一篇](/post/2010/09/14/ASPNET-MVC-Unit-Testing-Part-02-Testability)最後修改過的 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 物件應該已經有概念了，這時我們延續[上一篇](/post/2010/09/14/ASPNET-MVC-Unit-Testing-Part-02-Testability)的最後一個修改過後的測試程式，透過 [moq](http://code.google.com/p/moq/) 工具函式庫的 Mocking 的方式 **模仿出**(**Mocking**) MemberRepository 物件，如下圖示：

![alt](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/a48f0dff3f8297d29c0d-image_186.png "title")

首先，先解釋上述程式的程式說明：

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

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

![alt](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/af2c4c6b95aa234d4914-image_187.png "title")

那麼對 [moq](http://code.google.com/p/moq/) 來說，什麼才是可以被 Mock 的類別成員呢？

1.  有被標注 [public](http://msdn.microsoft.com/zh-tw/library/yzh058ae.aspx) 與 [virtual](http://msdn.microsoft.com/zh-tw/library/9fkccyh4.aspx) 修飾詞的成員 ( 也就是可被覆寫的公開方法 )
2.  有被標注 [public](http://msdn.microsoft.com/zh-tw/library/yzh058ae.aspx) 的 **介面** (**Interface**) 所有成員都能被 Mock

**備註：像是** [**JustMock**](http://www.telerik.com/products/mocking.aspx) **這套賣錢的 Mock 函示庫宣稱什麼都能 Mock，的確是個不錯的產品！**

如果以上述規則來說，難道我們把 MemberRepository 物件的 GetMemberByAccount 方法標注為 [virtual](http://msdn.microsoft.com/zh-tw/library/9fkccyh4.aspx) 就可以 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](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/3f678d211be89dd178ba-image_188.png "title")

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

因為我們在執行 Mock 任務時，[moq](http://code.google.com/p/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](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/d90d6729b96f9a2bcd71-image_thumb_27.png "title")](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/dbd8ab65b5085f7e2dd0-image_189.png)

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

![alt](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/64d25c820160c6c41ec1-SNAGHTML17d65b94.png "title")

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

![alt](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/d092b76f501508fc367f-image_190.png "title")

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

![alt](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/7c651450f69b7b3716ed-image_191.png "title")

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

 ![alt](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/babd624c45e093e015e0-image_192.png "title")

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

 ![alt](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/cd0224b8e53970889cf3-image_193.png "title")

![alt](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/1b69e7f50f451a5a96b3-image_194.png "title")

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

![alt](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/92c81dcb2c5e5fba8def-image_195.png "title")

太棒了，終於成功了，而且執行速度比之前快好多好多啊！（是有感覺的那種快）

但是你可能會納悶 [moq](http://code.google.com/p/moq/) 是如何辦到的？他只需要一個 **介面型別** (Interface Type) 就能產生一個物件？沒錯，就是這樣！[moq](http://code.google.com/p/moq/) 使用 [Castle DynamicProxy](https://www.castleproject.org/projects/dynamicproxy/) 達成這件任務，基本的原理就是它利用 **反映**(Reflection) 機制的 [Emit](http://msdn.microsoft.com/en-us/library/system.reflection.emit.aspx) 功能動態產生一個 **空類別** (也就是所有介面的方法都實做，但都沒有任何功能，只是一個程式骨架而已)，所以 Mock 的能力就在於可以利用 DynamicProxy 的機制快速產生出一個**假物件**出來，用以模仿**真物件**的行為，是不是很神奇呢！

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

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

**程式碼下載**

-   [MvcApplication3.zip](http://cid-ce50199d9432ea57.office.live.com/self.aspx/%e5%85%ac%e9%96%8b/ASP.NET%20MVC%20%e5%96%ae%e5%85%83%e6%b8%ac%e8%a9%a6%e7%b3%bb%e5%88%97/MvcApplication3.zip)

**相關連結**

-   [moq - The simplest mocking library for .NET 3.5 and Silverlight with deep C# 3.0 integration](http://code.google.com/p/moq/)
-   [Castle DynamicProxy](https://www.castleproject.org/projects/dynamicproxy/)
-   [virtual (C# 參考)](http://msdn.microsoft.com/zh-tw/library/9fkccyh4.aspx)
-   [interface (C#)](http://msdn.microsoft.com/zh-tw/library/87d83y5b\(VS.80\).aspx)
-   [Using Reflection Emit](http://msdn.microsoft.com/zh-tw/library/3y322t50.aspx)
-   [与protected成员有关的单元测试方式](http://www.cnblogs.com/JeffreyZhao/archive/2009/08/28/unit-test-protected-method.html)
