How can I unit test code that contain Task.Delay?

A timer is just another form of external dependency. The only difference is that it depends on the clock instead of a database. And like any other external dependency, it makes testing hard; the answer, therefore, is to improve the design until it's easy to test.

Following the Dependency Injection principle suggests that you should pass the timer in on the object's constructor, which enables you to inject a test stub instead of relying on a real timer.

Note how this can improve your design. In many cases, the timeout period varies based on who's calling. This moves the responsibility for establishing the timeout period to the layer of your application that's waiting on it. That is the layer that should understand how long it's willing to wait.


Based on alexey anwser to create a wrapper for Task.Delay, here is how to create a Task.Delay that uses Reactive Extensions' IScheduler, so you can use virtual time to test for delays:

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

This uses compilation symbols to completely avoid Rx if you are not unit testing.

AppContext is just a context object that has reference to your schedulers. In your tests, you can set AppContext.DefaultScheduler = testScheduler and the delay will be caused by the virtual time scheduler.

There is caveat though. The TestScheduler is synchronous, so you cannot start a task and use TaskEx.Delay inside, because the scheduler will advance before the task is scheduled.

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);

Instead, you need to always start the task using Observable.Start(task, scheduler), so the task runs in order:

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);

This is certainly trickier, so I wouldn't use it everywhere I use Task.Delay. But there are some specific pieces of code where the delay changes the behavior of the app and you need to test that, so it is useful for these special cases.


Is there anything like rx virtual time-based scheduling available in task parallel library?

No, there is not. Your options are to either define a "timer service" that you can implement with a test stub, or using Microsoft Fakes to intercept the call to Task.Delay. I prefer the latter, but it is only an option on VS Ultimate.


1) Define your own implementation of 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) Use TaskEx.Delay instead of Task.Delay everywhere in your code.

3) In your tests use TaskEx.SkipDelays:

[Test]
public async Task MyTest()
{
    using (TaskEx.SkipDelays())
    {
        // your code that will ignore delays
    }
}