There is only one thing worse than having no tests and that is having flaky tests. You know, tests that sometimes fail and sometimes succeed when the code itself didn't change. One of the biggest reasons this can happen is when your tests have time related logic.
My tests succeed on Monday but not during the weekend.
Sidenote: there is a cool Azure DevOps feature specifically to handle and fix flaky tests. Anyway that is not the topic of this post.
In .NET 8 Microsoft tried to fix this by introducing a new time abstraction API. Instead of directly calling DateTime.Now
or DateTime.UtcNow
inside your code, you can now use the TimeProvider class and ITimer interface. Together they offer time abstraction functionality, facilitating the simulation of time in testing scenarios.
public abstract class TimeProvider | |
{ | |
public static TimeProvider System { get; } | |
protected TimeProvider() | |
public virtual DateTimeOffset GetUtcNow() | |
public DateTimeOffset GetLocalNow() | |
public virtual TimeZoneInfo LocalTimeZone { get; } | |
public virtual long TimestampFrequency { get; } | |
public virtual long GetTimestamp() | |
public TimeSpan GetElapsedTime(long startingTimestamp) | |
public TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp) | |
public virtual ITimer CreateTimer(TimerCallback callback, object? state,TimeSpan dueTime, TimeSpan period) | |
} | |
public interface ITimer : IDisposable, IAsyncDisposable | |
{ | |
bool Change(TimeSpan dueTime, TimeSpan period); | |
} |
Remark: Microsoft was so nice to backport these APIs and made them available in a separate NuGet package Microsoft.Bcl.TimeProvider.
So now we can update our code to use the TimeProvider class instead:
public class Calendar | |
{ | |
private readonly TimeProvider _timeProvider; | |
public Calendar(TimeProvider timeProvider) | |
{ | |
_timeProvider = timeProvider; | |
} | |
public bool IsItWeekend() | |
{ | |
return _timeProvider.GetLocalNow().DayOfWeek == DayOfWeek.Saturday || DayOfWeek.Sunday; | |
} | |
} |
If we now want to test our code we can use the Microsoft.Extensions.TimeProvider.Testing package. This includes a TimeProvider
implementation called FakeTimeProvider
which gives us explicit control over the flow of time.
An example:
[Fact] | |
public void IsItWeekend() | |
{ | |
// Arrange | |
var fakeTimeProvider = new FakeTimeProvider(); | |
// Set the current UTC time | |
fakeTimeProvider.SetUtcNow(new DateTime(2024, 2, 7)); | |
fakeTimeProvider.SetLocalTimeZone(TimeZoneInfo.FindSystemTimeZoneById("Greenwich Standard Time")); | |
// Act | |
var result = new Calendar(fakeTimeProvider); | |
//Assert | |
Assert.False(result.IsItWeekend()); | |
} |
These new abstractions can also be used in asynchronous program execution thanks to some new overloads:
public partial class Task : IAsyncResult, IDisposable | |
{ | |
public static Task Delay(TimeSpan delay, TimeProvider timeProvider) | |
} | |
public partial class Task<TResult> : Task | |
{ | |
public new Task<TResult> WaitAsync(TimeSpan timeout, TimeProvider timeProvider) | |
} |
Nice!