Where mocking fails
I mentioned before that Rhino Mocks is a very powerful framework. This can be a problem at times, because it make it hard to notice that you cross the line into mock abuse.
Let us look at this test for example:
[Test] public void Will_raise_message_arrived_event_for_each_message_in_batch_and_across_batches() { var stubbedIncomingMessageRepository = MockRepository.GenerateStub<IIncomingMessageRepository>(); var queue = new Queue(new Uri("queue://localhost/testQueue"), MockRepository.GenerateStub<IOutgoingMessageRepository>(), stubbedIncomingMessageRepository); var msgQueue = new Queue<QueueMessage>(); stubbedIncomingMessageRepository .Stub(x => x.GetEarliestMessage()) .Return(null) .Do(invocation => { lock (msgQueue) { invocation.ReturnValue = msgQueue.Count == 0 ? null : msgQueue.Dequeue(); } }) .Repeat.Any(); stubbedIncomingMessageRepository .Stub(x => x.Transaction(Arg<Action>.Is.Anything)) .Do(invocation => ((Action) invocation.Arguments[0])()) .Repeat.Any(); ; stubbedIncomingMessageRepository .Stub(x => x.Save(Arg<QueueMessage>.Is.Anything)) .Do(invocation => { lock (msgQueue) { msgQueue.Enqueue((QueueMessage)invocation.Arguments[0]); } }) .Repeat.Any(); var callCount = 0; var e = new ManualResetEvent(false); queue.MessageArrived += (obj => { if (Interlocked.Increment(ref callCount) >= 100) e.Set(); }); for (int i = 0; i < 50; i++) { queue.AcceptMessages( new QueueMessage(), new QueueMessage() ); } e.WaitOne(); Assert.AreEqual(100, callCount); }
Here we are using Rhino Mocks to fake the entire behavior of the type. Note the thread handling in the mock object, yuck! A large portion of the test is implementing a fake message queue, in a non intuitive way.
Let us explore a better way:
[Test] public void Will_raise_message_arrived_event_for_each_message_in_batch_and_across_batches() { var queue = new Queue(new Uri("queue://localhost/testQueue"), MockRepository.GenerateStub<IOutgoingMessageRepository>(), new FakeIncomingMessageRepository()); var callCount = 0; var e = new ManualResetEvent(false); queue.MessageArrived += (obj => { if (Interlocked.Increment(ref callCount) >= 100) e.Set(); }); for (int i = 0; i < 50; i++) { queue.AcceptMessages( new QueueMessage(), new QueueMessage() ); } e.WaitOne(); Assert.AreEqual(100, callCount); } public class FakeIncomingMessageRepository : IIncomingMessageRepository { readonly Queue<QueueMessage> msgQueue = new Queue<QueueMessage>(); public QueueMessage GetEarliestMessage() { lock (msgQueue) { return msgQueue.Count == 0 ? null : msgQueue.Dequeue(); } } public void Save(QueueMessage msg) { lock(msgQueue) { msgQueue.Enqueue(msg); } } public void Transaction(Action action) { action(); } }
Here we handed coded the fake object, and used that in the test. The reduction in complexity is quite significant.
So, when should you avoid mocks? When there is more pain in using them than not.
Not a helpful metric, but that is how I do it.
Comments
Hand coded mocks or stubs are also the way to go if you find yourself re-using that particular fake object in multiple tests and/or fixtures. You could use RhinoMocks in a common setup method, but I often find in these cases there are just too many methods or interactions to warrant mocking in a meaningful manner, especially when Re# will quite quickly produce an implementation of an interface. Perhaps this is just a code smell?
I generally find that hand coded fake objects are easier to deal with and understand when you have more than just a few lines of code to setup a mock, like in your example. After all, not everything is a nail.
Comment preview