C# 未观察到的异常测试

C# 未观察到的异常测试,c#,exception,nunit,extension-methods,unobserved-exception,C#,Exception,Nunit,Extension Methods,Unobserved Exception,我有一个C#扩展方法,可以与任务一起使用,以确保抛出的任何异常都能被观察到最低程度,从而不会使宿主进程崩溃。在.NET4.5中,行为已发生轻微更改,因此不会发生这种情况,但仍会触发未观察到的异常事件。我在这里的挑战是编写一个测试来证明扩展方法是有效的。我使用的是NUnit测试框架,ReSharper是测试运行者 我试过: var wasUnobservedException = false; TaskScheduler.UnobservedTaskException += (s, args) =

我有一个C#扩展方法,可以与任务一起使用,以确保抛出的任何异常都能被观察到最低程度,从而不会使宿主进程崩溃。在.NET4.5中,行为已发生轻微更改,因此不会发生这种情况,但仍会触发未观察到的异常事件。我在这里的挑战是编写一个测试来证明扩展方法是有效的。我使用的是NUnit测试框架,ReSharper是测试运行者

我试过:

var wasUnobservedException = false;
TaskScheduler.UnobservedTaskException += (s, args) => wasUnobservedException = true;
var res = TaskEx.Run(() =>
                          {
                              throw new NaiveTimeoutException();
                              return new DateTime?();
                          });
GC.Collect();
GC.WaitForPendingFinalizers();
Assert.IsTrue(wasUnobservedException);
Assert.IsTrue
上,测试总是失败。当我手动运行这个测试时,在类似LINQPad的程序中,我得到了
wasnobservedexception
的预期行为,返回为
true

我猜测试框架正在捕获异常并观察它,这样就不会触发
TaskScheduler.unobservedtaskeexception

我已尝试按如下方式修改代码:

var wasUnobservedException = false;
TaskScheduler.UnobservedTaskException += (s, args) => wasUnobservedException = true;
var res = TaskEx.Run(async () =>
                          {
                              await TaskEx.Delay(5000).WithTimeout(1000).Wait();
                              return new DateTime?();
                          });
GC.Collect();
GC.WaitForPendingFinalizers();
Assert.IsTrue(wasUnobservedException);
我在这段代码中所做的尝试是使任务在抛出异常之前得到GC'd,这样终结器将看到一个未捕获的、未观察到的异常。然而,这导致了上述相同的故障


事实上,是否存在某种由测试框架连接的异常处理程序?如果是这样的话,有办法解决吗?或者我只是把事情搞砸了,有更好/更容易/更干净的方法吗?

我发现这种方法存在一些问题

首先,有一个明确的种族条件。当
TaskEx.Run
返回一个任务时,它只是将一个请求排入线程池的队列;这项任务不一定已经完成

其次,您遇到了一些垃圾收集器的详细信息。当在debug中编译时——实际上,只要编译器喜欢,本地变量(即,
res
)的生存期就会延长到方法的末尾

考虑到这两个问题,我能够通过以下代码:

var wasUnobservedException = false;
TaskScheduler.UnobservedTaskException += (s, args) => wasUnobservedException = true;
var res = Task.Run(() =>
{
    throw new NotImplementedException();
    return new DateTime?();
});
((IAsyncResult)res).AsyncWaitHandle.WaitOne(); // Wait for the task to complete
res = null; // Allow the task to be GC'ed
GC.Collect();
GC.WaitForPendingFinalizers();
Assert.IsTrue(wasUnobservedException);
但是,仍然存在两个问题:

(技术上)仍然存在一种竞赛条件。尽管任务终结器引发了
未观察到的TaskException
,但不能保证它是从任务终结器引发的。目前,似乎是这样,但在我看来,这是一个非常不稳定的解决方案(考虑到终结器应该受到多大的限制)。因此,在该框架的未来版本中,如果我知道终结器只是将
未观察到的taskexception
排队到线程池,而不是直接执行它,我也不会太惊讶。在这种情况下,您不能再依赖于这样一个事实,即在任务完成时事件已经得到了处理(这是上面代码所做的一个隐式假设)

