### [30天快速上手TDD][Day 7]Unit Test - Stub, Mock, Fake 簡介

[30天快速上手TDD][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

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

        [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

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

        [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

        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. 同一測試案例中，請避免 stub 與 mock 在同一個案例一起驗證。原因就如同一直在強調的單元測試準則，一次只驗證一件事。而 stub 與 mock 的用途本就不同，stub 是用來輔助驗證回傳值或目標物件狀態，而 mock 是用來驗證目標物件與相依物件互動的情況是否符合預期。既然八竿子打不著，又怎麼會在同一個測試案例中，驗證這兩個完全不同的情況呢？
2. Mock 的驗證可以相當複雜，但越複雜代表維護成本越高，代表越容易因為需求異動而改變。所以，請謹慎使用 mock，更甚至於當發生問題時，針對問題的測試案例才增加 mock 的測試，筆者都認為是合情合理的。
3. 當要測試一個目標物件，要 stub/mock/fake 的 object 太多時，請務必思考目標物件的設計是否出現問題，是否與太多細節耦合，是否可將這些細節職責合併。
4. 當測試程式寫的一狗票落落長時，請確認目標物件的職責是否太肥，或是方法內容太長。這都是因為目標物件設計不良，導致測試程式不容易撰寫或維護的情況。問題根源在目標物件的設計品質。
5. 將測試程式當作 production code 的一部份，production code 中不該出現的壞味道，一樣不該出現在測試程式中，尤其是重複的程式碼。所以測試程式，基本上也需要進行重構。但也請務必提醒自己，測試程式基本上不會包含邏輯，因為包含了邏輯，您就應該再寫一段測試程式，來測這個測試程式是否符合預期

### Reference

1. Microsoft Fakes 入門 （跟煥麟老師的這篇文交叉參考一下 :P）

 想收到第一手公開培訓課程資訊，或想詢問企業內訓、顧問、教練、諮詢服務的，請洽 Facebook 粉絲專頁：91敏捷開發之路。 如您覺得這篇文章對你有幫助，想請我喝杯咖啡，可以用街口小額贊助一下 (NT\$ 30)。