dotnet-testing-datetime-testing-timeprovider

使用 TimeProvider 測試時間相依邏輯的專門技能。當需要測試 DateTime、控制時間流逝、處理時區轉換、測試過期邏輯時使用。涵蓋 TimeProvider 抽象化、FakeTimeProvider 時間控制、時間凍結與快轉等。 Make sure to use this skill whenever the user mentions DateTime testing, TimeProvider, FakeTimeProvider, time-dependent logic, cache expiration, or token expiration testing, even if they don't explicitly ask for time testing guidance. Keywords: datetime, time testing, 時間測試, TimeProvider, FakeTimeProvider, DateTime.Now, 時間相依, 快取過期, token 過期, Microsoft.Bcl.TimeProvider, GetUtcNow, SetUtcNow, Advance, time freeze, 時間凍結, 時間快轉

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "dotnet-testing-datetime-testing-timeprovider" with this command: npx skills add kevintsengtw/dotnet-testing-agent-skills/kevintsengtw-dotnet-testing-agent-skills-dotnet-testing-datetime-testing-timeprovider

DateTime 與時間相依性測試指南

核心原則

原則一:時間抽象化 - 以 TimeProvider 取代 DateTime

傳統問題程式碼

// ❌ 無法測試 - 直接使用靜態時間
public class OrderService
{
    public bool CanPlaceOrder()
    {
        var now = DateTime.Now;
        return now.Hour >= 9 && now.Hour < 17;
    }
}

可測試的重構

// ✅ 可測試 - 透過依賴注入接收 TimeProvider
public class OrderService
{
    private readonly TimeProvider _timeProvider;
    
    public OrderService(TimeProvider timeProvider)
    {
        _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
    }
    
    public bool CanPlaceOrder()
    {
        var now = _timeProvider.GetLocalNow();
        return now.Hour >= 9 && now.Hour < 17;
    }
}

依賴注入設定

// Program.cs - 生產環境使用系統時間
services.AddSingleton(TimeProvider.System);
services.AddScoped<OrderService>();

原則二:FakeTimeProvider 控制測試時間

FakeTimeProvider 提供完整的時間控制能力:

方法用途使用時機
SetUtcNow(DateTimeOffset)設定 UTC 時間需要精確 UTC 時間時
SetLocalTimeZone(TimeZoneInfo)設定本地時區測試時區相關邏輯
Advance(TimeSpan)時間快轉測試過期、延遲邏輯
GetUtcNow()取得 UTC 時間讀取當前模擬時間
GetLocalNow()取得本地時間讀取本地模擬時間

建議擴充方法

public static class FakeTimeProviderExtensions
{
    /// <summary>
    /// 設定 FakeTimeProvider 的本地時間
    /// </summary>
    public static void SetLocalNow(this FakeTimeProvider fakeTimeProvider, DateTime localDateTime)
    {
        fakeTimeProvider.SetLocalTimeZone(TimeZoneInfo.Local);
        var utcTime = TimeZoneInfo.ConvertTimeToUtc(localDateTime, TimeZoneInfo.Local);
        fakeTimeProvider.SetUtcNow(utcTime);
    }
}

原則三:每個測試使用獨立的時間環境

// ✅ 正確:每個測試獨立建立 FakeTimeProvider
public class OrderServiceTests
{
    [Fact]
    public void CanPlaceOrder_在營業時間內_應回傳True()
    {
        // Arrange - 獨立實例
        var fakeTimeProvider = new FakeTimeProvider();
        fakeTimeProvider.SetLocalNow(new DateTime(2024, 3, 15, 14, 0, 0));
        var sut = new OrderService(fakeTimeProvider);
        
        // Act
        var result = sut.CanPlaceOrder();
        
        // Assert
        result.Should().BeTrue();
    }
}

// ❌ 避免:多個測試共用靜態實例
public class BadTestClass
{
    private static readonly FakeTimeProvider SharedProvider = new(); // 會互相干擾
}

進階時間控制技術

時間凍結

當需要驗證多個操作發生在「同一時間點」:

