這篇文章簡介一下,如何經過 mock framework,來輔助咱們更便利地模擬目標對象的依賴對象,而沒必要手工敲堆只爲了此次測試而存在的輔助類型。html
而模擬目標對象的部分,常見的有 stub object, mock object, fake object,本文也會簡單介紹一下三者的不一樣點,而且經過實例,幫助讀者快速的 pick up 實戰經驗。函數
安裝與範例說明
本文的範例,使用 VS2013 爲開發工具,mock framework 則是使用 Rhino.Mocks,經過 IoC 的方式,由構造函數來傳入 stub/mock/fake object。工具
注:在 Microsoft Fakes 裏面也有內建的 stub object,可是是相似 fake object 的方式產生,而非 Rhino.Mocks, moq 這類 mock framework 是使用動態產生 stub/mock object的方式。Isolating Code under Test with Microsoft Fakes單元測試
- 效益:顧客入場時,幫助店員統計出門票收入,確認是否核賬正確
- 角色:Pub 店員
- 目的:根據顧客與相關條件,算出對應的門票收入總值
public interface ICheckInFee
{
decimal GetFee(Customer customer);
}
public class Customer
{
public bool IsMale { get; set; }
public int Seq { get; set; }
}
public class Pub
{
private readonly ICheckInFee _checkInFee;
private decimal _inCome;
public Pub(ICheckInFee checkInFee)
{
this._checkInFee = checkInFee;
}
/// <summary>
/// 入場
/// </summary>
/// <param name="customers"></param>
/// <returns>收費的人數</returns>
public int CheckIn(List<Customer> customers)
{
var result = 0;
foreach (var customer in customers)
{
var isFemale = !customer.IsMale;
//女生免費入場
if (isFemale)
{
continue;
}
else
{
//for stub, validate status: income value
//for mock, validate only male
this._inCome += this._checkInFee.GetFee(customer);
result++;
}
}
//for stub, validate return value
return result;
}
public decimal GetInCome()
{
return this._inCome;
}
}
CheckIn 說明:開發工具
當顧客進場時,若是是女生,則免費入場。若爲男生,則根據 ICheckInFee 接口來取得門票的費用,並累計到 inCome 中。經過 GetInCome()
方法取得這一次的門票收入總金額。測試
Stub
Stub 一般使用在驗證目標回傳值,以及驗證目標對象狀態的改變。網站


這兩種驗證方式的重點,都擺在目標對象自身的邏輯。this
即測試目標對象時,並不在意目標對象與外部依賴對象如何互動,關注在當外部相依對象回傳什麼樣的數據時,會致使目標對象內部的狀態或邏輯變化。spa
因此這類的驗證方式,是經過 stub object 直接模擬外部依賴回傳的數據,來驗證目標對象行爲是否如同預期。.net
範例:
第一個測試,是驗證收費人數是否符合預期,代碼以下:
[TestMethod]
public void Test_Charge_Customer_Count()
{
//Arrange
var stubCheckInFee = MockRepository.GenerateMock<ICheckInFee>();
var target = new Pub(stubCheckInFee);
var customers = new List<Customer>
{
new Customer {IsMale = true},
new Customer {IsMale = false},
new Customer {IsMale = false},
};
decimal expected = 1;
//Act
var actual = target.CheckIn(customers);
//Assert
Assert.AreEqual(expected, actual);
}
使用 Rhino.Mocks 至關簡單,步驟以下:
- 經過
MockRepository.GenerateStub<T>()
,來創建某一個 T 類型的 stub object,以上面例子來講,是創建實現 ICheckInFee 接口的子類。
- 把該 stub object 經過構造函數,傳給測試目標對象。
- 定義當調用到該 stub object 的哪個方法時(例子中是GetFee方法),傳入的參數是什麼, stub 要回傳是什麼。
經過 Rhino.Mocks,就這麼簡單地經過 Lambda 的方式定義 stub object 的行爲,取代了本來要本身創建一個實體類型,並實現ICheckInFee 接口,定義 GetFee 要回傳的值。
上面的測試案例,是入場顧客人數3人,一男兩女,由於目前 Pub 的 CheckIn 方法,只針對男生收費,因此回傳收費人數應爲1人。
第二個測試,則是驗證收費的總數,是否符合預期。測試案例同樣是一男兩女,而經過 stub object模擬每一人收費爲100元,因此預期結果門票收入總數爲100。測試程序以下:
[TestMethod]
public void Test_Income()
{
//Arrange
var stubCheckInFee = MockRepository.GenerateMock<ICheckInFee>();
var target = new Pub(stubCheckInFee);
stubCheckInFee.Stub(x => x.GetFee(Arg<Customer>.Is.Anything)).Return(100);
var customers = new List<Customer>
{
new Customer {IsMale = true},
new Customer {IsMale = false},
new Customer {IsMale = false},
};
//Act
decimal inComeBeforeCheckIn = target.GetInCome();
//Assert
Assert.AreEqual(0, inComeBeforeCheckIn);
decimal expectedIncome = 100;
//Act
int chargeCustomerCount = target.CheckIn(customers);
var actualIncome = target.GetInCome();
//Assert
Assert.AreEqual(expectedIncome, actualIncome);
}
能夠看到這裏有兩個 Assert,由於咱們這裏是驗證狀態的改變,指望在調用目標對象的 CheckIn 方法以前,取得的門票收入應爲0。而調用以後,依照這個測試案例,門票收入應爲100。
經過這兩個測試案例,其實實際要測試的部分是,CheckIn 的方法只針對男生收費這一段邏輯。無論實際 production code,門票一人收費多少,都不會影響到這一份商業邏輯。
怎麼根據環境或顧客來進行計價,那是在 production code 中,實現 ICheckInFee 接口的子類,要本身進行測試的,與 Pub 對象無關。這樣一來,才能隔離 ICheckInFee 背後的變化。
Mock
使用時機:
上面提到驗證對象的第三種方式:「驗證目標對象與外部依賴接口的互動方式」,以下圖所示:

