Intro
.NET 8 将引入一个时间抽象 TimeProvider
,在之前的版本中遇到时间相关的逻辑一般想要 mock 时间如 (DateTime.UtcNow
/DateTime.Now
) 会非常的困难以后需要 mock 的逻辑使用 TimeProvider
就可以很容易进行 mock 了.
New API
新增的一些 API 如下:
public abstract class TimeProvider
{
public static TimeProvider System { get; }
protected TimeProvider()
public virtual DateTimeOffset GetUtcNow()
public DateTimeOffset GetLocalNow()
public virtual TimeZoneInfo LocalTimeZone { get; }
public virtual long TimestampFrequency { get; }
public virtual long GetTimestamp()
public TimeSpan GetElapsedTime(long startingTimestamp)
public TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp)
public virtual ITimer CreateTimer(TimerCallback callback, object? state,TimeSpan dueTime, TimeSpan period)
}
public interface ITimer : IDisposable, IAsyncDisposable
{
bool Change(TimeSpan dueTime, TimeSpan period);
}
public partial class CancellationTokenSource : IDisposable
{
public CancellationTokenSource(TimeSpan delay, TimeProvider timeProvider)
}
public sealed partial class PeriodicTimer : IDisposable
{
public PeriodicTimer(TimeSpan period, TimeProvider timeProvider)
}
public partial class Task : IAsyncResult, IDisposable
{
public static Task Delay(System.TimeSpan delay, System.TimeProvider timeProvider)
public static Task Delay(System.TimeSpan delay, System.TimeProvider timeProvider, System.Threading.CancellationToken cancellationToken)
public Task WaitAsync(TimeSpan timeout, TimeProvider timeProvider)
public Task WaitAsync(TimeSpan timeout, TimeProvider timeProvider, CancellationToken cancellationToken)
}
public partial class Task<TResult> : Task
{
public new Task<TResult> WaitAsync(TimeSpan timeout, TimeProvider timeProvider)
public new Task<TResult> WaitAsync(TimeSpan timeout, TimeProvider timeProvider, CancellationToken cancellationToken)
}
从上述 TimeProvider
是一个抽象类型,我们可以重写它的一些逻辑,下面我们来看一些示例:针对 CancellationTokenSource
和 Task.Delay
/Task.WaitAsybc
也支持了 TimeProvider
的参数,这也意味着我们可以方便的控制一些 task 的等待时间等,针对 task 的超时也可以比较容易的进行 mock
Samples
来看第一个示例:
var now = TimeProvider.System.GetUtcNow();
Console.WriteLine(now.ToString());
Console.WriteLine(TimeProvider.System.GetLocalNow());
Console.WriteLine();
var mockTimeProvider = new MockTimeProvider(now.AddYears(100));
Console.WriteLine(mockTimeProvider.GetUtcNow());
Console.WriteLine(mockTimeProvider.GetLocalNow());
Console.WriteLine();
TimeProvider.System
是框架提供的,TimeProvider.System.GetUtcNow()
等同于我们直接使用 System.DateTime.UtcNow
,这里我们使用了一个 MockTimeProvider
来模拟mock 一个 TimeProvider
, 实现如下:
file sealed class MockTimeProvider : TimeProvider
{
private readonly DateTimeOffset _dateTimeOffset;
public MockTimeProvider(DateTimeOffset dateTimeOffset)
{
_dateTimeOffset = dateTimeOffset;
}
public override DateTimeOffset GetUtcNow()
{
return _dateTimeOffset;
}
public override TimeZoneInfo LocalTimeZone => TimeZoneInfo.Utc;
}
这里重写了 TimeProvider
中的 GetUtcNow()
直接返回了一个固定的时间,并且指定了 LocalTimeZone
为 TimeZoneInfo.Utc
输出结果如下:
8/5/2023 15:21:39 +00:00
8/5/2023 23:21:39 +08:00
8/5/2123 15:21:39 +00:00
8/5/2123 15:21:39 +00:00
可以看到使用我们 mock 的 TimeProvider
输出的时间是我们输入的一个假的时间,并且 localNow 和 utcNow 是一致的,因为我们 mock 的 TimeProvider
指定了 local 时区也是 utc 时间让我们来看第二个示例:
var startTimestamp = TimeProvider.System.GetTimestamp();
Thread.Sleep(1000);
var elapsedTime = TimeProvider.System.GetElapsedTime(startTimestamp);
Console.WriteLine(elapsedTime);
Console.WriteLine();
var startTimestamp1 = mockTimeProvider.GetTimestamp();
Thread.Sleep(3000);
var elapsedTime1 = mockTimeProvider.GetElapsedTime(startTimestamp1);
Console.WriteLine(elapsedTime1);
Console.WriteLine();
这里我们使用 GetTimestamp()
来统计某个方法或操作的耗时,这的方法在 .NET 7 中引入到 Stopwatch 类型中,在 TimeProvider
中也引入这个方法TimeProvider.System.GetTimestamp()
/TimeProvider.System.GetElapsedTime
等同于直接使用 Stopwatch.GetTimestamp()
/Stopwatch.GetElapsedTime
这里我们对 mockTimeProvider 做改一点改造,重写了一下 TimestampFrequency
,将其扩大了 3 倍,这样我们的统计数来的耗时就会缩小 3 倍,让我们看一下输出结果,就比较清晰的理解了
file sealed class MockTimeProvider : TimeProvider
{
public override long TimestampFrequency => Stopwatch.Frequency * 3;
}
输出结果如下:
00:00:01.0147489
00:00:01.0034881
可以看到,我们两次输出的结果都是 1 秒,尽管我们第二次 sleep 的时间是 3 秒而非 1 秒,因为我们重写了 TimestampFrequency
导致时间缩短了最后我们再看一下 Task.Delay
的示例:
Console.WriteLine(TimeProvider.System.GetUtcNow());
Task.Delay(TimeSpan.FromSeconds(1), mockTimeProvider).Wait();
Console.WriteLine(TimeProvider.System.GetUtcNow());
这里我们对 mockTimeProvider 继续做一些修改:Task.Delay
是利用了一个 Timer
到时间了修改 task 的状态,所以我们需要重写 CreateTimer
方法,首先我们实现一个 MockTimer
,再重写方法
file sealed class MockTimer : ITimer
{
private readonly ITimer _time;
public MockTimer(
TimerCallback callback,
object? state,
TimeSpan dueTime,
TimeSpan period)
{
_time = TimeProvider.System.CreateTimer(callback, state, dueTime * 2, period);
}
public void Dispose()
{
_time.Dispose();
}
public ValueTask DisposeAsync()
{
return _time.DisposeAsync();
}
public bool Change(TimeSpan dueTime, TimeSpan period)
{
return _time.Change(dueTime, period);
}
}
file sealed class MockTimeProvider : TimeProvider
{
public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period)
{
return new MockTimer(callback, state, dueTime, period);
}
}
输出结果如下:
8/5/2023 15:50:02 +00:00
8/5/2023 15:50:04 +00:00
可以看到,我们 Task.Delay
1 秒实际经过我们的 mock TimeProvider
变成了两秒因为我们 MockTimer
对 dueTime
做了两倍的处理
More
TimeProvider 的引入能够为基于时间的测试 mock 变得非常简单,如果你也遇到需要对时间进行 mock 的场景,可以尝试一下。
这不仅限于 .NET 8 及以后的框架,针对之前的框架微软提供了一个单独的 nuget package Microsoft.Bcl.TimeProvider,如果是之前的框架不好升级可以引用这个 nuget package 使用