Skip to content

Unit Test 中使用假(Fake)物件隔離程式碼

Posted on:December 2, 2023 at 

需要 Visual Studio Enterprise

image.png

介紹

程式碼隔離(isolating code)是一種測試策略,透過使用假物件(例如 Microsoft Fakes),將受測程式碼與應用程式的其餘部分隔離開來。這樣可以專注於測試特定功能,並在測試失敗時迅速找到問題根源。此方法允許測試在其他應用程式部分尚未就緒時進行,提高了測試的獨立性

填充碼的運作方式是將「繞道 (detours)」插入至測試應用程式的程式碼基底中。 每當出現針對原始方法的呼叫時,Fakes 系統就會介入並重新導向該呼叫,從而執行您的自訂填充碼,而不是原始方法。 --- Microsoft Docs

image.png

下載 NuGet Package

使用時機

使用說明

產生 .fakes 檔案

<測試專案名稱>
    |_ 相依性
        |_ 專案
            |_ <受測專案名稱>
<測試專案名稱>
    |_ 相依性
        |_ 架構
            |_ Microsoft.NETCore.App
                |_ <要隔離物件的組件>

對著受測專案或要隔離的組件點右鍵

image.png

image.png

接著點「新增假功能組件」,然後會自動幫你生成一個 Fakes 的資料夾裡面包含受測專案的 .fakes 檔案

<測試專案名稱>
    |_ Fakes
        |_ <受測專案或組件名稱>.fakes

最後記得在專案檔(.csproj)裡面加上參考,要加上的 .dll 檔可在 FakesAssemblies 資料夾中找到其檔案名稱

<ItemGroup>
    <Reference Include="FakesAssemblies\<假功能組件名稱>.dll"/>
</ItemGroup>
<測試專案名稱>
    |_ FakesAssemblies
        |_ <假功能組件名稱>.dll

範例程式

ExampleHelper.cs

public class ExampleHelper
{
    public static string GetExampleName()
    {
        return "StaticExample";
    }
}

這裡建立一個 Static 方法來做模擬測試

FakeService.cs

public ExampleModel GetExampleModel()
{
    ExampleModel exampleModel = new ExampleModel()
    {
        Id = Guid.NewGuid(),
        Name = ExampleHelper.GetExampleName(), // Static 方法
        Description = DateTime.Now.ToString()  // System.Runtime.dll
    };

    return exampleModel;
}

在這個要被測試的方法中使用了 Static 方法與 .NET Framework 本身的物件

FakeServiceTests.cs

[Fact]
public void GetExampleModel_ReturnExampleModel()
{
    using (ShimsContext.Create())
    {
        // Arrange
        IFakeService fakeService = CreateDefaultFakeService();

        UnitTest.Example.Project.Helper.Fakes.ShimExampleHelper.GetExampleName = () =>
        {
            return "Example";
        };

        System.Fakes.ShimDateTime.NowGet = () =>
        {
            return new DateTime(1000, 1, 1);
        };

        // Act
        ExampleModel result = fakeService.GetExampleModel();

        // Assert
        Assert.NotNull(result);
        Assert.NotNull(result.Name);
        Assert.NotNull(result.Description);
        Assert.Equal("Example", result.Name);
        Assert.Equal("1000/1/1 上午 12:00:00", result.Description);
    }
}

最後撰寫單元測試時,記得使用 ShimsContext.Create() 並配合 using 來建立 Fakes 的方法,系統處置 ShimsContext 時,也會移除該期間內建立的所有使用中 Fakes 方法

使用結果

image.png

這樣就成功了,如此一來,便可以使外部相依的組件或自訂的 Static 方法也達到隔離的效果,增加測試的可靠度

執行測試

若是直接在 Visual Studio Enterprise 使用測試總管(Test Explorer)執行測試基本上沒什麼問題,但是若使用 CLI 執行測試則考慮以下情況

Microsoft.QualityTools.Testing.Fakes.UnitTestIsolation.UnitTestIsolationException : 無法解析來自 COR_PROFILER_PATH 和 COR_PROFILER 環境變數的分析工具路徑。
Microsoft.QualityTools.Testing.Fakes.UnitTestIsolation.UnitTestIsolationException : 無法解析來自 COR_PROFILER_PATH 和 COR_PROFILER 環境變數的分析工具路徑。
Success

Note

這裡我嘗試使用 Visual Studio Community 資料夾底下的 vstest.console.exe ,但會得到跟其他測試執行器一樣的結果,但用 Visual Studio Enterprise 資料夾底下的 vstest.console.exe 卻可以正常執行

以下 Visual Studio 測試執行器指的就是 vstest.console.exe

image.png

重構解法

為減少單元測試時的複雜性,所以建議使用控制翻轉(Inversion of Control,IoC)的方式來重構程式碼

TimeProvider.cs

public class TimeProvider : ITimeProvider
{
    public TimeProvider() {}

    public DateTime GetDateTimeNow()
    {
        return DateTime.Now;
    }
}

建立一個 Provider 將要使用到的 Static 方法或 .NET Framework 本身的物件放在這個類別的方法中

FakeService.cs

private readonly ITimeProvider _timeProvider;

public FakeService(ITimeProvider timeProvider)
{
    _timeProvider = timeProvider;
}

public ExampleModel GetExampleModel()
{
    ExampleModel exampleModel = new ExampleModel()
    {
        Id = Guid.NewGuid(),
        Name = "Name",
        Description = _timeProvider.GetDateTimeNow().ToString()
    };

    return exampleModel;
}

接著用 DI 的方式來使用 Provider,取代之前直接用 Static 方法或 .NET Framework 本身的物件

FakeServiceTests.cs

private Mock<ITimeProvider>? _mockTimeProvider;
private FakeService CreateDefaultFakeService()
{
    _mockTimeProvider = new Mock<ITimeProvider>();

    return new FakeService(_mockTimeProvider.Object);
}

[Fact]
public void GetExampleModel_ReturnExampleModel()
{
    using (ShimsContext.Create())
    {
        // Arrange
        IFakeService fakeService = CreateDefaultFakeService();

        _mockTimeProvider?.Setup(provider => provider.GetDateTimeNow()).Returns(new DateTime(1000, 1, 1));

        // Act
        ExampleModel result = fakeService.GetExampleModel();

        // Assert
        Assert.NotNull(result);
        Assert.NotNull(result.Name);
        Assert.NotNull(result.Description);
        Assert.Equal("Example", result.Name);
        Assert.Equal("1000/1/1 上午 12:00:00", result.Description);
    }
}

最後,可以使用 Moq 這個套件將我們要模擬的物件給取代並回傳預期的結果

Note

在 .NET 8 已經有定義了一個時間的抽象類別 TimeProvider 可以讓我們用上述的方式來使用,不必再自己撰寫一個 TimeProvider 類別,這樣可以使單元測試更加的順利

Reference