DAY 7
5

## [Day 7]Unit Test - Stub, Mock, Fake簡介

＠前言

1. 效益：顧客入場時，幫助店員統計出門票收入，確認是否核帳正確
2. 角色：Pub店員
3. 目的：根據顧客與相關條件，算出對應的門票收入總值

``````    public interface ICheckInFee
{
decimal GetFee(Customer customer);
}

public class Customer
{
public bool IsMale { get; set; }

public int Seq { get; set; }
}

public class Pub
{
private ICheckInFee _checkInFee;
private decimal _inCome = 0;

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說明：

＠Stub

1. 使用時機：
[Day 2]Unit Testing 簡介中，提到了驗證物件行為是否符合預期有三種方式。Stub通常使用在驗證目標回傳值，以及驗證目標物件狀態的改變。

這兩種驗證方式的重點，都擺在目標物件自身的邏輯。

1. 範例：
第一個測試，是驗證收費人數是否符合預期，程式碼如下：

``````    [TestMethod]
public void Test_Charge_Customer_Count()
{
//arrange
ICheckInFee stubCheckInFee = MockRepository.GenerateStub<ICheckInFee>();
Pub 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},
};

decimal expected = 1;

//act
var actual = target.CheckIn(customers);

//assert
Assert.AreEqual(expected, actual);
}
``````

(1)透過MockRepository.GenerateStub<T>()，來建立某一個T型別的stub object，以上面例子來說，是建立ICheckInFee介面的實作子類。
(2)把該stub object透過建構式，設定給測試目標物件。
(3)定義當呼叫到該stub object的哪一個方法時，若傳入的參數為何，則stub要回傳什麼

``````        [TestMethod]
public void Test_Income()
{
//arrange
ICheckInFee stubCheckInFee = MockRepository.GenerateStub<ICheckInFee>();
Pub 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},
};

var inComeBeforeCheckIn = target.GetInCome();
Assert.AreEqual(0, inComeBeforeCheckIn);

decimal expectedIncome = 100;

//act
var chargeCustomerCount = target.CheckIn(customers);

var actualIncome = target.GetInCome();

//assert
Assert.AreEqual(expectedIncome, actualIncome);
}
``````

＠Mock

1. 使用時機：
上面提到驗證物件的第三種方式：「驗證目標物件與外部相依介面的互動方式」，如下圖所示：

這聽起來可能相當抽象，但在實務上，的確可能會碰到這樣的測試需求。

mock的驗證比起stub要複雜許多，變動性通常也會大一點，但往往在驗證一些void的行為會使用到，例如：在某個條件發生時，要記錄Log。這種情境，用stub就很難驗證，因為對目標物件來說，沒有回傳值，也沒有狀態變化，就只能透過mock object來驗證，目標物件是否正確的與Log介面進行互動。

1. 範例：
以這個範例來說，我們想驗證的是：在2男1女的測試案例中，是否只呼叫ICheckInFee介面兩次。程式碼如下：

``````    [TestMethod]
public void Test_CheckIn_Charge_Only_Male()
{
//arrange mock
var customers = new List<Customer>();

//2男1女
var customer1 = new Customer { IsMale = true };
var customer2 = new Customer { IsMale = true };
var customer3 = new Customer { IsMale = false };

MockRepository mock = new MockRepository();
ICheckInFee stubCheckInFee = mock.StrictMock<ICheckInFee>();

using (mock.Record())
{
//期望呼叫ICheckInFee的GetFee()次數為2次
stubCheckInFee.GetFee(customer1);

LastCall
.IgnoreArguments()
.Return((decimal)100)
.Repeat.Times(2);
}

using (mock.Playback())
{
var target = new Pub(stubCheckInFee);

var count = target.CheckIn(customers);
}
}
``````

mock的API相當多樣與複雜，有興趣的讀者朋友請自行參閱官方API document的說明。

＠Fake

1. 使用時機：
當目標物件使用到靜態方法，或.net framework本身的物件，甚至於針對一般直接相依的物件，我們都可以透過fake object的方式，直接模擬相依物件的行為。

2. 範例：
以這例子來說，假設CheckIn的需求改變，從原本的「女生免費入場」變成「只有當天為星期五，女生才免費入場」，修改程式碼如下：

