👉🏼 Click here to Join I ❤️ .NET WhatsApp Channel to get 🔔 notified about new articles and other updates.
Unit Testing Anti-Pattern: Working with Time

Unit Testing Anti-Pattern: Working with Time

Authors - Abdul Rahman (Content Writer), Regina Sharon (Graphic Designer)

Testing

7 Articles

Improve

Table of Contents

  1. What we gonna do?
  2. Why we gonna do?
  3. How we gonna do?
  4. Summary

What we gonna do?

Many applications require access to the current date and time. However, testing code that depends on time can lead to unreliable results since the time during the Act phase may differ from the Assert phase. There are three ways to manage this dependency, with one being an anti-pattern and two being preferable.

Why we gonna do?

Hardcoding time-dependent logic can make tests inconsistent, leading to failures that are unrelated to the actual business logic. The best approach is to manage time as an explicit dependency rather than using static accessors like DateTime.Now . This improves testability and ensures that unit tests remain isolated and predictable. The introduction of FakeTimeProvider in .NET 8 offers another efficient way to control time in tests.

How we gonna do?

One approach is to use the Ambient Context pattern, where time is accessed through a shared static class:

public static class TimeProvider
{
    private static Func<DateTime> _currentTime;
    
    public static DateTime Now => _currentTime();

    public static void Configure(Func<DateTime> timeProvider)
    {
        _currentTime = timeProvider;
    }
}

// Production setup
TimeProvider.Configure(() => DateTime.Now);

// Test setup
TimeProvider.Configure(() => new DateTime(2025, 1, 1));
            

While this allows controlling time in tests, it introduces global state, making tests interdependent and harder to maintain. Instead, a better approach is to pass time as an explicit dependency:

public interface ITimeService
{
    DateTime Now { get; }
}

public class SystemTimeService : ITimeService
{
    public DateTime Now => DateTime.UtcNow;
}
            

This service can be injected into dependent classes, such as a controller:

public class OrderController
{
    private readonly ITimeService _timeService;

    public OrderController(ITimeService timeService)
    {
        _timeService = timeService;
    }

    public void ConfirmOrder(int orderId)
    {
        Order order = GetOrderById(orderId);
        order.Confirm(_timeService.Now);
        SaveOrder(order);
    }
}
            

Injecting time as a service improves testability, but the best practice is to pass the time as a plain value once inside a business operation:

public class OrderController
{
    private readonly ITimeService _timeService;

    public OrderController(ITimeService timeService)
    {
        _timeService = timeService;
    }

    public void ConfirmOrder(int orderId)
    {
        DateTime currentTime = _timeService.Now;
        Order order = GetOrderById(orderId);
        order.Confirm(currentTime);
        SaveOrder(order);
    }
}
            

Another modern approach in .NET 8 is using FakeTimeProvider from the Microsoft.Extensions.TimeProvider.Testing package. This enables full control over time during testing, allowing for precise and deterministic tests.

public class OrderController
{
    private readonly TimeProvider _timeProvider;

    public OrderController(TimeProvider timeProvider)
    {
        _timeProvider = timeProvider;
    }

    public void ConfirmOrder(int orderId)
    {
        Order order = GetOrderById(orderId);
        order.Confirm(_timeProvider.GetUtcNow().UtcDateTime);
        SaveOrder(order);
    }
}

[Fact]
public void ConfirmOrder_UsesProvidedTime()
{
    // Arrange
    var fakeTimeProvider = new FakeTimeProvider();
    var fixedTime = new DateTime(2025, 3, 16, 12, 0, 0, DateTimeKind.Utc);
    fakeTimeProvider.SetUtcNow(fixedTime); // Set the fake time

    var controller = new OrderController(fakeTimeProvider);
    int testOrderId = 1;

    // Act
    controller.ConfirmOrder(testOrderId);

    // Assert
    Order order = controller.GetOrderById(testOrderId);
    Assert.Equal(fixedTime, order.ConfirmedAt);
}
        

Unlike traditional approaches, FakeTimeProvider gives fine-grained control over time behavior, making tests more predictable and eliminating race conditions. We can also use Advance method to move forward in time.

Summary

Using an Ambient Context for time management is an anti-pattern because it introduces shared dependencies, making tests unreliable. Instead, time should be injected as a dependency, preferably as a plain value within business operations. A modern alternative is FakeTimeProvider, introduced in .NET 8, which allows precise control over time-dependent operations, resulting in highly deterministic tests.

👉🏼 Click here to Join I ❤️ .NET WhatsApp Channel to get 🔔 notified about new articles and other updates.
  • Testing
  • Anti Pattern
  • Unit Testing