SuperAPI AI Directory

dotnet-testing-datetime-testing-timeprovider

Testing & quality

View repository

Updated 10-15 23:45

About this skill

Guide to DateTime and Time-Dependent Testing

Core Principles

Principle 1: Time Abstraction - Replace DateTime with TimeProvider

Traditional Problematic Code:

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

Testable Refactoring:

// ✅ 可測試 - 透過依賴注入接收 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;
    }
}

Dependency Injection Setup:

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

Principle 2: FakeTimeProvider Controls Test Time

FakeTimeProvider provides complete time control capabilities:

Method Purpose When to Use
SetUtcNow(DateTimeOffset) Set UTC time When precise UTC time is needed
SetLocalTimeZone(TimeZoneInfo) Set local time zone Testing time zone-related logic
Advance(TimeSpan) Fast-forward time Testing expiration and delay logic
GetUtcNow() Get UTC time Read current simulated time
GetLocalNow() Get local time Read local simulated time

Recommended Extension Methods:

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);
    }
}

Principle 3: Each Test Uses an Independent Time Environment

// ✅ 正確:每個測試獨立建立 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(); // 會互相干擾
}

Advanced Time Control Techniques

Time Freezing

When you need to verify that multiple operations occur at "the same point in time":

[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);
}

Fast-Forwarding Time (Advance)

Testing time-sensitive logic such as cache expiration and token invalidation:

[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();
}

Important:Advance() is non-blocking, completes the time jump instantly, and does not actually wait.

Rewinding Time

Testing historical data processing or replay scenarios:

[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);
}

Real-World Testing Patterns

Pattern 1: Parameterized Boundary Testing

[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);
}

Pattern 2: Transaction Time Window Testing

[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);
}

Pattern 3: Scheduled Trigger Logic Testing

[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 Integration

FakeTimeProviderCustomization

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

AutoDataWithCustomization Attribute

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

Using 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("週五快樂:九折優惠");
}

Key: Must use [Frozen(Matching.DirectBaseType)], otherwise AutoFixture cannot correctly inject FakeTimeProvider into constructors that require TimeProvider.


Best Practices Checklist

Code Design

  • All time-dependent classes receive TimeProvider
  • Use _timeProvider.GetLocalNow() instead of DateTime.Now
  • Use _timeProvider.GetUtcNow() instead of DateTime.UtcNow
  • DI container registers TimeProvider.System as the production implementation

Test Design

  • Each test method uses an independent FakeTimeProvider instance
  • Use SetLocalNow() extension methods to simplify time setup
  • Use Advance() to test time-sensitive logic (caching, expiration, delay)
  • Tests cover boundary conditions (start time, end time, critical points)

Advanced Considerations

  • FakeTimeProvider is thread-safe and can be used for parallel testing
  • Use the IDisposable pattern to properly dispose of FakeTimeProvider
  • For time zone testing, use SetLocalTimeZone() to explicitly set the time zone

Output Format

  • Generate a service class that uses the TimeProvider abstraction
  • Generate a test class that uses FakeTimeProvider
  • Includes test examples for time freezing, fast-forwarding, and time zone conversion
  • Provides .csproj package reference (Microsoft.Bcl.TimeProvider)

Reference Resources

Original Article

The content of this skill is distilled from the "Old-School Software Engineer's Testing Practice - 30-Day Challenge" article series:

Official Documentation

Related Skills

  • autofixture-basics - AutoFixture automatic test data generation
  • nsubstitute-mocking - Test doubles and mocking
  • autodata-xunit-integration - AutoData integration of xUnit and AutoFixture