NUnit 不会因终结器中的异常而失败

NUnit does not fail on exception in Finalizer

在我们的框架中,有一些具有文件句柄或 WCF 客户端连接的关键对象。这些对象是 IDiposable 并且我们有验证代码(抛出异常)以确保它们在不再需要时得到正确处理。 (仅调试,这样我们就不想在发布时崩溃)。这不一定是关机。

除此之外,我们还对 运行 我们的代码进行了单元测试,因此如果我们忘记了此类处理,我们预计它们会失败。

问题是:在 .NET 4.5.1 上,使用 NUnit (2.6.3.13283) 运行ner(或使用 ReSharper 或 TeamCity)不会触发测试抛出 Finalizer 中的此类异常时失败。

奇怪的是:使用NCrunch(也超过了 NUnit),单元测试 DO 失败! (在本地对我来说,至少我能找到这样丢失的处置)

这很糟糕,因为我们的构建机器 (TeamCity) 没有看到此类故障,我们认为一切都很好!但是运行我们的软件(在调试中)确实会崩溃,这表明我们忘记了一个处置

这里有一个例子表明 NUnit 不会失败

public class ExceptionInFinalizerObject
{
    ~ExceptionInFinalizerObject()
    {
        //Tried here both "Assert.Fail" and throwing an exception to be sure
        Assert.Fail();
        throw new Exception();
    }
}

[TestFixture]
public class FinalizerTestFixture
{
    [Test]
    public void FinalizerTest()
    {
        CreateFinalizerObject();

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

    public void CreateFinalizerObject()
    {
        //Create the object in another function to put it out of scope and make it available for garbage collection
        new ExceptionInFinalizerObject();
    }
}

运行 NUnit 中的这个 运行ner:一切都是绿色的。 让 ReSharper 调试这个测试确实会进入 Finalizer。

终结器中的异常不同,请参见c# finalizer throwing exception?

在早期的 .Net 中,它们被忽略了。在较新的版本中,CLR 退出并出现致命错误。

quote Eric Lippert(谁知道的和其他人一样多):

Call the aptly named WaitForPendingFinalizers after calling Collect if you want to guarantee that all finalizers have run. That will pause the current thread until the finalizer thread gets around to emptying the queue. And if you want to ensure that those finalized objects have their memory reclaimed then you're going to have to call Collect a second time. [Emphasis Added]

在不同环境中 运行 时的不一致行为恰恰凸显了预测 GC 行为有多么困难。有关垃圾回收的更多信息,请参阅 Raymond Chen 的文章:

或 Eric 的博客条目:

所以在 Eric Lippert 的帮助下,我发现 Exceptions 在另一个线程上时不会被 NUnit 捕获。所以终结器线程也会发生同样的情况。

我尝试在NUnit的设置中寻找解决方案,但无济于事。

所以我想到了子类化我所有的 TestFixture,这样我的所有测试都有一个共同的 [SetUp][TearDown]

public class BaseTestFixture
{
    private UnhandledExceptionEventHandler _unhandledExceptionHandler;
    private bool _exceptionWasThrown;

    [SetUp]
    public void UnhandledExceptionRegistering()
    {
        _exceptionWasThrown = false;
        _unhandledExceptionHandler = (s, e) =>
        {
            _exceptionWasThrown = true;
        };

        AppDomain.CurrentDomain.UnhandledException += _unhandledExceptionHandler;
    }

    [TearDown]
    public void VerifyUnhandledExceptionOnFinalizers()
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();

        Assert.IsFalse(_exceptionWasThrown);

        AppDomain.CurrentDomain.UnhandledException -= _unhandledExceptionHandler;
    }
}

显然,通过这段代码我只能知道抛出了一个异常,但我不知道是哪一个异常。但是,对于我的使用来说,这已经足够了。如果以后改了,我会尝试更新这个(或者如果有人有更好的解决方案,我很高兴设置为解决方案!)

我有两个场景需要涵盖,所以我将它们包括在这里:

[TestFixture]
public class ThreadExceptionTestFixture : BaseTestFixture
{
    [Test, Ignore("Testing-Testing test: Enable this test to validate that exception in threads are properly caught")]
    public void ThreadExceptionTest()
    {
        var crashingThread = new Thread(CrashInAThread);
        crashingThread.Start();
        crashingThread.Join(500);
    }

    private static void CrashInAThread()
    {
        throw new Exception();
    }

    [Test, Ignore("Testing-Testing test: Enable this test to validate that exceptions in Finalizers are properly caught")]
    public void FinalizerTest()
    {
        CreateFinalizerObject();

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

    public void CreateFinalizerObject()
    {
        //Create the object in another function to put it out of scope and make it available for garbage collection
        new ExceptionInFinalizerObject();
    }
}

public class ExceptionInFinalizerObject
{
    ~ExceptionInFinalizerObject()
    {
        throw new Exception();
    }
}

至于为什么 NCrunch 做得好,这是一个很好的问题...