今天接下來會探討第三種型別,並非透過建構函式或屬性注入的方式建置假物件,而且在對被測試物件進行操作前才獲得該物件的執行個體。因此,接下來會引用單元測試的藝術中提到以工廠類別的例子做示範,所以在這之前要先對工廠類別做個簡單的概述,而這邊以Yan(硯取歪)撰寫的 工廠模式 Factory Pattern 為參考來源。
那為了因應中秋節,我們就以月餅做為工廠類別的範例吧XD,以下為工廠類別要實作的月餅產品:
// 月餅 (MoonCake)
public interface MoonCake
{
string moonCakeType();
}
// 月餅-原味 (TraditionalMoonCake)
public class TraditionalMoonCake : MoonCake
{
public string moonCakeType() {
return "原味月餅";
}
}
// 月餅-芋頭 (TaroMoonCake)
public class TaroMoonCake : MoonCake
{
public string moonCakeType() {
return "芋頭月餅";
}
}
以下則是工廠介面與工廠實作類別:
// 月餅工廠
public interface MoonCakeFactory
{
public MoonCake generateMoonCake();
}
// 月餅工廠-原味 (TraditionalMoonCake)
public class TraditionalMoonCakeFactory : MoonCakeFactory
{
public MoonCake generateMoonCake() {
return new TraditionalMoonCake();
}
}
// 月餅工廠-芋頭 (TaroMoonCake)
public class TaroMoonCakeFactory : MoonCakeFactory
{
public MoonCake generateMoonCake() {
return new TaroMoonCake();
}
}
簡單測試工廠類別產製的月餅:
public class DemoMoonCakeFactoryTest {
[Test]
public void test(){
// Arrange
TaroMoonCakeFactory taroMCFactory = new TaroMoonCakeFactory();
// Act
var taroSample = taroMCFactory.generateMoonCake();
// Assert
Assert.AreEqual("芋頭月餅", taroSample.moonCakeType());
}
}
那簡單看完工廠模式,那接下來可以來看工廠模式對於昨天範例的寫法。
那一樣,我們先來看昨天 LogAnalyzer 的原始碼,只是這次的方式是要用工廠類別的方式新增;於是乎,程式碼改寫如下:
public class LogAnalyzer
{
private IExtensionManager Manager;
public LogAnalyzer()
{
Manager = ExtensionManagerFactory.Create();
}
public bool IsValidLogFileName(string fileName)
{
return Manager.IsValid(fileName);
}
}
public interface IExtensionManager
{
bool IsValid(string fileName);
}
public class FileExtensionManager
{
public bool IsValid(string fileName)
{
// Read some file here
}
}
public class ExtensionManagerFactory
{
private IExtensionManager CustomManager = null;
public IExtensionManager Create()
{
return new FileExtensionManager();
}
public void SetManager(IExtensionManager inCustomManager)
{
CustomManager = inCustomManager;
}
}
透過工廠模式,我們把平常使用 FileExtensionManager 物件與做測試可做為接縫的 SetManager 方法都設想好了,這樣的話撰寫測試就可抽換成虛設常式,如下:
[TestFixture]
public class LogAnalyzerUnitTests
{
[Test]
public void DemoFactoryTest()
{
// Arrange
var manager = Substitute.For<IExtensionManager>();
manager.IsValid(default).ReturnsForAnyArgs(true);
var ExtensionManagerFactory = new ExtensionManagerFactory();
ExtensionManagerFactory.SetManager(manager);
var log = new LogAnalyzer();
// Act
bool result = log.IsValidLogFileName("short.ext");
// Assert
Assert.True(result);
}
}
如此一來,透過工廠模式的方式,就可以在工廠模式裡面的 SetManager 抽換檔案系統,改寫成虛設常式。那在這邊會提到第三種方式—在方法被呼叫前注入一個假物件,其原因在於許多已開發的專案伴隨著不同的設計模式(又或是根本沒有),因應不同的情況我們要思索出相對應的接縫點,以這個用工廠模式的範例,其實有好幾個開設接縫點的策略,如被測試類別撰寫建構函式、屬性注入又或是在工廠模式新設方法注入。
此外,依據不同的接縫策略,所採取的策略也不一樣,單元測試的藝術以 Day-19 與 Day-20 的範例提供了三種不同的中間層深度等級(撰寫接縫的地方),見以下表單:
被測試類別 | 可以進行的操作 |
---|---|
層次深度 1:針對類別的 FileExtensionManager 類別 | 新增建構函式或屬性注入,被測試類別僅一個成員被偽造。 |
層次深度 2:針對工廠注入被測試類別的相依物件 | 透過工廠類別的賦值方法設定相對應假的相依物件。此時僅工廠內的成員為偽造的,被測試類別不需要調整。 |
層次深度 3:偽造假工廠類別 | 建立假的工廠類別,如此裡面所有的工廠方法都可依自己意思撰寫,可抽離相依物件的掌握度是最高的,但相對應撰寫成本極高且使測試變複雜。 |
PS:作者未提供第三種寫法,其原因在於實務上工廠類別的方法極多,若所有的方法都撰寫假方法,僅只用其中的三四成方法又或是更少,其效益極低。
除了上述提到的三種層次以外,那作者提供了第四種方式(不屬於以上三種其中一種):使用一個區域的工廠方法(擷取與覆寫),顧名思義,先在被測試類別寫一隻區域的工廠方法;而要撰寫測試的時候,先寫一隻繼承自被測試類別的類別(好像繞口令XD)。
然後再這支類別裡面新增建構函式(又或是屬性注入),讓假物件可注入進去,程式碼如下:
public class LogAnalyzerUsingFactoryMethod
{
public bool IsValidLogFileName(string fileName)
{
return GetManager().IsValid(fileName);
}
public virtual IExtensionManager GetManager()
{
return new FileExtensionManager();
}
}
public class TestableLogAnalyzer : LogAnalyzerUsingFactoryMethod
{
private IExtensionManager Manager;
public TestableLogAnalyzer(IExtensionManager inManager)
{
Manager = inManager;
}
protected override IExtensionManager GetManager()
{
return Manager;
}
}
public interface IExtensionManager
{
bool IsValid(string fileName);
}
public class FileExtensionManager
{
public bool IsValid(string fileName)
{
// Read some file here
}
}
測試碼:
[TestFixture]
public class LogAnalyzerUnitTests
{
[Test]
public void DemoExtractAndOverrideTest()
{
// Arrange
var manager = Substitute.For<IExtensionManager>();
manager.IsValid(default).ReturnsForAnyArgs(true);
var log = new TestableLogAnalyzer(manager);
// Act
bool result = log.IsValidLogFileName("short.ext");
// Assert
Assert.True(result);
}
}
這個手法的好處在於,如果原本的程式碼沒有適當的接縫點,新增繼承的類別手法可避免原生的程式碼;然而,若原本的程式碼已經有適當的接縫點,且具備可測試性的性質,這樣就反而多此一舉。所以,接縫也算是單元測試中一門很重要的學問,寫出好的接縫點也是需要經驗累積。