| name | dotnet-testing-datetime-testing-timeprovider |
| description | 使用 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, 時間凍結, 時間快轉
|
DateTime 與時間相依性測試指南
核心原則
原則一:時間抽象化 - 以 TimeProvider 取代 DateTime
傳統問題程式碼:
public class OrderService
{
public bool CanPlaceOrder()
{
var now = DateTime.Now;
return now.Hour >= 9 && now.Hour < 17;
}
}
可測試的重構:
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;
}
}
依賴注入設定:
services.AddSingleton(TimeProvider.System);
services.AddScoped<OrderService>();
原則二:FakeTimeProvider 控制測試時間
FakeTimeProvider 提供完整的時間控制能力:
| 方法 | 用途 | 使用時機 |
|---|
SetUtcNow(DateTimeOffset) | 設定 UTC 時間 | 需要精確 UTC 時間時 |
SetLocalTimeZone(TimeZoneInfo) | 設定本地時區 | 測試時區相關邏輯 |
Advance(TimeSpan) | 時間快轉 | 測試過期、延遲邏輯 |
GetUtcNow() | 取得 UTC 時間 | 讀取當前模擬時間 |
GetLocalNow() | 取得本地時間 | 讀取本地模擬時間 |
建議擴充方法:
public static class FakeTimeProviderExtensions
{
public static void SetLocalNow(this FakeTimeProvider fakeTimeProvider, DateTime localDateTime)
{
fakeTimeProvider.SetLocalTimeZone(TimeZoneInfo.Local);
var utcTime = TimeZoneInfo.ConvertTimeToUtc(localDateTime, TimeZoneInfo.Local);
fakeTimeProvider.SetUtcNow(utcTime);
}
}
原則三:每個測試使用獨立的時間環境
public class OrderServiceTests
{
[Fact]
public void CanPlaceOrder_在營業時間內_應回傳True()
{
var fakeTimeProvider = new FakeTimeProvider();
fakeTimeProvider.SetLocalNow(new DateTime(2024, 3, 15, 14, 0, 0));
var sut = new OrderService(fakeTimeProvider);
var result = sut.CanPlaceOrder();
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");
fakeTimeProvider.Advance(TimeSpan.FromMinutes(3));
cache.Get("key").Should().Be("value");
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)]
[InlineData(9, true)]
[InlineData(12, true)]
[InlineData(16, true)]
[InlineData(17, false)]
[InlineData(18, false)]
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)
{
var fridayTime = new DateTime(2024, 3, 15, 14, 0, 0);
fakeTimeProvider.SetLocalNow(fridayTime);
sut.GetTimeBasedDiscount().Should().Be("週五快樂:九折優惠");
}
關鍵:必須使用 [Frozen(Matching.DirectBaseType)],否則 AutoFixture 無法正確將 FakeTimeProvider 注入到需要 TimeProvider 的建構式中。
最佳實踐檢查清單
程式碼設計
測試設計
進階考量
輸出格式
- 產生使用 TimeProvider 抽象的服務類別
- 產生使用 FakeTimeProvider 的測試類別
- 包含時間凍結、快轉、時區轉換測試範例
- 提供 .csproj 套件參考(Microsoft.Bcl.TimeProvider)
參考資源
原始文章
本技能內容提煉自「老派軟體工程師的測試修練 - 30 天挑戰」系列文章:
- Day 16 - 測試日期與時間:Microsoft.Bcl.TimeProvider 取代 DateTime
官方文件
相關技能
autofixture-basics - AutoFixture 自動測試資料生成
nsubstitute-mocking - 測試替身與模擬
autodata-xunit-integration - xUnit 與 AutoFixture 的 AutoData 整合