TGViewer
.NET Разработчик .NET Разработчик @netdeveloperdiary · 6.74K subscribers
Post #3313 1.56K
День 2768. #ЗаметкиНаПолях
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/
  • 👍 6
More from @netdeveloperdiary
  1. Sep 26, 2026День 2796. #ЗаметкиНаПолях #AI Рабочий процесс с Copilot для .NET. Продолжение Начало Три…
  2. Sep 25, 2026День 2795. #ЗаметкиНаПолях #AI Рабочий процесс с Copilot для .NET. Начало Проблема с позиц…
  3. Sep 24, 2026День 2794. #Оффтоп #Здоровье Сегодня будет необычный пост. Завтра в Москве стартует конфер…
  4. Sep 23, 2026День 2793. #ЗаметкиНаПолях #SQL 10 Редких Возможностей SQL, Которые Стоит Знать Каждому. Ч…
  5. Sep 22, 2026День 2792. #ЗаметкиНаПолях #SQL 10 Редких Возможностей SQL, Которые Стоит Знать Каждому. Ч…
  6. Sep 21, 2026🔍Тестовое собеседование с Senior C# разработчиком уже завтра 22 сентября(уже завтра!) в 1…
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →