Testing
Test utilities for deterministic time handling.
The Problem
Section titled “The Problem”Tests that depend on DateTime.Now are:
- Non-deterministic
- Hard to reproduce
- Flaky around midnight, month boundaries, etc.
The Solution: IClock
Section titled “The Solution: IClock”Never use DateTime.Now directly. Use IClock:
public interface IClock{ DateTimeOffset UtcNow { get; } LocalDate Today { get; }}Production
Section titled “Production”// Register system clockservices.AddSingleton<IClock, SystemClock>();// Use TestClock for deterministic timevar clock = new TestClock();clock.SetDateTime(2024, 6, 15, 10, 0, 0);TestClock
Section titled “TestClock”Set Specific Time
Section titled “Set Specific Time”var clock = new TestClock();
// Set to specific date/timeclock.SetDateTime(2024, 6, 15, 10, 30, 0);
// Set from DateTimeOffsetclock.SetDateTime(new DateTimeOffset(2024, 6, 15, 10, 30, 0, TimeSpan.Zero));Advance Time
Section titled “Advance Time”var clock = new TestClock();clock.SetDateTime(2024, 6, 15, 10, 0, 0);
// Advance by durationclock.Advance(TimeSpan.FromHours(2));// Now: 2024-06-15 12:00:00
// Advance by daysclock.AdvanceDays(1);// Now: 2024-06-16 12:00:00Freeze at Specific Instant
Section titled “Freeze at Specific Instant”var clock = new TestClock();clock.Freeze(new DateTimeOffset(2024, 6, 15, 10, 0, 0, TimeSpan.Zero));
// Multiple reads return same timevar t1 = clock.UtcNow;await Task.Delay(100);var t2 = clock.UtcNow;
t1.Should().Be(t2); // Same instantTestTemporalContext
Section titled “TestTemporalContext”Pre-configured context for common test scenarios:
// Rome timezone contextvar context = TestTemporalContext.ForRome();context.Culture // "it-IT"context.TimeZone // "Europe/Rome"context.Today // LocalDate in Rome
// New York contextvar nyContext = TestTemporalContext.ForNewYork();
// Custom contextvar custom = TestTemporalContext.Create("de-DE", "Europe/Berlin");TestHolidayProvider
Section titled “TestHolidayProvider”Holiday provider for tests:
var holidays = new TestHolidayProvider() .AddHoliday(2024, 12, 25, "Christmas") .AddHoliday(2024, 12, 26, "Boxing Day");
var calculator = new TemporalCalculator(holidays);Common Test Patterns
Section titled “Common Test Patterns”Testing Time-Based Logic
Section titled “Testing Time-Based Logic”public class SubscriptionServiceTests{ private readonly TestClock _clock; private readonly SubscriptionService _service;
public SubscriptionServiceTests() { _clock = new TestClock(); _service = new SubscriptionService(_clock); }
[Fact] public void Subscription_ExpiresAfter30Days() { // Arrange _clock.SetDateTime(2024, 1, 1, 0, 0, 0); var subscription = _service.Create();
// Act - advance 30 days _clock.AdvanceDays(30);
// Assert subscription.IsExpired(_clock).Should().BeTrue(); }
[Fact] public void Subscription_ValidBefore30Days() { // Arrange _clock.SetDateTime(2024, 1, 1, 0, 0, 0); var subscription = _service.Create();
// Act - advance 29 days _clock.AdvanceDays(29);
// Assert subscription.IsExpired(_clock).Should().BeFalse(); }}Testing Timezone Conversion
Section titled “Testing Timezone Conversion”[Fact]public void DisplaysTimeInUserTimezone(){ // Arrange var clock = new TestClock(); clock.SetDateTime(2024, 6, 15, 14, 0, 0); // 2 PM UTC
// Act var romeTime = ZonedDateTime.FromUtc(clock.UtcNow, "Europe/Rome"); var nyTime = ZonedDateTime.FromUtc(clock.UtcNow, "America/New_York");
// Assert romeTime.LocalDateTime.Hour.Should().Be(16); // 4 PM nyTime.LocalDateTime.Hour.Should().Be(10); // 10 AM}Testing DST Transitions
Section titled “Testing DST Transitions”[Fact]public void HandlesDstSpringForward(){ // March 10, 2024 - US DST spring forward at 2 AM var clock = new TestClock(); clock.SetDateTime(2024, 3, 10, 6, 0, 0); // 6 AM UTC
var eastern = ZonedDateTime.FromUtc(clock.UtcNow, "America/New_York");
// Should be 2 AM EDT (not 1 AM EST) eastern.Offset.Should().Be(TimeSpan.FromHours(-4)); // EDT}Testing Business Day Calculations
Section titled “Testing Business Day Calculations”[Fact]public void AddBusinessDays_SkipsChristmas(){ // Arrange var holidays = new TestHolidayProvider() .AddHoliday(2024, 12, 25, "Christmas"); var calculator = new TemporalCalculator(holidays);
// Tuesday Dec 24 var start = new LocalDate(2024, 12, 24);
// Act - add 1 business day var result = calculator.AddBusinessDays(start, 1);
// Assert - should skip Christmas, land on Dec 26 result.Should().Be(new LocalDate(2024, 12, 26));}Dependency Injection Pattern
Section titled “Dependency Injection Pattern”// Production setupservices.AddSingleton<IClock, SystemClock>();services.AddScoped<ITemporalContext, TemporalContext>();
// Test setupservices.AddSingleton<IClock>(new TestClock());services.AddScoped<ITemporalContext, TestTemporalContext>();Best Practices
Section titled “Best Practices”- Never use DateTime.Now - Always inject IClock
- Freeze time in tests - Avoid flaky tests from time passing during execution
- Test boundary conditions - Midnight, month/year boundaries, DST transitions
- Test with realistic data - Use real timezone names, realistic dates