相信許多人剛接觸完虛設常式與模擬物件,會說不出兩者之間確切的差別,有種曖昧糾纏的感覺。兩者都是假物件,之間最大的差異就在於是否有被驗證,我們來看張對照圖:
從這張對照圖,可以看出左邊是在 Day-10 撰寫 Stub 時的流程圖,我們待測試的商業邏輯注入了虛擬常式後,呼叫商業邏輯的方法得到資料後,做為我們驗證的基準;另一方面,Day-12 撰寫 Mock 時,我們呼叫待測試的商業邏輯方法後,會去執行模擬物件的方法,因此,我們要驗證模擬物件是否真的有被執行。
我們在撰寫單元測試時,很有可能存在許多假物件,有零或一個的模擬物件與一個以上的虛設常式;同時,也有可能在第一個單元測試擔任虛設常式,在第二個測試擔任模擬物件。接下來一樣,我們來用程式碼說故事,今天開發者又接到新的需求,要把先前所撰寫的 Email 通知系統與 Log 紀錄系統結合,發送完 Email 再紀錄 Log,程式碼如下:
public class EmailWithLogSystem
{
private IEmailService EmailService;
private ILogService LogService;
public EmailWithLogSystem(IEmailService inEmailService, ILogService inLogService)
{
EmailService = inEmailService;
LogService = inLogService;
}
public string SendFunction(string mailAddress, string mailMessage)
{
var SendResult = EmailService
.SendEmail(mailAddress, mailMessage);
if (SendResult == "Fail")
{
LogService.Log(mailAddress + " is not send yet!");
}
return SendResult;
}
}
public interface IEmailService()
{
public string SendEmail(string mailAddress, string mailMessage);
}
public interface ILogService()
{
public string Log(string logMessage);
}
可以看出我們在呼叫 SendFunction 的時候,一開始會先呼叫 EmailService 的 SendEmail 方法,會回傳字串 SendResult,若發生失敗的情況則再中間會呼叫 LogService 的 Log 方法。
因此,單元測試可撰寫好幾種情況,SendEmail 成功與失敗的情況分別回傳的字串,以及失敗的時候是否會呼叫 Log 方法。
對此,測試 SendFunction 的策略,我們先測試失敗時是否會呼叫 Log 方法。首先,創建一個虛設常式代表 EmailService 做前置的過程,最後在呼叫 LogService 的 Log 時當做模擬物件,測試碼如下:
using NUnit3;
[TestFixture]
public class EmailWithLogSystemUnitTests
{
[Test]
public void SendFunction_CallLog_Success()
{
// Arrange
StubEmailFailSerivce stubEmailService = new StubEmailFailSerivce();
MockLogSerivce mockLogService = new MockLogSerivce();
EmailWithLogSystem EmailWithLogService = new EmailWithLogSystem(stubEmailService, mockLogService);
// Act
var result = EmailWithLogService.SendFunction("Test@abc.com.tw", "Test Demo");
// Assert
Assert.AreEqual("Test@abc.com.tw is not send yet!", mockLogService.logMessage);
}
}
public class StubEmailFailSerivce : IEmailService
{
public string SendEmail(mailAddress, mailMessage)
{
return "Fail";
}
}
public class MockLogSerivce : ILogService
{
public string logMessage;
public string Log(string LogMessage)
{
logMessage = LogMessage;
}
}
OK,這樣撰寫完 StubEmailFailSerivce 成功扮演了虛設常式的工作,把商業邏輯中間的過程給處理掉;而 MockLogSerivce 則是擔任了模擬物件,做為測試方法 SendFunction_CallLog_Success 中,最後是否有成功產製 "Test@abc.com.tw is not send yet!" 這段文字。
那假設今天我們要看的不是 Log 方法是否有呼叫,而是 SendEmail 後是否有得到相對應的字串呢?那此時,在上隻擔任 MockLogSerivce 的任務則在這隻測試就變成 StubLogSerivce,測試碼如下:
using NUnit3;
[TestFixture]
public class EmailWithLogSystemUnitTests
{
[Test]
public void SendFunction_CatchSendResult_Success()
{
// Arrange
StubEmailSuccessSerivce stubEmailService = new StubEmailSuccessSerivce();
StubLogSerivce stubLogService = new StubLogSerivce();
EmailWithLogSystem EmailWithLogService = new EmailWithLogSystem(stubEmailService, stubLogService);
// Act
var result = EmailWithLogService.SendFunction("Test@abc.com.tw", "Test Demo");
// Assert
Assert.AreEqual("Success", result);
}
}
public class StubEmailSuccessSerivce : IEmailService
{
public string SendEmail(mailAddress, mailMessage)
{
return "Success";
}
}
public class StubLogSerivce : ILogService
{
public string logMessage;
public string Log(string LogMessage)
{
logMessage = LogMessage;
}
}
有趣吧,一下擔任模擬物件、一下又擔任虛設常式,這也是單元測試中很吃靈感的地方。(我寫到這邊其實腦袋快燒乾了)
那接下來明天會進一步探討一些單元測試需要考量的情境,如只針對一個關注點測試、過度指定、假物件鏈等。