.NET 8 — Time Abstraction
The Time Abstraction feature introduced in .NET 8, brings the abstract class TimeProvider
and the ITimer
interface, which makes handling time-related operations easier, and it allows you to easily mock time in test scenarios. In this article, I present how to use and create tests using Time Abstraction.
The Time Abstraction supports the following essential time operations:
- Retrieve local and UTC time.
- Obtain a timestamp for performance measurement.
- Create your own custom timer.
- Use Time abstraction to mock Task operations that rely on time progression using
Task.Delay
andTask.WaitAsync
.
For demonstration purposes, I created a .NET 8 Console Application and a Unit Test project. The Console App contains two methods: one that will return the period of the day based on the hour, and another method that will run with a delay operation.
Previous Approach
Before going to the Time Abstraction functionality, let's recap how was the situation in a previous .NET version. When you need to work with time, you could use the DateTimeOffset.UtcNow
or DateTime.Now
, but mocking it could be challenging. For example, consider the code below:
public string GetTimeOfDay()
{
var currentTime = DateTimeOffset.UtcNow;
var message = currentTime.Hour switch
{
>= 6 and <= 12 => "Morning",
> 12 and <= 18 => "Afternoon",
> 18 and <= 24 => "Evening",
_ => "Night"
};
return message;
}
Note that this method uses the DateTimeOffset.UtcNow
. In this case, you will not be able to easily mock the result of UtcNow
, and this means that only the test cases for one specific period of the day will succeed.
Note that this method uses the DateTimeOffset.UtcNow. In this case, you will not be able to easily mock the result of UtcNow, and this means that only the test cases for one specific period of the day will succeed.
public interface IDateTimeOffsetProvider
{
DateTimeOffset UtcNow { get; }
}
public class DateTimeOffsetProvider : IDateTimeOffsetProvider
{
public DateTimeOffset UtcNow => DateTimeOffset.UtcNow;
}
And then use this wrapper via DI in your class:
public class DemoService
{
private readonly IDateTimeOffsetProvider _dateTimeOffsetProvider;
public DemoService(IDateTimeOffsetProvider dateTimeOffsetProvider)
{
_dateTimeOffsetProvider = dateTimeOffsetProvider;
}
public string GetTimeOfDay()
{
var currentTime = _dateTimeOffsetProvider.UtcNow;
var message = currentTime.Hour switch
{
>= 6 and <= 12 => "Morning",
> 12 and <= 18 => "Afternoon",
> 18 and <= 24 => "Evening",
_ => "Night"
};
return message;
}
}
Now in your tests, it is possible to mock the UtcNow
result, to return the date you need, for example:
_dateTimeOffsetProviderMock.Setup(c => c.UtcNow).Returns(expecteDate);
New Approach: Time Abstraction
.NET 8 make this easier for us with the Time Abstraction feature. Now you don’t need to create this wrapper class anymore, instead, you can make use of the TimeProvider
, and use it via dependency injection, and use it to get the time:
public class DemoService
{
private readonly TimeProvider _timeProvider;
public DemoService(TimeProvider timeProvider)
{
_timeProvider = timeProvider;
}
public string GetTimeOfDay()
{
var currentTime = _timeProvider.GetUtcNow();
var message = currentTime.Hour switch
{
>= 6 and <= 12 => "Morning",
> 12 and <= 18 => "Afternoon",
> 18 and <= 24 => "Evening",
_ => "Night"
};
return message;
}
}
And you can also easily Mock it in your tests:
_timeProviderMock.Setup(c => c.GetUtcNow()).Returns(expectedDate);
For example, in the code below you can see a unit test class that tests the GetTimeOfDay
method. For this method, there are test cases for the four periods of the day:
public class DemoServiceTests
{
private readonly DemoService _demoService;
private readonly Mock<TimeProvider> _timeProviderMock = new Mock<TimeProvider>();
public DemoServiceTests()
{
_demoService = new DemoService(_timeProviderMock.Object);
}
[Theory]
[MemberData(nameof(TimeOfDayTestCases))]
public void GetTimeOfDay_ShouldReturnExpectedTimeOfDay(
DateTimeOffset date, string expectedMessage)
{
// Arrange
_timeProviderMock.Setup(c => c.GetUtcNow()).Returns(date);
// Act
var result = _demoService.GetTimeOfDay();
// Assert
result.Should().Be(expectedMessage);
}
public static IEnumerable<object[]> TimeOfDayTestCases()
{
yield return new object[] {
new DateTimeOffset(2024, 1, 1, 6, 0, 0, TimeSpan.Zero), "Morning" };
yield return new object[] {
new DateTimeOffset(2024, 1, 1, 12, 59, 59, TimeSpan.Zero), "Morning" };
yield return new object[] {
new DateTimeOffset(2024, 1, 1, 13, 0, 0, TimeSpan.Zero), "Afternoon" };
yield return new object[] {
new DateTimeOffset(2024, 1, 1, 18, 59, 59, TimeSpan.Zero), "Afternoon" };
yield return new object[] {
new DateTimeOffset(2024, 1, 1, 19, 0, 0, TimeSpan.Zero), "Evening" };
yield return new object[] {
new DateTimeOffset(2024, 1, 1, 23, 59, 59, TimeSpan.Zero), "Evening" };
yield return new object[] {
new DateTimeOffset(2024, 1, 1, 00, 0, 0, TimeSpan.Zero), "Night" };
yield return new object[] {
new DateTimeOffset(2024, 1, 1, 05, 59, 59, TimeSpan.Zero), "Night" };
}
}
This is the result of the tests using the Time Abstraction
:
Beyond that, you can also use the time abstraction to mock Task
operations that rely on time progression using Task.Delay
and Task.WaitAsyn
.
FakeTimerProvider
The FakeTimerProvider
class can be used to set a specific behaviour in your tests, allowing you to easily mock the TimeProvider
. To use it, you need to install the package Microsoft.Extensions.Time.Testing
.
With this class, you can for example, make use of the SetUtcMethod
, to configure the time you want to return in your test:
[Fact]
public void GetTimeOfDay_ShouldReturnMorning_WhenItIsMorning()
{
// Arrange
var fakeTimeProvider = new FakeTimeProvider();
fakeTimeProvider.SetUtcNow(new DateTime(2024, 1, 1, 8, 0, 0));
var demoService = new DemoService(fakeTimeProvider);
// Act
var result = demoService.GetTimeOfDay();
// Assert
result.Should().Be("Morning");
}
To check the other methods from FakeTimerProvider, check the official Microsoft Documentation page: FakeTimerProvider Class
Custom Time Provider
Time Abstraction also allows you to create a Custom Time Provider and override the methods you want. For example, using similar examples as before, you can have a custom timer provider that will always return a specific period of the day when the GetUtcNow
method is executed, for example:
public class NightTimeProvider : TimeProvider
{
public override DateTimeOffset GetUtcNow()
{
return new DateTimeOffset(2024, 1, 1, 3, 0, 0, TimeSpan.Zero);
}
}
With that, when you use this custom time provider in your tests, and call the GetUtcNow
method it will always return 3am.
Task.Delay example
With Time Abstraction it is also possible to easily test Timer
, Task.Delay
and Task.WaitAsync
operations. These functions accept an TimeProvider
argument, which can be easily mocked, and you can then override the CreateTimer
method and set a waiting time of TimeSpan.Zero
for your tests. The ITimer
created by CreateTimer
is tied to the TimeProvider
so the timers trigger as time advances.
For example, consider the code below, which has a Task.Delay
, and receives TimeSpan
and a TimeProvider
as parameters:
public async Task MethodWithDelay()
{
Console.WriteLine($"Start of Task.Delay: {_timeProvider.GetUtcNow()}");
await Task.Delay(TimeSpan.FromSeconds(3), _timeProvider);
Console.WriteLine($"End of Task.Delay: {_timeProvider.GetUtcNow()}");
}
// Output:
Start of Task.Delay: 06/01/2024 21:27:30 +00:00
End of Task.Delay: 06/01/2024 21:27:33 +00:00
In your unit tests, you don’t want to wait for this delay of course. For that, you can create a Custom time provider, and override the CreateTimer
method, passing the value TimeSpan.Zero
as a parameter.
public class NoDelayTimeProvider : TimeProvider
{
public override ITimer CreateTimer(
TimerCallback callback,
object? state,
TimeSpan dueTime,
TimeSpan period)
{
return base.CreateTimer(callback, state, TimeSpan.Zero, period);
}
}
The ITimer
created by the CreateTimer
, is related to the TimeProvider
, making it possible to trigger the timer when the time progresses, this way you can use this custom time provider to test your methods that contain a Task.Delay
, without needing to wait for this delay. The example below demonstrates that there is no delay when the MethodWithDelay
is executed in the unit test:
[Fact]
public async Task MethodWithDelay_ShouldHaveNoDelay_WhenUsingNoDelayTimeProvider()
{
// Arrange
var noDelayTimeProvider = new NoDelayTimeProvider();
var demoService = new DemoService(noDelayTimeProvider);
// Act
var startTime = noDelayTimeProvider.GetUtcNow();
await demoService.MethodWithDelay();
var endTime = noDelayTimeProvider.GetUtcNow();
// Assert
var elapsedSeconds = (endTime - startTime).Seconds;
elapsedSeconds.Should().Be(0);
}
You can also achieve similar results when using the Task.WaitAsync
, which also receives a TimeSpan
and a TimeProvider
as parameters.
Conclusion
Time Abstraction is a great feature that comes with .NET 8, which saves you from needing to manually write wrappers to handle time operations in order to be able to mock time. Now you can use the TimeProvider
class, and easily mock your time results, or can also use the FakeTimerProvider
class for it. Beyond that, you can also easily create your own custom time providers and override the methods you want according to your needs.
This is the link for the project on GitHub, with all the examples: https://github.com/henriquesd/TimeAbstractionDemo
If you like this demo, I kindly ask you to give a ⭐️ in the repository.
Thanks for reading!