Поделиться через


Тестирование с помощью FakeTimeProvider

Пакет 📦 Microsoft.Extensions.TimeProvider.Testing NuGet предоставляет FakeTimeProvider класс, который обеспечивает детерминированное тестирование кода, зависящее от времени. Эта фиктивная реализация позволяет вам контролировать системное время в ваших тестах, обеспечивая предсказуемые и повторяемые результаты.

Почему используйте FakeTimeProvider

Тестирование кода, зависящего от текущего времени или использования таймеров, может быть сложной задачей:

  • Недетерминированные тесты: тесты, зависящие от реального времени, могут привести к несогласованным результатам.
  • Медленные тесты: тесты, которые должны ждать, пока пройдет реальное время, могут значительно замедлить выполнение тестов.
  • Условия гонки: логика, зависящая от времени, может вводить условия гонки, которые трудно воспроизвести.
  • Пограничные случаи: тестирование логики, зависящей от времени, в определенные моменты времени (например, полночь или границы месяца) сложно при использовании реального времени.

решает эти проблемы, принимая следующие меры:

  • Предоставление полного контроля за текущим временем.
  • Позволяя вам быстро продвигать время, не ожидая.
  • Включение детерминированного тестирования поведения на основе времени.
  • Упрощая тестирование крайних случаев и граничных условий.

Начать

Чтобы начать работу с FakeTimeProvider, установите пакет NuGet Microsoft.Extensions.TimeProvider.Testing.

dotnet add package Microsoft.Extensions.TimeProvider.Testing

Дополнительные сведения см. в разделе dotnet add package или Manage package dependencies в приложениях .NET.

Базовое использование

FakeTimeProvider расширяет TimeProvider, чтобы обеспечить управляемое время для тестирования.

var fakeTimeProvider = new FakeTimeProvider();

// Get the current time (defaults to January 1, 2000, midnight UTC).
Console.WriteLine($"Start time: {fakeTimeProvider.GetUtcNow()}");

Инициализация в заданное время

Вы можете инициализировать FakeTimeProvider с определенным временем начала:

DateTimeOffset startTime = new(2025, 10, 20, 12, 0, 0, TimeSpan.Zero);
fakeTimeProvider = new FakeTimeProvider(startTime);

Console.WriteLine($"Started at: {fakeTimeProvider.GetUtcNow()}");

Заранеее время

Метод Advance перемещает время вперед по заданной длительности:

// Advance time by 30 minutes.
fakeTimeProvider.Advance(TimeSpan.FromMinutes(30));
Console.WriteLine($"After advancing 30 minutes: {fakeTimeProvider.GetUtcNow()}");

Настройка часового пояса

Задайте локальный часовой пояс для поставщика поддельных часов:

var timeZoneId = OperatingSystem.IsWindows() ? "Pacific Standard Time" : "America/Los_Angeles";
var pacificTimeZone = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId);
fakeTimeProvider.SetLocalTimeZone(pacificTimeZone);

var localTime = fakeTimeProvider.GetLocalNow();
Console.WriteLine($"Local time: {localTime}");

Тестирование отложенных операций

FakeTimeProvider особенно полезно для операций тестирования, связанных с задержками:

public class DelayedOperationTests
{
    [Fact]
    public async Task DelayedOperation_CompletesAfterDelay()
    {
        // Arrange
        var fakeTimeProvider = new FakeTimeProvider();
        var operation = new DelayedOperation(fakeTimeProvider);

        // Act
        Task task = operation.ExecuteAsync(TimeSpan.FromMinutes(5));

        // Assert - operation should not be complete yet
        Assert.False(task.IsCompleted);

        // Advance time by 5 minutes
        fakeTimeProvider.Advance(TimeSpan.FromMinutes(5));

        // Wait for the task to complete
        await task;

        // Operation should now be complete
        Assert.True(task.IsCompleted);
    }
}

public class DelayedOperation(TimeProvider timeProvider)
{
    public async Task ExecuteAsync(TimeSpan delay)
    {
        await Task.Delay(delay, timeProvider);
    }
}

Тестирование периодических операций

Тестовые операции, которые выполняются периодически с помощью таймеров:

public class PeriodicOperationTests
{
    [Fact]
    public void PeriodicOperation_ExecutesAtIntervals()
    {
        // Arrange
        var fakeTimeProvider = new FakeTimeProvider();
        var counter = new PeriodicCounter(fakeTimeProvider);
        counter.Start(TimeSpan.FromSeconds(10));

        // Act & Assert
        Assert.Equal(0, counter.Count);

        // Advance by 10 seconds
        fakeTimeProvider.Advance(TimeSpan.FromSeconds(10));
        Assert.Equal(1, counter.Count);

        // Advance by 20 more seconds
        fakeTimeProvider.Advance(TimeSpan.FromSeconds(20));
        Assert.Equal(3, counter.Count);

        // Clean up
        counter.Stop();
    }
}

