mercredi 22 mars 2017

Why does this async unit test block the thread forever?

Objective

I want to compose asynchronous workflows in C# using Task<T> with custom extension methods. I want to unit test each of the basic extension methods I will use to ensure that they work properly, so there are no surprises in behavior when I start composing them together. One of the most important aspects of this testing is making sure that these methods run tasks when they should, NOT immediately when the workflow is being composed, but only when the composed workflow is awaited.

In practice, my extension methods seem to be working fine. However, I have not been able to create a unit test that tests the awaiting aspect of these methods without blocking the test thread.

I am using Visual Studio 2015, C# 5, .NET 4.5.1, and NUnit 3.

Code

Here is one such extension method I would like to test:

public static async Task<T> Let<T>(this Task<T> source, Action<T> action)
{
    var result = await source;
    action(result);
    return result;
}

Here are the tests I've made for that method. (This project is using Shouldly which provides a fluent interface for NUnit, so you can write x.ShouldBe(3) to mean Assert.AreEqual(3, x).)

[TestFixture, Category("Task")]
public class WhenLettingTask {

    private static bool sourceExecuted;
    private static int sideEffectResult;

    private static Task<int> Source =>
        new Task<int>(() => {
            sourceExecuted = true;
            return 1;
        });

    private static readonly Action<int> SideEffect =
        n => { sideEffectResult = n; };

    [SetUp]
    public void TestSetup() {
        sourceExecuted = false;
        sideEffectResult = 0;
    }


    [Test]
    public void ShouldNotExecuteAnythingImmediately() {
        var composed = Source.Let(SideEffect);

        sourceExecuted.ShouldBeFalse();
        sideEffectResult.ShouldBe(0);
    }

    [Test]
    public async Task ReturnedTaskShouldExecuteInputsOnlyOnce() {
        var composed = Source.Let(SideEffect);

        var result = await composed; //Blocks forever
        sourceExecuted.ShouldBeTrue();
        sideEffectResult.ShouldBe(1);

        sourceExecuted = false;
        sideEffectResult = 0;

        for (var i = 0; i < 10; i++) {
            result = await composed;

            sourceExecuted.ShouldBeFalse();
            sideEffectResult.ShouldBe(0);
        }
    }
}

The first test works as expected, but the second one blocks on the first await forever.

Research

Interestingly, if I remove the await inside the method under test, the test will not block.

public static async Task<T> Let<T>(this Task<T> source, Action<T> action)
{
    T result = default(T);
    action(result);
    return result;
}

In looking for help regarding async testing, I've seen a lot of posts recommending the use of Task.FromResult to get async tests to work, but this is basically short-circuiting the awaiting aspect, since a Task<T> created this way starts with a status of RanToCompletion and never needs to be awaited.

Aucun commentaire:

Enregistrer un commentaire