测试未观察到的异常

时间:2014-01-21 18:21:32

标签: c# exception nunit extension-methods unobserved-exception

我有一个C#Extension方法,可以与任务一起使用,以确保抛出的任何异常都被观察到最低限度,以免崩溃托管进程。在.NET4.5中,行为略有改变,因此不会发生这种情况,但仍会触发未观察到的异常事件。我在这里的挑战是编写一个测试来证明扩展方法是有效的。我正在使用NUnit Test Framework,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之类的操作中,我得到wasUnobservedException的预期行为,即true

我猜测测试框架正在捕获异常并观察它,以致TaskScheduler.UnobservedTaskException永远不会被触发。

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

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,以便终结器将看到未捕获的,未观察到的异常。然而,这导致了上述相同的失败。

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

2 个答案:

答案 0 :(得分:6)

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

首先,有一个明确的竞争条件。当TaskEx.Run返回任务时,它只是将请求排队到线程池;任务尚未完成。

其次,你遇到了一些垃圾收集器细节。在调试中编译时 - 实际上,无论何时编译器都感觉如此 - 局部变量的生命周期(即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);

然而,仍有两个问题:

仍有(技术上)竞争条件。尽管由于任务终结器而引发UnobservedTaskException,但无法保证AFAIK从任务终结器中引发。目前,它似乎是,但在我看来,这似乎是一个非常不稳定的解决方案(考虑到限制终结器假设)。因此,在框架的未来版本中,我不会太惊讶于终结器只是将UnobservedTaskException排队到线程池而不是直接执行它。在这种情况下,您不能再依赖于事件在任务完成时处理的事实(上述代码隐含的假设)。

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

考虑到这两个问题,我最终得到:

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

考虑到它的复杂性,它也通过但是值得怀疑。

答案 1 :(得分:2)

只需在@Stephen Cleary的解决方案中添加一个补语(请不要支持我的回答)。

如前所述,在“调试”模式下编译时,局部变量的生存期会延长到方法的末尾,因此,所提出的解决方案仅在以“发布”模式下编译代码时有效(不附带调试器)

如果您确实需要对该行为进行单元测试(或使其在“调试”模式下工作),则可以“欺骗” GC,将启动Task的代码放入Action(或本地函数)中。这样做之后,在调用操作之后,该任务将可供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;
}