dotnet-testing-datetime-testing-timeprovider
Skill29 starsChanged 45 days agoMITView on GitHub
使用 TimeProvider 測試時間相依邏輯的專門技能。當需要測試 DateTime、控制時間流逝、處理時區轉換、測試過期邏輯時使用。涵蓋 TimeProvider 抽象化、FakeTimeProvider 時間控制、時間凍結與快轉等。 Keywords: datetime, time testing, 時間測試, TimeProvider, FakeTimeProvider, DateTime.Now, 時間相依, 快取過期, token 過期, Microsoft.Bcl.TimeProvider, GetUtcNow, SetUtcNow, Advance, time freeze, 時間凍結, 時間快轉
In this repository28 other files it gives its agents.
- dotnet-testing-advanced-aspire-testingSkill
- dotnet-testing-advanced-aspnet-integration-testingSkill
- dotnet-testing-advancedSkill
- dotnet-testing-advanced-testcontainers-databaseSkill
- dotnet-testing-advanced-testcontainers-nosqlSkill
- dotnet-testing-advanced-tunit-advancedSkill
- dotnet-testing-advanced-tunit-fundamentalsSkill
- dotnet-testing-advanced-webapi-integration-testingSkill
- dotnet-testing-advanced-xunit-upgrade-guideSkill
- dotnet-testing-autodata-xunit-integrationSkill
- dotnet-testing-autofixture-basicsSkill
- dotnet-testing-autofixture-bogus-integrationSkill
- dotnet-testing-autofixture-customizationSkill
- dotnet-testing-autofixture-nsubstitute-integrationSkill
- dotnet-testing-awesome-assertions-guideSkill
- dotnet-testing-bogus-fake-dataSkill
- dotnet-testing-code-coverage-analysisSkill
- dotnet-testing-complex-object-comparisonSkill
- dotnet-testing-filesystem-testing-abstractionsSkill
- dotnet-testing-fluentvalidation-testingSkill
- dotnet-testing-nsubstitute-mockingSkill
- dotnet-testing-private-internal-testingSkill
- dotnet-testingSkill
- dotnet-testing-test-data-builder-patternSkill
- dotnet-testing-test-naming-conventionsSkill
- dotnet-testing-test-output-loggingSkill
- dotnet-testing-unit-test-fundamentalsSkill
- dotnet-testing-xunit-project-setupSkill
The file
Files
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 控制測試時間
套件名與命名空間不同:安裝
Microsoft.Extensions.TimeProvider.Testing,程式碼中寫using Microsoft.Extensions.Time.Testing;。
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);
}⚠️
SetUtcNow是單向的:同一個FakeTimeProvider實例只能把時間往前設, 回設較早的時間會拋ArgumentOutOfRangeException: Cannot go back in time.(8.0.0/9.0.0/10.9.0 實測一致)。上面能設定 2020 年,是因為那是一個剛建立、 尚未推進的實例,不是「可以倒轉」。因此,若測試類別把
FakeTimeProvider提為共用欄位(在 constructor 建立、 多個測試方法共用同一實例),初始時間必須取所有測試會用到的最早時間, 其後各測試以SetUtcNow()或Advance()往前推進。 不想受此限制時,回到〈原則三〉:每個測試各自new FakeTimeProvider()。
實戰測試模式
模式一:參數化邊界測試
[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實例 - 若改用共用實例,初始時間取所有測試會用到的最早時間(
SetUtcNow不可回設) - 整合測試中由 DI 註冊的
FakeTimeProvider同屬共用實例:不要在每個測試開始時設回固定起點,時間以GetUtcNow()為基準推算 - 使用
SetLocalNow()擴充方法簡化時間設定 - 使用
Advance()測試時間敏感邏輯(快取、過期、延遲) - 測試涵蓋邊界條件(開始時間、結束時間、臨界點)
進階考量
- FakeTimeProvider 是執行緒安全的,可用於並行測試
- 使用
IDisposable模式正確釋放 FakeTimeProvider - 時區測試使用
SetLocalTimeZone()明確設定時區
輸出格式
- 產生使用 TimeProvider 抽象的服務類別
- 產生使用 FakeTimeProvider 的測試類別
- 包含時間凍結、快轉、時區轉換測試範例
- 提供 .csproj 套件參考(Microsoft.Bcl.TimeProvider)
參考資源
範例檔案
- templates/timeprovider-basics.cs - 把時間相依的程式碼重構為可測試設計,含
LegacyOrderService與OrderService的前後對照、TimeProviderAPI 對照表與 DI 註冊 - templates/faketimeprovider-examples.cs - 以
FakeTimeProvider控制時間,涵蓋邊界條件、時間凍結與批次處理情境 - templates/autofixture-integration.cs - 與 AutoFixture 整合,含
FakeTimeProviderCustomization與自訂的 AutoData 屬性
原始文章
本技能內容提煉自「老派軟體工程師的測試修練 - 30 天挑戰」系列文章:
- Day 16 - 測試日期與時間:Microsoft.Bcl.TimeProvider 取代 DateTime
官方文件
相關技能
autofixture-basics- AutoFixture 自動測試資料生成nsubstitute-mocking- 測試替身與模擬autodata-xunit-integration- xUnit 與 AutoFixture 的 AutoData 整合
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

