Unit testing Finalizers in C#

Finalizers are generally non-deterministic. If you leave the GC to its job, it will finalize eligible objects at some point. This doesn't work very well for us if we are needing to test that our disposable types are behaving.

Let's look at a base type I provide as part of my framework; the DisposableBase. This type provides a base implementation of disposable operations. Any time I need to implement something as disposable, I inherit from this type. Here's what it looks like:

public abstract class DisposableBase : IDisposable
{
    public bool Disposed { get; private set; }
    
    public void Dispose()
        => Dispose(true);
        
    protected void Dispose(bool disposing)
    {
        if (!Disposed)
        {
            Disposed = true;
            
            if (disposing)
            {
                DisposeExplicit();
            }
            
            DisposeImplicit();
            
            GC.SupressFinalize(this);
        }
    }
    
    protected virtual DisposeExplicit() { }
    protected virtual DisposeImplicit() { }
    
    ~DisposableBase()
    {
        Dispose(false);
    }
}

My implementation covers a couple of scenarios - implicit and explicit disposal. Explicit disposal occurs when I make calls to .Dispose() - either directly, or through a using statement:

using (new SomethingThatInheritsDisposableBase())
{
}

I've named this explicit, because for it to trigger, users have to explicity call this. One the other hand, the implicit disposal can occur either through explicit disposal, or through object finalization.

Given that I have an implementation, I now need to write tests to ensure it behaves the way I expect it to. Easy for explicit disposal, it is deterministic - I call it and then check the postconditions. Let's have a look at a test type and a simple test, using XUnit as my test framework of choice:

class Disposable : DisposableBase
{
    private Action _onExplicitDispose;
    private Action _onImplicitDispose;
    
    public Disposable(Action onExplicitDispose, Action onImplicitDispose)
    {
       _onExplicitDispose = onExplicitDispose;
       _onImplicitDispose = onImplicitDispose;
    }
    
    protected override void DisposeExplicit()
        => _onExplicitDispose?.DynamicInvoke();
    protected override void DisposeImplicit()
        => _onImplicitDispose?.DynamicInvoke();
}

[Fact]
public void Dispose_CallsExplicitAndImplicitDisposal()
{
    // Arrange
    bool @explicit = false;
    bool @implicit = false;
    var disposable = new Disposable(
        onExplicitDispose: () => @explicit = true,
        onImplicitDispose: () => @implicit = true);
        
    // Act
    disposable.Dispose();
    
    // Assert
    Assert.True(@explicit);
    Assert.True(@implicit);
}

Nice and simple, I can check to make sure my dispose method is doing what I expect. But, if I try the same for a finalizer, how do I actually trigger it?

The GC will not finalize objects if the reference can be obtained by walking other reference graphs, or the object itself is rooted. A reference is considered a root in the following conditions:

  1. A local variable in the currently running method, or
  2. Static references

Check out this great article on further details about the GC process itself.

So given the above restrictions, how do we make a finalizer deterministic? The secret is a combination of WeakReference[<T>], delegated execution of your test action, and blocked execution until the finalizers have executed:

[Fact]
public void Dispose_CallsImplicitOnlyOnFinalization()
{
    // Arrange
    bool @explicit = false;
    bool @implicit = false;
    WeakReference<Disposable> weak = null;
    Action dispose = () => 
    {
        // This will go out of scope after dispose() is executed
        var disposable = new Disposable(
            onExplicitDispose: () => @explicit = true,
            onImplicitDispose: () => @implicit = true);
        weak = new WeakReference<Disposable>(disposable, true);
    };
        
    // Act
    dispose();
    GC.Collect(0, GCCollectionMode.Forced);
    GC.WaitForPendingFinalizers();
    
    // Assert
    Assert.False(@explicit); // Not called through finalizer
    Assert.True(@implicit);
}

Let's look at what is going on here in detail:

  • WeakReference<Disposable> weak = null; - we can't initialize the weak reference until we've created our disposable - which we don't want to do in the current scope
  • Action dispose = () => ... - we create a delegate we can execute later in the test
  • weak = new WeakReference<Disposable>(disposable, true); - Create our weak reference, rooted in the outer scope, and passing true to the second argument will allow it to continue tracking the object after finalization
  • dispose(); - Execute our test action
  • GC.Collect(0, GCCollectionMode.Forced); - Force the GC to collect at this point. Given we havent GC'd at this point before and our references are heap allocated, this means they exist in generation 0
  • GC.WaitForPendingFinalizers(); - Block until the GC has finished processing the finalizer queue for collected objects

With this general pattern of weak references, delegated actions, GC collection and finalization, we can test our finalizer code deterministically.