Testen mit FakeTimeProvider

Das 📦 Microsoft.Extensions.TimeProvider.Testing NuGet-Paket stellt eine FakeTimeProvider Klasse bereit, die deterministische Tests von Code ermöglicht, der von der Zeit abhängt. Diese gefälschte Implementierung ermöglicht es Ihnen, die Systemzeit innerhalb Ihrer Tests zu steuern, um vorhersehbare und wiederholbare Ergebnisse sicherzustellen.

Gründe für die Verwendung von FakeTimeProvider

Das Testen von Code, der von der aktuellen Zeit abhängt oder Zeitgeber verwendet, kann eine Herausforderung darstellen:

  • Nicht deterministische Tests: Tests, die von Echtzeit abhängen, können inkonsistente Ergebnisse erzeugen.
  • Langsame Tests: Tests, die darauf warten müssen, dass die tatsächliche Zeit vergeht, können die Testausführung erheblich verlangsamen.
  • Rennbedingungen: Zeitabhängige Logik kann Rennbedingungen einführen, die schwer zu reproduzieren sind.
  • Randfälle: Das Testen zeitbasierter Logik zu bestimmten Zeiten (z. B. Mitternacht oder Monatsende) ist in Echtzeit schwierig.

Die FakeTimeProvider adressiert diese Herausforderungen durch:

  • Bieten vollständige Kontrolle über die aktuelle Zeit.
  • Sie können die Zeit sofort ohne Wartezeit voranschreiten.
  • Aktivieren von deterministischen Tests des zeitbasierten Verhaltens.
  • Dies erleichtert das Testen von Randfällen und Grenzbedingungen.

Loslegen

Um mit FakeTimeProvider zu beginnen, installieren Sie das Microsoft.Extensions.TimeProvider.Testing NuGet-Paket.

dotnet add package Microsoft.Extensions.TimeProvider.Testing

Weitere Informationen finden Sie unter dotnet add package or Manage package dependencies in .NET applications.

Grundlegende Nutzung

Die FakeTimeProvider verlängert TimeProvider, um eine steuerbare Zeit für Tests bereitzustellen.

var fakeTimeProvider = new FakeTimeProvider();

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

Initialisieren mit einem festgelegten Zeitpunkt

Sie können FakeTimeProvider mit einer bestimmten Startzeit initialisieren.

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

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

Vorlaufzeit

Die Advance Methode verschiebt die Zeit um eine angegebene Dauer vorwärts:

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

Konfigurieren der Zeitzone

Legen Sie die lokale Zeitzone für den gefälschten Zeitanbieter fest:

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}");

Verzögerte Operationen testen

FakeTimeProvider ist besonders nützlich für Testvorgänge, die Verzögerungen erfordern:

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);
    }
}

Regelmäßige Vorgänge testen

Testvorgänge, die in regelmäßigen Abständen mit Zeitgebern ausgeführt werden:

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();
    }
}

Testen zeitbasierter Geschäftslogik

Testen Sie geschäftslogik, die von bestimmten Uhrzeiten oder Datumsangaben abhängt:

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;
        }
    }
}

Integration mit Abhängigkeitsinjektion

Verwendung FakeTimeProvider in Tests für Dienste, die mit Abhängigkeitsinjektion registriert sind:

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);
}

Bewährte Methoden

Berücksichtigen Sie bei der Verwendung FakeTimeProviderdie folgenden bewährten Methoden:

  • Inject TimeProvider: Verwenden Sie TimeProvider immer als Abhängigkeit, anstatt DateTime oder DateTimeOffset direkt zu verwenden. Dadurch kann Ihr Code getestet werden.
  • Verwenden Sie UTC-Zeit: Arbeiten Sie mit UTC-Zeit in Ihrer Geschäftslogik, und konvertieren Sie sie nur bei Bedarf für die Anzeige in ortsspezifische Zeit.
  • Testen Sie Randfälle: Verwenden Sie FakeTimeProvider, um Randfälle wie Mitternacht, Monatsgrenzen, Sommerzeitübergänge und Schaltjahre zu testen.
  • Bereinigen von Zeitgebern: Aufräumen von Zeitgebern, die mit CreateTimer erstellt wurden, um Ressourcenlecks in Ihren Tests zu vermeiden.
  • Zeit bewusst vorverlegen: Verlegen Sie in Ihren Tests die Zeit bewusst, um das Verhalten der Tests klar und vorhersehbar zu gestalten.
  • Vermeiden Sie das Mischen von realer und simulierten Zeit: Mischen Sie nicht reale Zeit mit im selben Test, da dies zu unvorhersehbarem Verhalten führen kann.

Siehe auch