Shim 型別 是 Microsoft 偽造範例架構中兩項技術的其中一個,讓您輕鬆地隔離環境測試中的元件。填充碼會轉換為特定方法的程式碼做為測試的一部分,您可以撰寫。許多方法傳回不同的結果取決於外部條件,不過, Shim 受測試的控制項,並且可能傳回一致的結果在每個呼叫。這可讓測試更容易撰寫。
使用填充碼隔離上不屬於您的方案一部分的組件的程式碼。若要找出您的彼此的方案的元件,我們建議您使用 Stub。
如需概觀和快速入門指南,請參閱 使用 Microsoft Fakes 在測試期間隔離程式碼
需求
- Visual Studio Ultimate
請參閱 視訊 (1h16):與 Fakes 測試的非可測試的程式碼在 Visual Studio 2012
本主題內容
您在這個主題將學習:
加入 Fakes 組件
使用 ShimsContext
撰寫含填充碼的測試
範例: Y2K Bug
請考慮在 2000 年 1 月 1 日擲回例外狀況的方法:
// code under test
public static class Y2KChecker {
public static void Check() {
if (DateTime.Now == new DateTime(2000, 1, 1))
throw new ApplicationException("y2kbug!");
}
}
若要測試這個方法會特別有問題,因為程式依賴 DateTime.Now,一個取決於電腦時鐘、環境相依且不具決定性的方法。此外, DateTime.Now 是靜態屬性,因此不能在這個位置使用 Stub 型別。這個問題是單元測試中隔離問題的根源: 直接呼叫資料庫應用程式開發介面並與 Web 服務進行通訊的程式,一直是相當困難的是單元測試,因為其邏輯取決於環境。
這是 Shim 型別應該使用的位置。Shim 型別提供一個機制,針對使用者定義的委派略涵蓋所有 .NET 方法。Shim 型別是由偽造產品產生器所產生的程式碼,並使用我們稱為 Shim 型別的委派指定新的方法實作。
下列測試顯示如何使用 Shim 型別, ShimDateTime,提供 DateTime.Now 的自訂實作:
//unit test code
// create a ShimsContext cleans up shims
using (ShimsContext.Create()
// hook delegate to the shim method to redirect DateTime.Now
// to return January 1st of 2000
ShimDateTime.NowGet = () => new DateTime(2000, 1, 1);
Y2KChecker.Check();
}
如何使用填充碼
加入 Fakes 組件
在方案總管中,展開單元測試專案的參考。
- 如果您在 Visual Basic 中工作,您必須先在方案總管工具列中的 [顯示所有檔案] ],才能看見參考目錄。
選取包含類別定義您要建立填充碼的組件。例如,在中,如果您要填滿 DateTime,選取 System.dll
在捷徑功能表上選擇 [Add Fakes Assembly]。
使用 ShimsContext
當使用 Shim 時輸入單元測試架構,您就必須封裝在 ShimsContext 的測試程式碼控制的存留期 (Lifetime) Shim。如果我們不需要這種情況,您的 Shim 仍會繼續,直到 AppDomain 關閉。如下列程式碼所示,最簡單的方法來建立 ShimsContext 是藉由使用靜態方法 Create() :
//unit test code
[Test]
public void Y2kCheckerTest() {
using(ShimsContext.Create()) {
...
} // clear all shims
}
適當地設定每個 Shim 內容是很重要的。根據經驗,請務必在陳述式 using 內的呼叫 ShimsContext.Create 確保已註冊的 Shim 會適當清除。例如,您可能會以一個永遠都會傳回 2000 年一月一日的委派來註冊一個取代測試方法 DateTime.Now 。如果您忘記清除在測試方法中註冊的 Shim,測試回合的其餘部分則一定會傳回2000年一月一日作為DateTime.Now 值。這可能會很驚訝和混淆。
撰寫含填充碼的測試
在您的測試程式碼,請將您要偽造的方法 繞道 。例如:
[TestClass]
public class TestClass1
{
[TestMethod]
public void TestCurrentYear()
{
int fixedYear = 2000;
using (ShimsContext.Create())
{
// Arrange:
// Detour DateTime.Now to return a fixed date:
System.Fakes.ShimDateTime.NowGet =
() =>
{ return new DateTime(fixedYear, 1, 1); };
// Instantiate the component under test:
var componentUnderTest = new MyComponent();
// Act:
int year = componentUnderTest.GetTheCurrentYear();
// Assert:
// This will always be true if the component is working:
Assert.AreEqual(fixedYear, year);
}
}
}
<TestClass()> _
Public Class TestClass1
<TestMethod()> _
Public Sub TestCurrentYear()
Using s = Microsoft.QualityTools.Testing.Fakes.ShimsContext.Create()
Dim fixedYear As Integer = 2000
' Arrange:
' Detour DateTime.Now to return a fixed date:
System.Fakes.ShimDateTime.NowGet = _
Function() As DateTime
Return New DateTime(fixedYear, 1, 1)
End Function
' Instantiate the component under test:
Dim componentUnderTest = New MyComponent()
' Act:
Dim year As Integer = componentUnderTest.GetTheCurrentYear
' Assert:
' This will always be true if the component is working:
Assert.AreEqual(fixedYear, year)
End Using
End Sub
End Class
填充碼類別名稱是藉由在原始類型名稱前面加上 Fakes.Shim 而構成。
Shim 運作中插入 繞道旁邊 輸入應用程式的受測試程式碼。對原始方法的呼叫失敗, Fakes 系統執行繞道,因此,而不是呼叫虛擬方法,您的 Shim 程式碼呼叫。
請注意繞道建立和刪除在執行階段。您必須永遠會在 ShimsContext的有效期間內繞道。當它被配置,您建立的任何填充碼,以便使用中移除時。最好的作法是在 using 陳述式內。
您可能會看到建置錯誤,指出 Fakes 命名空間不存在。這個錯誤有時會在有其他編譯錯誤。更正其他錯誤,而且會消失。
不同方法類型的填充碼
Shim 型別可讓您在您的委派取代所有 .NET 方法,包括靜態方法或非虛擬方法。
靜態方法
附加 Shim設定為靜態方法的屬性在 Shim 型別中。每個屬性都只有一個 Setter可以附加委派至目標方法。例如,含MyMethod靜態方法的 MyClass類別 :
//code under test
public static class MyClass {
public static int MyMethod() {
...
}
}
我們可以附加一定會傳回 5 的 Shim 至 MyMethod :
// unit test code
ShimMyClass.MyMethod = () =>5;
執行個體方法 (所有執行個體)
相同於靜態方法,執行個體方法可填入於所有執行個體。這些附加 Shim 的屬性放置在名為 AllInstances 的巢狀型別以避免混淆。例如,給一個含 MyMethod執行個體方法的 MyClass類別 :
// code under test
public class MyClass {
public int MyMethod() {
...
}
}
不論這個執行個體,可以附加 Shim至一定會傳回 5 的 MyMethod ,:
// 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 {
...
}
}
}
}
請注意在這種情況下,偽造範例會透過執行階段執行個體做為委派的第一個引數。
執行個體方法 (執行階段執行個體)
根據呼叫的接收,執行個體方法可以由不同的委派來填滿。這可讓相同的執行個體方法具有不同行為的每個型別的執行個體。設定這些 Shim 的屬性是 Shim 型別的執行個體方法。每一個具現化的 Shim 型別也與已填滿的型別之未經處理的執行個體有關聯。
例如,給一個含 MyMethod執行個體方法的 MyClass類別 :
// code under test
public class MyClass {
public int MyMethod() {
...
}
}
我們可以設定 MyMethod 的兩種 Shim 型別,第一個一定會傳回 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;
Shim 型別也會對已填入型別做隱含轉換,因此,您通常可以直接使用 Shim 類型:
// unit test code
var shim = new ShimMyClass();
MyClass instance = shim; // implicit cast retrieves the runtime
// instance
建構函式
建構函式也可以用來附加 Shim 型別到未來的物件。每個建構函式會公開為 Shim 型別的靜態方法建構函式。例如,給一個接受整數之建構函式的 MyClass類別 :
// code under test
public class MyClass {
public MyClass(int value) {
this.Value = value;
}
...
}
我們設定建構函式的 Shim 型別後,以後不論在建構函式中的值為多少,當值 getter 叫用時,每個後續執行個體傳回-5:
// unit test code
ShimMyClass.ConstructorInt32 = (@this, value) => {
var shim = new ShimMyClass(@this) {
ValueGet = () => -5
};
};
請注意每個 Shim 型別會公開至兩個建構函式。在新需要執行個體時,應使用預設建構函式;反之,填入執行個體當做引數的建構函式,只能用來建構函式 Shim:
// 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) { }
...
}
基底成員
基底成員 Shim 屬性可以藉由建立基底型別的一個 Shim 和傳遞子執行個體當做參數至基底 Shim 類別的建構函式來進行存取。
例如,給一個執行個體方法 MyMethod 和子型別 MyChild的 MyBase 類別:
public abstract class MyBase {
public int MyMethod() {
...
}
}
public class MyChild : MyBase {
}
我們可以透過建立新 ShimMyBase Shim 設定 MyBase Shim:
// unit test code
var child = new ShimMyChild();
new ShimMyBase(child) { MyMethod = () => 5 };
請注意,當做參數傳遞至基底 Shim 建構函式時,子 Shim 型別會隱含轉換成子執行個體。
ShimMyChild 和 ShimMyBase 產生的型別結構類似下列程式碼:
// 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。
完成項
完成項在偽造範例並不支援。
私用方法
偽造範例程式碼產生器會建立只有可以看到這個有可見項目的簽章、 ie 、參數型別和傳回型別的私用方法的 Shim 屬性。
繫結介面
當已填滿的型別實作介面時,程式碼產生器會允許同時繫結該介面所有成員的方法。
例如,給一個類別 MyClass 實作 IEnumerable<int>:
public class MyClass : IEnumerable<int> {
public IEnumerator<int> GetEnumerator() {
...
}
...
}
我們可以藉由呼叫方法來繫結在 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) {
...
}
}
變更預設行為。
每個產生的 Shim 型別傳遞 ShimBase<T>.InstanceBehavior 屬性保存 IShimBehavior 介面的執行個體。每當用戶端呼叫沒有明確地填入執行個體成員時,此使用行為會發生。
如果行為未明確設定,則會使用靜態 ShimsBehaviors.Current 屬性所傳回的執行個體。根據預設,這個屬性會傳回 NotImplementedException 擲回例外狀況的行為。
這個行為可以在任何 Shim 執行個體的屬性 InstanceBehavior 隨時變更。例如,下列程式碼片段變更 Shim行為成,不做任何動作也不會傳回行為類型,即為使用預設的預設值 (T):
// unit test code
var shim = new ShimMyClass();
//return default(T) or do nothing
shim.InstanceBehavior = ShimsBehaviors.DefaultValue;
行為可以透過明確設定的所有已填滿的全域執行個體進行變更 InstanceBehavior 屬性不是 ShimsBehaviors.Current 屬性:
// unit test code
// change default shim for all shim instances
// where the behavior has not been set
ShimsBehaviors.Current =
ShimsBehaviors.DefaultValue;
偵測到環境中存取
附加行為對於所有成員都是可能的,包括靜態方法,透過指派 ShimsBehaviors.NotImplemented 特定型別至行為對應的 Shim 型別的靜態屬性 Behavior :
// unit test code
// assigning the not implemented behavior
ShimMyClass.Behavior = ShimsBehaviors.NotImplemented;
// shorthand
ShimMyClass.BehaveAsNotImplemented();
並行
Shim 型別適用於在 AppDomain 的所有執行緒,且並沒有執行緒相似性。這是很重要的情況,如果您計劃使用支援並存的測試執行器:包含 shim 類型的測試無法同時執行。這個屬性不是由執行階段偽造範例強制設定。
從 Shim 方法呼叫原始方法
假設我們驗證完傳遞給方法的檔案名稱,並且在檔案系統實際要寫入文字。在該情況下,我們會在 Shim 方法中呼叫原始方法。
解決這個問題的第一個方法是使用委派和 ShimsContext.ExecuteWithoutShims() 在下列程式碼包裝原始方法的方法呼叫:
// unit test code
ShimFile.WriteAllTextStringString = (fileName, content) => {
ShimsContext.ExecuteWithoutShims(() => {
Console.WriteLine("enter");
File.WriteAllText(fileName, content);
Console.WriteLine("leave");
});
};
另一種方式是設定 Shim null,呼叫原來的方法和還原 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 無法使用以 .NET 基底類別庫 mscorlib 和 系統的所有型別。
外部資源
指引
使用 Visual Studio 2012 測試持續傳遞 – 第 2 章:單元測試:測試內部