在单元测试中修改全局状态(
unobservedtaskeexception
)也存在问题

考虑到这两个问题,我得出以下结论:

var mre = new ManualResetEvent(initialState: false);
EventHandler<UnobservedTaskExceptionEventArgs> subscription = (s, args) => mre.Set();
TaskScheduler.UnobservedTaskException += subscription;
try
{
    var res = Task.Run(() =>
    {
        throw new NotImplementedException();
        return new DateTime?();
    });
    ((IAsyncResult)res).AsyncWaitHandle.WaitOne(); // Wait for the task to complete
    res = null; // Allow the task to be GC'ed
    GC.Collect();
    GC.WaitForPendingFinalizers();
    if (!mre.WaitOne(10000))
        Assert.Fail();
}
finally
{
    TaskScheduler.UnobservedTaskException -= subscription;
}
var mre=new ManualResetEvent(初始状态:false);
EventHandler订阅=(s,args)=>mre.Set();
TaskScheduler.UnobservedTaskException+=订阅;
尝试
{
var res=Task.Run(()=>
{
抛出新的NotImplementedException();
返回新的日期时间?();
});
((IAsyncResult)res.AsyncWaitHandle.WaitOne();//等待任务完成
res=null;//允许对任务进行GC
GC.Collect();
GC.WaitForPendingFinalizers();
如果(!mre.WaitOne(10000))
Assert.Fail();
}
最后
{
TaskScheduler.UnobservedTaskException-=订阅;
}

它也通过了,但考虑到它的复杂性,它的价值相当值得怀疑。

只是补充了@Stephen Cleary的解决方案(请不要对我的答案投赞成票)

正如他之前提到的,在“调试”模式下编译时,局部变量的生存期会延长到方法的末尾,因此建议的解决方案仅在代码在“发布”模式下编译时有效(未附加调试器)

如果确实需要单元测试此行为(或使其在“调试”模式下工作),可以“欺骗”GC,将启动任务的代码放入操作(或本地函数)中。这样做,在调用操作之后,任务将可由GC收集

var mre = new ManualResetEvent(initialState: false);
EventHandler<UnobservedTaskExceptionEventArgs> subscription = (s, args) => mre.Set();
TaskScheduler.UnobservedTaskException += subscription;
try
{
    Action runTask = () =>
    {
        var res = Task.Run(() =>
        {
            throw new NotImplementedException();
            return new DateTime?();
        });
        ((IAsyncResult)res).AsyncWaitHandle.WaitOne(); // Wait for the task to complete
    };

    runTask.Invoke();

    GC.Collect();
    GC.WaitForPendingFinalizers();

    if (!mre.WaitOne(10000))
        Assert.Fail();
}
finally
{
    TaskScheduler.UnobservedTaskException -= subscription;
}
var mre=new ManualResetEvent(初始状态:false);
EventHandler订阅=(s,args)=>mre.Set();
TaskScheduler.UnobservedTaskException+=订阅;
尝试
{
操作runTask=()=>
{
var res=Task.Run(()=>
{
抛出新的NotImplementedException();
返回新的日期时间?();
});
((IAsyncResult)res.AsyncWaitHandle.WaitOne();//等待任务完成
};
runTask.Invoke();
GC.Collect();
GC.WaitForPendingFinalizers();
如果(!mre.WaitOne(10000))
Assert.Fail();
}
最后
{
TaskScheduler.UnobservedTaskException-=订阅;
}

还没有机会实现这个答案并尝试一下,但到目前为止它看起来不错。到目前为止看起来不错。我同意,这个值有点可疑,但我想不出其他方法来证明代码是有效的。这对我不起作用。NUnit 2.6.4和.NET 4.6安装并测试了针对4.5.1的项目。这件事根本就没有被提起过。我尝试了许多不同的变体,但都没有成功。@anjdreas try.Net previor 4.5=)@anjdreas请注意,我的答案中的解决方案只有在调试器外部运行时才能在发布模式下工作。