- Home
- .NET tutorials
- TimeProvider makes it easier to mock time in .NET 8
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
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.
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.