Beautiful (nontrivial) Code - Rhino Mocks 3.5's AssertWasCalled
Beautiful code is not something that is easy to define. I think of this as something that is extremely elegant, that solve a hard problem in a way that isn't brute force. I think that the way Rhino Mocks implements the AssertWasCalled functionality is elegant, and I would like to point it out.
I know of at least one contributor to Rhino Mocks who consider that piece of code scary, by the way, so it is not cut & dry.
Here is the actual method call:
public static void AssertWasCalled<T>(this T mock, Action<T> action, Action<IMethodOptions<object>> setupConstraints) { ExpectationVerificationInformation verificationInformation = GetExpectationsToVerify(mock, action, setupConstraints); foreach (var args in verificationInformation.ArgumentsForAllCalls) { if (verificationInformation.Expected.IsExpected(args)) { verificationInformation.Expected.AddActualCall(); } } if (verificationInformation.Expected.ExpectationSatisfied) return; throw new ExpectationViolationException( verificationInformation.Expected.BuildVerificationFailureMessage()); }
We will get the GetExpectaionsToVerify in a bit, but broadly, it gets the expectation that should have been called and then it execute the same logic that it would have in the Record/Replay model. In fact, it is an exact reversal of the Record/Replay model. Now we record all the actual calls, and then we create an expectation and try to match it against the actual calls that were made against the actual object.
Of even more interest is how we get the expectation that we are verifying:
private static ExpectationVerificationInformation GetExpectationsToVerify<T>(T mock, Action<T> action, Action<IMethodOptions<object>> setupConstraints) { IMockedObject mockedObject = MockRepository.GetMockedObject(mock); MockRepository mocks = mockedObject.Repository; if (mocks.IsInReplayMode(mockedObject) == false) { throw new InvalidOperationException( "Cannot assert on an object that is not in replay mode." + " Did you forget to call ReplayAll() ?"); } var mockToRecordExpectation = (T)mocks.DynamicMock( mockedObject.ImplementedTypes[0], mockedObject.ConstructorArguments); action(mockToRecordExpectation); AssertExactlySingleExpectaton(mocks, mockToRecordExpectation); IMethodOptions<object> lastMethodCall = mocks.LastMethodCall<object>(mockToRecordExpectation); lastMethodCall.TentativeReturn(); if (setupConstraints != null) { setupConstraints(lastMethodCall); } ExpectationsList expectationsToVerify = mocks.Replayer.GetAllExpectationsForProxy(mockToRecordExpectation); if (expectationsToVerify.Count == 0) { throw new InvalidOperationException( "The expectation was removed from the waiting expectations list,"+ " did you call Repeat.Any() ? This is not supported in AssertWasCalled()"); } IExpectation expected = expectationsToVerify[0]; ICollection<object[]> argumentsForAllCalls = mockedObject.GetCallArgumentsFor(expected.Method); return new ExpectationVerificationInformation { ArgumentsForAllCalls = new List<object[]>(argumentsForAllCalls), Expected = expected }; }
This is even more interesting. We create a new mocked object, and execute it in record mode against the expectation that we wish to verify. We gather this expectation and extract that from the newly created mock object, to pass it to the AssertWasCalled method, where we verify that against the actual calls made against the object.
What I find elegant in the whole thing is not just the reversal of the record / replay model, it is the use of Rhino Mocks to extend Rhino Mocks.
Comments
"Extending Rhino Mocks with Rhino Mocks" - No kidding, sometimes I look at some object implementation in a system and wonder why I bother with an implementation if I could instead use Rhino Mocks, declare what I want the instance to do and be done with it.
While I'm sure it will take me a bit to get my head fully wrapped around everything that is going on in GetExpectationsToVerify, I do have a small comment about AssertWasCalled:
Law of Demeter violation alert!
You begin a minor violation at the foreach line, which gets much more serious when ".Expected." starts showing up repeatedly. If these things were factored into their proper locations (probably everything from the foreach on down first being moved into ExpectationVerificationInformation, and then all of the ".Expected." blocks moved into their appropriate types), with clear names assigned to each new member generated in the process, perhaps that Rhino.Mocks contributor, and others, would have an easier time building up an understanding of the code.
The scary thing about this is it's getting to the point were I expect no less from you Oren. You're code repeatedly makes me look at how we can achieve what is required in different (and better) ways - you take thinking outside the box to a whole new level. Nice code, great way to solve the problem and I am looking forward to being able to use it if I ever get round to building some .NET 3.5 bits :)
I think it's an ingenious approach for adding AssertWasCalled to a record replay framework without completely redoing the plumbing. I applaud you for that.
It's very complicated though and someone who is not totally familiar with the internals of Rhino.Mocks will find it scary. Scary doesn't mean bad, it just means that its not understandable at first glance. It requires some digging and who knows what you'll find.
Jermey,
ExpectationVerificationInformation is a DTO, it contains just the expectation and the actual arguments made.
@Oren - Ahh, that sheds a bit more light on things. Too bad, though, as I have often found that refactorings to clean up Demeter / Tell, Don't Ask violations end up introducing nice clear names for the actions being taken by the code.
In any case, thanks for taking the time to post up some of your favorite tidbits from within Rhino.Mocks. For folks like me who love using Rhino.Mocks but who haven't had an opportunity to dig into its code, the little bits of insight into its inner workings are greatly appreciated.
Comment preview