测试未观察到的异常

Pet*_*ano 6 c# extension-methods nunit exception 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);
Run Code Online (Sandbox Code Playgroud)

测试总是失败的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);
Run Code Online (Sandbox Code Playgroud)

我在此代码中进行的尝试是在抛出异常之前使任务获得GC'd,以便终结器将看到未捕获的,未观察到的异常.然而,这导致了上述相同的故障.

事实上,是否存在某种由测试框架连接的异常处理程序?如果是这样,有办法吗?或者我只是把事情弄得一团糟,有更好/更容易/更清洁的方法吗?

Ste*_*ary 8

我看到这种方法存在一些问题.

首先,有一个确定的竞争条件.当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);
Run Code Online (Sandbox Code Playgroud)

但是,仍然存在两个问题:

(技术上)仍有竞争条件.尽管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;
}
Run Code Online (Sandbox Code Playgroud)

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


小智 5

只是为@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;
}
Run Code Online (Sandbox Code Playgroud)