如何对包含Task.Delay的代码进行单元测试?

问题描述 投票:6回答:6

如何单元测试具有等待Task.Delay的组件,而不必等待它。

例如,

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

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

在任务并行库中是否有类似rx virtual time-based scheduling的东西?

unit-testing tdd task-parallel-library async-await system.reactive
6个回答
7
投票

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

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

请注意这可以改善您的设计。在许多情况下,超时时间根据呼叫者的不同而不同。这将建立超时期限的责任转移到等待它的应用程序层。这层应该了解它愿意等多久。


4
投票

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

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


2
投票

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

1
投票

您可以考虑在测试中添加超时,如果必须等待那么长则让它失败。 或者您可以考虑传递超时,或者以不同方式为测试配置超时。

在async和TDD上有一个blog和另一个here,虽然这些更多指出了一般可能出错的异步代码而不是特定处理Task.Delay


1
投票

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

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


0
投票

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

© www.soinside.com 2019 - 2024. All rights reserved.