public class PeriodicCounter(TimeProvider timeProvider)
{
    private ITimer? _timer;

    public int Count { get; private set; }

    public void Start(TimeSpan interval)
    {
        _timer = timeProvider.CreateTimer(
            callback: _ => Count++,
            state: null,
            dueTime: interval,
            period: interval);
    }

    public void Stop()
    {
        _timer?.Dispose();
    }
}

Тестирование бизнес-логики, связанной со временем

Протестируйте бизнес-логику, зависящую от определенных времен или дат:

public class SubscriptionTests
{
    [Fact]
    public void Subscription_ExpiresAfterOneYear()
    {
        // Arrange
        var fakeTimeProvider = new FakeTimeProvider();
        var startDate = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero);
        fakeTimeProvider.SetUtcNow(startDate);

        var subscription = new Subscription(fakeTimeProvider);
        subscription.Activate();

        // Assert - subscription is active
        Assert.True(subscription.IsActive);

        // Act - advance time by 11 months
        fakeTimeProvider.Advance(TimeSpan.FromDays(30 * 11));
        Assert.True(subscription.IsActive);

        // Advance time by 2 more months
        fakeTimeProvider.Advance(TimeSpan.FromDays(60));
        Assert.False(subscription.IsActive);
    }
}

public class Subscription(TimeProvider timeProvider)
{
    private DateTimeOffset _activationDate;

    public void Activate()
    {
        _activationDate = timeProvider.GetUtcNow();
    }

    public bool IsActive
    {
        get
        {
            DateTimeOffset currentTime = timeProvider.GetUtcNow();
            DateTimeOffset expirationDate = _activationDate.AddYears(1);
            return currentTime < expirationDate;
        }
    }
}

Интеграция с внедрением зависимостей

Используйте FakeTimeProvider в тестах для служб, зарегистрированных с использованием внедрения зависимостей:

public class CacheServiceTests
{
    [Fact]
    public void Cache_ExpiresAfterTimeout()
    {
        // Arrange
        var fakeTimeProvider = new FakeTimeProvider();

        var services = new ServiceCollection();
        services.AddSingleton<TimeProvider>(fakeTimeProvider);
        services.AddSingleton<CacheService>();

        ServiceProvider provider = services.BuildServiceProvider();
        CacheService cache = provider.GetRequiredService<CacheService>();

        // Act
        cache.Set("key", "value", TimeSpan.FromMinutes(10));

        // Assert - value is present
        Assert.True(cache.TryGet("key", out string? value));
        Assert.Equal("value", value);

        // Advance time beyond expiration
        fakeTimeProvider.Advance(TimeSpan.FromMinutes(11));

        // Value should be expired
        Assert.False(cache.TryGet("key", out _));
    }
}

public class CacheService(TimeProvider timeProvider)
{
    private readonly Dictionary<string, CacheEntry> _cache = [];

    public void Set(string key, string value, TimeSpan expiration)
    {
        DateTimeOffset expiresAt = timeProvider.GetUtcNow() + expiration;
        _cache[key] = new CacheEntry(value, expiresAt);
    }

    public bool TryGet(string key, out string? value)
    {
        if (_cache.TryGetValue(key, out CacheEntry? entry))
        {
            if (timeProvider.GetUtcNow() < entry.ExpiresAt)
            {
                value = entry.Value;
                return true;
            }

            // Entry expired, remove it
            _cache.Remove(key);
        }

        value = null;
        return false;
    }

    private record CacheEntry(string Value, DateTimeOffset ExpiresAt);
}

Лучшие практики

При использовании FakeTimeProviderрассмотрите следующие рекомендации.

  • Внедрение TimeProvider: всегда внедрять TimeProvider как зависимость, а не использовать DateTime или DateTimeOffset напрямую. Это делает код тестируемым.
  • Используйте время UTC: работа с временем UTC в бизнес-логике и преобразование в локальное время только при необходимости для отображения.
  • Тестирование крайних случаев: используйте FakeTimeProvider для тестирования крайних случаев, таких как полночь, месячные границы, переходы на летнее время и високосные годы.
  • Очистка таймеров: удаление таймеров, созданных с CreateTimer целью предотвращения утечки ресурсов в тестах.
  • Явное продвижение времени: Явно продвигайте время в ваших тестах, чтобы сделать поведение теста более ясным и предсказуемым.
  • Избегайте смешивания реального и поддельного времени: не смешивайте реальные с TimeProvider.System в одном и том же тесте, поскольку это может привести к непредсказуемому поведению.

См. также