使用填充碼隔離應用程式以進行單元測試

填充碼類型,Microsoft Fakes Framework 所使用的兩項關鍵技術之一,可用於在測試中隔離您的應用程式元件。 填充碼可以攔截對特定方法的呼叫,讓您將該呼叫轉向至您測試中的自訂程式碼。 此功能可讓您管理這些方法的輸出,確保每次呼叫的結果都保持一致且符合預期,不受外部條件影響。 此控制層級可精簡測試流程,並協助產生更可靠且準確的結果。

當您要在程式碼以及非解決方案的組件之間建立邊界時,便可採用填充碼。 如果目標是將解決方案內的元件彼此隔離,則建議使用虛設常式

(如需虛設常式更詳細的描述,請參閱使用虛設常式隔離應用程式的各個組件,方便進行單元測試。)

填充碼限制

請務必留意填充碼本身的限制性。

填充碼不適用於 .NET 基底類別中特定程式庫的所有類型,具體來說是 .NET Framework 中的 mscorlibSystem,以及 .NET Core 或 .NET 5+ 當中 System.Runtime 的程式庫。 在測試的規劃和設計階段應考慮這項限制,以確保您的測試策略成功且有效。

建立填充碼:逐步指南

假設您的元件包含對 System.IO.File.ReadAllLines 的呼叫:

// Code under test:
this.Records = System.IO.File.ReadAllLines(path);

建立類別庫

  1. 開啟 Visual Studio 並建立 Class Library 專案

    Visual Studio 中 NetFramework 類別庫專案的螢幕擷取畫面。

  2. 將專案命名為 HexFileReader

  3. 將解決方案命名為 ShimsTutorial

  4. 將專案的目標框架設為 .NET Framework 4.8

  5. 刪除預設資料夾 Class1.cs

  6. 新增檔案 HexFile.cs 並新增下列類別定義:

    // HexFile.cs
    public class HexFile
    {
        public string[] Records { get; private set; }
    
        public HexFile(string path)
        {
            this.Records = System.IO.File.ReadAllLines(path);
        }
    }
    

建立測試專案

  1. 以滑鼠右鍵按一下方案,然後新增專案 MSTest Test Project

  2. 將專案命名為 TestProject

  3. 將專案的目標框架設為 .NET Framework 4.8

    Visual Studio 中 NetFramework 測試專案的螢幕擷取畫面。

新增 Fakes 組件

  1. 新增專案參考至 HexFileReader

    新增專案參考命令的螢幕擷取畫面。

  2. 新增 Fakes 組件

    • 在 [方案總管] 中,

      • 針對舊版 .NET Framework 專案 (非 SDK 樣式),請展開您單元測試專案的參考節點。

      • 針對以 .NET Framework、.NET Core 或 .NET 5+ 為目標的 SDK 樣式專案,請展開 [相依性] 節點以在 [組件]、[專案] 或 [套件] 底下尋找要假造的組件。

      • 如果您是使用 Visual Basic,必須選取 [方案總管] 工具列中的 [顯示所有檔案] 才會看見 [參考] 節點。

    • 選取 System 組件,其中包含 System.IO.File.ReadAllLines 的定義。

    • 在捷徑功能表上,選取 [新增 Fakes 組件]

    新增 Fakes 組件命令的螢幕擷取畫面。

由於並非所有類型都適用於填充碼,因此組建會產生警告和錯誤,您必須修改 Fakes\mscorlib.fakes 的內容才能排除它們。

<Fakes xmlns="http://schemas.microsoft.com/fakes/2011/" Diagnostic="true">
  <Assembly Name="mscorlib" Version="4.0.0.0"/>
  <StubGeneration>
    <Clear/>
  </StubGeneration>
  <ShimGeneration>
    <Clear/>
    <Add FullName="System.IO.File"/>
    <Remove FullName="System.IO.FileStreamAsyncResult"/>
    <Remove FullName="System.IO.FileSystemEnumerableFactory"/>
    <Remove FullName="System.IO.FileInfoResultHandler"/>
    <Remove FullName="System.IO.FileSystemInfoResultHandler"/>
    <Remove FullName="System.IO.FileStream+FileStreamReadWriteTask"/>
    <Remove FullName="System.IO.FileSystemEnumerableIterator"/>
  </ShimGeneration>
</Fakes>

