Using TimeProvider for Easier Unit Testing in .NET 8
Tags: dotnet, dotnetcore
Published: 2024-05-11 20:43:55

#### Introduction
In .NET 8, Microsoft introduced `TimeProvider`, a new feature that simplifies testing time-based logic. Previously, mocking time often required third-party libraries or custom wrappers around `DateTime`. This article provides a step-by-step guide to using `TimeProvider` for more effective unit testing.

#### What is TimeProvider?
`TimeProvider` is an abstract class that allows developers to override how time is provided in their applications. This enables better control over time for testing purposes. The default implementation, `TimeProvider.System`, uses the system clock, but you can easily create custom implementations.

#### Example Scenario: Time-Based Greetings
We'll implement a service that provides greetings based on the current time, e.g., "Morning," "Afternoon," "Evening," and "Night."

#### Step 1: Create the Time-Based Service
Create a service class that categorizes greetings according to the time of day using `TimeProvider`:

```csharp
// UsingTime/TimeOfDayService.cs
namespace UsingTime
{
    public class TimeOfDayService
    {
        private readonly TimeProvider _timeProvider;

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

        public string GetTimeOfDay()
        {
            var currentTime = _timeProvider.GetLocalNow();

            return currentTime.Hour switch
            {
                <= 6 => "Night",
                > 6 and <= 12 => "Morning",
                > 12 and <= 18 => "Afternoon",
                > 18 and <= 24 => "Evening",
                _ => "Invalid hour"
            };
        }
    }
}
```

#### Step 2: Create Custom Time Providers
Implement custom `TimeProvider` classes to simulate different times of day for testing purposes:

```csharp
// UsingTimeTest/NightTimeProvider.cs
namespace UsingTimeTest
{
    public class NightTimeProvider : TimeProvider
    {
        public override DateTimeOffset GetUtcNow()
        {
            return new DateTimeOffset(2023, 12, 1, 1, 0, 0, TimeSpan.Zero); // 1 AM
        }
    }
}
```

Repeat this for other times:

```csharp
// UsingTimeTest/MorningTimeProvider.cs
namespace UsingTimeTest
{
    public class MorningTimeProvider : TimeProvider
    {
        public override DateTimeOffset GetUtcNow()
        {
            return new DateTimeOffset(2023, 12, 1, 8, 0, 0, TimeSpan.Zero); // 8 AM
        }
    }
}
```

```csharp
// UsingTimeTest/AfternoonTimeProvider.cs
namespace UsingTimeTest
{
    public class AfternoonTimeProvider : TimeProvider
    {
        public override DateTimeOffset GetUtcNow()
        {
            return new DateTimeOffset(2023, 12, 1, 16, 0, 0, TimeSpan.Zero); // 4 PM
        }
    }
}
```

```csharp
// UsingTimeTest/EveningTimeProvider.cs
namespace UsingTimeTest
{
    public class EveningTimeProvider : TimeProvider
    {
        public override DateTimeOffset GetUtcNow()
        {
            return new DateTimeOffset(2023, 12, 1, 20, 0, 0, TimeSpan.Zero); // 8 PM
        }
    }
}
```

#### Step 3: Write Unit Tests
Write unit tests to verify the `TimeOfDayService` using these custom time providers.

```csharp
// UsingTimeTest/TimeOfDayServiceTests.cs
using Xunit;
using UsingTime;

namespace UsingTimeTest
{
    public class TimeOfDayServiceTests
    {
        private TimeOfDayService _timeOfDayService;
        private TimeProvider _timeProvider;

        [Fact]
        public void TimeOfDay_ShouldReturnMorning_WhenItsMorning()
        {
            _timeProvider = new MorningTimeProvider();
            _timeOfDayService = new TimeOfDayService(_timeProvider);

            var result = _timeOfDayService.GetTimeOfDay();
            Assert.Equal("Morning", result);
        }

        [Fact]
        public void TimeOfDay_ShouldReturnAfternoon_WhenItsAfternoon()
        {
            _timeProvider = new AfternoonTimeProvider();
            _timeOfDayService = new TimeOfDayService(_timeProvider);

            var result = _timeOfDayService.GetTimeOfDay();
            Assert.Equal("Afternoon", result);
        }

        [Fact]
        public void TimeOfDay_ShouldReturnEvening_WhenItsEvening()
        {
            _timeProvider = new EveningTimeProvider();
            _timeOfDayService = new TimeOfDayService(_timeProvider);

            var result = _timeOfDayService.GetTimeOfDay();
            Assert.Equal("Evening", result);
        }

        [Fact]
        public void TimeOfDay_ShouldReturnNight_WhenItsNight()
        {
            _timeProvider = new NightTimeProvider();
            _timeOfDayService = new TimeOfDayService(_timeProvider);

            var result = _timeOfDayService.GetTimeOfDay();
            Assert.Equal("Night", result);
        }
    }
}
```

#### Conclusion
With `TimeProvider` in .NET 8, mocking time-based behavior is now easier and more standardized. Instead of relying on third-party tools or custom abstractions, developers can use the built-in API to manipulate time in unit tests. This results in more reliable and maintainable tests, simplifying the development process.