15

如何对具有 await Task.Delay 的组件进行单元测试,而无需真正等待。

例如,

public void Retry()
{
    // doSomething();
    if(fail)
       await Task.Delay(5000);
}

我需要测试失败分支,但我不希望我的测试等待 5 秒。

任务并行库中是否有类似rx 虚拟基于时间的调度?

4

7 回答 7

13

计时器只是另一种形式的外部依赖。唯一的区别是它依赖于时钟而不是数据库。和任何其他外部依赖一样,它使测试变得困难;因此,答案是改进设计,直到它易于测试。

遵循依赖注入原则建议您应该将计时器传递给对象的构造函数,这使您能够注入测试存根而不是依赖于真正的计时器。

请注意这可以如何改进您的设计。在许多情况下,超时期限因呼叫者而异。这将建立超时期限的责任转移到正在等待它的应用程序层。那是应该了解它愿意等待多长时间的层。

于 2013-07-30T19:44:15.353 回答
5

任务并行库中是否有类似 rx 虚拟基于时间的调度?

不,那里没有。您的选择是定义一个可以使用测试存根实现的“计时器服务”,或者使用 Microsoft Fakes 来拦截对Task.Delay. 我更喜欢后者,但它只是 VS Ultimate 的一个选项。

于 2013-07-29T17:17:16.383 回答
5

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
    }
}
于 2014-08-08T09:06:59.687 回答
4

基于 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 的任何地方都使用它。但是有一些特定的代码片段,延迟会改变应用程序的行为,您需要对其进行测试,因此它对这些特殊情况很有用。

于 2014-09-03T15:06:14.297 回答
2

正如 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;

当然,这样做的干净方法是声明一个接口并通过您的构造函数获取它。

于 2017-12-18T10:48:13.893 回答
1

你可以考虑在你的测试中添加一个超时,如果它必须等待那么长时间就让它失败。
或者您可以考虑传入超时,或者以任何方式为测试配置不同的超时。

有一个关于 async 和 TDD 的博客和另一个在这里,尽管这些更多地指出了异步代码通常会出错的地方,而不是专门处理Task.Delay.

于 2013-07-29T17:04:46.773 回答
1

您可以添加测试接缝。这很简单,但不是很干净。您可能会或可能不会使用它,这取决于您是否相信其他人不会滥用它:-)

将公共静态属性添加到您的类

/// /// 延迟,仅用于测试的更改 /// public static int DelayMs { get; 放; } = 1000;

在您的代码中使用它

await Task.Delay(DelayMs, cancellationToken);

在您的单元测试中,将其更改为 0

MyClass.DelayMs = 0;
于 2020-03-06T16:18:19.743 回答