如何让Visual Studio Test Explorer在终结器抛异常时完成全部NUnit测试?
我的NUnit测试需要调用第三方DLL中的代码,该代码可能在垃圾回收期间(尤其是Finalize方法执行时)抛出异常。当此类异常发生时,Visual Studio Test Explorer/运行器会停止测试套件运行,导致部分测试未执行。
由于仅在测试线程完成前触发垃圾回收时才会出现该问题,因此它是间歇性的,但添加以下代码可稳定复现:
GC.Collect(); GC.WaitForPendingFinalizers();
我无法修改第三方源码。以下是一个可复现该行为的.NET 6 NUnit测试项目类:单独运行Test2可通过,但运行整个测试夹具时Test2永远不会执行。
namespace TestProject { public class Tests { [SetUp] public void Setup() { } [Test] public void Test1() { var x = new BrokenFinalizer(); GC.Collect(); GC.WaitForPendingFinalizers(); } [Test] public void Test2() { var x = new BrokenFinalizer(); GC.Collect(); GC.WaitForPendingFinalizers(); } } public class BrokenFinalizer { public BrokenFinalizer() { } ~BrokenFinalizer() { throw new Exception(); } } }
请问如何强制Visual Studio Test Explorer/运行器在遇到此类「损坏的终结器」情况时,仍完成项目中所有测试的运行?
方法1:全局捕获未处理异常,阻止进程终止
终结器抛出的未处理异常默认会终止进程,这是CLR的核心行为。可以通过注册全局异常事件,捕获这类异常并避免进程直接崩溃。
在测试项目中添加全局初始化类,注册异常处理逻辑:
using NUnit.Framework; using System; [SetUpFixture] public class GlobalTestSetup { [OneTimeSetUp] public void InitializeBeforeAllTests() { // 捕获未处理异常,记录信息但不终止进程 AppDomain.CurrentDomain.UnhandledException += (sender, args) => { Console.WriteLine($"捕获终结器未处理异常:{args.ExceptionObject}"); }; // 可选:提前捕获首次机会异常,定位终结器问题 AppDomain.CurrentDomain.FirstChanceException += (sender, args) => { if (args.Exception.TargetSite?.DeclaringType?.Name.Contains("ThirdParty") ?? false) { Console.WriteLine($"第三方对象终结器抛出异常:{args.Exception}"); } }; } }
注意:.NET Core/.NET 5+ 默认不会因未处理终结器异常终止进程,.NET Framework 中即使捕获事件仍会终止进程,但注册事件至少可以记录异常信息,便于排查。
方法2:让每个测试/夹具在独立进程中运行
NUnit支持将测试放在独立进程中执行,单个进程的终止不会影响其他测试的运行。
创建或修改测试项目的.runsettings文件,配置独立进程模式:
<?xml version="1.0" encoding="utf-8"?> <RunSettings> <NUnit> <!-- 每个测试夹具在独立进程运行 --> <ProcessModel>Separate</ProcessModel> <!-- 若要每个测试都用独立进程,改为Multiple --> <!-- <ProcessModel>Multiple</ProcessModel> --> <DomainModel>Single</DomainModel> </NUnit> </RunSettings>
这种方法能完全隔离异常影响,但会增加测试运行的时间开销。
方法3:跳过有问题的终结器执行
如果确认第三方对象的终结器没有必要的资源释放逻辑,可以在创建对象后调用GC.SuppressFinalize,阻止CLR执行该对象的终结器:
[Test] public void Test1() { var thirdPartyObj = new BrokenFinalizer(); // 跳过该对象的终结器执行 GC.SuppressFinalize(thirdPartyObj); GC.Collect(); GC.WaitForPendingFinalizers(); }
使用前需确认:跳过终结器不会导致资源泄漏(比如第三方对象的资源已通过其他方式释放)。
内容的提问来源于stack exchange,提问作者Slack Groverglow

