Microsoft Fakes 入門

最近看了點單元測試的東西,發覺 MSDN 網站上的這篇文章蠻容易入手:Isolating Code under Test with Microsoft Fakes,便做了點整理。這篇筆記裡面的範例也有一些取自該文,只是寫的比較囉嗦一些。

簡介

Microsoft Fakes 是一套用來協助單元測試的框架,其目的在於隔離受測的部分程式碼,以便在某個單元測試失敗時,你能夠確定問題出在測試標的本身,而不是其他地方(就不用疑神疑鬼:到底是哪個類別出錯了)。

你可以利用 Fakes 來產生特定物件或方法的「替身」來把一些與測試目的無關的程式碼替換掉。Fakes 的替身有兩種:
  • stub - 用來替換某些實作了相同介面的類別實作。適用場合:你的受測元件必須依賴介面(或抽象類別)、而非依賴特定具象類別。也就是說,前提是要遵循「針對介面來寫程式」的原則。嚴格來說,stub 並不等於 mock,因為它不提供行為驗證的檢查。有關 stub、shim、mock 等名詞的意義與用途,可參考 91 的文章<Unit Test - Stub, Mock, Fake簡介>。
  • shim - 能夠將已經編譯好的 IL code 替換成你提供的程式碼。適用場合:欲隔離的類別並未實作特定介面,或者根本沒有原始碼,而只有編譯過的組件(例如 .NET Framework 組件)。

如果沒有實際用過,恐怕不容易體會 Fakes 的用途,以及 stub 和 shim 的差別。底下分別就 stub 和 shim 提供實作練習的範例。

Note:這裡我用「替身」一詞來泛指 Microsoft Fakes 產生的假物件,係為了方便說明,也許不是那麼精確。

工具:Visual Studio 2012 Ultimate。

Stub 實作練習

步驟如下:
  1. 建立一個空的 Solution,取名 FakesDemo。
  2. 在此 solution 中加入新的 Class Library 專案,取名 MyLib。
  3. 在 MyLib 中加入一個 Interface,取名 IFruit。
  4. 在 MyLib 中加入一個 Class,取名 FruitStore。

設想的情境是這樣:FruitStore 類別要提供一個 GetPrice 方法,此方法須傳入一個 IFruit 型別的參數,並傳回該水果的價格。

IFruit 介面的定義如下:

public interface IFruit
{
    int GetPrice();
}

然後是 FruitStore 類別:

public class FruitStore
{
    public int GetPrice(IFruit aFruit)
    {
        return aFruit.GetPrice();
    }
}

還缺什麼呢?實作 IFruit 介面的具象類別。所以,接著再加入一個新的類別:Apple。程式碼如下:

public class Apple : IFruit
{
    public int GetPrice()
    {
        throw new NotImplementedException();
    }
}

注意這時候我們還沒有提供 Apple.GetPrice() 方法的實作,目前只是簡單丟出一個 exception。因為我們打算再實作各類水果類別之前,先測試水果商店,也就是 FruitStore 類別的邏輯。

所以,接著在 FakesDemo solution 中加入一個新的 Unit Test 專案,取名為 MyLibTest。然後加入專案組件參考:MyLib(因為我們要對這個組件寫單元測試)。

在測試專案中加入 MyLib 組件參考之後,接著加入一個 Unit Test 類別:FruitStoreTest。然後為此類別加入一個測試方法,程式碼如下:

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MyLib;

namespace MyLibTest
{
    [TestClass]
    public class FruitStoreTest
    {
        [TestMethod]
        public void TestGetPrice()
        {
            IFruit apple = new Apple();
            int actualValue = new FruitStore().GetPrice(apple);
            Assert.AreEqual(10, actualValue);
        }
    }
}

TIP: 如果你曾用過 Visual Studio 2010 來撰寫單元測試,也許會發現原本在受測類別中點右鍵就可以產生對應的單元測試,可是在 Visual Studio 2012 中卻找不到這項功能了。是的,由於這項功能跟 MS-Test 太過緊密耦合,而且非常依賴私有存取子(private accessor)來產生單元測試代碼,所以被拿掉了

執行測試看看(主選單 \ TEST \ Run \ All Tests)。此時應該會看到測試結果是失敗的:


這是因為 Apple 類別的 GetPrice() 方法尚未提供實作,僅單純拋出例外的緣故。但我們目前還不想去處理 Apple 類別,只想先測試 FruitStore 而已。這個時候,Fakes 就派上用場了。

產生替身

現在我們要對 MyLib 組件產生一些替身程式碼,做法是在 Solution Explorer 中展開 MyLibTest 專案的 Reference 節點,然後在 MyLib 項目上點右鍵,選 Add Fakes Assembly。參考下圖:


接著你會看到這個專案裡面多了一些新東西:



這些就是我們目前所需要的了。接著修改剛才的測試程式碼,改成這樣:

        [TestMethod]
        public void TestGetPriceFaked()
        {
            IFruit fakedApple = new MyLib.Fakes.StubIFruit()
            {
                GetPrice = () => { return 10; }
            };

            int actualValue = new FruitStore().GetPrice(fakedApple);
            Assert.AreEqual(10, actualValue);
        }

注意這裡的變化:

  • 原先使用 Apple 類別的 instance,現在改成使用 Fakes.StubIFruit。這個 StubIFruit 就是實作了 IFruit 介面的替身類別。Mcirosoft Fakes 預設的命名方式會在介面名稱前面冠上 "Stub"。
  • 建立替身物件的同時,也設定了委派 GetPrice,令它指向我們提供的一個匿名方法,這個匿名方法只是單純傳回 10(程式碼的第 6 行)。
  • 呼叫 FruitStore 物件的 GetPrice() 方法時,傳入我們剛才建立的假蘋果。

