Search code examples
c#garbage-collectionnunitunhandled-exceptionfinalizer

Visual Studio Test Explorer doesn't finish running NUnit tests when exception in finalizer is thrown


My NUnit tests need to run some code from a third party dll. That code may throw exceptions during garbage collection. Specifically during Finalize methods. When such an exception occurs, the Visual Studio Test Explorer/Runner stops running the test suite, so I'm left with tests not run.

Since this only occurs if garbage collection is called before the test thread compelets, this is intermittent, but I can make it always reproducible for a test if I add

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

I cannot change the third party source code.

Below is a class that can show this behavior with an .Net 6 Nunit Test Project. You will notice how Test2 passes if ran alone, but never runs if running the whole fixture.

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

How can I force Visual Studio Text Explorer/Runner to finish running all tests in the project, even when it needs to handle this "Broken Finalizer" case?


Solution

  • Unfortunately, the best solution I could find is to use GC.SuppressFinalize for the third party objects that have this issue.

    Since I may still want to run the Finalizer and abort at runtime I wrote this class to give the me some control of when the finalizer is suppressed

    public class FinalizerControlled
    { 
       public FinalizerControlled(object value, bool forceSuppress = false)
       {
          if (IsTesting() || forceSuppress)
          {
             GC.SuppressFinalize(value);
          }
       }
    
       private static bool IsTesting()
       {
          ...
       }
    }
    

    Then for all instances of the EditOperation object that are causing unit tests abort I add the line

    var _ = new FinalizerControlled(editOperation);