共用方式為


使用填充碼將應用程式與其他組件隔離,方便進行單元測試

Shim 型別 是 Microsoft 偽造範例架構中兩項技術的其中一個,讓您輕鬆地隔離環境測試中的元件。填充碼會轉換為特定方法的程式碼做為測試的一部分,您可以撰寫。許多方法傳回不同的結果取決於外部條件,不過, Shim 受測試的控制項,並且可能傳回一致的結果在每個呼叫。這可讓測試更容易撰寫。

使用填充碼隔離上不屬於您的方案一部分的組件的程式碼。若要找出您的彼此的方案的元件,我們建議您使用 Stub。

如需概觀和快速入門指南,請參閱 使用 Microsoft Fakes 在測試期間隔離程式碼

需求

  • Visual Studio Ultimate

請參閱 視訊 (1h16):與 Fakes 測試的非可測試的程式碼在 Visual Studio 2012

本主題內容

您在這個主題將學習:

範例: Y2K Bug

如何使用填充碼

  • 加入 Fakes 組件

  • 使用 ShimsContext

  • 撰寫含填充碼的測試

不同方法類型的填充碼

變更預設行為。

偵測到環境中存取

並行

從 Shim 方法呼叫原始方法

使用限制

範例: 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 組件

  1. 在方案總管中,展開單元測試專案的參考

    • 如果您在 Visual Basic 中工作,您必須先在方案總管工具列中的 [顯示所有檔案] ],才能看見參考目錄。
  2. 選取包含類別定義您要建立填充碼的組件。例如,在中,如果您要填滿 DateTime,選取 System.dll

  3. 在捷徑功能表上選擇 [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 章:單元測試:測試內部

請參閱

概念

使用 Microsoft Fakes 在測試期間隔離程式碼

其他資源

Peter 大學 level 1.1.4 長的部落格:Visual Studio 2012 Shim

視訊 (1h16):與 Fakes 測試的非可測試的程式碼在 Visual Studio 2012