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:
- A local variable in the currently running method, or
- 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 scopeAction dispose = () => ...
- we create a delegate we can execute later in the testweak = new WeakReference<Disposable>(disposable, true);
- Create our weak reference, rooted in the outer scope, and passingtrue
to the second argument will allow it to continue tracking the object after finalizationdispose();
- Execute our test actionGC.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 0GC.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.