Shim 類型,作為 Microsoft Fakes Framework 使用的兩大核心技術之一,在測試期間對應用程式元件進行隔離,是非常重要的。 它們的運作方式是攔截並轉移呼叫至特定方法,然後您可以直接在測試中導向自定義程序代碼。 這項功能可讓您管理這些方法的結果,確保無論外部條件為何,在每次呼叫期間的結果都一致且可預測。 這種控制層級可簡化測試程式,並協助達成更可靠且精確的結果。
當您需要在程式代碼與不屬於您解決方案的一部分的組件之間建立界限時,請使用 阻隔。 當目標是將解決方案的元件彼此隔離時,建議使用 存根 。
(如需存根的更詳細描述,請參閱 使用存根來隔離應用程式的部分,以進行單元測試。
垫片限制
請務必注意墊片有其局限性。
偽碼不能用於 .NET 基類中特定庫的所有類型,特別是 .NET Framework 中的 mscorlib 和 System,以及 .NET Core 或 .NET 5+ 中的 System.Runtime。 此條件約束應在測試規劃和設計階段期間納入考慮,以確保成功且有效的測試策略。
建立介面填充層:詳細說明
假設您的元件包含對System.IO.File.ReadAllLines的呼叫:
// Code under test:
this.Records = System.IO.File.ReadAllLines(path);
建立類別庫
開啟 Visual Studio 並建立
Class Library專案
設定項目名稱
HexFileReader設定專案名稱
ShimsTutorial。將項目的目標 Framework 設定為 .NET Framework 4.8
刪除預設檔案
Class1.cs新增檔案
HexFile.cs並新增下列類別定義:
建立測試專案
以滑鼠右鍵點擊解決方案並新增專案
MSTest Test Project設定項目名稱
TestProject將項目的目標 Framework 設定為 .NET Framework 4.8
新增Fakes組件
將項目參考新增至
HexFileReader
新增 Fakes 組件
在 [方案總管] 中,
若為舊版 .NET Framework 專案 (非 SDK 樣式),請展開單元測試專案的 [參考 ] 節點。
針對以 .NET Framework、.NET Core 或 .NET 5+ 為目標的 SDK 樣式專案,展開 [相依性] 節點,以在 [元件]、[專案] 或 [套件] 下尋找您想要偽造的元件。
如果您正在 Visual Basic 中工作,請選取 [方案總管] 工具列中的 [顯示所有檔案],以查看 [參考] 節點。
選擇包含
System.IO.File.ReadAllLines定義的System元件。在快捷方式功能表上,選取 [新增Fakes組件]。
建置結果會產生某些警告和錯誤,因為並非所有類型都可以使用 shims,因此您必須修改 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>
建立單元測試
修改預設檔案
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); } }以下是方案總管視窗,顯示所有檔案
開啟 [測試總管] 並執行測試。
正確處置每個檔板上下文非常重要。 根據經驗法則,在 using 敘述內呼叫 ShimsContext.Create ,以確保正確清除已註冊的填充層。 例如,您可以為測試方法註冊一個填充程序,把DateTime.Now方法改為一個預設返回2000年1月1日的委派函式。 如果您忘記在測試方法中清除已註冊的填充碼,測試執行的其餘部分總是會傳回 2000 年 1 月 1 日作為DateTime.Now的值。 這可能令人吃驚和困惑。
Shim類別的命名慣例
Shim 類別名稱是透過在原始類型名稱前加上 Fakes.Shim 來組成的。 參數名稱會附加至方法名稱。 (您不需要將任何組件參考新增至 System.Fakes。)
System.IO.File.ReadAllLines(path);
System.IO.Fakes.ShimFile.ReadAllLinesString = (path) => new string[] { "Hello", "World", "Shims" };
瞭解墊片的運作方式
填充碼的運作方式是將 繞道 引入要測試之應用程式的程式代碼基底。 每當有原始方法的呼叫時,Fakes 系統會介入以重新導向該呼叫,讓您的自定義 Shim 代碼執行,而不是原始方法。
請務必注意,這些繞道會在運行時間動態建立和移除。 應在 ShimsContext 的生命週期內建立繞道。 釋放 ShimsContext 時,會同時移除在其中建立的所有活躍的填充碼。 若要有效率地管理此作業,建議您在 using 陳述句中封裝建立轉向。
用於各種方法的墊片
Shim 支援各種類型的方法。
靜態方法
填充靜態方法時,保留填充碼的屬性會存放在填充碼類型內。 這些屬性僅僅具有一個 setter,用來將委派附加至目標方法。 例如,如果我們有一個名為MyClass的類別,其中有一個靜態方法MyMethod:
//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 會將運行時間實例當做委派的第一個自變數傳遞。
實體方法 (單一的執行階段實例)
實例方法也可以使用不同的委派來填充,視呼叫的接收者而定。 這可讓相同的實例方法顯示每個型別實例的不同行為。 保留這些 shim 的屬性是 shim 類別本身的實例方法。 每個具現化的襯板類型都會連結到一個襯板類型的原始未經處理的實例。
例如,給定具有實例方法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 {
...
}
}
}
實際的填充類型實例可以透過 Instance 屬性來存取:
// 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;
}
...
}
您可以設定建構函式的 shim 類型,如此一來,不論傳遞至建構函式的值為何,任何未來的實例都會在當呼叫 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 {
}
可以藉由啟動新的 ShimMyBase 接合層來設定 MyBase 的接合層。
// 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 { ... } }
}
靜態建構函式
Shim 類型會公開靜態方法 StaticConstructor 來填充類型的靜態建構函式。 由於靜態建構函式只會執行一次,因此在存取類型的任何成員之前,您必須確定已設定 shim。
終結器
Fakes 不支援終結器。
私人方法
Fakes 程式碼產生器會為簽章中只有可見類型的私用方法建立 shim 屬性,也就是可見的參數類型和傳回型別。
系結介面
當填充類型實作介面時,程式代碼產生器會發出方法,讓它一次系結來自該介面的所有成員。
例如,假設有一個類別MyClass實作IEnumerable<int>:
public class MyClass : IEnumerable<int> {
public IEnumerator<int> GetEnumerator() {
...
}
...
}
您可以透過呼叫 Bind 方法,將 MyClass 中的 IEnumerable<int> 實作進行 shim 處理:
// 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) {
...
}
}
變更預設行為
每個產生的 shim 類型都包含 IShimBehavior 介面的實例,可透過 ShimBase<T>.InstanceBehavior 屬性來存取。 每當用戶端呼叫尚未明確填充的實例成員時,就會叫用此行為。
根據預設,如果未設定任何特定行為,它會使用靜態 ShimBehaviors.Current 屬性所傳回的實例,這通常會擲回 NotImplementedException 例外狀況。
您可以調整任何 "shim" 實例的 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),您可以使用填充碼將特定行為指派給類型的所有成員。 這包括靜態方法。 藉由在 shim 類型的靜態Behavior屬性上設定ShimBehaviors.NotImplemented行為,任何對尚未明確設定 shim 的該類型成員的存取都可能會擲回 NotImplementedException。 這在測試期間可作為有用的訊號,指出您的程式代碼嘗試存取外部系統或相依性。
以下是如何在單元測試程式代碼中設定這項設定的範例:
// unit test code
// Assign the NotImplementedException behavior to ShimMyClass
ShimMyClass.Behavior = ShimBehaviors.NotImplemented;
為了方便起見,也會提供速記方法以達到相同的效果:
// Shorthand to assign the NotImplementedException behavior to ShimMyClass
ShimMyClass.BehaveAsNotImplemented();
在介面層方法中調用原始方法
在某些情況下,您可能需要在執行Shim方法時執行原始方法。 例如,在驗證傳遞至 方法的檔名之後,您可能會想要將文字寫入文件系統。
處理這種情況的其中一種方法是使用委託和 ShimsContext.ExecuteWithoutShims()封裝對原始方法的呼叫,如下列程式碼所示。
// unit test code
ShimFile.WriteAllTextStringString = (fileName, content) => {
ShimsContext.ExecuteWithoutShims(() => {
Console.WriteLine("enter");
File.WriteAllText(fileName, content);
Console.WriteLine("leave");
});
};
或者,您可以將 shim 置為無效,呼叫原始方法,然後恢復 shim。
// 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;
使用 Shim 類型處理並行性
Shim 類型會在 AppDomain 內的所有執行緒上運作,並且沒有執行緒親和性。 如果您打算使用支援並行的測試執行器,這個屬性非常重要。 值得注意的是,涉及填充碼類型的測試無法同時進行,不過 Fakes 執行環境不會強制施加這項限制。
調適 System.Environment
如果您想要填補 System.Environment 類別,您必須對 mscorlib.fakes 檔案進行一些修改。 在 Assembly 元素之後,新增下列內容:
<ShimGeneration>
<Add FullName="System.Environment"/>
</ShimGeneration>
一旦您進行這些變更並重建解決方案,類別中的 System.Environment 方法和屬性現在即可被插入。 以下是如何將行為指派給 方法的 GetCommandLineArgsGet 範例:
System.Fakes.ShimEnvironment.GetCommandLineArgsGet = ...
藉由進行這些修改,您可以開啟控制及測試程序代碼如何與系統環境變數互動的可能性,這是綜合單元測試的重要工具。