TIP: 如果是第一次在單元測試中使用替身物件,不妨以單步追蹤的方式把測試程式碼逐行跑一遍,對於了解其中的來龍去脈應該會有些幫助。

再跑一次測試,這次就能通過了,因為我們已經用假蘋果取代真蘋果。換言之,受測標的 FruitStore 已經和 Apple 類別隔離開了。

Shim 實作練習

如前面提過的,shim 適合用於欲隔離之類別沒有實作特定介面,或者根本沒有原始碼的情況。MSDN 那篇文章是以 .NET BCL 中的 DateTime 來當作範例。簡單起見,我就依樣畫葫蘆,並做點補充。

跟剛才的 stub 實作練習一樣,先看看沒有使用替身物件的情況。

沿用先前的實作範例,在 MyLib 專案中加入一個類別:MyDateTime。程式碼很簡單:

public class MyDateTime
{
    public int GetCurrentMonth()
    {
        return DateTime.Now.Month;
    }
}

GetCurrentMonth 方法會傳回目前的月份。

然後加入測試程式碼:在 MyLibTest 專案中加入一個 Unit Test 類別:MyDateTimeTest。程式碼如下:

[TestClass]
public class MyDateTimeTest
{

    [TestMethod]
    public void TestCurrentMonth()
    {
        MyDateTime dt = new MyDateTime();
        int expected = 10;
        Assert.AreEqual(expected, dt.GetCurrentMonth());
    }
}

OK! 我是在十月份的時候寫這個單元測試,所以預期 GetCurrentMonth 方法一定會傳回 10。可是,等到十一月份以後再來跑這個單元測試又不會過了,還得修改這段測試程式碼,這未免太麻煩了。可是,我們沒有 DateTime 的原始碼,而且它也沒有實作特定介面,無法套用剛才的 stub 伎倆。這時候就可以試試 shim 了。

DateTime 類別隸屬 System.dll 組件,所以現在我們要產生 System.dll 的替身:在 MyLibTest 專案的 References 節點中對 System 組件點右鍵,選 Add Fakes Assembly。

接著修改剛才的測試方法,如下所示:

[TestMethod]
public void TestCurrentMonth()
{
    using (ShimsContext.Create())
    {
        System.Fakes.ShimDateTime.NowGet = () =>
        {
            return new DateTime(2012, 10, 1);
        };

        MyDateTime dt = new MyDateTime();
        int expected = 10;
        Assert.AreEqual(expected, dt.GetCurrentMonth());
    }
}

幾個值得注意的地方:
  • 預設的 shim 類別命名規則,是在目標類別名稱前面加上 "Shim",所以這裡會是 ShimDateTime。原本的 DateTime.Now 是個靜態的唯讀屬性(只有 getter 沒有 setter),Fakes 在產生其對應的委派時,會在名稱後面加上 "Get",所以就成了 NowGet。
  • 我們將 NowGet 委派指向一個內嵌的匿名方法,此匿名方法會固定傳回 2012 年 10 月 1 日,而非當下的日期。
  • 在 ShimsContext 物件的勢力範圍內的 DateTime 物件都會被 ShimDateTime 這個替身所取代。如此一來,當受測類別 MyDateTime 有用到 DateTime.Now 時,就會轉而執行我們的替身物件的 NowGet 委派。(試試看把後面三行程式碼搬到 using 區塊外層,看看結果有何不同。)

試試單步追蹤剛才的測試程式碼,並且追到 MyDateTime.GetCurrentMonth 方法裡面去,看看當程式執行到 DateTime.Now() 的時候,接下來會跑到哪裡。此時你應該會對 Fakes 的運作原理更有感覺,也更清楚「替身」、「攔截」等詞彙在這裡的含意。

最後再附上一個 shim 的單元測試範例,主要是練習如何替換建構子和改寫的 ToString 方法。

這是欲替換掉的類別:

public class Foo
{
    private string _name;

    public Foo(string name)
    {
        _name = name;
    }

    public override string ToString()
    {
        return base.ToString();
    }
}

這是單元測試:

[TestClass]
public class FooTest
{
    [TestMethod]
    public void TestFoo()
    {
        using (ShimsContext.Create())
        {
            MyLib.Fakes.ShimFoo.ConstructorString = delegate(Foo f, string s)
            {
                var shimFoo = new MyLib.Fakes.ShimFoo(f);
                shimFoo.ToString = () => { return "Faked " + s; };
            };
            
            Foo foo = new Foo("Michael");
            string actual = foo.ToString();
            Assert.AreEqual("Faked Michael", actual);
        }
    }
}

簡單解釋一下:
  • 此範例要假造的類別名稱是 Foo,所以 Fakes 產生的 shim 類別叫做 ShimFoo。
  • Foo 類別的建構子需要傳入一個字串參數,所以 Fakes 會產生一個用來替換該建構子的委派,名叫 ConstructorString。如果建構子需要傳入兩個 string 參數,此替身委派的名稱就會變成 ConstructorStringString。
  • 在建構子委派方法中有建立一個 ShimFoo 的物件實體,然後把該物件的 ToString 委派指向我們的匿名方法。

不知不覺越寫越多了....以入門練習來說,應該差不多夠了。先這樣吧!

延伸閱讀

Post Comments

技術提供:Blogger.