Growing IDisposable Guided By Tests

Now to deal with the duplication

Both the finalizer and the Dispose() method call Dispose(bool) and then release the unmanaged reference. It makes sense to move the common code into Dispose(bool). The ResourceOwner now looks like this:

public class ResourceOwner : IDisposable
{
    protected IntPtr AnUnmanagedResource = Marshal.StringToCoTaskMemAuto("Foo");
    protected MemoryStream ADisposableResource = new MemoryStream();
 
    ~ResourceOwner()
    {
        Dispose(false);
    }
 
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
 
    protected virtual void Dispose(bool disposing)
    {
        Marshal.FreeCoTaskMem(AnUnmanagedResource);
    }
}

and synchronize (which is getting very annoying – I would love to find a different way of doing this)

public class ResourceSpy : ResourceOwner
{
    ...
    public void MimicFinalizer()
    {
        Dispose(false);
    }
    ...
}

The tests still pass.

 

Test 8: The ResourceOwner handles Dispose() being called multiple times

As the ResourceOwner is still accessible from the client after Dispose() has been called, it is possible for Dispose() to be called again. We can’t catch finalization issues inside the test because they happen once the test is finished and GC is run. The best we can do is watch to see if the test suite crashes.

[Fact]
public void HandlesDisposeBeingCalledMultipleTimes()
{
    var resourceOwner = new ResourceOwner();
    resourceOwner.Dispose();
    resourceOwner.Dispose();
}

The test suite crashes. We flag when Dispose(bool) is called and guard against the code running more than once:

public class ResourceOwner : IDisposable
{
    ...
    private bool _disposed;
    ...
    protected virtual void Dispose(bool disposing)
    {
        if (_disposed) return;
        ...
        _disposed = true;
    }
}

The test passes and the test suite does not crash.

 

Test 9: When Dispose() is called all managed Disposable objects are disposed
[Fact]
public void DisposesDisposableResourcesFromDispose()
{
    var spy = new ResourceSpy();
    spy.Dispose();
    spy.DisposableResourcesHaveBeenDisposed.Should().BeTrue();
}

To make it work, we rely on MemoryStream.ReadByte() throwing an ObjectDisposedException once it has been disposed:

public class ResourceSpy : ResourceOwner
{
    ...
    public bool DisposableResourcesHaveBeenDisposed
    {
        get
        {
            try
            {
                ADisposableResource.ReadByte();
            }
            catch (ObjectDisposedException)
            {
                return true;
            }
            return false;
        }
    }
    ...
}

The test fails. To make it pass:

public class ResourceOwner : IDisposable
{
    ...
    protected virtual void Dispose(bool disposing)
    {
        ...
        ADisposableResource.Dispose();
        Marshal.FreeCoTaskMem(AnUnmanagedResource);
        ...
    }
}

The test passes.

 

Test 10: When the Finalizer is called it does NOT dispose Disposable resources

There are important constraints that need to be addressed by our code – the order in which the GC calls finalizers on the queue is not known and the thread which runs the Finalization Queue is also not known. Therefore, we cannot call the Dispose() method of any other managed code when the Finalization Queue is doing the destruction of our object. We still call GC.SuppressFinalize() to explicitly recognize that the finalizer cannot be called twice in the real world, even though in this instance there would be no problems if we left it out.

[Fact]
public void DoesNotDisposeDisposableResourcesFromFinalizer()
{
    var spy = new ResourceSpy();
    spy.MimicFinalizer();
    spy.DisposableResourcesHaveBeenDisposed.Should().BeFalse();
    GC.SuppressFinalize(spy);
}

The test fails. To make it pass we make calling Dispose() on Disposable references conditional:

public class ResourceOwner : IDisposable
{
    ...
    protected virtual void Dispose(bool disposing)
    {
        ...
        if (disposing)
        {
            ADisposableResource.Dispose();
        }
        ...
    }
}

The test passes.

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.