如何对具有等待Task.Delay的组件进行单元测试而无需实际等待。
例如,
public void Retry()
{
// doSomething();
if(fail)
await Task.Delay(5000);
}
我需要测试故障分支,但我不想让我的测试等待5秒钟。
在任务并行库中是否有类似rx基于虚拟时间的调度的东西可用?
如何对具有等待Task.Delay的组件进行单元测试而无需实际等待。
例如,
public void Retry()
{
// doSomething();
if(fail)
await Task.Delay(5000);
}
我需要测试故障分支,但我不想让我的测试等待5秒钟。
在任务并行库中是否有类似rx基于虚拟时间的调度的东西可用?
1) 自定义实现Task.Delay。
public static class TaskEx
{
private static bool _shouldSkipDelays;
public static Task Delay(TimeSpan delay)
{
return _shouldSkipDelays ? Task.FromResult(0) : Task.Delay(delay);
}
public static IDisposable SkipDelays()
{
return new SkipDelaysHandle();
}
private class SkipDelaysHandle : IDisposable
{
private readonly bool _previousState;
public SkipDelaysHandle()
{
_previousState = _shouldSkipDelays;
_shouldSkipDelays = true;
}
public void Dispose()
{
_shouldSkipDelays = _previousState;
}
}
}
2) 在您的代码中,应该使用TaskEx.Delay而不是Task.Delay。
3) 在您的测试中使用TaskEx.SkipDelays:
[Test]
public async Task MyTest()
{
using (TaskEx.SkipDelays())
{
// your code that will ignore delays
}
}
在任务并行库中是否有类似于rx虚拟时间调度的东西呢?
很遗憾,没有。你的选择是要么定义一个“计时器服务”,可以用测试桩实现,要么使用Microsoft Fakes截取对Task.Delay
的调用。我更喜欢后者,但它只适用于VS Ultimate。
基于Alexey的回答,创建了一个Task.Delay的包装器。下面是如何创建一个使用响应式扩展(IScheduler)的Task.Delay,这样您可以使用虚拟时间来测试延迟:
using System;
using System.Reactive.Linq;
using System.Reactive.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;
public static class TaskEx
{
public static Task Delay(int millisecondsDelay, CancellationToken cancellationToken = default(CancellationToken))
{
#if TEST
return Observable.Timer(TimeSpan.FromMilliseconds(millisecondsDelay), AppContext.DefaultScheduler).ToTask(cancellationToken);
#else
return Task.Delay(millisecondsDelay, cancellationToken);
#endif
}
}
如果您不进行单元测试,可以使用编译符号完全避免使用 Rx。
AppContext只是一个上下文对象,它引用了你的调度程序。在你的测试中,你可以设置 AppContext.DefaultScheduler = testScheduler
,延迟将由虚拟时间调度器引起。
然而,需要注意一点。TestScheduler是同步的,因此您不能启动任务并在其中使用TaskEx.Delay,因为调度程序会先于任务被调度。
var scheduler = new TestScheduler();
AppContext.DefaultScheduler = scheduler;
Task.Run(async () => {
await TaskEx.Delay(100);
Console.Write("Done");
});
/// this won't work, Task.Delay didn't run yet.
scheduler.AdvanceBy(1);
相反,您需要始终使用Observable.Start(task, scheduler)
来启动任务,以便任务按顺序运行:
var scheduler = new TestScheduler();
AppContext.DefaultScheduler = scheduler;
Observable.Start(async () => {
await TaskEx.Delay(100);
Console.Write("Done");
}, scheduler);
/// this runs the code to schedule de delay
scheduler.AdvanceBy(1);
/// this actually runs until the delay is complete
scheduler.AdvanceBy(TimeSpan.FromMilliseconds(100).Ticks);
这确实更加棘手,因此我不会在使用Task.Delay的所有地方都使用它。但是有一些特定的代码片段,在其中延迟会改变应用程序的行为并需要进行测试,因此对于这些特殊情况非常有用。
private Func<int, Task> delayer = millisecondsDelay => Task.Delay(millisecondsDelay);
public Func<int, Task> Delayer
{
get { return delayer; }
set { delayer = value ?? (millisecondsDelay => Task.Delay(millisecondsDelay)) }
}
sut.Delayer = _ => Task.CompletedTask;
await Task.Delay(DelayMs, cancellationToken);
MyClass.DelayMs = 0;