如何对包含Task.Delay的代码进行单元测试?
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何对包含Task.Delay的代码进行单元测试?相关的知识,希望对你有一定的参考价值。
如何单元测试具有等待Task.Delay的组件,而不必等待它。
例如,
public void Retry()
{
// doSomething();
if(fail)
await Task.Delay(5000);
}
我需要测试失败分支,但我不希望我的测试等待5秒。
在任务并行库中是否有类似rx virtual time-based scheduling的东西?
计时器只是外部依赖的另一种形式。唯一的区别是它取决于时钟而不是数据库。和任何其他外部依赖一样,它使测试变得困难;因此,答案是改进设计,直到它易于测试。
遵循依赖注入原则建议您应该在对象的构造函数中传递计时器,这使您能够注入测试存根而不是依赖于实时计时器。
请注意这可以改善您的设计。在许多情况下,超时时间根据呼叫者的不同而不同。这将建立超时期限的责任转移到等待它的应用程序层。这层应该了解它愿意等多久。
在任务并行库中是否有类似rx虚拟时间调度的功能?
不,那里没有。您可以选择定义可以使用测试存根实现的“计时器服务”,也可以使用Microsoft Fakes拦截对Task.Delay
的调用。我更喜欢后者,但它只是VS Ultimate的一个选项。
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
}
}
您可以考虑在测试中添加超时,如果必须等待那么长则让它失败。 或者您可以考虑传递超时,或者以不同方式为测试配置超时。
在async和TDD上有一个blog和另一个here,虽然这些更多指出了一般可能出错的异步代码而不是特定处理Task.Delay
。
正如John Deters所提到的,这是对计算机时钟的外部依赖(就像你需要获取当前时间一样,虽然很容易调用DateTime.UtcNow它仍然是依赖项)。
但是,这些是特殊的依赖项,因为您可以提供始终有效的默认值(Task.Delay或DateTime.UtcNow)。因此,您可以拥有一个属性来执行此操作:
private Func<int, Task> delayer = millisecondsDelay => Task.Delay(millisecondsDelay);
public Func<int, Task> Delayer
{
get { return delayer; }
set { delayer = value ?? (millisecondsDelay => Task.Delay(millisecondsDelay)) }
}
使用此方法,您可以在测试中替换对Task.Delay的调用。
sut.Delayer = _ => Task.CompletedTask;
当然,干净的方法是声明一个接口并通过构造函数获取它。
基于alexey anwser为Task.Delay创建包装器,这里是如何创建使用Reactive Extensions的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。但是有一些特定的代码片段,其中延迟会改变应用程序的行为,您需要对其进行测试,因此对于这些特殊情况非常有用。
以上是关于如何对包含Task.Delay的代码进行单元测试?的主要内容,如果未能解决你的问题,请参考以下文章
android中如何对一个包含调用数据库的activity进行单元测试啊?
如何使用 Jest 和 Enzyme 对包含 history.push 的反应事件处理程序进行单元测试?