[Fact]
public void ProcessBatch_在固定時間點_應產生相同時間戳()
{
    var fakeTimeProvider = new FakeTimeProvider();
    var fixedTime = new DateTime(2024, 12, 25, 10, 30, 0);
    fakeTimeProvider.SetLocalNow(fixedTime);
    
    var processor = new BatchProcessor(fakeTimeProvider);
    
    var result1 = processor.ProcessItem("Item1");
    var result2 = processor.ProcessItem("Item2");
    
    // 時間被凍結,兩次操作的時間戳相同
    result1.Timestamp.Should().Be(result2.Timestamp);
}

時間快轉 (Advance)

測試快取過期、Token 失效等時間敏感邏輯:

[Fact]
public void Cache_經過過期時間_應清除項目()
{
    var fakeTimeProvider = new FakeTimeProvider();
    fakeTimeProvider.SetLocalNow(new DateTime(2024, 3, 15, 10, 0, 0));
    
    var cache = new TimedCache(fakeTimeProvider, TimeSpan.FromMinutes(5));
    cache.Set("key", "value");
    
    // 3 分鐘後 - 尚未過期
    fakeTimeProvider.Advance(TimeSpan.FromMinutes(3));
    cache.Get("key").Should().Be("value");
    
    // 再 3 分鐘後(共 6 分鐘)- 已過期
    fakeTimeProvider.Advance(TimeSpan.FromMinutes(3));
    cache.Get("key").Should().BeNull();
}

重要Advance() 是非阻塞的,瞬間完成時間跳躍,不會真正等待。

時間倒轉

測試歷史資料處理或重播場景:

[Fact]
public void HistoricalDataProcessor_回到過去時間_應正確處理()
{
    var fakeTimeProvider = new FakeTimeProvider();
    var historicalTime = new DateTime(2020, 1, 15, 9, 0, 0);
    fakeTimeProvider.SetLocalNow(historicalTime);
    
    var processor = new HistoricalDataProcessor(fakeTimeProvider);
    var result = processor.ProcessDataForDate(historicalTime.Date);
    
    result.ProcessedAt.Should().Be(historicalTime);
}

實戰測試模式

模式一:參數化邊界測試

[Theory]
[InlineData(8, false)]   // 上午 8 點 - 營業時間前
[InlineData(9, true)]    // 上午 9 點 - 剛開始營業
[InlineData(12, true)]   // 中午 12 點 - 營業時間內
[InlineData(16, true)]   // 下午 4 點 - 營業時間內
[InlineData(17, false)]  // 下午 5 點 - 剛結束營業
[InlineData(18, false)]  // 下午 6 點 - 營業時間後
public void CanPlaceOrder_不同時間點_應回傳正確結果(int hour, bool expected)
{
    var fakeTimeProvider = new FakeTimeProvider();
    fakeTimeProvider.SetLocalNow(new DateTime(2024, 3, 15, hour, 0, 0));
    
    var sut = new OrderService(fakeTimeProvider);
    
    sut.CanPlaceOrder().Should().Be(expected);
}

模式二:交易時間窗口測試

[Theory]
[InlineData("09:30:00", true)]   // 上午交易時間
[InlineData("12:00:00", false)]  // 中午休息
[InlineData("14:30:00", true)]   // 下午交易時間
[InlineData("15:30:00", false)]  // 交易結束後
public void IsInTradingHours_不同時間_應回傳正確結果(string timeStr, bool expected)
{
    var fakeTimeProvider = new FakeTimeProvider();
    var testTime = DateTime.Today.Add(TimeSpan.Parse(timeStr));
    fakeTimeProvider.SetLocalNow(testTime);
    
    var sut = new TradingService(fakeTimeProvider);
    
    sut.IsInTradingHours().Should().Be(expected);
}

模式三:排程觸發邏輯測試

