Growing IDisposable Guided By Tests

My previous post looked at the limitations of the .NET Garbage Collection process. The right way to mitigate these limitations is by allowing an objects owner to call Dispose() to release resources on demand rather than having to wait for the Finalization Queue to run. In this post I will grow a worked example that conforms to the basic design pattern for implementing Dispose. Tests will drive the design of the sample class to ensure that:

  • when the Finalizer is called it releases all unmanaged resources
  • when the Finalizer is called it does NOT Dispose its managed resources (remember that the order of finalization is not guaranteed, so the managed resource may already have been finalized)
  • when Dispose() is called all unmanaged resources are released
  • when Dispose() is called all managed Disposable objects are Disposed

To conform to the IDisposable design pattern, the sample class must also:

  • implement IDisposable
  • provide a protected virtual Dispose(bool disposing) method
  • the Dispose() method must call Dispose(true)
  • the Dispose() method must suppress finalization
  • if a finalizer is needed, it must call Dispose(false)
  • handle Dispose() being called multiple times
  • throw an ObjectDisposedException in any other public method tries to access disposed objects

For these tests I will use xUnit and FluentAssertions.

Sample class

We begin with a sample class that owns an unmanaged resource (in this case a block of memory allocated from the unmanaged COM task allocator) as well as a disposable managed resource (in this case a MemoryStream).

public class ResourceOwner
{
    protected IntPtr AnUnmanagedResource = Marshal.StringToCoTaskMemAuto("Foo");
    protected MemoryStream ADisposableResource = new MemoryStream();
}
Test 1: When the Finalizer is called it releases all unmanaged resources

Ideally the test would simply call the Finalizer and then check that the unmanaged resource has been released. Unfortunately there is no way to call the Finalizer. The best we can do is use a testing strategy called “subclass to test“, create a method to mimic the Finalizer and visually ensure that it and the Finalizer are always in sync. Terrible, I know, but I can’t think of any other way. DI won’t work because if we inject the finalizable resources then the ResourceOwner won’t own them so shouldn’t dispose of them.¬†¬†Subclassing also allows us to spy on the non-public resources owned by the base class. It is important to remember that the subclass is part of the test suite, not the system under test, so it will not be test-driven (but we can trust it anyway). The first test looks like this:

 [Fact]
public void ReleasesUnmanagedResourcesWhenFinalized()
{
    var spy = new ResourceSpy();
    spy.MimicFinalizer();
    spy.UnmanagedResourcesHaveBeenReleased.Should().BeTrue();
}

To make it compile we add:

 public class ResourceSpy : ResourceOwner
{
    public void MimicFinalizer(){}
 
    public bool UnmanagedResourcesHaveBeenReleased
    {
    get
        {
            var referencedString = Marshal.PtrToStringAuto(AnUnmanagedResource);
            return referencedString != "Foo";
        }
    }
}

The test fails. To make it pass we release the unmanaged resources – we call FreeCoTaskMem as advised here:

public class ResourceSpy : ResourceOwner
{
    public void MimicFinalizer()
    {
        Marshal.FreeCoTaskMem(AnUnmanagedResource);
    }
    ...
}

… and synchronize the Finalizer:

public class ResourceOwner
{
    ...
    ~ResourceOwner()
    {
        Marshal.FreeCoTaskMem(AnUnmanagedResource);
    }
}

Now the test passes, but then the test suite crashes! The reason is that FreeCoTaskMem is being called twice – once during the test by MimicFinalizer and then when the GC calls the actual finalizer. In the real world the finalizer only ever gets called once so this doesn’t need to be handled by the ResourceOwner, rather we suppress the finalizer at the end of the test:

[Fact]
public void ReleasesUnmanagedResourcesWhenFinalized()
{
    var spy = new ResourceSpy();
    spy.MimicFinalizer();
    spy.UnmanagedResourcesHaveBeenReleased.Should().BeTrue();
    GC.SuppressFinalize(spy);
}

The test passes and the test suite does not crash.

2 thoughts on “Growing IDisposable Guided By Tests

  1. Regarding test 3, you could check your call to GC.SuppressFinalize(this) by introducing a non-public instance method SuppressFinalizeOnThis() which delegates to GC. This then acts like a collaborator that, in some languages, you might pull out into a mixin/trait.

    I don’t think I’d suggest it until you had reason to believe that you weren’t calling GC.SuppressFinalize() correctly.

    1. Hi J.B. Thanks for the reply.

      I thought about doing something like that but it just seems to shift the problem (if I am understanding you correctly). While this would allow me to test that SuppressFinalizeOnThis() was called, the thing I really need to test is that GC.SuppressFinalize(this) was called. I would still need to confirm that this call was being made inside SuppressFinalizeOnThis(). I don’t know how to do that except by visual inspection, which gets me back to the original problem.

Comments are closed.