使用填充碼將應用程式與其他組件隔離,方便進行單元測試
Shim 型別 是 Microsoft Fakes Framework 使用可讓您輕鬆地隔離元件在環境上執行測試下這兩項技術的其中一個。 填充碼會轉換為特定方法的程式碼做為測試的一部分,您可以撰寫。 許多方法傳回不同的結果取決於外部條件,不過, 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 型別。 這個問題是單元測試中隔離問題的根源: 直接呼叫資料庫 API 並與 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();
}
如何使用 Shim
將 Fakes 組件。
在方案總管中,展開您的單元測試專案的 [參考]。
- 如果您在 Visual Basic 中工作,您必須先在方案總管工具列中的 [顯示所有檔案] ],才能看見參考目錄。
選取包含類別定義您要建立填充碼的組件。 例如,在中,如果您要填滿 DateTime,選取 System.dll
在捷徑功能表上,選擇 [將 Fakes 組件。]。
使用 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
Shim 類別名稱傳遞給對原始型別名稱的 Fakes.Shim 組成。
Shim 運作中插入 繞道旁邊 輸入應用程式的受測試程式碼。 對原始方法的呼叫失敗, Fakes 系統執行繞道,因此,而不是呼叫虛擬方法,您的 Shim 程式碼呼叫。
請注意繞道建立和刪除在執行階段。 您必須永遠會在 ShimsContext的有效期間內繞道。 當它被配置,您建立的任何填充碼,以便使用中移除時。 最好的作法是在 using 陳述式內。
您可能會看到建置錯誤,指出 Fakes 命名空間不存在。 這個錯誤有時會在有其他編譯錯誤。 更正其他錯誤,而且會消失。
不同類型的 Shim 方法
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() {
...
}
}
不論執行個體,您可以將永遠傳回 5 的 Shim 到 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 2012RC–第 2 章:單元測試:內部測試