调用带有匿名异步方法的方法时,xUnit测试挂起/死锁

3

我有一个xUnit(2.1.0)测试始终会挂起/死锁。以下是代码,其中类/方法的名称已更改以提高清晰度和保密性:

[Fact]
public void DocumentModificationTest()
{
    Exception ex = null;
    using (TestDependency testDependency = new TestDependency())
    {
        TestDependencyDocument testDependencyDoc = testDependency.Document;
        MockTestDependency fakeDependency = new MockTestDependency();
        try
        {
            DoStuffToTheDocument(testDependencyDoc, "fileName.doc", fakeDependency);
        }
        catch (Exception e)
        {
            ex = e;
        }
    }
    Assert.Null(ex);
}

如果我设置了一个断点,并一步一步地执行,直到 assert 语句,我可以看到 ex 是 null,测试应该通过并完成,但它却停住了,我从来没有在运行器上看到“测试成功”的提示。

下面是 DoStuffToTheDocument 的代码:

public static void DoStuffToTheDocument(TestDependencyDocument document, string pFileName, MockTestDependency pContainer)
{
    pContainer.CheckInTheDocFirst(async () =>
    {
        //check some stuff
        //test returns early here

        //check other stuff(test never gets here)
        //await method (thus the async anonymous method)
    });
}

最后,这就是CheckInTheDocFirst的样子:
public void CheckInTheDocFirst(Action pConfirmAction) 
{
    pConfirmAction(); //since this is a method in a mock class only used for testing we just call the action
}

这里发生了什么事情?是我的异步等待范式有问题导致测试挂起吗?


3
将异步的 Lambda 表达式传递给 Action 参数将创建一个 async void 方法,这种方法非常难以测试,但不应该导致死锁。你是否存在任何阻塞?(请提供一个最简但完整的例子)。 - Stephen Cleary
@StephenCleary 我不认为有任何阻塞。事实上,在测试中达到断言的事实使我相信没有任何阻塞。我省略了唯一的代码(被测试命中的)是在匿名异步函数中对Path类进行几次调用。下面有更复杂的代码,但由于我提前返回,所以它永远不会被执行。 - Casey Hancock
啊,我错过了它确实到达了断言;那么它就不会阻塞。你的异步 lambda 表达式是否曾经完成? - Stephen Cleary
2个回答

2
原来这是由于xUnit中异步void测试方法的支持引起的问题:https://github.com/xunit/xunit/issues/866#issuecomment-223477634 虽然你确实应该一直使用async,但有时由于互操作性问题,这并不可行。您不能总是更改您正在使用的所有内容的签名。
但是,请注意关于您的assert,即使在异步void方法(在此处为lambda)中引发异常,它也始终会被证明为真。这是因为异步void的异常处理方式不同
“异步void方法具有不同的错误处理语义。当从异步任务或异步任务方法抛出异常时,该异常将被捕获并放置在Task对象上。对于异步void方法,没有Task对象,因此从异步void方法抛出的任何异常都将直接在启动异步void方法时活动的SynchronizationContext上引发。图2说明了无法自然地捕获从异步void方法抛出的异常。”

1
当你有一个异步函数时,你应该全程使用异步。否则,你会遇到同步上下文被阻塞的问题,这将导致死锁。 pContainer.CheckInTheDocFirst 应该是一个异步函数,并返回一个Task(因为它正在调用一个返回Task对象的异步函数)。
DoStuffToDocument 应该是一个异步函数,返回一个Task,因为它调用了一个异步函数。
最后,测试本身也应该是一个异步方法,返回一个任务。
如果你把异步一路传递到堆栈顶部,我认为你会发现事情只是起作用。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接