今天的文章進入了新的系列,那因為接下來的概念是非常抽象的,所以會介紹數個核心技術的概念,接著探討如何使用。與前一系列最大的差別在於我們所撰寫的商業邏輯系統的方法開始有外部引用的情況,比如:Web 服務、系統時間、執行緒或其他類似的第三方套件。
而單元測試的藝術提到這個關鍵點在於:在你的測試中你無法決定相依物件的回傳值,也無法控制他的行為(例如你想模擬他將拋出一個例外)
。此時,就是假物件(Fake)該介入的時機了,假物件又分虛設常式(Stub)與模擬物件(Mock),而我們先介紹虛設常式。在單元測試的藝術中,是這樣定義虛設常式:
是在系統中產生一個可控的替代物件,來取代一個外部相依物件。
我相信許多人看到上述的定義,應該會一頭霧水(當時我看到這段也覺得好抽象),那麼接下來會用故事搭配撰寫程式碼的手法來說明虛設常式。
今天有個開發者,聽到指示說要寫一隻 E-mail 通知系統,在法呼叫這隻方法時,就會發起 E-mail,並且在寄完後要回傳寄送情況(預設為一串字串),於是這位開發者就寫了如下的程式碼:
using JJEmail;
public class EmailSystem
{
public EmailSystem()
{
}
public string EmailFunction(string mailAddress, string mailMessage)
{
var EmailService = new JJEmailService();
var SendResult = EmailService.SendEmail(Address, Message);
return SendResult;
}
}
這個開發者很開心的引用 JJEmail 解決需求上的問題,並在 Code Review 階段提供程式碼,而測試工程師以測試的觀點說明了這隻方法目前的窘境,如下:
using NUnit.Framework;
[TestFixture]
public class EmailSystemUnitTests
{
[Test]
public void EmailFunctionTest()
{
// Arrange
var TestEmailSystem = new EmailSystem();
// Act
var TestResult = TestEmailSystem
.EmailFunction("Test@abc.com", "Test Demo");
// Assert
Assert.AreEqual(TestResult, "Success");
}
}
上述的程式碼出現了兩個問題點:
(1) 從測試程式中,可以看出在 Act 階段,呼叫了 EmailFunction 方法,而回看原始碼也的確有 EmailFunction 的流程;但是今天假設這個 JJEmail 的內部程式碼有出錯,會導致預期的結果永遠出不來(比如他們官方的文件寫會回傳 Success,但他們套件的開發人員卻寫成 success)。而大多時候 C# 引用的第三方套件是已經編譯好的 .dll 檔案,所以很難更動裡面的原始碼;於是乎,這隻測試就是被 JJEmail 套件綁架的測試,不符合優秀測試所提到的要百分百掌控的精神。
(2) 此外,這隻方法已經與 E-mail 套件產生了耦合,對而後擴充與維護不利。而從測試的觀點,假設今天要導入假物件(虛設常式或模擬物件),會找不到適合的切入點(在測試的術語叫接縫,Seam)替換掉第三方套件。因此,導入 OOP 的精神可使程式碼解耦,並且對系統有具有較高的彈性,這也是為什麼在討論測試時,也會一併討論目前的設計模式,因應不同的設計模式會有不同的測試切入與手法。
那明天,會說明如何把上面所述的狀況拆解,讓測試開發好並找切入點撰寫測試。