为什么这个异步单元测试会永久阻止该线程?

时间:2017-03-22 12:55:42

标签: c# testing asynchronous async-await nunit

目标

我想使用Task<T>使用自定义扩展方法在C#中编写异步工作流。我想对我将使用的每个基本扩展方法进行单元测试,以确保它们正常工作,因此当我开始将它们组合在一起时,行为就不会出现意外。此测试最重要的一个方面是确保这些方法在应用时执行任务,而不是在编写工作流时立即运行,而是仅在等待组合工作流时。

在实践中,我的扩展方法似乎工作正常。但是,我无法创建一个单元测试来测试这些方法的等待方面,而不会阻塞测试线程。

我使用的是Visual Studio 2015,C#5,.NET 4.5.1和NUnit 3。

代码

以下是我想测试的一种扩展方法:

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

以下是我为该方法所做的测试。 (此项目使用Shouldly为NUnit提供了流畅的界面,因此您可以将x.ShouldBe(3)写为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);
        }
    }
}

第一个测试按预期工作,但第二个测试永远阻止第一个await

研究

有趣的是,如果我删除了测试方法中的await,测试将不会阻止。

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

在寻找有关异步测试的帮助时,我发现有很多帖子建议使用Task.FromResult来使异步测试工作,但这基本上是等待等待的方面,因为以这种方式创建的Task<T>以状态RanToCompletion开头,永远不需要等待。

解决 根据Scott Chamberlain的回答,我将测试夹具中的Source属性更改为:

private static Task<int> Source =>
    Task.Run<int>(() => {
        Thread.Sleep(1000);
        sourceExecuted = true;
        return 1;
    });

Task.Run将立即启动任务,这允许我的第二次测试通过。 Thread.Sleep是必需的,以便第一次测试仍然通过。

1 个答案:

答案 0 :(得分:4)

你永远不会开始任务,你有一个所谓的冷却任务&#34;。 Await不会启动任务,只会等待它们完成。

除非您正在编写任务调度程序,否则永远不需要调用new Task (,而是在函数返回后使用Task.Run (创建一个已处于运行状态的热任务。