``````    public int CheckIn(List<Customer> customers)
{
var result = 0;

foreach (var customer in customers)
{
var isFemale = !customer.IsMale;
//for fake
var isLadyNight = DateTime.Today.DayOfWeek == DayOfWeek.Friday;
//禮拜五女生免費入場
{
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;
}
``````

``````        [TestMethod]
public void Test_Friday_Charge_Customer_Count()
{
using (ShimsContext.Create())
{
System.Fakes.ShimDateTime.TodayGet = () =>
{
//2012/10/19為Friday
return new DateTime(2012, 10, 19);
};

//arrange
ICheckInFee stubCheckInFee = MockRepository.GenerateStub<ICheckInFee>();
Pub 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},
};

decimal expected = 1;

//act
var actual = target.CheckIn(customers);

//assert
Assert.AreEqual(expected, actual);
}
}
``````

(1)在using (ShimsContext.Create()){}範圍中，會使用Fake組件。
(2)當在fake context環境下，呼叫到System.DateTime.Today時，會轉呼叫System.Fakes.ShimDateTime.TodayGet，並定義其回傳值為2012/10/19，因為這一天是星期五。

``````        [TestMethod]
public void Test_Saturday_Charge_Customer_Count()
{

using (ShimsContext.Create())
{
System.Fakes.ShimDateTime.TodayGet = () =>
{
//2012/10/20為Saturday
return new DateTime(2012, 10, 20);
};

//arrange
ICheckInFee stubCheckInFee = MockRepository.GenerateStub<ICheckInFee>();
Pub 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},
};

decimal expected = 3;

//act
var actual = target.CheckIn(customers);

//assert
Assert.AreEqual(expected, actual);
}
}
``````

1. 補充：
連System.dll都可以進行fake object模擬了，所以即使是我們自訂的class，直接相依，也可以透過這種方式來模擬。

by the way, 沒在VS2012的環境底下，也可以到Microsoft Research上download Moles: Moles - Isolation framework for .NET 使用

＠結論

1. 同一測試案例中，請避免stub與mock一起使用。原因就如同一直在強調的單元測試準則，一次只驗證一件事。而stub與mock的用途本就不同，stub是用來輔助驗證回傳值或目標物件狀態，而mock是用來驗證目標物件與相依物件互動的情況是否符合預期。既然八竿子打不著，又怎麼會在同一個測試案例中，驗證這兩個完全不同的情況呢？

2. mock的驗證可以相當複雜，但越複雜代表維護成本越高，代表越容易因為需求異動而改變。所以，請謹慎使用mock，更甚至於當發生問題時，針對問題的測試案例才增加mock的測試，筆者都認為是合情合理的。

3. 當要測試一個目標物件，要stub/mock/fake的object太多時，請務必思考目標物件的設計是否出現問題，是否與太多細節耦合，是否可將這些細節職責合併。

4. 當測試程式寫的一狗票落落長時，請確認目標物件的職責是否太肥，或是方法內容太長。這都是因為目標物件設計不良，導致測試程式不容易撰寫或維護的情況。問題根源在目標物件的設計品質。

5. 請將測試程式當作production code的一部份，production code中不該出現的壞味道，一樣不該出現在測試程式中，尤其是重複的程式碼。所以測試程式，基本上也需要進行重構。但也請務必提醒自己，測試程式基本上不會包含邏輯，因為包含了邏輯，您就應該再寫一段測試程式，來測這個測試程式是否符合預期。

30天快速上手TDD31

### 3 則留言

0
ted99tw
iT邦高手 1 級 ‧ 2012-10-15 07:57:55

pajace2001 iT邦研究生 1 級 ‧ 2012-10-15 08:16:43 檢舉

0
pajace2001
iT邦研究生 1 級 ‧ 2012-10-15 08:17:13

ted99tw iT邦高手 1 級 ‧ 2012-10-15 08:23:18 檢舉

pajace2001 iT邦研究生 1 級 ‧ 2012-10-15 20:51:40 檢舉

h大辛苦了.

0
pajace2001
iT邦研究生 1 級 ‧ 2012-10-15 20:49:59