C# 使用 TimeProvider 在測試中模擬時間

假設申請單編號是年份加上流水號,總長度為 9 碼,例如 2024 年的第 100 張申請單就是 202400100 ,程式可以這樣寫:
    
public class ApplicationNumberGenerator
{
    /// <summary>
    /// 格式化申請單編號
    /// </summary>
    public string FormatApplicationNumber(int applicationNumber)
    {
        int year = DateTime.Now.Year;
        var applicationNumberString = applicationNumber.ToString("D5");
        return $"{year}{applicationNumberString}";
    }
}
    

到目前為止都沒有問題,但是如果要加上測試呢?
雖然在測試的程式碼一樣可以先取得系統時間然後取得年分再組合出預期的結果,再將兩者比對,不過這就讓測試的程式碼需要依附於外部環境。以前在遇到其他依賴時間的程式碼時都是自己多寫一個 DateTime 的介面,讓測試時可以注入指定的時間,不過現在在 .NET 8 中有多了 TimeProvider ,可以簡化這些步驟、增加測試的方便性。

安裝套件

在 .NET 8 或以上不需要額外安裝套件,如果在 .NET 8 以下則可以透過安裝 Microsoft.Bcl.TimeProvider 套件 來使用 TimeProvider :
	
dotnet add package Microsoft.Bcl.TimeProvider
    

示範

回到一開始的 ApplicationNumberGenerator 類別,在建構子中注入 TimeProvider ,我們不使用原先的 DateTime.Now 而是改成 _timeProvider.GetLocalNow()
    
public class ApplicationNumberGenerator
{
    private readonly TimeProvider _timeProvider;

    public ApplicationNumberGenerator(TimeProvider timeProvider)
    {
        _timeProvider = timeProvider;
    }
    
    /// <summary>
    /// 格式化申請單編號
    /// </summary>
    public string FormatApplicationNumber(int applicationNumber)
    {
        int year = _timeProvider.GetLocalNow().Year;
        var applicationNumberString = applicationNumber.ToString("D5");
        return $"{year}{applicationNumberString}";
    }
}
    

平時實例化類別的用法:
	
var instance = new ApplicationNumberGenerator(TimeProvider.System);
    

假設在 ASP.NET Core 中要增加以下程式碼注入 TimeProvider:
	
builder.Services.AddSingleton<TimeProvider>(TimeProvider.System);
    

在撰寫測試程式碼前需要先安裝 Microsoft.Extensions.TimeProvider.Testing 套件,才可以使用 FakeTimeProvider:
	
dotnet add package Microsoft.Extensions.TimeProvider.Testing
    

在測試時我們可以建立 FakeTimeProvider ,指定時間,傳入 ApplicationNumberGenerator 類別中,就可以指定時間,測試的結果就可以指定為一個字串,不用再取得時間並轉換:
    
public class Tests
{
    [Test]
    public void FormatApplicationNumber_WithFakeTimeProvider_ReturnsExpected()
    {
        var fakeTime = new FakeTimeProvider(new DateTimeOffset(2022, 4, 8, 0, 0, 0, TimeSpan.Zero));

        var instance = new ApplicationNumberGenerator(fakeTime);
        var result = instance.FormatApplicationNumber(100);

        Assert.That(result, Is.EqualTo("202200100"));
    }
}
    



參考資料:
Microsoft.Learn - TimeProvider Class
.NET 8 執行階段的新功能

留言