You have an interface that allows you to treat all worker classes – classes that handle a specific type of work – as just one type: IWorker
;
interface IWorker { public Boolean CanHandle(IWork work); public void Handle(IWork work); }
You want your worker classes to be able to do their work without having to check their backs every step of the way.
BUT
You have no way of guaranteeing that the classes that are handing out work to workers call Handle
only if CanHandle
returns true
.
This may be a bit of a contrived example, as in this case you’d probably be better off avoiding the problem altogether. But for the sake of argument let’s say that is not an option and that the calling `CanHandle` from `Handle` is just an example of you needing to make sure that the implementer of one method always calls another and and acts according to the returned value.
To make every worker call CanHandle
itself and only proceed with the actual work if everything is ok, you could of course provide a base class that does this. Ensuring with tests that the base class sticks to the pattern of calling CanHandle
from Handle
would be easy to do. And it would allow specific workers to focus on their own specific processing.
Hang on though … having such a base class kinda defeats the object of having an interface: not tying yourself to a distinct class hierarchy.
So how can you ensure that every class that implements the IWorker
interface has implemented its Handle
method in a way that protects the actual processing code from being handed cows when it is designed to handle chickens? Working out that you ended up with a sliced and diced pair of jeans because some distributor blindly handed your Levi’s to a kitchen worker instead of a laundry worker, is not exactly fun or easy.
The way to protect yourself from late night debugging sessions has three parts:
- You need a way to test every class implementing
IWorker
with the same set of tests. - You need a way to substitute the actual implementation of
CanHandle
with an implementation that does exactly what your tests need it to do. - You need two tests to verify that
Handle
callsCanHandle
and proceeds in accordance with whatCanHandle
returns.
1. Executing the same tests for every class implementing IWorker
Ensuring that every implementer of IWorker
is tested with the same set of tests, is pretty simple when you are using NUnit and not much more difficult when using MSTest (just a little more work).
When using NUnit this boils down to using a generic test class and using the TestFixture
attribute to specify the classes to be tested.
[TestFixture(typeof(KitchenWorker))] [TestFixture(typeof(BathroomWorker))] class IWorker_ContractTests<T> where T : IWorker, new() { IWorker MakeWorker() { return new T(); } [Test] public void TestMethod1() { IWorker worker = MakeWorker(); // ...snip... } }
For more information check out these two posts:
- Testing every implementer of an interface with the same tests using NUnit
- Testing every implementer of an interface with the same tests using MSTest
2. Substituting CanHandle
To verify that Handle
acts according to what CanHandle
returns, you need to be able to control what CanHandle
returns. This means you need to substitute the actual implementation of CanHandle
with whatever your tests needs it to do.
Using a mocking framework that is a piece of cake. But what if you can’t use a mocking framework?
Oops?!?
Yup. Without a mocking framework you are definitely in a bit of a pickle.
Unless…
When dealing with interfaces, you always have the option of wrapping the implementing class that you want to test in a “Test wrapper” and selectively delegate its methods to the “actual” implementer. Something like this.
class TestWrapper : IWorker { IWorker _WorkerUnderTest; EnsureCanHandle _ReturnsThis; public TestWrapper(IWorker workerUnderTest, EnsureCanHandle returnsThis) { _WorkerUnderTest = workerUnderTest; _ReturnsThis = returnsThis; } public Boolean CanHandle(IWork work) { return (_ReturnsThis == EnsureCanHandle.ReturnsTrue); } public void Handle(IWork work) { _WorkerUnderTest.Handle(work); } } enum EnsureCanHandle { ReturnsFalse, ReturnsTrue }
The test wrapper in this example takes the actual worker to be tested as the first parameter of its constructor. The second parameter dictates the result that CanHandle
will return. The test wrapper’s implementation of CanHandle
does what is required by the test, while its implementation of Handle
delegates to the actual worker.
3. Testing Handle
You want all classes implementing IWorker
to call CanHandle
from Handle
. And you only want Handle
to proceed with the actual work when CanHandle
returns true
. When CanHandle
returns false
, Handle
should throw an IndecentProposalError
.
To achieve this using our test wrapper, you need to tweak the MakeWorker
helper method of the generic test class to take a parameter that will dictate the result CanHandle
returns. And of course you need to pass the desired CanHandle
result in each of your tests. After that, all that’s left to do is to write the actual tests.
Like so.
[TestFixture(typeof(KitchenWorker))] [TestFixture(typeof(BathroomWorker))] class IWorker_ContractTests<T> where T : IWorker, new() { IWorker MakeWorker(EnsureCanHandle returnsThis) { return new TestWrapper(new T(), returnsThis); } [Test] public void Handle_CanHandle_Returns_False_Should_Throw_IndecentProposalError() { IWorker worker = MakeWorker(EnsureCanHandle.ReturnsFalse); IWork work = new Work_Fake(); Assert.Throws<IndecentProposalError>( delegate { worker.Handle(work); } ); } [Test] public void Handle_CanHandle_Returns_True_Should_Not_Throw_Anything() { IWorker worker = MakeWorker(EnsureCanHandle.ReturnsTrue); IWork work = new Work_Fake(); Assert.DoesNotThrow( delegate { worker.Handle(work); } ); } }
That’s it. That’s how you make sure all classes implementing IWorker
call CanHandle
from Handle
and proceed appropriately when you can’t use a mocking framework. Enjoy!
What code had you wondering how to test it? I’d love hearing from you by email or in the comments below. I read everything and will try to help where and as best I can.
Leave a Reply