Testando com FakeTimeProvider

O 📦 Microsoft.Extensions.TimeProvider.Testing pacote NuGet fornece uma FakeTimeProvider classe que permite o teste determinístico de código que depende do tempo. Essa implementação falsa permite controlar o tempo do sistema em seus testes, garantindo resultados previsíveis e repetíveis.

Por que usar FakeTimeProvider

O código de teste que depende da hora atual ou usa temporizadores pode ser desafiador:

  • Testes não determinísticos: testes que dependem de tempo real podem produzir resultados inconsistentes.
  • Testes lentos: testes que precisam esperar que o tempo real passe podem atrasar significativamente a execução dos testes.
  • Condições de corrida: a lógica dependente do tempo pode introduzir condições de corrida difíceis de reproduzir.
  • Casos extremos: testar a lógica baseada em tempo em momentos específicos (como à meia-noite ou em limites de mês) é difícil com tempo real.

O FakeTimeProvider enfrenta esses desafios por:

  • Fornecendo controle completo sobre a hora atual.
  • Permitir que você avance o tempo instantaneamente sem esperar.
  • Habilitando o teste determinístico do comportamento baseado em tempo.
  • Facilitando o teste de casos extremos e condições de fronteira.

Comece agora

Para começar com FakeTimeProvider, instale o pacote NuGet Microsoft.Extensions.TimeProvider.Testing.

dotnet add package Microsoft.Extensions.TimeProvider.Testing

Para obter mais informações, consulte dotnet add package ou Gerenciar as dependências de pacotes em aplicativos .NET.

Uso Básico

FakeTimeProvider estende TimeProvider para fornecer tempo controlável para testes:

var fakeTimeProvider = new FakeTimeProvider();

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

Inicializar com tempo específico

Você pode inicializar FakeTimeProvider com um horário de início específico:

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

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

Tempo de antecedência

O Advance método move o tempo para frente por uma duração especificada:

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

Configurar fuso horário

Defina o fuso horário local para o provedor de horário falso:

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

Testar operações atrasadas

FakeTimeProvider é particularmente útil para testar operações que envolvem atrasos:

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

Testar operações periódicas

Operações de teste que são executadas periodicamente usando temporizadores:

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

Testar a lógica de negócios baseada em tempo

Testar a lógica de negócios que depende de datas ou horas específicas:

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

Integração com injeção de dependência

Use FakeTimeProvider em testes para serviços registrados com injeção de dependência:

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

Práticas recomendadas

Ao usar FakeTimeProvider, considere as seguintes práticas recomendadas:

  • Injetar TimeProvider: Sempre injete TimeProvider como uma dependência em vez de usar DateTime ou DateTimeOffset diretamente. Isso torna seu código testável.
  • Use a hora UTC: trabalhe com a hora UTC em sua lógica de negócios e converta em hora local somente quando necessário para exibição.
  • Testes de casos extremos: use FakeTimeProvider para testar casos extremos como meia-noite, limites de mês, transições de horário de verão e anos bissextos.
  • Limpar temporizadores: Elimine os temporizadores criados com CreateTimer para evitar vazamentos de recursos durante seus testes.
  • Ajuste deliberado do tempo: avance o tempo explicitamente em seus testes para tornar o comportamento do teste claro e previsível.
  • Evite misturar tempo real e falso: não misture o tempo real TimeProvider.System com FakeTimeProvider no mesmo teste, pois isso pode levar a um comportamento imprevisível.

Consulte também