TimeProvider makes it easier to mock time in .NET 8

Published: Friday 10 November 2023

The TimeProvider class has been introduced in .NET 8 which provides abstractions to mock time and create a timer.

Combined with the FakeTimeProvider class that allows us to set the current UTC time and local time zone, we can use it to write unit tests that involve a time and a timer.

The TimeProvider class

The TimeProvider class provides two abstractions that we can use to get the current UTC time and local time zone.

C# coding challenges

C# coding challenges

Our online code editor allows you to compile the answer.

Both the GetUtcNow and LocalTimeZone properties have been marked with the virtual keyword meaning that they can be overridden in a child class.

Combined with running the GetLocalNow method, we can get the local time as it's set in the TimeProvider instance.

public abstract class TimeProvider
{
	...	
	public virtual DateTimeOffset GetUtcNow() => DateTimeOffset.UtcNow;

	public DateTimeOffset GetLocalNow()
	{
		...
	}
	
	public virtual TimeZoneInfo LocalTimeZone => TimeZoneInfo.Local;	
	...
}

The FakeTimeProvider class

The FakeTimeProvider class which inherits the TimeProvider class provides us with the SetUtcNow and SetLocalTimeZone methods to allow us to set the UTC time and local time zone.

This can be added to a unit test project with the Microsoft.Extensions.TimeProvider.Testing NuGet package.

Mock time with the FakeTimeProvider class

Mock time with the FakeTimeProvider class

Once added, we can start writing some unit tests and mock the time.

Writing unit tests to mock and test the time

We are going to create a MyCalendar class. This will provide an instance of the TimeProvider class in the constructor where the reference will be stored as a private readonly field.

In-addition, we are going to create an IsItWednesday method. This will check the current local time to see if the day of the week is currently Wednesday.

public class MyCalendar
{
	private readonly TimeProvider _timeProvider;

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

	public bool IsItWednesday()
	{
		return _timeProvider.GetLocalNow().DayOfWeek == DayOfWeek.Wednesday;
	}
}

With that done, we are going to write some unit tests in xUnit. The first is to mock the time so the date is set to a Sunday.

We'll mock the time so the date is set to 5th November 2023 which is a Sunday. We'll create a new instance of the FakeTimeProvider class, call the SetUtcNow and SetLocalTimeZone methods, and then create a new MyCalendar instance, passing in the FakeTimeProvider instance as the constructors parameter.

Then we'll do an assertion on the IsItWednesday method in the MyCalendar class. As the date is a Sunday, we expect it to return false.

[Fact]
public void MockSunday_CallIsItWednesday_ShouldReturnFalse()
{
	// Arrange
	var fakeTimeProvider = new FakeTimeProvider();

	// Set the current UTC time
	fakeTimeProvider.SetUtcNow(new DateTime(2023, 11, 5));
	fakeTimeProvider.SetLocalTimeZone(TimeZoneInfo.FindSystemTimeZoneById("Greenwich Standard Time"));

	// Act
	var result = new MyCalendar(fakeTimeProvider);

	Assert.False(result.IsItWednesday());
}

We can do a similar check to ensure that the IsItWednesday method returns true if the date is set to Wednesday.

This time, we will set the date to 8th November 2023, and change the assertion to true.

[Fact]
public void MockWednesday_CallIsItWednesday_ShouldReturnTrue()
{
	// Arrange
	var fakeTimeProvider = new FakeTimeProvider();

	// Set the current UTC time
	fakeTimeProvider.SetUtcNow(new DateTime(2023, 11, 8));
	fakeTimeProvider.SetLocalTimeZone(TimeZoneInfo.FindSystemTimeZoneById("Greenwich Standard Time"));

	// Act
	var result = new MyCalendar(fakeTimeProvider);

	Assert.True(result.IsItWednesday());
}

Mock a timer

The TimeProvider abstract class also provides a method that allows us to create a timer:

public abstract class TimeProvider
{
	...	
	public virtual ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period)
	{
		...
	}
	...
}

Combined with the FakeTimeProvider class, we are also able to write a unit test when the callback is made.

We are going to create a Callback class. Within it, we are going to provide a TimeProvider instance in the constructor and create the timer.

The timer will run once 20 seconds after it has been created and will set the HasCalledBack property to true once the timer's callback has been invoked.

public class Callback
{
	public bool HasCalledBack { get; private set; }

	public Callback(TimeProvider timeProvider)
	{
		timeProvider.CreateTimer(_ => HasCalledBack = true, this, TimeSpan.FromSeconds(20), Timeout.InfiniteTimeSpan);
	}
}

With that, we can write the xUnit unit test. Like with the previous tests, we will create a new FakeTimeProvider instance and set the current UTC time and local time zone.

We'll then pass the FakeTimeProvider instance into a new instance of the Callback class as the constructor's parameter.

[Fact]
public void Timer_InvokesCallback_SetsCallbackToTrue()
{
	var fakeTimeProvider = new FakeTimeProvider();

	var date = new DateTime(2023, 11, 8);
	fakeTimeProvider.SetUtcNow(date);
	fakeTimeProvider.SetLocalTimeZone(TimeZoneInfo.FindSystemTimeZoneById("Greenwich Standard Time"));

	var callback = new Callback(fakeTimeProvider);

	Assert.False(callback.HasCalledBack);

	fakeTimeProvider.SetUtcNow(date.AddSeconds(30));

	Assert.True(callback.HasCalledBack);
}

Initially, we expect the HasCalledBack property to still be false and we can set an assertion for that.

Then we can add 30 seconds to the current UTC time in the FakeTimeProvider instance.

Once done, we can do another assertion on the HasCalledBack property, but this time, we expect it to return true.

Watch the video

Watch our video where we talk about the TimeProvider abstract class and how we can add the FakeTimeProvider class to our xUnit test project.

In-addition, we write some unit tests to test the time and a timer.

In-addition, there is also a code example that can be downloaded which uses the same project featured in this tutorial.