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 ofDateTime.Now - Use
_timeProvider.GetUtcNow()instead ofDateTime.UtcNow - DI container registers
TimeProvider.Systemas the production implementation
Test Design
- Each test method uses an independent
FakeTimeProviderinstance - 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
IDisposablepattern 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:
- Day 16 - Testing Date and Time: Microsoft.Bcl.TimeProvider Replaces DateTime
- Ironman contest article:https://ithelp.ithome.com.tw/articles/10375821
- Sample code:https://github.com/kevintsengtw/30Days_in_Testing_Samples/tree/main/day16
Official Documentation
Related Skills
autofixture-basics- AutoFixture automatic test data generationnsubstitute-mocking- Test doubles and mockingautodata-xunit-integration- AutoData integration of xUnit and AutoFixture