這聽起來可能至關抽象,但在開發中,的確可能會碰到這樣的測試需求。
Mock 的驗證比起 stub 要複雜許多,變更性一般也會大一點,但每每在驗證一些 void 的行爲會使用到,例如:在某個條件發生時,要記錄 Log。這種情境,用 stub 就很難驗證,由於對目標對象來講,沒有回傳值,也沒有狀態變化,就只能透過 mock object 來驗證,目標對象是否正確的與Log 接口進行互動。
範例:
以這個範例來講,咱們想驗證的是:在2男1女的測試案例中,是否只呼叫 ICheckInFee 接口兩次。程序代碼以下:
[TestMethod]
public void Test_CheckIn_Charge_Only_Male()
{
//Arrange
//兩男一女
var customers = new List<Customer>
{
new Customer {IsMale = true},
new Customer {IsMale = true},
new Customer {IsMale = false},
};
MockRepository mock=new MockRepository();
ICheckInFee stubCheckInFee = mock.StrictMock<ICheckInFee>();
using (mock.Record())
{
//指望調用ICheckInFee的GetFee()次數爲2
//Assert
stubCheckInFee.GetFee(customers.ElementAt(0));
LastCall.IgnoreArguments()
.Return((decimal) 100)
.Repeat.Times(2);
}
using (mock.Playback())
{
var target = new Pub(stubCheckInFee);
//Act
target.CheckIn(customers);
}
}
Fake
使用時機:
當目標對象使用到靜態方法,或 .net framework 自己的對象,甚至於針對通常直接相依的對象,咱們均可以透過 fake object 的方式,直接仿真相依對象的行爲。
範例:
以這例子來講,假設 CheckIn 的需求改變,從本來的「女生免費入場」變成「只有當天爲星期五,女生才免費入場」,修改程序代碼以下:
01 |
public int CheckIn(List<Customer> customers) |
05 |
foreach (var customer in customers) |
07 |
var isFemale = !customer.IsMale; |
09 |
var isLadyNight = DateTime.Today.DayOfWeek == DayOfWeek.Friday; |
11 |
if (isLadyNight && isFemale) |
19 |
this ._inCome += this ._checkInFee.GetFee(customer); |
碰到 DateTime.Today
這類東西,測試案例就會卡住。總不能每次測試都去改測試機上面的日期,或是隻有星期五或非星期五才執行某些測試吧。
因此,咱們得透過 Isolation framework 來輔助,針對使用到的組件,創建 fake object。
首先,由於這個例子創建的 fake object,是針對 System.DateTime
,因此在測試項目上,針對System.dll來新增 Fake 組件,以下圖所示:

能夠看到增長了一個 Fakes 的 folder,其中會針對要 fake 的 dll,產生對應的程序代碼,以便咱們進行攔截與改寫。