建立單元測試

  1. 修改預設檔案 UnitTest1.cs 以新增下列 TestMethod

    [TestMethod]
    public void TestFileReadAllLine()
    {
        using (ShimsContext.Create())
        {
            // Arrange
            System.IO.Fakes.ShimFile.ReadAllLinesString = (s) => new string[] { "Hello", "World", "Shims" };
    
            // Act
            var target = new HexFile("this_file_doesnt_exist.txt");
    
            Assert.AreEqual(3, target.Records.Length);
        }
    }
    

    顯示所有檔案的 [方案總管] 如此處所示

    顯示所有檔案之方案總管的螢幕擷取畫面。

  2. 開啟 [測試總管] 並執行測試。

請務必適當處置每個填充碼內容。 根據經驗,您需要呼叫 using 陳述式內的 ShimsContext.Create 以確保正確清除已註冊的填充碼。 例如,您可能會註冊測試方法的填充碼,將 DateTime.Now 方法取代成永遠都會傳回 2000 年 1 月 1 日的委派。 如果您忘記清除在測試方法中註冊的填充碼,則測試回合的其餘部分一定會傳回 2000 年 1 月 1 日做為 DateTime.Now 的值。 這可能會讓人感到意外和混淆。


填充碼類別的 .NET 命名慣例

填充碼類別名稱是在原始類型名稱前面加上 Fakes.Shim 而構成。 方法名稱後面要加上參數名稱。 (您不必將任何組件參考加入 System.Fakes)。

    System.IO.File.ReadAllLines(path);
    System.IO.Fakes.ShimFile.ReadAllLinesString = (path) => new string[] { "Hello", "World", "Shims" };

了解填充碼的運作方式

填充碼的運作方式是將「繞道 (detours)」插入至測試應用程式的程式碼基底中。 每當出現針對原始方法的呼叫時,Fakes 系統就會介入並重新導向該呼叫,從而執行您的自訂填充碼,而不是原始方法。

請特別注意,這些繞道會在執行階段動態建立和移除。 繞道應一律在 ShimsContext 的存留時間內建立。 系統處置 ShimsCoNtext 時,也會移除該期間內建立的所有使用中填充碼。 若要有效率地管理此作業,建議您將繞道的建立封裝在 using 陳述式中。


不同方法類型的填充碼

填充碼支援各種類型的方法。

靜態方法

填充靜態方法時,保存填充碼的屬性會存放在填充碼類型內。 這些屬性只有 setter,用來將委派附加至目標方法。 例如,如果我們有使用靜態方法 MyMethod 呼叫 MyClass 的類別:

//code under test
public static class MyClass {
    public static int MyMethod() {
        ...
    }
}

我們可以將填充碼附加至 MyMethod,使其一致傳回 5:

// unit test code
ShimMyClass.MyMethod = () => 5;

執行個體方法 (針對所有執行個體)

如同靜態方法,執行個體方法也可針對所有執行個體進行填充。 保留這些填充碼的屬性會放在名為 AllInstances 的巢狀型別中,以避免混淆。 如果我們有具有實例方法 MyMethod 的類別 MyClass

// code under test
public class MyClass {
    public int MyMethod() {
        ...
    }
}

我們可以將填充碼附加至 MyMethod,使其無論執行個體為何都一致傳回 5:

// unit test code
ShimMyClass.AllInstances.MyMethod = () => 5;

ShimMyClass 的產生類別結構會顯示如下:

// Fakes generated code
public class ShimMyClass : ShimBase<MyClass> {
    public static class AllInstances {
        public static Func<MyClass, int>MyMethod {
            set {
                ...
            }
        }
    }
}

在這種情況下,Fakes 會將執行階段執行個體做為委派的第一個引數傳遞。

執行個體方法 (單一執行階段執行個體)

執行個體方法也可以使用不同的委派來填充,視呼叫的接收者而定。 這可讓相同的執行個體方法針對每種執行個體類型呈現出不同的行為。 保留這些填充碼的屬性是此填充碼類型本身的執行個體方法。 每一個具現化填充碼類型都會連結到已填充類型的原始執行個體例。

例如,給定一個具有 MyMethod 執行個體方法的 MyClass 類別:

// code under test
public class MyClass {
    public int MyMethod() {
        ...
    }
}

我們可以為 MyMethod 建立兩個填充碼類型,讓第一個一致傳回 5,而第二個一致傳回 10:

