【问题标题】:Test for UnObserved Exceptions测试未观察到的异常
【发布时间】:2014-01-21 18:21:31
【问题描述】:

我有一个可用于任务的 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 之类的工具中手动运行此测试时,我得到 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,这样终结器就会看到一个未捕获的、未观察到的异常。然而,这导致了上述相同的失败。

实际上,是否存在某种由测试框架连接的异常处理程序?如果是这样,有没有办法解决它?还是我只是完全把事情搞砸了,有更好/更容易/更清洁的方法吗?

【问题讨论】:

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


    【解决方案1】:

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

    首先,有一个明确的竞争条件。当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 不能保证它是由任务终结器引发的。目前,它似乎是,但在我看来,这是一个非常不稳定的解决方案(考虑到 supposed 限制终结器的程度)。因此,在框架的未来版本中,如果知道终结器只是将 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;
    }
    

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

    【讨论】:

    • 还没有机会实现这个答案并尝试一下,但到目前为止看起来不错。
    • 目前看起来不错。我同意,这个值相当可疑,但我想不出另一种方法来证明代码有效。
    • 这对我不起作用。 NUnit 2.6.4 和 .NET 4.6 安装并测试针对 4.5.1 的项目。该事件根本不会被引发。我尝试了许多不同的变体,但都没有成功。
    • @anjdreas 尝试 .Net 4.5 之前的版本 =)
    • @anjdreas 请注意,我的答案中的解决方案仅在调试器外部运行时在发布模式下有效。
    【解决方案2】:

    只是为@Stephen Cleary 的解决方案添加一个补充(请不要对我的回答投赞成票)。

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

    如果你真的需要对这个行为进行单元测试(或者让它在“调试”模式下工作),你可以“欺骗”GC,将启动任务的代码放在一个动作(或本地函数)中。这样做,在调用 action 之后,Task 就可以被 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;
    }
    

    【讨论】:

      猜你喜欢
      • 2016-11-25
      • 2012-09-22
      • 1970-01-01
      • 2013-12-26
      • 2013-11-26
      • 1970-01-01
      • 2011-12-14
      • 1970-01-01
      • 2015-04-22
      相关资源
      最近更新 更多