[Theory]
[InlineData("2024-03-15 14:30:00", "2024-03-15 14:00:00", true)]   // 已到執行時間
[InlineData("2024-03-15 13:30:00", "2024-03-15 14:00:00", false)]  // 尚未到時間
public void ShouldExecuteJob_根據時間判斷_應回傳正確結果(
    string currentTimeStr, string scheduledTimeStr, bool expected)
{
    var fakeTimeProvider = new FakeTimeProvider();
    fakeTimeProvider.SetLocalNow(DateTime.Parse(currentTimeStr));
    
    var schedule = new JobSchedule { NextExecutionTime = DateTime.Parse(scheduledTimeStr) };
    var sut = new ScheduleService(fakeTimeProvider);
    
    sut.ShouldExecuteJob(schedule).Should().Be(expected);
}

AutoFixture 整合

FakeTimeProviderCustomization

public class FakeTimeProviderCustomization : ICustomization
{
    public void Customize(IFixture fixture)
    {
        fixture.Register(() => new FakeTimeProvider());
    }
}

AutoDataWithCustomization 屬性

public class AutoDataWithCustomizationAttribute : AutoDataAttribute
{
    public AutoDataWithCustomizationAttribute() : base(CreateFixture)
    {
    }
    
    private static IFixture CreateFixture()
    {
        return new Fixture()
            .Customize(new AutoNSubstituteCustomization())
            .Customize(new FakeTimeProviderCustomization());
    }
}

使用 Matching.DirectBaseType

[Theory]
[AutoDataWithCustomization]
public void GetTimeBasedDiscount_週五_應回傳九折優惠(
    [Frozen(Matching.DirectBaseType)] FakeTimeProvider fakeTimeProvider,
    OrderService sut)
{
    // Matching.DirectBaseType 讓 AutoFixture 知道:
    // 當需要 TimeProvider(基底類型)時,使用 FakeTimeProvider(衍生類型)
    
    var fridayTime = new DateTime(2024, 3, 15, 14, 0, 0); // 週五
    fakeTimeProvider.SetLocalNow(fridayTime);
    
    sut.GetTimeBasedDiscount().Should().Be("週五快樂:九折優惠");
}

關鍵:必須使用 [Frozen(Matching.DirectBaseType)],否則 AutoFixture 無法正確將 FakeTimeProvider 注入到需要 TimeProvider 的建構式中。


最佳實踐檢查清單

程式碼設計

  • 所有時間相依類別透過建構式接收 TimeProvider
  • 使用 _timeProvider.GetLocalNow() 取代 DateTime.Now
  • 使用 _timeProvider.GetUtcNow() 取代 DateTime.UtcNow
  • DI 容器註冊 TimeProvider.System 作為生產環境實作

測試設計

  • 每個測試方法使用獨立的 FakeTimeProvider 實例
  • 使用 SetLocalNow() 擴充方法簡化時間設定
  • 使用 Advance() 測試時間敏感邏輯(快取、過期、延遲)
  • 測試涵蓋邊界條件(開始時間、結束時間、臨界點)

進階考量

  • FakeTimeProvider 是執行緒安全的,可用於並行測試
  • 使用 IDisposable 模式正確釋放 FakeTimeProvider
  • 時區測試使用 SetLocalTimeZone() 明確設定時區

輸出格式

  • 產生使用 TimeProvider 抽象的服務類別
  • 產生使用 FakeTimeProvider 的測試類別
  • 包含時間凍結、快轉、時區轉換測試範例
  • 提供 .csproj 套件參考(Microsoft.Bcl.TimeProvider)

參考資源

原始文章

本技能內容提煉自「老派軟體工程師的測試修練 - 30 天挑戰」系列文章:

官方文件

相關技能

  • autofixture-basics - AutoFixture 自動測試資料生成
  • nsubstitute-mocking - 測試替身與模擬
  • autodata-xunit-integration - xUnit 與 AutoFixture 的 AutoData 整合

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Automation

dotnet-testing-advanced-webapi-integration-testing

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

dotnet-testing-unit-test-fundamentals

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

dotnet-testing-xunit-project-setup

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

dotnet-testing-advanced-aspnet-integration-testing

No summary provided by upstream source.

Repository SourceNeeds Review