// unit test code
var myClass1 = new ShimMyClass()
{
    MyMethod = () => 5
};
var myClass2 = new ShimMyClass { MyMethod = () => 10 };

ShimMyClass 的產生類別結構會顯示如下:

// Fakes generated code
public class ShimMyClass : ShimBase<MyClass> {
    public Func<int> MyMethod {
        set {
            ...
        }
    }
    public MyClass Instance {
        get {
            ...
        }
    }
}

實際已填充類型的執行個體可以透過執行個體屬性來存取:

// unit test code
var shim = new ShimMyClass();
var instance = shim.Instance;

填充碼類型也包含對已填充類型的隱含轉換,讓您直接使用填充碼類型:

// unit test code
var shim = new ShimMyClass();
MyClass instance = shim; // implicit cast retrieves the runtime instance

建構函式

建構函式的填充方式也不例外,同樣可透過填充將填充碼類型附加至未來建立的物件。 例如,每個建構函式都會表示為填充碼類型內名為 Constructor 的靜態方法。 假設有一個類別 MyClass,具有接受整數的建構函式:

public class MyClass {
    public MyClass(int value) {
        this.Value = value;
    }
    ...
}

您可以設定建構函式的填充碼類型,如此一來,無論傳遞至建構函式的值為何,每個未來的執行個體都會在叫用 Value getter 時傳回 -5:

// unit test code
ShimMyClass.ConstructorInt32 = (@this, value) => {
    var shim = new ShimMyClass(@this) {
        ValueGet = () => -5
    };
};

每個填充碼類型都會公開兩種類型的建構函式。 在需要新執行個體時,應使用預設建構函式,而採用已填充執行個體當做引數的建構函式只應該用於建構函式填充碼:

// unit test code
public ShimMyClass() { }
public ShimMyClass(MyClass instance) : base(instance) { }

ShimMyClass 的產生型別結構說明如下:

// Fakes generated code
public class ShimMyClass : ShimBase<MyClass>
{
    public static Action<MyClass, int> ConstructorInt32 {
        set {
            ...
        }
    }

    public ShimMyClass() { }
    public ShimMyClass(MyClass instance) : base(instance) { }
    ...
}

存取基底成員

您可以建立基底類型的填充碼,並將子執行個體放入基底填充碼類別的建構函式中,藉此取得基底成員的填充碼屬性。

例如,假設類別 MyBase 具有執行個體方法 MyMethod 和子類型 MyChild

public abstract class MyBase {
    public int MyMethod() {
        ...
    }
}

public class MyChild : MyBase {
}

MyBase 的填充碼可藉由起始新的 ShimMyBase 填充碼來設定:

// unit test code
var child = new ShimMyChild();
new ShimMyBase(child) { MyMethod = () => 5 };

請特別注意,將子填充碼類型當作參數傳遞至基底填充碼建構函式時,子填充碼類型將隱含轉換成子執行個體。

ShimMyChildShimMyBase 的產生類型結構類似於下列程式碼:

// Fakes generated code
public class ShimMyChild : ShimBase<MyChild> {
    public ShimMyChild() { }
    public ShimMyChild(Child child)
        : base(child) { }
}
public class ShimMyBase : ShimBase<MyBase> {
    public ShimMyBase(Base target) { }
    public Func<int> MyMethod
    { set { ... } }
}

靜態建構函式

填充碼類型會公開靜態方法 StaticConstructor,藉此填充類型的靜態建構函式。 因為靜態建構函式只能執行一次,您必須確保在存取該類型的任何成員之前,已設定填充碼。

完成項

完成項在 Fakes 中並不支援。

私用方法

針對在簽章中只有可見類型的私用方法 (亦即可見的參數類型與傳回類型),Fakes 程式碼產生器會建立這類私用方法的填充碼屬性。

繫結介面

當已填充的類型實作介面時,程式碼產生器會發出允許同時繫結該介面所有成員的方法。

例如,給定一個實作 IEnumerable<int>MyClass 類別:

public class MyClass : IEnumerable<int> {
    public IEnumerator<int> GetEnumerator() {
        ...
    }
    ...
}

您可以藉由呼叫 Bind 方法填充在 MyClass 中的 IEnumerable<int> 實作:

// unit test code
var shimMyClass = new ShimMyClass();
shimMyClass.Bind(new List<int> { 1, 2, 3 });

ShimMyClass 的產生類型結構類似下列程式碼:

