Test 5: Provide a protected virtual Dispose(bool disposing) method
Use reflection to confirm that there is a Dispose method which takes a boolean parameter, that method is protected and virtual. I would have liked to have used methodInfo.Should().NotBeNull()
for the first assertion, but a bug in FluentAssertions prevents NotBeNull()
from working correctly if the subject of Should()
is a MethodInfo:
[Fact] public void ProvidesAVirtualProtectedDisposeMethodWithABooleanParameter() { var methodInfo = typeof (ResourceOwner) .GetMethod("Dispose", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, null, new Type[] {typeof (bool)}, null); Assert.NotNull(methodInfo); //There is a Dispose(bool) method methodInfo.IsFamily.Should().BeTrue("the Dispose(bool) method is protected"); methodInfo.IsVirtual.Should().BeTrue("the Dispose(bool) method is virtual"); } |
The test fails. To make it pass we provide a method with the expected signature:
public class ResourceOwner : IDisposable { ... protected virtual void Dispose(bool disposing){} } |
The test passes.
Test 6: The Dispose() method must call Dispose(true)
[Fact] public void CallsDisposeWithTrueFromDispose() { var spy = new ResourceSpy(); spy.Dispose(); spy.DisposeWasCalled(true).Should().BeTrue(); } |
To make it compile:
public class ResourceSpy : ResourceOwner { private bool _disposeWasCalledWithTrue; private bool _disposeWasCalledWithFalse; ... protected override void Dispose(bool disposing) { _disposeWasCalledWithTrue = disposing; _disposeWasCalledWithFalse = !disposing; base.Dispose(disposing); } public bool DisposeWasCalled(bool disposing) { return expected ? _disposeWasCalledWithTrue : _disposeWasCalledWithFalse; } } |
The test fails. To make it pass:
public class ResourceOwner : IDisposable { ... public void Dispose() { Dispose(true); Marshal.FreeCoTaskMem(AnUnmanagedResource); GC.SuppressFinalize(this); } ... } |
The test passes.
Test 7: The finalizer must call Dispose(false)
[Fact] public void CallsDisposeWithFalseFromFinalizer() { var spy = new ResourceSpy(); spy.MimicFinalizer(); spy.DisposeWasCalled(false).Should().BeTrue(); GC.SuppressFinalize(spy); } |
The test fails. To make it pass:
public class ResourceSpy : ResourceOwner { ... public void MimicFinalizer() { Dispose(false); Marshal.FreeCoTaskMem(AnUnmanagedResource); } } |
and synchronize:
public class ResourceOwner : IDisposable { ... ~ResourceOwner() { Dispose(false); Marshal.FreeCoTaskMem(AnUnmanagedResource); } } |
The test passes.
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.
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.