使用 fake 對象也至關簡單,先以測試星期五爲例,程序代碼以下:
02 |
public void Test_Friday_Charge_Customer_Count() |
04 |
using (ShimsContext.Create()) |
06 |
System.Fakes.ShimDateTime.TodayGet = () => |
09 |
return new DateTime(2012, 10, 19); |
13 |
ICheckInFee stubCheckInFee = MockRepository.GenerateStub<ICheckInFee>(); |
14 |
Pub target = new Pub(stubCheckInFee); |
16 |
stubCheckInFee.Stub(x => x.GetFee(Arg<Customer>.Is.Anything)).Return(100); |
18 |
var customers = new List<Customer> |
20 |
new Customer{ IsMale= true }, |
21 |
new Customer{ IsMale= false }, |
22 |
new Customer{ IsMale= false }, |
28 |
var actual = target.CheckIn(customers); |
31 |
Assert.AreEqual(expected, actual); |
說明以下:
- 在
using (ShimsContext.Create()){}
的範圍中,會使用 Fake 組件。
- 當在 fake context 環境下,呼叫到
System.DateTime.Today
時,會轉呼叫 System.Fakes.ShimDateTime.TodayGet
,並定義其回傳值爲「2012/10/19」,由於這一天是星期五。
接着就跟本來的測試程序代碼同樣,當星期五時,只對男生收費。
偵錯時,能夠看到 DateTime.Today
變成咱們仿真的「2012/10/19」,但實際系統日期是「2012/10/15」。

再增長一個星期六的測試案例,程序代碼以下:
02 |
public void Test_Saturday_Charge_Customer_Count() |
05 |
using (ShimsContext.Create()) |
07 |
System.Fakes.ShimDateTime.TodayGet = () => |
10 |
return new DateTime(2012, 10, 20); |
14 |
ICheckInFee stubCheckInFee = MockRepository.GenerateStub<ICheckInFee>(); |
15 |
Pub target = new Pub(stubCheckInFee); |
17 |
stubCheckInFee.Stub(x => x.GetFee(Arg<Customer>.Is.Anything)).Return(100); |
19 |
var customers = new List<Customer> |
21 |
new Customer{ IsMale= true }, |
22 |
new Customer{ IsMale= false }, |
23 |
new Customer{ IsMale= false }, |
29 |
var actual = target.CheckIn(customers); |
32 |
Assert.AreEqual(expected, actual); |
由於是星期六,因此1男2女,收費人數爲3人。
補充:
連 System.dll 均可以進行 fake object 仿真了,因此即便是咱們自定義,直接相依,也能夠透過這種方式來仿真。
這樣一來,即使是直接相依的對象,也能夠進行獨立測試了。
但強烈建議,針對自定義對象的部分,這是黑魔法類型的做法,若是沒有包袱,建議對象設計仍是要採 IoC 方式設計。若是是 legacy code,想要進行重構,擺脫直接相依的問題,則可先透過 fake object 來創建單元測試,接下來進行重構,重構後當對象不直接相依時,再改用上面的 stub/mock 方式來進行測試。
能夠參考這篇在 Martin Fowler 網站上的文章:Modern Mocking Tools and Black Magic
注:即便不是在VS2012的環境底下,也能夠到 Microsoft Research 上 download Moles: Moles - Isolation framework for .NET使用
結論
今天這篇文章介紹了 stub, mock 與 fake 的用法,但依筆者實際經驗,使用 stub 的比例大概是8~9成,使用mock的比例大概僅1~2成。而 fake 的方式,則用在特例,例如靜態方法跟 .net framework 原生組件。
也請讀者朋友務必記得幾個基本原則:
- 同一測試案例中,請避免 stub 與 mock 在同一個案例一塊兒驗證。緣由就如同一直在強調的單元測試準則,一次只驗證一件事。而 stub 與 mock 的用途本就不一樣,stub 是用來輔助驗證回傳值或目標對象狀態,而 mock 是用來驗證目標對象與相依對象互動的狀況是否符合預期。既然八竿子打不着,又怎麼會在同一個測試案例中,驗證這兩個徹底不一樣的狀況呢?
- Mock 的驗證能夠至關複雜,但越複雜表明維護成本越高,表明越容易由於需求異動而改變。因此,請謹慎使用 mock,更甚至於當發生問題時,針對問題的測試案例才增長 mock 的測試,筆者都認爲是合情合理的。
- 當要測試一個目標對象,要 stub/mock/fake 的 object 太多時,請務必思考目標對象的設計是否出現問題,是否與太多細節耦合,是否可將這些細節職責合併。
- 當測試程序寫的一狗票落落長時,請確認目標對象的職責是否太肥,或是方法內容太長。這都是由於目標對象設計不良,致使測試程序不容易撰寫或維護的狀況。問題根源在目標對象的設計質量。
- 請將測試程序看成 production code 的一部份,production code 中不應出現的壞味道,同樣不應出如今測試程序中,尤爲是重複的程序代碼。因此測試程序,基本上也須要進行重構。但也請務必提醒本身,測試程序基本上不會包含邏輯,由於包含了邏輯,您就應該再寫一段測試程序,來測這個測試程序是否符合預期。