// Fakes generated code
public class ShimMyClass : ShimBase<MyClass> {
    public ShimMyClass Bind(IEnumerable<int> target) {
        ...
    }
}

變更預設行為

每個產生的填充碼類別均會包含一個 IShimBehavior 介面的執行個體,可透過 ShimBase<T>.InstanceBehavior 屬性存取。 每當用戶端呼叫沒有明確填充的執行個體成員時,便會叫用此行為。

根據預設,如果未設定任何特定行為,將會使用靜態 ShimBehaviors.Current 屬性所傳回的執行個體,這通常會擲回 NotImplementedException 例外狀況。

您可以隨時修改此行為,只需調整任意填充碼執行個體的 InstanceBehavior 屬性即可。 例如,下列程式碼片段會將行為改變為不執行任何動作,或傳回傳回類型的預設值,亦即 default(T)

// unit test code
var shim = new ShimMyClass();
//return default(T) or do nothing
shim.InstanceBehavior = ShimBehaviors.DefaultValue;

您也可以藉由設定靜態 ShimBehaviors.Current 屬性,全域變更所有未明確定義 InstanceBehavior 屬性的填充執行個體行為:

// unit test code
// change default shim for all shim instances where the behavior has not been set
ShimBehaviors.Current = ShimBehaviors.DefaultValue;

識別與外部相依性的互動

為了協助識別程式碼何時與外部系統或相依性 (稱為 environment) 互動,您可以使用填充碼將特定行為指派給類型的所有成員。 這包括靜態方法。 藉由在填充碼類型的靜態 Behavior 屬性上設定 ShimBehaviors.NotImplemented 行為,任何對未明確填充類型成員的存取都會擲回 NotImplementedException。 這在測試期間是一項實用的訊號,表示您的程式碼嘗試存取外部系統或相依性。

以下是如何在單元測試程式碼中設定這項設定的範例:

// unit test code
// Assign the NotImplementedException behavior to ShimMyClass
ShimMyClass.Behavior = ShimBehaviors.NotImplemented;

為了方便起見,也有提供效果相同的速記方法:

// Shorthand to assign the NotImplementedException behavior to ShimMyClass
ShimMyClass.BehaveAsNotImplemented();

在填充碼方法期間叫用原始方法

在某些情況下,您可能需要在填充碼方法的執行期間執行原始方法。 例如,在驗證傳遞至方法的檔案名稱後,您可能會想要將文字寫入檔案系統。

處理這種情況的其中一種方法是使用委派和 ShimsContext.ExecuteWithoutShims() 來封裝對原始方法的呼叫,如下列程式碼所示:

// unit test code
ShimFile.WriteAllTextStringString = (fileName, content) => {
  ShimsContext.ExecuteWithoutShims(() => {

      Console.WriteLine("enter");
      File.WriteAllText(fileName, content);
      Console.WriteLine("leave");
  });
};

或者,您可以將填充碼設為空值、呼叫原始方法,然後還原填充碼。

// unit test code
ShimsDelegates.Action<string, string> shim = null;
shim = (fileName, content) => {
  try {
    Console.WriteLine("enter");
    // remove shim in order to call original method
    ShimFile.WriteAllTextStringString = null;
    File.WriteAllText(fileName, content);
  }
  finally
  {
    // restore shim
    ShimFile.WriteAllTextStringString = shim;
    Console.WriteLine("leave");
  }
};
// initialize the shim
ShimFile.WriteAllTextStringString = shim;

使用填充碼類型處理並行

填充碼類型會在 AppDomain 內的所有執行緒上運作,而且沒有執行緒親和性。 如果您打算使用支援並行的測試執行器,需要特別留意這個屬性。 值得注意的是,涉及填充碼類型的測試無法同時執行,不過 Fakes 執行階段不會強制執行這項限制。

填充 System.Environment

如果您想要填充 System.Environment 類別,必須對 mscorlib.fakes 檔案進行一些修改。 在 Assembly 元素後方新增下列內容:

<ShimGeneration>
    <Add FullName="System.Environment"/>
</ShimGeneration>

在您進行這些變更並重建解決方案後,System.Environment 類別中的方法和屬性便可供填充。 以下是如何將行為指派給 GetCommandLineArgsGet 方法的範例:

System.Fakes.ShimEnvironment.GetCommandLineArgsGet = ...

透過這些修改,您就能夠控制及測試程式碼如何與系統內容變數互動,這是綜合單元測試的重要工具。