Time-related code is usually the root cause of flaky tests. DateTime.Now checks, Thread.Sleep in unit tests, timers that fire a bit too early or too late. .NET 8 introduced TimeProvider, a built-in abstraction for clocks and timers. It’s also available for older targets via a NuGet package. With it, you can inject “time” the same way you inject loggers or random number generators, and your tests can fast-forward the clock instead of waiting in real time.
What TimeProvider gives you
GetUtcNow()/GetLocalNow()→ current time asDateTimeOffset.- High-res timestamps:
GetTimestamp()andGetElapsedTime(...)(thinkStopwatch, but injectable). - Timers:
CreateTimer(...)returns anITimerbound to the provider. - Overloads on common APIs to pass a provider, like
Task.Delay(...)andTask.WaitAsync(...).
Registering and injecting
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton(TimeProvider.System);
// Your services can ask for TimeProvider in their constructors
builder.Services.AddSingleton<TokenCache>();
builder.Services.AddHostedService<HeartbeatWorker>();
var app = builder.Build();
app.Run();
For older targets (e.g., .NET 6/7 or .NET Framework), add the package Microsoft.Bcl.TimeProvider and use the same code as those types are backported.
Example 1: replace UtcNow checks
A small service that invalidates tokens after a TTL:
public sealed class TokenCache
{
private readonly TimeProvider _time;
private readonly TimeSpan _ttl;
private readonly ConcurrentDictionary<string, (string Value, DateTimeOffset Created)> _store = new();
public TokenCache(TimeProvider time, TimeSpan ttl)
{
_time = time;
_ttl = ttl;
}
public void Put(string key, string token)
{
_store[key] = (token, _time.GetUtcNow());
}
public bool TryGet(string key, out string? token)
{
if (_store.TryGetValue(key, out var entry))
{
var age = _time.GetUtcNow() - entry.Created;
if (age <= _ttl) { token = entry.Value; return true; }
_store.TryRemove(key, out _); // expired
}
token = null;
return false;
}
}
Example 2: periodic testable work
Two options:
A) PeriodicTimer (nice for async loops)
In .NET 9+, PeriodicTimer has a constructor that accepts a TimeProvider:
public sealed class HeartbeatWorker : BackgroundService
{
private readonly TimeProvider _time;
public HeartbeatWorker(TimeProvider time) => _time = time;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5), _time);
while (await timer.WaitForNextTickAsync(stoppingToken))
{
await DoHeartbeatAsync(stoppingToken);
}
}
private Task DoHeartbeatAsync(CancellationToken ct)
=> Task.CompletedTask; // your work here
}
B) TimeProvider.CreateTimer(...) (works in .NET 8+)
If you prefer callback-style timers or need to test on .NET 8, create an ITimer:
public sealed class ReminderService : IAsyncDisposable
{
private readonly ITimer _timer;
public ReminderService(TimeProvider time, TimeSpan due, TimeSpan period, Action tick)
{
_timer = time.CreateTimer(_ => tick(), state: null, due, period);
}
public ValueTask DisposeAsync() => _timer.DisposeAsync();
}
CreateTimer ties the schedule to the provider, so advancing a fake clock will invoke the callback.
Deterministic tests with FakeTimeProvider
Use Microsoft.Extensions.TimeProvider.Testing. It ships a FakeTimeProvider that starts at a timestamp you pick and moves only when you tell it to. Advancing time triggers timers and completes delays bound to that provider.
using Microsoft.Extensions.Time.Testing;
using Xunit;
public class TokenCacheTests
{
[Fact]
public void Expiry_Is_Driven_By_Fake_Clock()
{
var start = new DateTimeOffset(2000, 1, 1, 0, 0, 0, TimeSpan.Zero);
var clock = new FakeTimeProvider(start);
var cache = new TokenCache(clock, TimeSpan.FromMinutes(10));
cache.Put("k", "v1");
Assert.True(cache.TryGet("k", out var v) && v == "v1");
// jump 10 minutes + 1 second; no Thread.Sleep
clock.Advance(TimeSpan.FromMinutes(10) + TimeSpan.FromSeconds(1));
Assert.False(cache.TryGet("k", out _));
}
}
Test delay/timeout logic:
[Fact]
public async Task TryPingAsync_Times_Out_Without_Wait()
{
var clock = new FakeTimeProvider(DateTimeOffset.UnixEpoch);
var neverCompletes = new Func<CancellationToken, Task>(_ => Task.Delay(Timeout.InfiniteTimeSpan));
var ok = await TryPingAsync(neverCompletes, TimeSpan.FromSeconds(30), clock);
// still false until time advances
Assert.False(ok);
clock.Advance(TimeSpan.FromSeconds(30));
// the WaitAsync(timeout, timeProvider) will now complete due to the fake clock
// Re-run with a task that now "completes" if you want to assert success path
}
DI in tests
Swap the clock in your test host:
var fake = new FakeTimeProvider(DateTimeOffset.Parse("2025-01-01T00:00:00Z"));
var app = new WebApplicationFactory<Program>()
.WithWebHostBuilder(b => b.ConfigureServices(s =>
{
// replace System with fake
s.AddSingleton<TimeProvider>(fake);
}));
// You have the ability to jump forward in time 🙂
fake.Advance(TimeSpan.FromMinutes(2));
Conclusion
Treat time as a dependency, not a global. Swap DateTime/Stopwatch/Timer calls for TimeProvider, pass it to delays and waits, and drive tests with FakeTimeProvider. You’ll cut flakiness, drop Thread.Sleep/await Task.Delay(), and keep production behavior intact while tests run fast and repeatable.
Affiliate promo
If you love learning new stuff and want to support me, consider buying a course from Dometrain using this link: Browse courses – Dometrain. Thank you!
Leave a comment