TimeProvider — Конец Нетестируемого DateTime.Now. Окончание
Начало
Тестирование с FakeTimeProvider
.NET предоставляет класс FakeTimeProvider в отдельном NuGet-пакете:
dotnet add package Microsoft.Extensions.TimeProvider.Testing
Вы можете задать начальную точку или оставить значение по умолчанию, если вас не интересует точное значение:
// Определённый момент времени
var fakeTime = new FakeTimeProvider(
new DateTimeOffset(2026, 8, 29, 0, 0, 0, TimeSpan.Zero));
// По умолчанию
var fakeTime = new FakeTimeProvider();
В предыдущем примере SubscriptionReminderService принимает TimeProvider (абстрактный базовый класс), куда также можно напрямую передать FakeTimeProvider:
using Microsoft.Extensions.Time.Testing;
[Fact]
public async Task GetExpiringSubs_ReturnsSubsWithin7Days()
{
var fakeTime = new FakeTimeProvider(
new DateTimeOffset(2026, 9, 1, 0, 0, 0, TimeSpan.Zero));
var repo = new InMemorySubscriptionRepository(new[]
{
new Subscription {
Id = 1,
Expires = new DateTimeOffset(2026, 9, 6, 0, 0, 0, TimeSpan.Zero) },
new Subscription {
Id = 2,
Expires = new DateTimeOffset(2026, 9, 8, 0, 0, 0, TimeSpan.Zero) },
});
var service =
new SubscriptionReminderService(fakeTime, repo);
var expiring = await service.GetExpiringSubsAsync();
Assert.Single(expiring);
Assert.Equal(1, expiring.First().Id);
}
1я подписка истекает 6 сентября (через 6 дней), вторая – 8 сентября (не входит в 7-дневное окно истекающих подписок). Тест детерминирован вне зависимости от того, когда он выполняется.
Перемотка времени вперед
Настоящая мощь проявляется в сценариях, связанных с течением времени. У класса FakeTimeProvider есть метод
Advance(TimeSpan delta), который переводит часы вперёд на указанную величину:[Fact]
public async Task GetExpiringSubs_AfterTimeAdvances_CapturesMoreSubscriptions()
{
var fakeTime = new FakeTimeProvider(
new DateTimeOffset(2026, 9, 1, 0, 0, 0, TimeSpan.Zero));
var repos = new InMemorySubscriptionRepository(new[]
{
new Subscription {
Id = 1,
Expires = new DateTimeOffset(2026, 9, 6, 0, 0, 0, TimeSpan.Zero) },
new Subscription {
Id = 2,
Expires = new DateTimeOffset(2026, 9, 8, 0, 0, 0, TimeSpan.Zero) },
});
var service =
new SubscriptionReminderService(fakeTime, repo);
// 1 сентября только 1я подписка в истекающих
var check1 = await service.GetExpiringSubsAsync();
Assert.Single(check1);
// 5 сентября - обе
fakeTime.Advance(TimeSpan.FromDays(5));
var check2 = await service.GetExpiringSubsAsync();
Assert.Equal(2, check2.Count());
}
Если нужно перейти к точному моменту времени, это сделает метод
SetUtcNow(DateTimeOffset utcNow). Оба метода также запускают любые ожидающие таймеры, время срабатывания которых попадает в диапазон перемотки (подробности ниже).Эти методы особенно полезны для тестирования логики, которая накапливает состояние с течением времени: истечение срока действия кэша, задержка повторной попытки, вычисления с использованием скользящего окна. Вы точно контролируете, когда наступит «сейчас», без фактического ожидания.
Таймеры
TimeProvider также предоставляет метод
CreateTimer(), который возвращает ITimer из System.Threading:ITimer timer = tp.CreateTimer(
callback: state => DoSomeWork(),
state: null,
dueTime: TimeSpan.FromMinutes(5),
period: TimeSpan.FromMinutes(5));
Он идентичен System.Threading.Timer (те же параметры, тот же метод
Change(TimeSpan dueTime, TimeSpan period) для перепланирования), но основан на абстракции. В FakeTimeProvider вызов Advance(), приводящий к истечению времени таймера, запустит обратный вызов таймера (в примере выше – DoSomeWork()). Не нужно использовать Thread.Sleep или Task.Delay.Это важно, если у вас есть фоновые сервисы или рабочие процессы, которые выполняют периодическую работу с помощью таймеров. Как только они принимают TimeProvider, весь цикл планирования становится тестируемым.
Источник: https://blog.maartenballiauw.be/posts/2026-07-22-timeprovider-and-the-end-of-untestable-datetime-now/